데코레이터 패턴

데코레이터 패턴은 객체를 동적으로 기능을 확장하거나 추가할 수 있는 패턴입니다. 특징으로는 객체를 감싸는 방식으로 기능을 추가하는건데요. 말로 설명하는 것보다 예제를 통해 배워보도록 하겠습니다.

 

 

 

예제

커피를 예로 들어보겠습니다.

커피의 종류는 아메리카노, 카페라떼, 등등이 있습니다.

이 커피들에 설탕이나 샷을 추가할 수 있다고 했을때, 어떻게 구현해야 할까요?

 

커피와 추가메뉴를 나누고 거기에서 커피객체에 메뉴를 추가하는 식으로 구현할 수 있을것입니다. 그러나 오늘은 데코레이터 패턴을 사용해상속을 통한 확장이 아닌 객체를 감싸는 방식으로 확장을 해보겠습니다.

 

전체적인 구조는 다음과 같습니다.

 

 

 

Coffee

public interface Coffee {

    String getDescription();
    int getCost();
    
}
public class Americano implements Coffee {

    @Override
    public String getDescription() {
        return "아메리카노";
    }

    @Override
    public int getCost() {
        return 5000;
    }
}

 

Coffee에는 메뉴의 이름을 반환하는 getDescription가 가격을 반환하는 getCost()가 있습니다.

Coffee를 상속받은 Americano를 구현했습니다.

 

 

 

CoffeeDecorator

public abstract class CoffeeDecorator implements Coffee {

    private final Coffee coffee;

    public CoffeeDecorator(Coffee coffee) {
        this.coffee = coffee;
    }

    @Override
    public String getDescription() {
        return coffee.getDescription();
    }

    @Override
    public int getCost() {
        return coffee.getCost();
    }

}

 

public class ShotDecorator extends CoffeeDecorator {

    public ShotDecorator(Coffee coffee) {
        super(coffee);
    }

    @Override
    public String getDescription() {
        return super.getDescription() + ", 샷 추가";
    }

    @Override
    public int getCost() {
        return super.getCost() + 500;
    }

}

public class SugarDecorator extends CoffeeDecorator {
    
    public SugarDecorator(Coffee coffee) {
        super(coffee);
    }

    @Override
    public String getDescription() {
        return super.getDescription() + ", 설탕 추가";
    }

    @Override
    public int getCost() {
        return super.getCost() + 300;
    }

}

 

CoffeeDecorator를 상속받아 커피에 추가할 수 있는 옵션인 설탕과 샷 객체를 구현했습니다.

CoffeeDecorator의 구현객체를 보면 멤버변수에 Coffee를 가지고있습니다. 추가옵션은 Coffee가 반드시 필요하기 때문입니다.

 

실행결과

    public static void main(String[] args) {
        Coffee coffee = new SugarDecorator(new ShotDecorator(new Americano()));

        System.out.println(coffee.getDescription());
        System.out.println(coffee.getCost());

        // 아메리카노, 샷 추가, 설탕 추가
        // 5800
    }

 

coffee 변수안에 Decorator를 사용해서 추가옵션을 넣어 줄 수 있었습니다.

이렇게 하면 기존코드를 수정하거나 변경없이도 추가 확장할 수 있습니다.

 

여기서 살짝 아쉬운부분은 CoffeeDecorator에서 Coffee를 이미 Override를 했기때문에 Decorator를 구현할때 Override를 강제하기 않는다는것이었는데요. 그래서 강제할 수 있도록 약간 더 수정해보았습니다.

 

public abstract class CoffeeDecorator implements Coffee {

    private final Coffee coffee;

    public CoffeeDecorator(Coffee coffee) {
        this.coffee = coffee;
    }

    @Override
    public String getDescription() {
        return coffee.getDescription() + getExtra();
    }

    @Override
    public int getCost() {
        return coffee.getCost() + getExtraCost();
    }

    abstract String getExtra();
    abstract int getExtraCost();
    
}

 

public class ShotDecorator extends CoffeeDecorator {

    public ShotDecorator(Coffee coffee) {
        super(coffee);
    }

    @Override
    String getExtra() {
        return ", 샷 추가";
    }

    @Override
    int getExtraCost() {
        return 500;
    }
}

public class SugarDecorator extends CoffeeDecorator {
    
    public SugarDecorator(Coffee coffee) {
        super(coffee);
    }

    @Override
    String getExtra() {
        return ", 설탕 추가";
    }

    @Override
    int getExtraCost() {
        return 300;
    }
}

 

CoffeeDecorator에서 getExtra()와 getExtraCost()를 추상화하여 상속받은 클래스에게 구현을 강제하게 했습니다.

이렇게 하니까 더 코드가 깔끔해진것 같습니다.

 

이 데코레이터 패턴은 유연하게 확장이 가능하고, 책임을 분리하고, DIP를 준수하는 등 좋은 장점들이 많으나 코드 복잡성이 증가하고, 순서에 의존하는 단점도 존재합니다.

+ Recent posts