데코레이터 패턴에 대해서 공부한 내용 정리.
Decorator 패턴?
객체에 추가 요소를 동적으로 더해 기능을 확장하는 방법.
OCP원칙을 지키면서 기능을 추가할 수 있는 기초적인 방법이라고 한다.
=> 기존 객체에 상황에 맞게 기능을 덧붙이는 패턴이라고 할 수 있다.
OCP : 클래스는 확장에는 열려 있고 변경에는 닫혀 있어야 한다는 원칙.
Component: 기본 인터페이스 (추상 클래스)
ConcreteComponent: 기본 기능을 구현하는 클래스
Decorator: Component 인터페이스를 구현하고, Component 객체를 가지고 있는 추상 클래스
ConcreteDecorator: Decorator의 구체적인 서브클래스. 기능을 확장함
공부하면서 찾아보니 카페 음료를 예제로 많이 사용해서 비슷하게 구현했다.
class Beverage {
public:
virtual ~Beverage() = default; // default : 컴파일러가 만들어준 기본 소멸사 사용.
virtual std::string getDescription() const = 0;
virtual double cost() const = 0;
};
Component 역할을 하는 Beverage 클래스를 만들었다.
ConcreteComponent 클랙스 구현
class Espresso : public Beverage {
public:
std::string getDescription() const override {
return "Esproesso";
}
double cost() const override {
return 1.99;
}
};
기본 기능을 구현한 ConcreteComponent다.
Espresso 말고 Americano 클래스도 만들 수 있겠다.
-> 어떤 행위를 할 때 가장 기본 베이스가 되는 클래스라고 생각하면 될 듯 하다.
// Beverage(Component)를 구현하고 Beverage(Component) 객체를 가지고 있음.
class CondimentDecorator : public Beverage {
protected:
Beverage* beverage;
public:
CondimentDecorator(Beverage* b) : beverage(b) {}
virtual ~CondimentDecorator() {
delete beverage;
}
};
Decorator 구현
Component를 상속받아 구현하고, Component를 멤버 변수로 가지고 있다.
Component(Beverage)를 멤버 변수로 가지고 있기 때문에 확장이 가능하다.
ConcreteDecorator 구현
class Milk : public CondimentDecorator {
public:
Milk(Beverage* b) : CondimentDecorator(b) {}
std::string getDescription() const override {
return beverage->getDescription() + ", Milk";
}
double cost() const override {
return beverage->cost() + 0.30;
}
};
class Mocha : public CondimentDecorator {
public:
Mocha(Beverage* b) : CondimentDecorator(b) {}
std::string getDescription() const override {
return beverage->getDescription() + ", Mocha";
}
double cost() const override {
return beverage->cost() + 0.20;
}
};
ConcreteDecorator를 구현했다. Moca와 Milk가 있는데 이들은 Espresso에 추가되거나(확장) 안될 수도 있는 요소들이다.
Console에서 테스트.
int main()
{
Beverage* beverage = new Espresso();
std::cout << beverage->getDescription() << " $" << beverage->cost() << std::endl;
beverage = new Milk(beverage);
std::cout << beverage->getDescription() << " $" << beverage->cost() << std::endl;
beverage = new Mocha(beverage);
std::cout << beverage->getDescription() << " $" << beverage->cost() << std::endl;
delete beverage;
return 0;
}
Beverage* beverage = new Espresso();
std::cout << beverage->getDescription() << " $" << beverage->cost() << std::endl;
이 부분에서 beverage는 Espresso 객체를 가리키는 포인터다.
따라서 getDescription()은 "Espresso"를 반환하고, cost()는 1.99를 반환한다.
beverage = new Milk(beverage);
std::cout << beverage->getDescription() << " $" << beverage->cost() << std::endl;
여기서 새로운 Milk객체를 생선한다. 그 생성자에 현재 (beverage : Espresso) 포인터를 전달한다.
그러면 Milk객체는 내부적으로 beverage를 멤버 변수로 저장하는데 이는 beverage 멤버 변수에
Espresso 객체 포인터가 저장되어 있음을 뜻한다.
=> Milk 겍체가 Espresso 를 감싸게 된 것이다.
따라서 cost() 호출 시 Milk 객체의 cost() 메서드는 내부의 beverage 객체(Espresso)의 cost() 메서드를 호출하고, 0.30을 추가한다.
결과는 2.29가 된다.
Mocha를 추가하는 과정도 같은 원리다.
데코레이터 패턴은 팩토리 패턴과 빌더 패턴으로 이어진다고 하는데 이 부분도 나중에 공부해서 정리하려 한다..