Menu
Coddy logo textTech

Wzorzec Dekorator

Część sekcji Programowanie obiektowe ścieżki Python w Coddy. Lekcja 51 z 64.

Wzorzec Dekoratora dynamicznie dodaje nowe funkcje do obiektów bez zmieniania ich struktury. Opakowuje obiekty, aby rozszerzyć ich zachowanie.

Oto prosty przykład z kawą:

class Coffee:
    def cost(self):
        return 5
    
    def description(self):
        return "Simple coffee"

Utwórz dekoratory, które dodają funkcje do kawy:

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"

Każdy dekorator opakowuje inny obiekt i dodaje własną funkcjonalność.

Użyj dekoratorów, aby tworzyć spersonalizowaną kawę:

# Zacznij od zwykłej kawy
my_coffee = Coffee()
print(f"{my_coffee.description()}: ${my_coffee.cost()}")

# Dodaj mleko
my_coffee = MilkDecorator(my_coffee)
print(f"{my_coffee.description()}: ${my_coffee.cost()}")

# Dodaj cukier
my_coffee = SugarDecorator(my_coffee)
print(f"{my_coffee.description()}: ${my_coffee.cost()}")

Utwórz kolejny przykład z formatowaniem tekstu:

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>"

# Twórz sformatowany tekst krok po kroku
message = Text("Hello World")
message = BoldDecorator(message)
message = ItalicDecorator(message)

print(message.render())

Wynik:

Simple coffee: $5
Simple coffee + Milk: $7
Simple coffee + Milk + Sugar: $8
<i><b>Hello World</b></i>

Kluczowa kwestia: Wzorzec dekoratora opakowuje obiekty, aby dodać nowe zachowanie bez zmieniania oryginalnego obiektu. Każdy dekorator przechowuje referencję do opakowanego obiektu i dodaje własną funkcjonalność. Dzięki temu możesz łączyć wiele dekoratorów, tworząc elastyczne kombinacje funkcji bez konieczności tworzenia wielu podklas.

challenge icon

Wyzwanie

Średni

W tym wyzwaniu zaimplementujesz wzorzec projektowy Dekorator, aby stworzyć elastyczny system zamawiania kawy dla kawiarni. Wzorzec Dekorator pozwala dynamicznie dodawać zachowanie do poszczególnych obiektów bez wpływu na zachowanie innych obiektów tej samej klasy.

Budujesz system dla kawiarni, który musi:

  • Oferować różne rodzaje napojów (Espresso, Dark Roast, House Blend, Decaf)
  • Pozwalać klientom dodawać dodatki (Milk, Mocha, Soy, Whipped Cream) do napojów
  • Obliczać koszt napojów z dodanymi dodatkami
  1. Uzupełnij abstrakcyjną klasę Beverage i jej konkretne implementacje w pliku beverage.py
  2. Zaimplementuj abstrakcyjną klasę CondimentDecorator i konkretne dekoratory w pliku condiment_decorator.py
  3. Zaimplementuj scenariusze testowe w pliku driver.py, aby zweryfikować swoją implementację

Spróbuj swoich sił

# Zaimportuj wszystkie niezbędne klasy
from beverage import Espresso, HouseBlend, DarkRoast, Decaf
from condiment_decorator import Milk, Mocha, Soy, Whip

# Kompleksowa obsługa przypadków testowych
test_case = input()

if test_case == "basic_beverage_test":
    # Przetestuj podstawowe działanie napojów
    espresso = Espresso()
    print(f"Description: {espresso.get_description()}")
    print(f"Cost: ${espresso.cost():.2f}")

elif test_case == "all_beverages_test":
    # Przetestuj wszystkie rodzaje napojów
    beverages = [Espresso(), HouseBlend(), DarkRoast(), Decaf()]
    for beverage in beverages:
        print(f"{beverage.get_description()}: ${beverage.cost():.2f}")

elif test_case == "single_condiment_test":
    # Przetestuj dodanie jednego dodatku
    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":
    # Przetestuj dodanie wielu dodatków
    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":
    # Przetestuj wszystkie dodatki w jednym napoju
    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":
    # Przetestuj obliczanie kosztów różnych kombinacji
    espresso = Espresso()
    
    # Pojedyncze dodatki
    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":
    # Sprawdź, czy kolejność dodawania dodatków nie wpływa na wynik końcowy
    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":
    # Przetestuj złożone zamówienia napojów
    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":
    # Przetestuj wielokrotne dodanie tego samego dodatku
    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":
    # Porównaj ceny różnych napojów z tymi samymi dodatkami
    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":
    # Przetestuj ceny poszczególnych dodatków
    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":
    # Znajdź najtańszy i najdroższy napój podstawowy
    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":
    # Przetestuj głęboko zagnieżdżone dekoratory
    beverage = HouseBlend()
    # Dodaj 5 warstw dekoratorów
    decorated = Whip(Soy(Mocha(Milk(Whip(beverage)))))
    print(f"Description: {decorated.get_description()}")
    print(f"Cost: ${decorated.cost():.2f}")
    
    # Policz dodatki
    description = decorated.get_description()
    condiment_count = description.count(" + ")
    print(f"Number of condiments: {condiment_count}")

elif test_case == "cost_breakdown_test":
    # Szczegółowe zestawienie kosztów
    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":
    # Przetestuj spójność formatowania opisu
    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}'")
        # Sprawdź, czy opis zaczyna się od nazwy napoju podstawowego
        starts_correctly = description.startswith("Espresso")
        print(f"Starts with base name: {starts_correctly}")
        print("---")
quiz iconSprawdź się

Ta lekcja zawiera krótki quiz. Zacznij lekcję, żeby na niego odpowiedzieć i śledzić swoje postępy.

Wszystkie lekcje w sekcji Programowanie obiektowe

Poćwicz samodzielnie: Kompilator Python online