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.
Desafío
IntermedioEn 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
- Completa la clase abstracta
Beveragey sus implementaciones concretas enbeverage.py - Implementa la clase abstracta
CondimentDecoratory los decoradores concretos encondiment_decorator.py - Implementa escenarios de prueba en
driver.pypara 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: $8Ejemplo 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("---")Esta lección incluye un breve cuestionario. Empieza la lección para responderlo y registrar tu progreso.
Todas las lecciones de Object Oriented Programming
1Fundamentos de la POO
Archivos externosIntroducción a la POOClases vs ObjetosEl parámetro selfMétodosAtributosMétodo constructor (__init__)Resumen - Calculadora simple4Herencia
Herencia básicaLa función super()Sobrescritura de métodosHerencia múltipleOrden de resolución de métodosResumen - Jerarquía de empleados7Métodos especiales
Introducción a los métodos mágicosSobrecarga de operadoresMétodos mágicos de contenedoresRepaso - Lista personalizada10Patrones de diseño Parte 1
Introducción a los patrones de diseñoPatrón SingletonPatrón FactoryPatrón ObserverPatrón Strategy2Decoradores
Introducción a los decoradoresDecorador propertyDecorador de métodos estáticosDecorador de métodos de clase5Polimorfismo
Sobreescritura de métodos revisitadaDuck TypingClases y métodos abstractosDiseño de interfacesResumen - Calculadora de figuras8Conceptos avanzados de POO
Composición vs. HerenciaMixinsMétodos estáticos y de claseDecoradores de claseGestores de contexto11Patrones de diseño, parte 2
Patrón CommandPatrón AdapterPatrón DecoratorPatrón Template MethodPatrón StatePatrón Composite3Propiedades de clase
Variables de instancia vs de claseDecoradores de propiedadesAtributos privadosResumen - Gestor de cuentas bancarias