Menu
Coddy logo textTech

デコレーターパターン

CoddyのPythonジャーニー「Object Oriented Programming」セクションの一部 — レッスン 51/64。

デコレーターパターンは、オブジェクトの構造を変更することなく、動的に新しい機能を追加します。これはオブジェクトをラップすることで、その振る舞いを拡張します。

シンプルなコーヒーの例を以下に示します:

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>

要点:デコレーターパターンは、元のオブジェクトを変更することなく、新しい振る舞いを追加するためにオブジェクトをラップします。各デコレーターはラップされたオブジェクトへの参照を保持し、独自の機能を追加します。これにより、多くのサブクラスを作成することなく、複数のデコレーターを組み合わせて柔軟に機能を組み合わせることができます。

challenge icon

チャレンジ

中級

このチャレンジでは、デコレーター(Decorator)デザインパターンを実装して、コーヒーショップ向けの柔軟なコーヒー注文システムを作成します。デコレーターパターンを使用すると、同じクラスの他のオブジェクトの動作に影響を与えることなく、個々のオブジェクトに動的に動作を追加できます。

あなたは、以下の機能を必要とするコーヒーショップのシステムを構築しています:

  • さまざまな種類の飲料("Espresso"、"Dark Roast"、"House Blend Coffee"、"Decaf Coffee")を提供する
  • 顧客が飲料にトッピング("Milk"、"Mocha"、"Soy"、"Whip")を追加できるようにする
  • トッピングが追加された飲料の価格を計算する
  1. beverage.py 内の抽象クラス Beverage とその具体的な実装を完成させてください。
  2. condiment_decorator.py 内の CondimentDecorator 抽象クラスと具体的なデコレーターを実装してください。
  3. driver.py 内でテストシナリオを実装し、実装内容を検証してください。

チートシート

デコレーターパターンは、オブジェクトをラップしてその振る舞いを拡張することにより、構造を変更することなくオブジェクトに新しい機能を動的に追加します。

コーヒーの例を使用した基本構造:

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()

# デコレーターを追加します
my_coffee = MilkDecorator(my_coffee)
my_coffee = SugarDecorator(my_coffee)

print(f"{my_coffee.description()}: ${my_coffee.cost()}")
# 出力: Simple coffee + Milk + Sugar: $8

テキストフォーマットの例:

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())
# 出力: <i><b>Hello World</b></i>

主な利点:多数のサブクラスを作成することなく柔軟な機能の組み合わせが可能になり、ラップされたオブジェクトへの参照を維持し、機能を動的に追加できます。

自分で試してみよう

# 必要なクラスをすべてインポート
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":
    # 1つの飲み物に対してすべてのトッピングをテスト
    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("---")
quiz icon腕試し

このレッスンには短いクイズがあります。レッスンを始めて解答し、進捗を記録しましょう。

Object Oriented Programmingのすべてのレッスン