Menu
Coddy logo textTech

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

Часть раздела Object Oriented Programming путешествия по 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>

Ключевой момент: Паттерн Декоратор (Decorator Pattern) оборачивает объекты для добавления нового поведения без изменения исходного объекта. Каждый декоратор хранит ссылку на обернутый объект и добавляет свою собственную функциональность. Это позволяет комбинировать несколько декораторов для гибкого сочетания функций без создания множества подклассов.

challenge icon

Задание

Средне

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

Вы строите систему для кофейни, которой необходимо:

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