Menu
Coddy logo textTech

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.

challenge icon

Desafío

Fácil

Vamos 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 Pizza con métodos virtuales puros getDescription() (devuelve un string) y getCost() (devuelve un double), además de un destructor virtual.

    Luego, crea una clase concreta PlainPizza que represente la pizza base. Debe devolver "Pizza" como su descripción y tener un costo base de 8.0.

  • ToppingDecorator.h: Construye la clase base del decorador y los decoradores de ingredientes concretos.

    Crea una clase ToppingDecorator que herede de Pizza y contenga un std::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 y 1.5 al costo
    • PepperoniTopping — añade ", Pepperoni" a la descripción y 2.0 al costo
    • MushroomTopping — añade ", Mushrooms" a la descripción y 1.0 al 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á yes o no):

    1. ¿Añadir queso?
    2. ¿Añadir pepperoni?
    3. ¿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.5

Con las entradas yes, no, yes:

Order: Pizza, Cheese, Mushrooms
Total: $10.5

Con las entradas no, no, no:

Order: Pizza
Total: $8

Con las entradas yes, yes, yes:

Order: Pizza, Cheese, Pepperoni, Mushrooms
Total: $12.5

Observa 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.7

Cuá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;
}
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 Programación Orientada a Objetos