전체 페이지뷰

2017년 1월 25일 수요일

Decorator Pattern

데코레이터 패턴은 원래의 클래스 코드를 전혀 바꾸지 않은채 실행 중에 객체에 새로운 기능을 추가할 수 있습니다.


스타버즈 커피에서는 이제 주문 시스템을 갖추려고 합니다.
처음에 만들어진 클래스들은 이런 식이었습니다.

Beverage는 음료를 나타내는 추상 클래스이며, description은 각 서브클래스에서 작성하게 되어있는 음료에 대한 설명입니다. GetDescription()으로 description 내용을 알수 있게 되어있습니다. Cost()는 추상 메소드이며 각 서브클래스에서 구현하게 되어있습니다.

그런데 스팀 우유, 모카, 두유, 휘핑 크림 등을 얹을 때 추가 가격에 계산되므로 각각의 경우에 대해 전부 서브 클래스를 만들어 사용하다보니 클래스의 수가 너무나 많아졌습니다. 클래스의 수가 많아지면 각각의 가격도 모두 구현해야 하고, 새로운 토핑이 추가되는 등의 경우에 손을 대야할 곳이 너무나 많아집니다.

지금까지 배운 디자인 원칙을 이용해서 필드를 추가하고 상속하는 방법을 써보기로 하면 어떻게 될까요?

각 추가 요소에 대한 변수를 만들었으며,
각각의 첨가물이 있는지 boolean 값을 알아내거나 설정하기 위한 게터,세터 메소드를 추가하였습니다.

Cost()는 추상메소드로 하지 않고 구현하기로 하고, 서브클래스에서도 오버라이드 하면 기본가격에 추가된 가격을 합해 반환해줄수 있게 됩니다.

그러나 문제는 이렇게 해도 첨가물 가격 변동에 따라 기존 코드를 수정해야 하고, 첨가물이 추가되면 각각 또 메소드를 추가해야 한다는 점입니다. 또 기존의 첨가물이 들어가면 안되는 녹차같은 음료가 만들어진다고 해도 계속 필요없는 첨가물에 대한 메소드를 상속받아야 합니다.

상속의 방법으로 서브클래스를 구현하면 컴파일 시에 그 행동이 완전히 결정되고, 강제로 같은 행동을 상속 받아야 합니다. 그러나 데코레이터 패턴을 사용하면 실행 중에 동적으로 행동과 첨가물을 설정하는 코드를 만들 수 있습니다.

디자인 원칙
OCP(Open-Closed Principle):
클래스는 확장에 대해서는 열려있고, 코드 변경에 대해서는 닫혀있어야 합니다.

데코레이터 패턴

카페에서 손님이 "휘핑 크림을 올린 다크 로스트 모카 커피"를 주문했습니다.
이것을 데코레이터 패턴으로는 어떻게 처리할까요?
1. 다크로스트 객체를 생성합니다.
DarkRoast는 Beverage로부터 상속받으므로 내부에 Cost()가 있습니다.

2. Mocha 객체를 만들어 DarkRoast 객체를 감싸줍니다.
Mocha의 Cost()도 추가됩니다.

3. Whip 데코레이터를 만들어 Mocha를 감싸줍니다.
허접하지만 도식으로 나타내자면 위와 같습니다.

지금까지 알아본 데코레이터 패턴을 정리하자면,
1. 데코레이터의 수퍼클래스는 자신이 장식하고 있는 객체의 수퍼클래스와 동일
2. 하나의 객체를 여러 개로 감쌀 수 있음
3. 데코레이터는 모두 같은 수퍼 클래스를 갖고 있으므로 데코레이터 객체가 가장 안으로 들어가도 상관없음
4. 데코레이터는 자신이 장식하는 객체에게 어떤 행동을 위임하는 것 말고도 추가적인 작업 수행 가능
5. 객체는 언제든지 장식 가능하므로 실행 중에 데코레이터를 적용 가능

Beverage Class



코드 작성

먼저 기본이 되는 Beverage 클래스부터 시작합니다.
public abstract class Beverage
{
    protected string description = "제목 없음";
 
    public virtual string GetDescription()
    {
        return description;
    }
 
    public abstract double Cost();
}
cs

추상 클래스로 선언하였고, GetDescription(),과 Cost()라는 두 개의 메소드가 존재합니다. GetDescription()은 실제 구현해 놓았고-오버라이드가 가능하도록 virtual 처리했습니다-, Cost()는 추상클래스로 선언하여 서브클래스에서 구현토록 합니다.

다음으로 첨가물(condiment)을 뜻하는 추상 클래스를 구현해 봅니다.
public abstract class CondimentDecorator : Beverage
{
    public abstract override string GetDescription();
}
cs

첨가물 역시 Beverage 객체가 들어갈 자리에 들어갈 수 있어야 하기 때문에 Beverage를 상속 받습니다. 모든 첨가물 데코레이터에서 GetDescription() 메소드를 구현하도록 하기 위해 추상메소드로 선언했습니다.

음료 코드

이제 개별 실제 음료를 구현합니다. 일단 에스프레소와 하우스블렌드부터 시작합니다.
설명용 문자열을 설정하는 것과 Cost() 메소드 구현만 하면 됩니다.
public class Espresso : Beverage
{
    public Espresso()
    {
        description = "에스프레소";
    }
    public override double Cost()
    {
        return 1.99;
    }
}
 
