[디자인패턴 수업 8주차 2차시] Decorator Pattern(review), Cafe 예제

Jin Hur·2021년 10월 19일
0
post-custom-banner

reference: 수업 강의자료(헤드퍼스트 책) 그리고 https://gdtbgl93.tistory.com/9 사이트

(교재)

  • Intention: Attach additional responsibilities to an object dynamically. Decorators provide a flexible alternative to subclass for extending functionality.
  • Motivation: The object that you want to use does the basic functions you require. However, you may need to add some additional functionality to the object, occurring before or after object's basic functionality.
  • Solution: Allows for extending the functionality of an object without resorting to sub-classing.


    => 주어진 상황 및 용도에 따라 어떤 객체에 책임을 덧붙이는 패턴, 객체에 추가적인 요건을 동적으로 첨가(합성)하며, 기능 확장이 필요할 때 서브 클래싱 대신 쓸 수 있는 유연한 대안.

데코레이터 패턴의 예를 들기 아주 적절한 예시는 '스타벅스'이다.
스타벅스는 기본 메뉴에 추가로 자신이 원하는 옵션을 추가해 커스터마이징 할 수 있다.

예를 들어, 자바칩 프라프치노 기본 메뉴에 헤이즐럽 시럽을 추가하거나, 드리즐 등을 추가할 수 있다. 그렇다면 이러한 옵션별 경우의 수를 다 추가하여 새로운 메뉴들 하나하나를 메뉴판에서 보여야 하는가? 그럴 필요는 없다.

위 네 개의 자식 클래스가 기본 메뉴라 하였을 때, 추가되는 옵션에 따라 새로운 자식 클래스들을 만들 필요가 없다는 것이다.


source: https://gdtbgl93.tistory.com/9


문제점


source: https://gdtbgl93.tistory.com/9

// Beverage Class Method
public int cost() {
    int total = 0;
    
    // 추가되는 옵션에 따라 가격이 추가된다.
    if(hasMilk()) total += 500;
    if(hasShot()) total += 400;
    if(hasCream()) total += 300;
    if(hasJavachip()) total += 700;
    
    return total;
}
// 기본 메뉴 중 하나인 카페라떼 클래스
public class CaffeLatte extends Beverage {
    @Override
    public int cost() {
        // 기본 가격: 5000원 
        return 5000 + super.cost();
    }
}

그런데 이와 같은 설계에는 문제가 있다.

  1. 차후 옵션(milk, shot, cream, ..)가 추가되었을 시, Beverage 클래스(부모 클래스)를 고쳐야 함. 이는 OCP를 위반하는 것이다.

  2. 메뉴가 다양해 질 수록 Beverage 클래스 정의가 불분명해진다. 예를들어 차(tea)가 기본 메뉴로 등록되었다 생각해보자, 차는 휘핑크림이나 우유같은 것을 추가해 먹지 않는다. 위와 같은 설계에서 차 클래스(자식 클래스)는 불필요한 메서드를 상속받게 된다.

  3. 손님은 같은 옵션에 대해 두 번 이상 추가할 수 없습니다.


해결책: Decorator Pattern

public abstract class Beverage {
    // Beverage 클래스의 BeverageSize enum 클래스
    public enum BeverageSize{
        SHORT(0.8),
        TALL(1.0),
        GRANDE(1.5),
        VENTI(2.0);

        private final double rate;
        BeverageSize(double rate) {
            this.rate = rate;
        }
        public double getRate(){
            return rate;
        }
    }
    // 디폴트 사이즈
    BeverageSize beverageSize = BeverageSize.TALL;   // 디폴트 사이즈
    String description = "Unknown Beverage";

    // size get()/set()
    public BeverageSize getBeverageSize() {
        return beverageSize;
    }

    public void setBeverageSize(BeverageSize beverageSize) {
        this.beverageSize = beverageSize;
    }

    public String getDescription() {
        return description;
    }

    public abstract double cost();
}
// 기본 메뉴 중 하나인 HoueseBlend 클래스
public class HouseBlend extends Beverage {

    // 생성자
    // 사이즈를 설정하면 생성한다. 
    // 사이즈는 상위 클래스의 멤버인 enum 클래스 활용 
    public HouseBlend(BeverageSize beverageSize){
        this.description = "House Blend Coffee";
        this.beverageSize = beverageSize;

        if(beverageSize == BeverageSize.SHORT){
            description += " (size: Short)";
        }
        else if(beverageSize == BeverageSize.TALL){
            description += " (size: Tall)";
        }
        else if(beverageSize == BeverageSize.GRANDE){
            description += " (size: Grande)";
        }
        else{
            description += " (size: Venti)";
        }
    }

    @Override
    public double cost() {
        return .89 * beverageSize.getRate();
    }
}
// Decorator 상위 클래스에 해당하는 Condinent 클래스
public abstract class Condinent extends Beverage{
    protected Beverage decoratedBeverage;

    public abstract String getDescription();

    @Override
    public BeverageSize getBeverageSize() {
        return super.getBeverageSize();
    }
}
// Decorator 클래스이 하위 클래스인 Milk 클래스
// 추가되는 옵션에 해당한다. 
public class Milk extends Condinent{
    public Milk(Beverage beverage){
        this.decoratedBeverage = beverage;
        this.beverageSize = decoratedBeverage.beverageSize;
    }

    @Override
    public double cost() {
        //System.out.println("Milk 추가 비용: " + 0.10 * decoratedBeverage.beverageSize.getRate());
        return 0.10 * this.beverageSize.getRate() + decoratedBeverage.cost();
    }

    @Override
    public String getDescription() {
        return decoratedBeverage.getDescription() + " + Milk";
    }
}
// main class
public class Main {
    public static void main(String[] args) {

	// 톨 사이즈, 기본 하우스 블렌드 커피
        Beverage myBeverage = new HouseBlend(Beverage.BeverageSize.TALL);
        System.out.println(myBeverage.getDescription() + ", $: " + myBeverage.cost());
        // 숏 사이즈, 기본 하우스 블렌드 커피
        myBeverage = new HouseBlend(Beverage.BeverageSize.SHORT);
        System.out.println(myBeverage.getDescription() + ", $: " + myBeverage.cost());
        // 그란데 사이즈, 기본 하우스 블렌드 커피
        myBeverage = new HouseBlend(Beverage.BeverageSize.GRANDE);
        System.out.println(myBeverage.getDescription() + ", $: " + myBeverage.cost());
        // 벤티 사이즈, 기본 하우스 블렌드 커피
        myBeverage = new HouseBlend(Beverage.BeverageSize.VENTI);
        System.out.println(myBeverage.getDescription() + ", $: " + myBeverage.cost());

        // 벤티 사이즈, 하우스 블렌드 커피 + Milk + Whip
        // 아래와 같은 식으로 추가 옵션을 넣는다. 
        myBeverage = new HouseBlend(Beverage.BeverageSize.VENTI);
        myBeverage = new Milk(new Whip(myBeverage));
        System.out.println(myBeverage.getDescription() + ", $: " + myBeverage.cost());

        // 그란데 사이즈, Decaf 커피 + Caramel + Soy
        // 아래와 같은 식으로 추가 옵션을 넣는다. 
        myBeverage = new Decaf(Beverage.BeverageSize.GRANDE);
        myBeverage = new Caramel(new Soy(myBeverage));
        System.out.println(myBeverage.getDescription() + ", $: " + myBeverage.cost());
    }
}

결과

도식

post-custom-banner

0개의 댓글