데코레이터 패턴
Coddy C++ 여정의 객체 지향 프로그래밍 섹션에 포함된 레슨 — 104개 중 98번째.
데코레이터 패턴(Decorator pattern)은 객체들을 데코레이터라고 불리는 특별한 객체들로 감싸서 동적으로 새로운 동작들을 추가할 수 있게 해줍니다. 컴파일 타임에 동작을 추가하는 상속과 달리, 데코레이터는 원래의 클래스를 수정하지 않고도 런타임에 기능을 확장할 수 있게 해줍니다.
이 패턴은 원본 객체와 데코레이터가 모두 동일한 인터페이스를 구현하도록 함으로써 작동합니다. 각 데코레이터는 컴포넌트에 대한 참조를 유지하며, 래핑된 객체에 위임하기 전이나 후에 자신만의 동작을 추가합니다:
#include <iostream>
#include <memory>
// 컴포넌트 인터페이스
class Coffee {
public:
virtual std::string getDescription() const = 0;
virtual double getCost() const = 0;
virtual ~Coffee() = default;
};
// 구체적인 컴포넌트
class SimpleCoffee : public Coffee {
public:
std::string getDescription() const override { return "Coffee"; }
double getCost() const override { return 2.0; }
};
// 기본 데코레이터
class CoffeeDecorator : public Coffee {
protected:
std::unique_ptr<Coffee> coffee;
public:
CoffeeDecorator(std::unique_ptr<Coffee> c) : coffee(std::move(c)) {}
};
// 구체적인 데코레이터들
class MilkDecorator : public CoffeeDecorator {
public:
MilkDecorator(std::unique_ptr<Coffee> c) : CoffeeDecorator(std::move(c)) {}
std::string getDescription() const override {
return coffee->getDescription() + ", Milk";
}
double getCost() const override { return coffee->getCost() + 0.5; }
};
class SugarDecorator : public CoffeeDecorator {
public:
SugarDecorator(std::unique_ptr<Coffee> c) : CoffeeDecorator(std::move(c)) {}
std::string getDescription() const override {
return coffee->getDescription() + ", Sugar";
}
double getCost() const override { return coffee->getCost() + 0.2; }
};데코레이터는 중첩될 수 있습니다. 각 데코레이터는 이전 데코레이터를 감싸며, 기능을 계층별로 쌓아 올립니다:
int main() {
std::unique_ptr<Coffee> order = std::make_unique<SimpleCoffee>();
order = std::make_unique<MilkDecorator>(std::move(order));
order = std::make_unique<SugarDecorator>(std::move(order));
std::cout << order->getDescription() << ": $" << order->getCost();
// 출력: Coffee, Milk, Sugar: $2.7
}다른 객체에 영향을 주지 않고 객체에 동적으로 책임을 추가해야 할 때, 또는 서브클래싱을 통해 기능을 확장하는 것이 클래스 수의 폭발적인 증가를 초래할 때 데코레이터(Decorator)를 사용하세요.
챌린지
쉬움데코레이터(Decorator) 패턴을 사용하여 피자 주문 시스템을 구축해 봅시다. 고객이 기본 피자에서 시작하여 다양한 토핑을 추가할 수 있는 유연한 시스템을 만들게 됩니다. 각 토핑은 이전 주문을 감싸며 고유한 비용과 설명을 추가합니다. 이는 데코레이터 패턴의 완벽한 활용 사례입니다. 토핑은 수많은 서브클래스를 만들지 않고도 어떤 조합으로든 쌓을 수 있습니다.
코드는 다음 세 개의 파일로 구성됩니다:
Pizza.h: 컴포넌트 인터페이스와 기본 피자 클래스를 정의합니다.순수 가상 메서드인
getDescription()(문자열 반환)과getCost()(double 반환), 그리고 가상 소멸자를 가진 추상 클래스Pizza를 생성하세요.그런 다음 기본 피자를 나타내는 구체 클래스
PlainPizza를 생성하세요. 이 클래스는 설명으로"Pizza"를 반환하고 기본 비용으로8.0을 가져야 합니다.ToppingDecorator.h: 데코레이터 베이스 클래스와 구체적인 토핑 데코레이터들을 구축합니다.Pizza를 상속받고 래핑된 컴포넌트에 대한std::unique_ptr<Pizza>를 보유하는ToppingDecorator클래스를 생성하세요. 이것은 모든 토핑 데코레이터의 베이스 역할을 합니다.세 가지 구체적인 토핑 데코레이터를 구현하세요:
CheeseTopping— 설명에", Cheese"를 추가하고 비용에1.5를 추가합니다.PepperoniTopping— 설명에", Pepperoni"를 추가하고 비용에2.0을 추가합니다.MushroomTopping— 설명에", Mushrooms"를 추가하고 비용에1.0을 추가합니다.
각 데코레이터는 래핑된 피자에게 작업을 위임한 다음 자신의 기여분을 추가해야 합니다.
main.cpp: 사용자 입력을 기반으로 맞춤형 피자 주문을 생성합니다.세 개의 입력(각각
yes또는no)을 읽습니다:- 치즈를 추가하시겠습니까?
- 페퍼로니를 추가하시겠습니까?
- 버섯을 추가하시겠습니까?
PlainPizza로 시작하여 입력에 따라 적절한 토핑 데코레이터로 감싸세요 (순서: 치즈, 페퍼로니, 버섯). 마지막으로 주문 세부 정보를 다음 형식으로 출력하세요:Order: [description] Total: $[cost]
예를 들어, 입력이 yes, yes, no인 경우:
Order: Pizza, Cheese, Pepperoni
Total: $11.5입력이 yes, no, yes인 경우:
Order: Pizza, Cheese, Mushrooms
Total: $10.5입력이 no, no, no인 경우:
Order: Pizza
Total: $8입력이 yes, yes, yes인 경우:
Order: Pizza, Cheese, Pepperoni, Mushrooms
Total: $12.5각 데코레이터가 이전 데코레이터를 어떻게 감싸며 설명과 비용을 계층별로 쌓아가는지 확인해 보세요. 기존 피자나 토핑 코드를 수정하지 않고도 새로운 데코레이터 클래스를 생성하여 새로운 토핑을 쉽게 추가할 수 있습니다. 이것이 바로 데코레이터 패턴의 장점입니다.
치트 시트
데코레이터 패턴(Decorator pattern)은 객체를 데코레이터라고 불리는 특별한 객체로 감싸서 동적으로 새로운 행동을 추가할 수 있게 해줍니다. 컴파일 타임에 행동을 추가하는 상속과 달리, 데코레이터는 원래 클래스를 수정하지 않고도 런타임에 기능을 확장할 수 있게 해줍니다.
이 패턴은 원래 객체와 데코레이터가 모두 동일한 인터페이스를 구현하도록 함으로써 작동합니다. 각 데코레이터는 컴포넌트에 대한 참조를 유지하며, 감싸진 객체에 작업을 위임하기 전이나 후에 자신만의 행동을 추가합니다.
구조
컴포넌트 인터페이스(Component interface): 구체적인 컴포넌트와 데코레이터 모두를 위한 공통 인터페이스를 정의합니다.
class Coffee {
public:
virtual std::string getDescription() const = 0;
virtual double getCost() const = 0;
virtual ~Coffee() = default;
};구체적인 컴포넌트(Concrete component): 데코레이션될 수 있는 기본 객체입니다.
class SimpleCoffee : public Coffee {
public:
std::string getDescription() const override { return "Coffee"; }
double getCost() const override { return 2.0; }
};기본 데코레이터(Base decorator): 컴포넌트에 대한 참조를 유지하고 동일한 인터페이스를 구현합니다.
class CoffeeDecorator : public Coffee {
protected:
std::unique_ptr<Coffee> coffee;
public:
CoffeeDecorator(std::unique_ptr<Coffee> c) : coffee(std::move(c)) {}
};구체적인 데코레이터(Concrete decorators): 감싸진 객체에 작업을 위임하고 자신만의 기능을 추가함으로써 특정 행동을 더합니다.
class MilkDecorator : public CoffeeDecorator {
public:
MilkDecorator(std::unique_ptr<Coffee> c) : CoffeeDecorator(std::move(c)) {}
std::string getDescription() const override {
return coffee->getDescription() + ", Milk";
}
double getCost() const override { return coffee->getCost() + 0.5; }
};데코레이터 쌓기
데코레이터는 쌓일 수 있습니다. 각 데코레이터는 이전 데코레이터를 감싸며 층층이 기능을 쌓아 올립니다:
std::unique_ptr<Coffee> order = std::make_unique<SimpleCoffee>();
order = std::make_unique<MilkDecorator>(std::move(order));
order = std::make_unique<SugarDecorator>(std::move(order));
std::cout << order->getDescription() << ": $" << order->getCost();
// 출력: Coffee, Milk, Sugar: $2.7사용 시기
다른 객체에 영향을 주지 않고 동적으로 객체에 책임을 추가해야 할 때, 또는 서브클래싱을 통한 기능 확장이 클래스의 폭발적인 증가를 초래할 때 데코레이터 패턴을 사용하세요.
직접 해보기
#include <iostream>
#include <string>
#include <memory>
#include "Pizza.h"
#include "ToppingDecorator.h"
using namespace std;
int main() {
// 세 가지 입력 읽기 (yes 또는 no)
string addCheese, addPepperoni, addMushrooms;
cin >> addCheese;
cin >> addPepperoni;
cin >> addMushrooms;
// TODO: std::unique_ptr를 사용하여 PlainPizza로 시작하기
// TODO: addCheese가 "yes"인 경우, 피자를 CheeseTopping으로 감싸기
// TODO: addPepperoni가 "yes"인 경우, 피자를 PepperoniTopping으로 감싸기
// TODO: addMushrooms가 "yes"인 경우, 피자를 MushroomTopping으로 감싸기
// TODO: 다음 형식으로 주문 출력하기:
// Order: [description]
// Total: $[cost]
return 0;
}
이 레슨에는 짧은 퀴즈가 포함되어 있습니다. 레슨을 시작해 문제를 풀고 진행 상황을 기록하세요.