public class HouseBlend : Beverage
{
    public HouseBlend()
    {
        description = "하우스블렌드 커피";
    }
    public override double Cost()
    {
        return 0.89;
    }
}
cs

생성자에서  description을 설정하고, Cost()를 정의합니다.

첨가물 코드

이제 개별 구상 첨가물 데코레이터를 구현할 차례입니다.
모카를 한번 만들어 보겠습니다.
public class Mocha : CondimentDecorator
{
    Beverage beverage;
    public Mocha(Beverage beverage)
    {
        this.beverage = beverage;
    }
    public override string GetDescription()
    {
        return beverage.GetDescription() + ", 모카";
    }
    public override double Cost()
    {
        return .20 + beverage.Cost();
    }
}
cs

내부에 Beverage에 대한 레퍼런스로서 beverage가 선언되고, 생성자를 통해 그 변수를 전달합니다.  GetDescription()으로는 첨가되는 이름을 추가하도록 하는 코드를 넣었습니다.

이제 테스트를 해보도록 합니다.

static void Main(string[] args)
{
    Beverage beverage = new Espresso();
    Console.WriteLine(beverage.GetDescription() + " $" + beverage.Cost());
    Beverage beverage2 = new DarkRoast();
    beverage2 = new Mocha(beverage2);
    beverage2 = new Mocha(beverage2);
    Console.WriteLine(beverage2.GetDescription() + " $" + beverage2.Cost());
}
cs

결과)
에스프레소 $1.99
다크로스트 커피, 모카, 모카 $1.39

잘 작동하는 것을 알 수 있습니다.

모든 코드를 작성해서 보겠습니다.
using System;
 
namespace DecoratorPattern
{
    // 기본 추상클래스
    public abstract class Beverage
    {
        protected string description = "제목 없음";
 
        public virtual string GetDescription()
        {
            return description;
        }
        public abstract double Cost();
    }
 
    public abstract class CondimentDecorator : Beverage
    {
        public abstract override string GetDescription();
    }
 
    // 개별 음료 클래스
    public class Espresso : Beverage
    {
        public Espresso()
        {
            description = "에스프레소";
        }
        public override double Cost()
        {
            return 1.99;
        }
    }
 
    public class HouseBlend : Beverage
    {
        public HouseBlend()
        {
            description = "하우스블렌드 커피";
        }
        public override double Cost()
        {
            return 0.89;
        }
    }
 
    public class DarkRoast : Beverage
    {
        public DarkRoast()
        {
            description = "다크로스트 커피";
        }
        public override double Cost()
        {
            return 0.99;
        }
    }
 
    public class Decaf : Beverage
    {
        public Decaf()
        {
            description = "디카페인 커피";
        }
 
        public override double Cost()
        {
            return 1.05;
        }
    }
 
    // 첨가물 데코레이터
    public class Mocha : CondimentDecorator
    {
        Beverage beverage;
        public Mocha(Beverage beverage)
        {
            this.beverage = beverage;
        }
        public override string GetDescription()
        {
            return beverage.GetDescription() + ", 모카";
        }
        public override double Cost()
        {
            return .20 + beverage.Cost();
        }
    }
    public class Milk : CondimentDecorator
    {
        Beverage beverage;
        public Milk(Beverage beverage)
        {
            this.beverage = beverage;
        }
        public override string GetDescription()
        {
            return beverage.GetDescription() + ", 밀크";
        }
        public override double Cost()
        {
            return .10 + beverage.Cost();
        }
    }
    public class Soy : CondimentDecorator
    {
        Beverage beverage;
        public Soy(Beverage beverage)
        {
            this.beverage = beverage;
        }
        public override string GetDescription()
        {
            return beverage.GetDescription() + ", 두유";
        }
        public override double Cost()
        {
            return .15 + beverage.Cost();
        }
    }
    public class  Whip : CondimentDecorator
    {
        Beverage beverage;
        public Whip(Beverage beverage)
        {
            this.beverage = beverage;
        }
        public override string GetDescription()
        {
            return beverage.GetDescription() + ", 휘핑크림";
        }
        public override double Cost()
        {
            return .10 + beverage.Cost();
        }
    }
 
    // 테스트
    class Program
    {
        static void Main(string[] args)
        {
            Beverage beverage = new Espresso();
            Console.WriteLine(beverage.GetDescription() + " $" + beverage.Cost());
 
            Beverage beverage2 = new DarkRoast();
            beverage2 = new Mocha(beverage2);
            beverage2 = new Whip(beverage2);
            Console.WriteLine(beverage2.GetDescription() + " $" + beverage2.Cost());
 
            Beverage beverage3 = new Decaf();
            beverage3 = new Soy(beverage3);
            beverage3 = new Milk(beverage3);
            Console.WriteLine(beverage3.GetDescription() + " $" + beverage3.Cost());
        }
    }
}
cs

결과)
에스프레소 $1.99
다크로스트 커피, 모카, 휘핑크림 $1.29
디카페인 커피, 두유, 밀크 $1.3

상당히 괴이한 음료와 그 가격을 제시하는 것으로 데코레이터 패턴을 마칠까 합니다.

댓글 없음:

댓글 쓰기