Menu
CoddyTech

Паттерн «Декоратор»

Часть раздела Объектно-ориентированное программирование путешествия по Python на Coddy. Урок 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

Задание

Средне

В этом задании ты реализуешь паттерн проектирования «Декоратор», чтобы создать гибкую систему заказа кофе для кофейни. Паттерн «Декоратор» позволяет динамически добавлять поведение к отдельным объектам, не влияя на поведение других объектов того же класса.

Ты создаёшь систему для кофейни, которая должна:

  • Предлагать различные виды напитков (Espresso, Dark Roast, House Blend, Decaf)
  • Позволять клиентам добавлять в напитки дополнительные ингредиенты (Milk, Mocha, Soy, Whipped Cream)
  • Рассчитывать стоимость напитков с добавленными ингредиентами
  1. Заверши абстрактный класс Beverage и его конкретные реализации в файле beverage.py
  2. Реализуй абстрактный класс CondimentDecorator и конкретные декораторы в файле condiment_decorator.py
  3. Реализуй тестовые сценарии в файле 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("---")
quiz iconПроверьте себя

В этом уроке есть небольшой тест. Начните урок, чтобы ответить на вопросы и сохранить прогресс.

Все уроки раздела Объектно-ориентированное программирование

Потренируйтесь самостоятельно: Онлайн-компилятор Python