Menu
Coddy logo textTech

Padrão Decorator

Parte da seção Programação Orientada a Objetos do Journey de C++ da Coddy — lição 98 de 104.

O padrão Decorator permite adicionar novos comportamentos a objetos dinamicamente, envolvendo-os em objetos especiais chamados decoradores. Ao contrário da herança, que adiciona comportamento em tempo de compilação, os decoradores permitem estender a funcionalidade em tempo de execução sem modificar a classe original.

O padrão funciona fazendo com que tanto o objeto original quanto os decoradores implementem a mesma interface. Cada decorador mantém uma referência a um componente e adiciona seu próprio comportamento antes ou depois de delegar para o objeto envolvido:

#include <iostream>
#include <memory>

// Interface do 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; }
};

Decoradores podem ser empilhados - cada um envolve o anterior, construindo a funcionalidade camada por camada:

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();
    // Saída: Coffee, Milk, Sugar: $2.7
}

Use o Decorator quando você precisar adicionar responsabilidades a objetos dinamicamente sem afetar outros objetos, ou quando estender a funcionalidade através de subclasses resultaria em uma explosão de classes.

challenge icon

Desafio

Fácil

Vamos construir um Sistema de Pedidos de Pizza usando o padrão Decorator. Você criará um sistema flexível onde os clientes podem começar com uma pizza base e adicionar várias coberturas — cada cobertura envolve o pedido anterior e adiciona seu próprio custo e descrição. Este é um caso de uso perfeito para decorators: as coberturas podem ser empilhadas em qualquer combinação sem criar uma explosão de subclasses.

Você organizará seu código em três arquivos:

  • Pizza.h: Define a interface do componente e a classe base da pizza.

    Crie uma classe abstrata Pizza com métodos virtuais puros getDescription() (retorna uma string) e getCost() (retorna um double), além de um destrutor virtual.

    Em seguida, crie uma classe concreta PlainPizza que representa a pizza base. Ela deve retornar "Pizza" como sua descrição e ter um custo base de 8.0.

  • ToppingDecorator.h: Constrói a classe base do decorator e os decorators de cobertura concretos.

    Crie uma classe ToppingDecorator que herda de Pizza e contém um std::unique_ptr<Pizza> para o componente envolvido. Isso serve como base para todos os decorators de cobertura.

    Implemente três decorators de cobertura concretos:

    • CheeseTopping — adiciona ", Cheese" à descrição e 1.5 ao custo
    • PepperoniTopping — adiciona ", Pepperoni" à descrição e 2.0 ao custo
    • MushroomTopping — adiciona ", Mushrooms" à descrição e 1.0 ao custo

    Cada decorator deve delegar para a pizza envolvida e então adicionar sua própria contribuição.

  • main.cpp: Cria um pedido de pizza personalizado com base na entrada do usuário.

    Leia três entradas (cada uma será yes ou no):

    1. Adicionar queijo?
    2. Adicionar pepperoni?
    3. Adicionar cogumelos?

    Comece com uma PlainPizza, então envolva-a com os decorators de cobertura apropriados com base nas entradas (na ordem: queijo, pepperoni, cogumelos). Finalmente, imprima os detalhes do pedido neste formato:

    Order: [description]
    Total: $[cost]

Por exemplo, com as entradas yes, yes, no:

Order: Pizza, Cheese, Pepperoni
Total: $11.5

Com as entradas yes, no, yes:

Order: Pizza, Cheese, Mushrooms
Total: $10.5

Com as entradas no, no, no:

Order: Pizza
Total: $8

Com as entradas yes, yes, yes:

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

Observe como cada decorator envolve o anterior, construindo tanto a descrição quanto o custo camada por camada. Você pode facilmente adicionar novas coberturas criando novas classes decorator sem modificar o código da pizza ou das coberturas existentes — essa é a beleza do padrão Decorator.

Folha de consulta

O padrão Decorator permite adicionar novos comportamentos a objetos dinamicamente, envolvendo-os em objetos especiais chamados decoradores. Ao contrário da herança, que adiciona comportamento em tempo de compilação, os decoradores permitem estender a funcionalidade em tempo de execução sem modificar a classe original.

O padrão funciona fazendo com que tanto o objeto original quanto os decoradores implementem a mesma interface. Cada decorador mantém uma referência a um componente e adiciona seu próprio comportamento antes ou depois de delegar ao objeto envolvido.

Estrutura

Interface do Componente: Define a interface comum tanto para componentes concretos quanto para decoradores.

class Coffee {
public:
    virtual std::string getDescription() const = 0;
    virtual double getCost() const = 0;
    virtual ~Coffee() = default;
};

Componente concreto: O objeto base que pode ser decorado.

class SimpleCoffee : public Coffee {
public:
    std::string getDescription() const override { return "Coffee"; }
    double getCost() const override { return 2.0; }
};

Decorador base: Mantém uma referência a um componente e implementa a mesma interface.

class CoffeeDecorator : public Coffee {
protected:
    std::unique_ptr<Coffee> coffee;
public:
    CoffeeDecorator(std::unique_ptr<Coffee> c) : coffee(std::move(c)) {}
};

Decoradores concretos: Adicionam comportamentos específicos delegando ao objeto envolvido e adicionando sua própria funcionalidade.

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; }
};

Empilhando Decoradores

Os decoradores podem ser empilhados - cada um envolve o anterior, construindo a funcionalidade camada por camada:

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();
// Saída: Coffee, Milk, Sugar: $2.7

Quando Usar

Use o Decorator quando precisar adicionar responsabilidades a objetos dinamicamente sem afetar outros objetos, ou quando a extensão da funcionalidade por meio de subclasses resultaria em uma explosão de classes.

Experimente você mesmo

#include <iostream>
#include <string>
#include <memory>
#include "Pizza.h"
#include "ToppingDecorator.h"

using namespace std;

int main() {
    // Lê três entradas (yes ou no)
    string addCheese, addPepperoni, addMushrooms;
    cin >> addCheese;
    cin >> addPepperoni;
    cin >> addMushrooms;
    
    // TODO: Comece com uma PlainPizza usando std::unique_ptr
    
    // TODO: Se addCheese for "yes", envolva a pizza com CheeseTopping
    
    // TODO: Se addPepperoni for "yes", envolva a pizza com PepperoniTopping
    
    // TODO: Se addMushrooms for "yes", envolva a pizza com MushroomTopping
    
    // TODO: Imprima o pedido no formato:
    // Order: [description]
    // Total: $[cost]
    
    return 0;
}
quiz iconTeste seus conhecimentos

Esta lição inclui um quiz rápido. Comece a lição para respondê-lo e acompanhar seu progresso.

Todas as lições de Programação Orientada a Objetos