Menu
Coddy logo textTech

Patrón Decorator

Parte de la sección Object Oriented Programming del Journey de Python de Coddy — lección 51 de 64.

El patrón Decorator añade nueva funcionalidad a los objetos de forma dinámica sin cambiar su estructura. Envuelve objetos para extender su comportamiento.

Aquí tienes un ejemplo sencillo de café:

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

Crea decoradores que añadan funcionalidades al café:

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"

Cada decorador envuelve a otro objeto y añade su propia funcionalidad.

Usa decoradores para crear café personalizado:

# Comenzar con un café básico
my_coffee = Coffee()
print(f"{my_coffee.description()}: ${my_coffee.cost()}")

# Añadir leche
my_coffee = MilkDecorator(my_coffee)
print(f"{my_coffee.description()}: ${my_coffee.cost()}")

# Añadir azúcar
my_coffee = SugarDecorator(my_coffee)
print(f"{my_coffee.description()}: ${my_coffee.cost()}")

Crea otro ejemplo con formato de texto:

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

# Construye texto formateado paso a paso
message = Text("Hello World")
message = BoldDecorator(message)
message = ItalicDecorator(message)

print(message.render())

Salida:

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

Punto clave: El Patrón Decorador (Decorator Pattern) envuelve objetos para añadir un nuevo comportamiento sin cambiar el objeto original. Cada decorador mantiene una referencia al objeto envuelto y añade su propia funcionalidad. Esto permite combinar múltiples decoradores para obtener combinaciones de características flexibles sin crear muchas subclases.

challenge icon

Desafío

Intermedio

En este desafío, implementarás el patrón de diseño Decorator para crear un sistema flexible de pedidos de café para una cafetería. El patrón Decorator permite añadir comportamiento a objetos individuales de forma dinámica, sin afectar el comportamiento de otros objetos de la misma clase.

Estás construyendo un sistema para una cafetería que necesita:

  • Ofrecer varios tipos de bebidas ("Espresso", "Dark Roast", "House Blend Coffee", "Decaf Coffee")
  • Permitir a los clientes añadir condimentos (" + Milk", " + Mocha", " + Soy", " + Whip") a sus bebidas
  • Calcular el costo de las bebidas con los condimentos añadidos
  1. Completa la clase abstracta Beverage y sus implementaciones concretas en beverage.py
  2. Implementa la clase abstracta CondimentDecorator y los decoradores concretos en condiment_decorator.py
  3. Implementa escenarios de prueba en driver.py para verificar tu implementación

Hoja de referencia

El Patrón Decorador añade nueva funcionalidad a los objetos de forma dinámica sin cambiar su estructura, envolviendo los objetos para extender su comportamiento.

Estructura básica con un ejemplo de café:

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

Crear decoradores que envuelven y extienden la funcionalidad:

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"

Encadenar decoradores para construir objetos personalizados:

# Comenzar con un café básico
my_coffee = Coffee()

# Añadir decoradores
my_coffee = MilkDecorator(my_coffee)
my_coffee = SugarDecorator(my_coffee)

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

Ejemplo de formateo de texto:

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

# Encadenar decoradores
message = Text("Hello World")
message = BoldDecorator(message)
message = ItalicDecorator(message)

print(message.render())
# Salida: <i><b>Hello World</b></i>

Beneficios clave: Permite combinaciones flexibles de características sin crear muchas subclases, mantiene la referencia al objeto envuelto y añade funcionalidad dinámicamente.

Pruébalo tú mismo

# Importar todas las clases necesarias
from beverage import Espresso, HouseBlend, DarkRoast, Decaf
from condiment_decorator import Milk, Mocha, Soy, Whip

# Manejador de casos de prueba integral
test_case = input()

if test_case == "basic_beverage_test":
    # Probar la funcionalidad básica de la bebida
    espresso = Espresso()
    print(f"Description: {espresso.get_description()}")
    print(f"Cost: ${espresso.cost():.2f}")

elif test_case == "all_beverages_test":
    # Probar todos los tipos de bebidas
    beverages = [Espresso(), HouseBlend(), DarkRoast(), Decaf()]
    for beverage in beverages:
        print(f"{beverage.get_description()}: ${beverage.cost():.2f}")

elif test_case == "single_condiment_test":
    # Probar la decoración con un solo condimento
    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":
    # Probar decoraciones con múltiples condimentos
    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":
    # Probar todos los condimentos en una sola bebida
    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":
    # Probar cálculos de costo para diferentes combinaciones
    espresso = Espresso()
    
    # Adiciones individuales
    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":
    # Probar que el orden de decoración no afecta el resultado final
    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":
    # Probar pedidos de bebidas complejos
    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":
    # Probar la adición del mismo condimento varias veces
    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":
    # Comparar costos de diferentes bebidas con los mismos condimentos
    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":
    # Probar los costos individuales de los condimentos
    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":
    # Encontrar las bebidas base más baratas y más caras
    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":
    # Probar decoraciones profundamente anidadas
    beverage = HouseBlend()
    # Añadir 5 capas de decoraciones
    decorated = Whip(Soy(Mocha(Milk(Whip(beverage)))))
    print(f"Description: {decorated.get_description()}")
    print(f"Cost: ${decorated.cost():.2f}")
    
    # Contar el número de condimentos
    description = decorated.get_description()
    condiment_count = description.count(" + ")
    print(f"Number of condiments: {condiment_count}")

elif test_case == "cost_breakdown_test":
    # Desglose detallado de costos
    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":
    # Probar la consistencia del formato de la descripción
    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}'")
        # Comprobar si la descripción comienza con el nombre de la bebida base
        starts_correctly = description.startswith("Espresso")
        print(f"Starts with base name: {starts_correctly}")
        print("---")
quiz iconPonte a prueba

Esta lección incluye un breve cuestionario. Empieza la lección para responderlo y registrar tu progreso.

Todas las lecciones de Object Oriented Programming