Patrón Decorator
Parte de la sección Programación Orientada a Objetos del Journey de C++ de Coddy — lección 98 de 104.
El patrón Decorator permite añadir nuevos comportamientos a los objetos de forma dinámica envolviéndolos en objetos especiales llamados decoradores. A diferencia de la herencia, que añade comportamiento en tiempo de compilación, los decoradores permiten extender la funcionalidad en tiempo de ejecución sin modificar la clase original.
El patrón funciona haciendo que tanto el objeto original como los decoradores implementen la misma interfaz. Cada decorador mantiene una referencia a un componente y añade su propio comportamiento antes o después de delegar en el objeto envuelto:
#include <iostream>
#include <memory>
// Interfaz de componente
class Coffee {
public:
virtual std::string getDescription() const = 0;
virtual double getCost() const = 0;
virtual ~Coffee() = default;
};
// Componente concreto
class SimpleCoffee : public Coffee {
public:
std::string getDescription() const override { return "Coffee"; }
double getCost() const override { return 2.0; }
};
// Decorador base
class CoffeeDecorator : public Coffee {
protected:
std::unique_ptr<Coffee> coffee;
public:
CoffeeDecorator(std::unique_ptr<Coffee> c) : coffee(std::move(c)) {}
};
// Decoradores concretos
class MilkDecorator : public CoffeeDecorator {
public:
MilkDecorator(std::unique_ptr<Coffee> c) : CoffeeDecorator(std::move(c)) {}
std::string getDescription() const override {
return coffee->getDescription() + ", Milk";
}
double getCost() const override { return coffee->getCost() + 0.5; }
};
class SugarDecorator : public CoffeeDecorator {
public:
SugarDecorator(std::unique_ptr<Coffee> c) : CoffeeDecorator(std::move(c)) {}
std::string getDescription() const override {
return coffee->getDescription() + ", Sugar";
}
double getCost() const override { return coffee->getCost() + 0.2; }
};Los decoradores se pueden apilar: cada uno envuelve al anterior, construyendo la funcionalidad capa por capa:
int main() {
std::unique_ptr<Coffee> order = std::make_unique<SimpleCoffee>();
order = std::make_unique<MilkDecorator>(std::move(order));
order = std::make_unique<SugarDecorator>(std::move(order));
std::cout << order->getDescription() << ": $" << order->getCost();
// Salida: Coffee, Milk, Sugar: $2.7
}Utiliza Decorator cuando necesites añadir responsabilidades a los objetos de forma dinámica sin afectar a otros objetos, o cuando extender la funcionalidad mediante la creación de subclases daría lugar a una explosión de clases.
Desafío
FácilVamos a construir un Sistema de Pedido de Pizza utilizando el patrón Decorator. Crearás un sistema flexible donde los clientes pueden comenzar con una pizza base y añadir varios ingredientes; cada ingrediente envuelve el pedido anterior y añade su propio costo y descripción. Este es un caso de uso perfecto para los decoradores: los ingredientes se pueden apilar en cualquier combinación sin crear una explosión de subclases.
Organizarás tu código en tres archivos:
Pizza.h: Define la interfaz del componente y la clase de pizza base.Crea una clase abstracta
Pizzacon métodos virtuales purosgetDescription()(devuelve un string) ygetCost()(devuelve un double), además de un destructor virtual.Luego, crea una clase concreta
PlainPizzaque represente la pizza base. Debe devolver"Pizza"como su descripción y tener un costo base de8.0.ToppingDecorator.h: Construye la clase base del decorador y los decoradores de ingredientes concretos.Crea una clase
ToppingDecoratorque herede dePizzay contenga unstd::unique_ptr<Pizza>al componente envuelto. Esto sirve como base para todos los decoradores de ingredientes.Implementa tres decoradores de ingredientes concretos:
CheeseTopping— añade", Cheese"a la descripción y1.5al costoPepperoniTopping— añade", Pepperoni"a la descripción y2.0al costoMushroomTopping— añade", Mushrooms"a la descripción y1.0al costo
Cada decorador debe delegar en la pizza envuelta y luego añadir su propia contribución.
main.cpp: Crea un pedido de pizza personalizado basado en la entrada del usuario.Lee tres entradas (cada una será
yesono):- ¿Añadir queso?
- ¿Añadir pepperoni?
- ¿Añadir champiñones?
Comienza con una
PlainPizza, luego envuélvela con los decoradores de ingredientes apropiados según las entradas (en el orden: queso, pepperoni, champiñones). Finalmente, imprime los detalles del pedido en este formato:Order: [description] Total: $[cost]
Por ejemplo, con las entradas yes, yes, no:
Order: Pizza, Cheese, Pepperoni
Total: $11.5Con las entradas yes, no, yes:
Order: Pizza, Cheese, Mushrooms
Total: $10.5Con las entradas no, no, no:
Order: Pizza
Total: $8Con las entradas yes, yes, yes:
Order: Pizza, Cheese, Pepperoni, Mushrooms
Total: $12.5Observa cómo cada decorador envuelve al anterior, construyendo tanto la descripción como el costo capa por capa. Puedes añadir fácilmente nuevos ingredientes creando nuevas clases decoradoras sin modificar el código de la pizza o de los ingredientes existentes; esa es la belleza del patrón Decorator.
Hoja de referencia
El patrón Decorator permite añadir nuevos comportamientos a los objetos de forma dinámica envolviéndolos en objetos especiales llamados decoradores. A diferencia de la herencia, que añade comportamiento en tiempo de compilación, los decoradores permiten extender la funcionalidad en tiempo de ejecución sin modificar la clase original.
El patrón funciona haciendo que tanto el objeto original como los decoradores implementen la misma interfaz. Cada decorador mantiene una referencia a un componente y añade su propio comportamiento antes o después de delegar en el objeto envuelto.
Estructura
Interfaz del componente: Define la interfaz común tanto para los componentes concretos como para los decoradores.
class Coffee {
public:
virtual std::string getDescription() const = 0;
virtual double getCost() const = 0;
virtual ~Coffee() = default;
};Componente concreto: El objeto base que puede ser decorado.
class SimpleCoffee : public Coffee {
public:
std::string getDescription() const override { return "Coffee"; }
double getCost() const override { return 2.0; }
};Decorador base: Mantiene una referencia a un componente e implementa la misma interfaz.
class CoffeeDecorator : public Coffee {
protected:
std::unique_ptr<Coffee> coffee;
public:
CoffeeDecorator(std::unique_ptr<Coffee> c) : coffee(std::move(c)) {}
};Decoradores concretos: Añaden comportamientos específicos delegando en el objeto envuelto y añadiendo su propia funcionalidad.
class MilkDecorator : public CoffeeDecorator {
public:
MilkDecorator(std::unique_ptr<Coffee> c) : CoffeeDecorator(std::move(c)) {}
std::string getDescription() const override {
return coffee->getDescription() + ", Milk";
}
double getCost() const override { return coffee->getCost() + 0.5; }
};Apilamiento de decoradores
Los decoradores se pueden apilar: cada uno envuelve al anterior, construyendo la funcionalidad capa por capa:
std::unique_ptr<Coffee> order = std::make_unique<SimpleCoffee>();
order = std::make_unique<MilkDecorator>(std::move(order));
order = std::make_unique<SugarDecorator>(std::move(order));
std::cout << order->getDescription() << ": $" << order->getCost();
// Salida: Coffee, Milk, Sugar: $2.7Cuándo usarlo
Utilice Decorator cuando necesite añadir responsabilidades a los objetos de forma dinámica sin afectar a otros objetos, o cuando la extensión de la funcionalidad mediante la creación de subclases daría lugar a una explosión de clases.
Pruébalo tú mismo
#include <iostream>
#include <string>
#include <memory>
#include "Pizza.h"
#include "ToppingDecorator.h"
using namespace std;
int main() {
// Lee tres entradas (yes o no)
string addCheese, addPepperoni, addMushrooms;
cin >> addCheese;
cin >> addPepperoni;
cin >> addMushrooms;
// TODO: Comienza con una PlainPizza usando std::unique_ptr
// TODO: Si addCheese es "yes", envuelve la pizza con CheeseTopping
// TODO: Si addPepperoni es "yes", envuelve la pizza con PepperoniTopping
// TODO: Si addMushrooms es "yes", envuelve la pizza con MushroomTopping
// TODO: Imprime el pedido en el formato:
// Order: [description]
// Total: $[cost]
return 0;
}
Esta lección incluye un breve cuestionario. Empieza la lección para responderlo y registrar tu progreso.
Todas las lecciones de Programación Orientada a Objetos
1Fundamentos de OOP
Archivos externosConstrucción y compilación en C++Archivos de cabecera y archivos fuenteNamespaces y alcanceIntroducción a OOP en C++Clases vs ObjetosEl puntero 'this'Métodos (Funciones miembro)Atributos (Miembros de datos)Conceptos básicos de Ctors y DtorsResumen - Calculadora simple4Propiedades de clase
Miembros de instancia vs. estáticosGetters y SettersFunciones miembro constPalabra clave mutableMétodos y variables estáticosFunciones y clases amigasResumen - Gestor de cuentas bancarias7Herencia
Herencia básicaNiveles de acceso en la herenciaOrden de llamada de Ctor y DtorSobrescritura de métodosFunciones virtuales y VTableHerencia múltipleHerencia virtualResumen - Jerarquía de empleados2Gestión de memoria
Memoria Stack vs HeapPunteros y referenciasMemoria dinámica (new/delete)Punteros inteligentes en C++RAII en C++Resumen - Gestor de arrays dinámicos5Encapsulamiento
Especificadores de acceso en C++Especificadores de acceso en profundidadOcultamiento de informaciónStruct vs ClassClases anidadas e internasResumen - Sistema de registros de estudiantes8Polimorfismo
Polimorfismo: Compilación vs. Tiempo de ejecuciónSobrecarga de funcionesFunciones virtuales revisadasFunciones virtuales purasClases abstractasDiseño de interfaces en C++Dynamic Casting y RTTIResumen: Calculadora de figuras11Conceptos avanzados de POO
Composición vs. HerenciaMixins mediante CRTPIdioma PimplBorrado de tiposEnum Classes y tipado fuerteManejo de excepciones en POOJerarquías de excepciones personalizadas14Patrones de diseño - Parte 2
Patrón CommandPatrón AdapterPatrón DecoratorPatrón Template MethodPatrón StatePatrón CompositeRAII como patrón3Constructores y Destructores
Constructor por defectoConstructor parametrizadoConstructor de copiaConstructor de movimientoListas de inicialización del constructorConstructores delegadosAnálisis profundo del destructorRegla de tres / cinco / ceroResumen - Clase String6Sobrecarga de operadores
Introducción a la sobrecarga de operadoresSobrecarga de operadores aritméticosSobrecarga de operadores de comparaciónOperadores de flujo (Stream)Sobrecarga del operador de asignaciónSobrecarga de los operadores [] y ()Operadores de conversión de tiposResumen - Clase Matrix9Plantillas
Plantillas de funcionesPlantillas de clasesEspecialización de plantillasPlantillas variádicasConceptos básicos de SFINAE y Type TraitsResumen - Contenedor genérico