데코레이터 패턴
Coddy Python 여정의 Object Oriented Programming 섹션에 포함된 레슨 — 64개 중 51번째.
데코레이터 패턴(Decorator Pattern)은 객체의 구조를 변경하지 않고 동적으로 새로운 기능을 추가합니다. 이 패턴은 객체를 감싸서 그 동작을 확장합니다.
다음은 간단한 커피 예시입니다:
class Coffee:
def cost(self):
return 5
def description(self):
return "Simple coffee"커피에 기능을 추가하는 데코레이터를 생성합니다:
class MilkDecorator:
def __init__(self, coffee):
self.coffee = coffee
def cost(self):
return self.coffee.cost() + 2
def description(self):
return self.coffee.description() + " + Milk"
class SugarDecorator:
def __init__(self, coffee):
self.coffee = coffee
def cost(self):
return self.coffee.cost() + 1
def description(self):
return self.coffee.description() + " + Sugar"각 데코레이터는 다른 객체를 감싸고 자신만의 기능을 추가합니다.
데코레이터를 사용하여 맞춤형 커피를 만드세요:
# 기본 커피로 시작
my_coffee = Coffee()
print(f"{my_coffee.description()}: ${my_coffee.cost()}")
# 우유 추가
my_coffee = MilkDecorator(my_coffee)
print(f"{my_coffee.description()}: ${my_coffee.cost()}")
# 설탕 추가
my_coffee = SugarDecorator(my_coffee)
print(f"{my_coffee.description()}: ${my_coffee.cost()}")텍스트 서식을 사용한 또 다른 예제를 만듭니다:
class Text:
def __init__(self, content):
self.content = content
def render(self):
return self.content
class BoldDecorator:
def __init__(self, text):
self.text = text
def render(self):
return f"<b>{self.text.render()}</b>"
class ItalicDecorator:
def __init__(self, text):
self.text = text
def render(self):
return f"<i>{self.text.render()}</i>"
# 단계별로 포맷된 텍스트 빌드
message = Text("Hello World")
message = BoldDecorator(message)
message = ItalicDecorator(message)
print(message.render())출력:
Simple coffee: $5
Simple coffee + Milk: $7
Simple coffee + Milk + Sugar: $8
<i><b>Hello World</b></i>핵심 포인트: 데코레이터 패턴(Decorator Pattern)은 원래 객체를 변경하지 않고 새로운 동작을 추가하기 위해 객체를 감쌉니다. 각 데코레이터는 감싸진 객체에 대한 참조를 유지하며 자신만의 기능을 추가합니다. 이를 통해 수많은 서브클래스를 생성하지 않고도 여러 데코레이터를 결합하여 유연한 기능 조합을 만들 수 있습니다.
챌린지
중급이 챌린지에서는 데코레이터(Decorator) 디자인 패턴을 구현하여 커피숍을 위한 유연한 커피 주문 시스템을 만듭니다. 데코레이터 패턴을 사용하면 동일한 클래스의 다른 객체에 영향을 주지 않고 개별 객체에 동적으로 동작을 추가할 수 있습니다.
귀하는 다음과 같은 기능을 갖춘 커피숍 시스템을 구축하고 있습니다:
- 다양한 종류의 음료 제공 (Espresso, Dark Roast, House Blend, Decaf)
- 고객이 음료에 첨가물(Milk, Mocha, Soy, Whipped Cream)을 추가할 수 있도록 허용
- 첨가물이 추가된 음료의 가격 계산
beverage.py에서 추상 클래스Beverage와 이를 상속받는 구체적인 클래스들을 완성하세요.condiment_decorator.py에서CondimentDecorator추상 클래스와 구체적인 데코레이터들을 구현하세요.driver.py에서 구현 내용을 검증하기 위한 테스트 시나리오를 구현하세요.
직접 해보기
# 필요한 모든 클래스 가져오기
from beverage import Espresso, HouseBlend, DarkRoast, Decaf
from condiment_decorator import Milk, Mocha, Soy, Whip
# 종합 테스트 케이스 처리기
test_case = input()
if test_case == "basic_beverage_test":
# 기본 음료 기능 테스트
espresso = Espresso()
print(f"Description: {espresso.get_description()}")
print(f"Cost: ${espresso.cost():.2f}")
elif test_case == "all_beverages_test":
# 모든 음료 유형 테스트
beverages = [Espresso(), HouseBlend(), DarkRoast(), Decaf()]
for beverage in beverages:
print(f"{beverage.get_description()}: ${beverage.cost():.2f}")
elif test_case == "single_condiment_test":
# 단일 첨가물 데코레이션 테스트
espresso = Espresso()
espresso_with_milk = Milk(espresso)
print(f"Description: {espresso_with_milk.get_description()}")
print(f"Cost: ${espresso_with_milk.cost():.2f}")
elif test_case == "multiple_condiments_test":
# 여러 첨가물 데코레이션 테스트
house_blend = HouseBlend()
decorated_coffee = Mocha(Soy(Whip(house_blend)))
print(f"Description: {decorated_coffee.get_description()}")
print(f"Cost: ${decorated_coffee.cost():.2f}")
elif test_case == "all_condiments_test":
# 한 음료에 모든 첨가물 테스트
dark_roast = DarkRoast()
fully_loaded = Milk(Mocha(Soy(Whip(dark_roast))))
print(f"Description: {fully_loaded.get_description()}")
print(f"Cost: ${fully_loaded.cost():.2f}")
elif test_case == "cost_calculation_test":
# 다양한 조합에 대한 가격 계산 테스트
espresso = Espresso()
# 단일 추가
with_milk = Milk(espresso)
with_mocha = Mocha(espresso)
print(f"Espresso: ${espresso.cost():.2f}")
print(f"Espresso + Milk: ${with_milk.cost():.2f}")
print(f"Espresso + Mocha: ${with_mocha.cost():.2f}")
elif test_case == "decoration_order_test":
# 데코레이션 순서가 최종 결과에 영향을 미치지 않는지 테스트
decaf = Decaf()
order1 = Milk(Mocha(decaf))
order2 = Mocha(Milk(decaf))
print(f"Order 1 - {order1.get_description()}: ${order1.cost():.2f}")
print(f"Order 2 - {order2.get_description()}: ${order2.cost():.2f}")
print(f"Same cost: {order1.cost() == order2.cost()}")
elif test_case == "complex_order_test":
# 복잡한 음료 주문 테스트
orders = [
Mocha(Whip(Espresso())),
Soy(Milk(HouseBlend())),
Whip(Mocha(Soy(DarkRoast()))),
Milk(Decaf())
]
for i, order in enumerate(orders, 1):
print(f"Order {i}: {order.get_description()}")
print(f"Cost: ${order.cost():.2f}")
print("---")
elif test_case == "double_condiment_test":
# 동일한 첨가물을 여러 번 추가하는 테스트
espresso = Espresso()
double_mocha = Mocha(Mocha(espresso))
print(f"Description: {double_mocha.get_description()}")
print(f"Cost: ${double_mocha.cost():.2f}")
elif test_case == "beverage_comparison_test":
# 동일한 첨가물을 넣은 서로 다른 음료의 가격 비교
base_beverages = [Espresso(), HouseBlend(), DarkRoast(), Decaf()]
print("All beverages with Milk + Mocha:")
for beverage in base_beverages:
decorated = Milk(Mocha(beverage))
print(f"{decorated.get_description()}: ${decorated.cost():.2f}")
elif test_case == "condiment_cost_test":
# 개별 첨가물 가격 테스트
espresso = Espresso()
base_cost = espresso.cost()
condiments = [
("Milk", Milk(espresso)),
("Mocha", Mocha(espresso)),
("Soy", Soy(espresso)),
("Whip", Whip(espresso))
]
for name, decorated in condiments:
additional_cost = decorated.cost() - base_cost
print(f"{name} adds: ${additional_cost:.2f}")
elif test_case == "cheapest_most_expensive_test":
# 가장 저렴한 베이스 음료와 가장 비싼 베이스 음료 찾기
beverages = [
("Espresso", Espresso()),
("House Blend", HouseBlend()),
("Dark Roast", DarkRoast()),
("Decaf", Decaf())
]
beverages.sort(key=lambda x: x[1].cost())
print(f"Cheapest: {beverages[0][0]} - ${beverages[0][1].cost():.2f}")
print(f"Most Expensive: {beverages[-1][0]} - ${beverages[-1][1].cost():.2f}")
elif test_case == "nested_decoration_test":
# 깊게 중첩된 데코레이션 테스트
beverage = HouseBlend()
# 5단계 데코레이션 추가
decorated = Whip(Soy(Mocha(Milk(Whip(beverage)))))
print(f"Description: {decorated.get_description()}")
print(f"Cost: ${decorated.cost():.2f}")
# 첨가물 개수 세기
description = decorated.get_description()
condiment_count = description.count(" + ")
print(f"Number of condiments: {condiment_count}")
elif test_case == "cost_breakdown_test":
# 상세 가격 내역
dark_roast = DarkRoast()
print(f"Base Dark Roast: ${dark_roast.cost():.2f}")
with_soy = Soy(dark_roast)
print(f"+ Soy: ${with_soy.cost():.2f}")
with_mocha = Mocha(with_soy)
print(f"+ Mocha: ${with_mocha.cost():.2f}")
with_whip = Whip(with_mocha)
print(f"+ Whip: ${with_whip.cost():.2f}")
final_cost = with_whip.cost()
print(f"Final total: ${final_cost:.2f}")
elif test_case == "description_format_test":
# 설명 형식의 일관성 테스트
espresso = Espresso()
test_combinations = [
Milk(espresso),
Mocha(Milk(espresso)),
Whip(Mocha(Milk(espresso))),
Soy(Whip(Mocha(Milk(espresso))))
]
for combo in test_combinations:
description = combo.get_description()
print(f"Description: '{description}'")
# 설명이 베이스 음료 이름으로 시작하는지 확인
starts_correctly = description.startswith("Espresso")
print(f"Starts with base name: {starts_correctly}")
print("---")이 레슨에는 짧은 퀴즈가 포함되어 있습니다. 레슨을 시작해 문제를 풀고 진행 상황을 기록하세요.