ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 데코레이터 패턴
    Programming/Design Patterns 2010. 5. 3. 08:10

    3. 데코레이터 패턴

    1. 스타버즈에 오신것을 환영 합니다.

    1.1 처음 매장 오픈

    • 스타버즈 커피는 엄청난 급속도로 성장한 초대형 커피 전문점으로 유명하다.
    • 스타버즈 커피샵은 워낙 빠르게 생장했기 때문에 다양한 음료를 모두 포괄하는 주문시스템을 이제 갖추려고 한다.
    • 처음 사업을 시작할 무렵에 만들어진 클래스는 아래와 같다.

       

       

      • Beverage는 음료를 나타내는 추상 클래스이며, 커피샵에서 판매되는 모든 음료는 이 클래스의 서브클래스가 된다.
      • 추상메소드인 cost()메소드를 새로 정의하여 가격을 구현한다.
      • 처음에는 하우스블렌드, 다크로스트, 디카페인, 에스프레소 네 가지만 판매 하게 된다..

       

    • 커피를 주문할 때 스팀 우유나 두유, 모카를 추가하고, 그 위에 휘핑 크림을 얹기도 해야 한다면..

     

    • 클래스 개수가 말 그대로 폭발적으로 늘어납니다.
    • 정말 이대로 운영한다면 클래스 관리하는게 만만치가 않겠죠?
    • 우유 가격이 인상된다면?, 카라멜 토핑을 새로 추가한다면?

    1.2 첫 번째 리팩토링

    • 왜 이렇게 클래스가 많이 필요한 거죠?
      그냥 인스턴스 변수하고 슈퍼클래스 상속을 써서 추가 사항을 관리하면 안될까요?

     

    • Beverage라는 음료 클래스에 우유,두유,모카,휘핑크림을 나타내는 인스턴스 변수를 추가한다.
    • 첨가물에 첨부 여부와, 첨가물을 설정하기 위한 has,set메소드를 생성 한다.
    • cost()에서 추가된 첨가물의 가격을 계산한다. 서브클래스에서 cost() 메소드를 오버라이드 할 때
      그 기능을 확장 하여 특정 음료의 가격을 더한다.
    • 이제 클래스가 다섯개 밖에 안되네요. 진작에 이렇게 했어야죠?
    • 근데 나중에 어떻게 바뀌어야 할지 생각해 보면 이 접근법에도 문제가 있지 않을까요?

      public class Beverage{
        public float cost();
          float condimentCost = 0.0;

          if(hasMilk())
            condimentCost += milkCost;

          if(hasSoy())
            condimentCost += soyCost;

          ...
          return condimentCost;
      }

     

    public class DarkRoast extends Beverage{

      public DarkRoast(){
        description = "최고의 다크로스트";
      }

      public float cost(){
        return 1.99+super.cost();
      } 
    }

     

     

    • 어떤 문제가 있을까요?
      • 첨가물 가격이 바뀔 때마다 기존 코드를 수정해야 한다.
      • 첨가물의 종류가 많아지면 새로운 메소드를 추가해야 한다.
      • 새로운 음료가 출시될 수도 있습니다. 그 중에는 특정 첨가물이 들어가면 안 되는 경우도 있을 겁니다.
        아이스 티를 생각해 보면 Tea서브 클래스에서도 hasWhip()같은 메소드를 여전히 상속 받을 것이다.
      • 손님이 더블 모카를 주문하면 어떻게 해야 할까요?

     

     사부와 제자

    • 서브클래스를 만드는 방식으로 행동을 상속 받으면 그 행동은 컴파일시에 완전히 결정이 되고,
      모든 서브 클래스에서 똑같은 행동을 상속 받아야 한다.
    • 하지만 구성을 통해 객체의 행동을 확장하면 실행중에 동적으로 행동을 설정 할 수 있다

     

    OCP( Open-Closed Principle)

    • OCP( Open-Closed Principle)는 가장 중요한 디자인 원칙 가운에 하나다.
    • 클래스는 확장에 대해서는 열려 있어야 하지만 코드 변경에 대해서는 닫혀 있어야 한다.
    • 즉 기존 코드는 건드리지 않은 채로 확장을 통해서 새로운 행동을 간단하게 추가할 수 있도록 하면,
      새로운 기능을 유연하게 추가할 수 있어, 주변 환경에 잘 적응할 수 있으면서도 강하고 튼튼한 디자인을 만들 수 있다.

       

    1.3 데코레이터 패턴 적용

    • 이번에 사용 할 방법은 특정 음료에서 시작해서 첨가물로 그 음료를 장식(decorate) 할 것이다.
    • 예를 들어 모카와 휘핑크림을 추가한 다크로스트 커피를 주문 한다면 아래와 같이 할 수 있을 것이다.
      • ① DarkRoast 객체를 가져온다.
      • ② Mocha 객체로 장식한다.
      • ③ Whip 객체로 장식한다.
      • ④ cost() 메소드를 호출한다. 이때 첨가물의 가격을 계산하는 일은 해당 객체들에게 위임한다.

     

    ① DarkRoast 객체에서 시작 합니다. ② 손님이 Mocha를 주문했으니 Mocha객체를 만들고
    그 객체로 DarkRoast를 감쌉니다.
    ③ 손님이 휘핑크림도 같이 주문했기 때문에
    Whip 데코레이터를 만들고 그 ?체로 Mocha를 감쌉니다.
    1.jpg 
    • DarkRoast는 Beverage로 부터 상속을 받기때문에
      음료와 가격을 계산하는 메소드를 가지고 있다.
    • Mocha 객체는 데코레이터다
    • Mocha도 Beverage의 서브클래스 형식으로 정의하면,
      다형성을 통해서 Mocah가 감싸고 있는 것도 Beverage 객체로 간주 할 수 있다.
    • Whip도 Mocha와 같은 데코레이므로 동일하게 감싸주면 된다.
    ④ 이제 가격을 계산해 볼까요?
    가장 바깥쪽에 있는 데코레이터 Whip의 cost()를 호출하면 된다.
    • 가장 바깥쪽에 있는 데코레이터 Whip의 cost()를 호출한다.
    • Whip에서는 Mocha의 cost() 메소드를 호출한다.
    • Mocha에서는 다시 DarkRoast의 cost()를 호출한다.
    • DarkRoast에서는 가격과 이름을 반환한다.
    • Mocha에서는 DarkRoast의 리턴값과 모카값을 더해 반환한다.
    • Whip에서는 Mocha에서 받은 가격에 Whip가격을더해
      최종 가격을 반환한다.
    2.JPG 

     

    1.3.1 Beverage 클래스를 장식해 봅시다.

     

    111.JPG  

    • Beverage는 가장 기본이 되는 Component 추상 클래스로 볼 수 있다.
    • 커피 종류마다 Beverage에 대한 구상 클래스를 하나씩 만든다. (HouseBlend, DarkRoast, Expresso, Decaf)
      (Beverage클래스를 상속 받아 새로운 행동을 동적으로 추가하게 된다.)
    • 각각의 첨가물을 나타내는 데코레이터를 추가합니다. cost() 뿐만 아니라 getDescription() 도 구현해야 한다.
      (Mocha, Milk, Soy, Whip)
    • 각 데코레이터 안에는 Beverage 클래스가 들어있다.
      즉, 데코레이터에는 구성요소에 대한 레퍼런스가 들어있는 인스턴스 변수가 있지요.
    사무실에서 들은 이야기..
    • CondimentDecorator에서 Beverage클래스를 확장하는 것은 상속이 맞다
    • 데코레이터 패턴에서는 상속을 이용해서 형식을 맞추는 거지, 상속을 통해서 행동을 물려 받는게 목적이 아니다.
    • 데코레이터 패턴에서는 객체 구성(인스턴스 변수로 다른 객체를 저장하는 방식)을 이용하고 있기 때문에
      음료하고 첨가물을 다양하게 섞어도 유연성을 잃지 않을 수 있다.

     

    1.3.2 코드를 만들어 봅시다. 

     

    Beverage.java 

     public abstract class Beverage {

      protected String description = "제목없음";

      public abstract double cost();
        public String getDescription() {
          return description;

        }
    }

    CondimentDecorator.java

    public abstract class CondimentDecorator extends Beverage {

      //모든 첨가물 데코레이터에서 getDescription() 메소드를 새로 구현하도록 만들 계획임
      public abstract String getDescription();
    }

     Espresso.java

    //에스프레소 커피
    public class Espresso extends Beverage {

      public Espresso(){ //Beverage로부터 상속받음
        description = "에스프레소 커피";
      }

      @Override
      public double cost() {
        return 1.99;
      }
    }

    Mocha.java
    //Mocha는 데코레이터기 때문에 CondimentDecorator를 확장 합니다.
    public class Mocha extends CondimentDecorator {

      //감싸고자 하는 음료(하우스블렌드,다크로스트,디카페인,에스프레소)를 저장하는 인스턴스.
      Beverage beverage;

      //생성자를 이용해서 감싸고자 하는 음료 객체를 전달한다.
      public Mocha(Beverage beverage){
        this.beverage = beverage;
      }

      @Override
      public String getDescription() {
        //음료 명에 첨가물명을 추가한다.
        return beverage.getDescription() + ", 모카";
      }

      //CondimentDecorator는 Beverage를 확장 하죠
      @Override
      public double cost() {
        //음료 가격에 모카 가격을 추가한다.
        return .20 + beverage.cost();
      }
    }
    StarbuzzCoffee.java

     public class StarbuzzCoffee {
      public static void main(String[] args) {

        //에스프레소 커피
        Beverage espresso = new Espresso();
       System.out.println(espresso.getDescription()+
            " : $"+espresso.cost());

       //다크로스트 커피 + 모카+ 모카 + 휘핑크림
        Beverage darkRoast = new DarkRoast();  //다크로스트 커피
        darkRoast = new Mocha(darkRoast);     //모카 추가
        darkRoast = new Mocha(darkRoast);     //모카 한번 더 추가
        darkRoast = new Whip(darkRoast);     //휘핑크림 추가
        System.out.println(darkRoast.getDescription()+
            " : $"+darkRoast.cost());

        //하우스 블렌드 커피, 두유, 모카, 휘핑크림
        Beverage houseBlend = new HouseBlend();  //하우스 블렌드 커피
        houseBlend = new Soy(houseBlend);     //두유 추가
        houseBlend = new Mocha(houseBlend);  //모카 추가
        houseBlend = new Whip(houseBlend);   //휘핑크림 추가
        System.out.println(houseBlend.getDescription()+ " : $"+houseBlend.cost());
      }
    }

     에스프레소 커피 : $1.99
    다크 로스트 커피, 모카, 모카, 휘핑크림 : $1.49
    하우스 블렌드 커피, 두유, 모카, 휘핑크림 : $1.34

    2. 데코레이터 패턴(Decorator Pattern) 정의

    2.1 데코레이터 패턴(Decorator Pattern) ?

    • 데코레이터 패턴에서는 객체의 추가적인 요건을 동적으로 추가한다.
    • 데코레이터는 서브클래스를 만드는 것을 통해서 기능을 유연하게 확장할 수 있는 방법을 제공한다.

    2.2 데코레이터의 단점

    • 데코레이터 패턴을 이용해 디자인을 하다 보면 잡다한 클래스들이 많아 질 수 있다.
    • 겹겹이 애워싼 객체의 정체를 알기가 힘들다.

    3. 데코레이터가 적용된 예 : 자바 I/O

    • java.io 패키지에는 어마어마하게 많은 클래스들이 있지만, 많은 부분이 데코레이터 패턴을 바탕으로 만들어져 있다.

     

    1(1).jpg 

     

    • 스타 버즈 디자인하고 별로 다르지 않죠? 출력 스트림의 디자인도 똑같다.
    • 자바 I/O를 보면 데코레이터의 단점도 발견 할 수 있다.
      데코레이터 패턴을 이용해서 디자인을 하다 보면 잡다한 클래스들이 너무 많아 진다.

     

    3.1 자바 I/O 데코레이터

     LowerCaseInputStream.java

    //InputStream의 추상 데코레이터인 FilterInputStream을 확장 합니다.
    public class LowerCaseInputStream extends FilterInputStream {

    public LowerCaseInputStream(InputStream in) {
    super(in);
    }

    public int read() throws IOException {
    int c = super.read();
    return (c == -1 ? c : Character.toLowerCase((char)c));
    }

    public int read(byte[] b, int offset, int len) throws IOException {
    int result = super.read(b, offset, len);
    for (int i = offset; i < offset+result; i++) {
    b[i] = (byte)Character.toLowerCase((char)b[i]);
    }
    return result;
    }
    }

    LowerCaseTest.java

    public class LowerCaseTest {

    public static void main(String[] args) {
    int c;
    try{

    InputStream in =
    new LowerCaseInputStream(
    new BufferedInputStream(
    new FileInputStream("C:/test.txt")));

    while((c=in.read()) >= 0){
    System.out.print((char)c);
    }

    in.close();

    }catch(IOException ioe){
    ioe.printStackTrace();
    }
    }
    }

    4. 데코레이터 패턴 핵심정리

    • 상속을 통해 확장을 할 수도 있지만, 디자인 유연성 면에서는 별로 좋지 않다.
    • 기존 코드를 수정하지 않고도 행동을 확장하는 방법이 필요하다.
    • 구성과 위임을 통해서 실행중에 새로운 행동을 추가할 수 있다.
    • 상속대신 데코레이터 패턴을 통해서 행동을 확장 할 수 있다.
    • 데코레이터 패턴에서는 구상 구성요소를 감싸주는 데코레이터들을 사용한다.
    • 데코레이터 클래스의 형식은 그 클래스가 감싸고 있는 클래스의 형식을 반영한다.(상속 또는 인터페이스 구현을 통해서 자신이 감쌀 클래스와 같은 형식을 가진다.)
    • 데코레이터에서는 자기가 감싸고 있는 구성요소의 메소드를 호출한 결과에 새로운 기능을 더함으로써 행동을 확장한다.
    • 데코레이터의 수퍼클래스는 자신이 장식하고 있는 객체의 수퍼클래스와 같다.
    • 구성요소를 감싸는 데코레이터의 개수에는 제한이 없다.
    • 구성요소의 클라이언트 이장에서는 데코레이터의 존재를 알수 없습니다. 클라이언트에서 구성요소의 구체적인 형식에 의존하게 되는 경우는 예외입니다.
    • 데코레이터 패턴을 사용하면 자질한 객체들이 많이 추가될 수 있고, 데코레이터를 너무 많이 사용하면 코드가 필요 이상으로 복잡해 질 수 있다.

    데코레이터 패턴의 핵심 출발점은 위에서 보았듯이
    가장 기본적인 기능과 그것을 꾸며주는 기능을 완전히 분리 하는 겁니다.

    그런데 "가장 기본적인 기능 그리고 꾸며주는 기능의 분리" 라고 쓴 위글은 사실 다음과도 같습니다.
    "변화하는것 과 변화하지 않는것을 분리하는것이 패턴의 시작이다."

    가장기본적인 기능 = 변화 하지 않는것
    꾸며주는 기능(계속 추가되는 기능 혹은 추가 될수 있는 기능) = 변화하는것

    시작점은 항상 같습니다. 먼저 직관적으로 무엇이 핵심인지 파악 하고 시작 하면 될거 같습니다.

    'Programming > Design Patterns' 카테고리의 다른 글

    디자인 패턴 요약 정리  (0) 2010.05.03
    팩토리 패턴  (0) 2010.05.03
    옵저버 패턴  (0) 2010.05.03
    스트래티지 패턴  (0) 2010.05.03
Designed by Tistory.