patron-decorator

Patrón Decorator: añade comportamiento por composición

  • 5 min

El Decorator es un objeto que envuelve a otro con la misma interfaz para añadirle comportamiento. Podemos apilar varios decoradores y combinar responsabilidades sin crear una subclase para cada combinación.

Imagina que tenemos una clase Notificador que envía emails. Queremos añadir la opción de enviar también por SMS. Lo fácil es crear una subclase NotificadorConSMS. ¿Y si queremos también notificar por Facebook? Creamos NotificadorConFacebook.

Pero, ¿y si queremos SMS y Facebook? ¿Creamos NotificadorConSMSYFacebook? ¿Y si queremos SMS y Slack?

Esto se conoce como Explosión Combinatoria de Clases. Si intentamos cubrir todas las combinaciones posibles usando herencia, acabaremos con cientos de subclases inmanejables. Además, la herencia es estática: no podemos quitarle la funcionalidad de SMS a un objeto en tiempo de ejecución.

El patrón Decorator nos permite añadir responsabilidades a objetos individuales de forma dinámica y transparente, envolviéndolos en objetos “decoradores”.

La Metáfora: Capas de Ropa

Piensa en un decorador como en la ropa.

  • El objeto base eres tú.
  • Si tienes frío, te pones un jersey.
  • Si sigue haciendo frío, añades un abrigo.
  • Si llueve, añades un chubasquero.

La cadena sigue ofreciendo la misma interfaz, pero incorpora nuevas responsabilidades. Para retirar una capa normalmente reconstruimos o reconfiguramos la cadena; el patrón no exige que los decoradores sean mutables.

El Problema: La Cafetería

El ejemplo clásico (y el que mejor se entiende) es el sistema de cobro de una cafetería Starbuzz (guiño, guiño).

Tenemos una bebida base (CafeSolo) que cuesta 1€. Los clientes pueden pedir extras: Leche, Chocolate, Canela, Nata…

Si usamos herencia, tendríamos clases como CafeConLeche, CafeConLecheYChocolate, CafeConNataYCanela… Una locura.

La Solución: Envolver Objetos

En lugar de heredar, usamos Composición.

Definimos una interfaz común para la bebida y los condimentos (IBebida).

Creamos una clase base para los decoradores que también implemente IBebida y que contenga una referencia a otra IBebida.

Los decoradores concretos (Leche, Chocolate) “envuelven” a la bebida original, añaden su precio y delegan el resto.

:::

Implementación en C#

Vamos a montar nuestra cafetería flexible.

El Componente (La Bebida)

public interface IBebida
{
    string GetDescripcion();
    double GetCosto();
}

// Implementación base (El núcleo de la cebolla)
public class CafeSolo : IBebida
{
    public string GetDescripcion() => "Café Solo";
    public double GetCosto() => 1.00;
}

public class CafeDescafeinado : IBebida
{
    public string GetDescripcion() => "Café Descafeinado";
    public double GetCosto() => 1.20;
}
Copied!

El Decorador Base

Esta clase es la clave. Es un híbrido: es una IBebida (para poder ser envuelta a su vez) y tiene una IBebida (la que envuelve).

public abstract class DecoradorBebida : IBebida
{
    // La variable que almacena al objeto que estamos "envolviendo"
    protected IBebida _bebida;

    public DecoradorBebida(IBebida bebida)
    {
        _bebida = bebida;
    }

    // Por defecto, delegamos en la bebida envuelta.
    // Los hijos sobrescribirán esto para añadir su comportamiento.
    public virtual string GetDescripcion() => _bebida.GetDescripcion();
    public virtual double GetCosto() => _bebida.GetCosto();
}
Copied!

Los Decoradores Concretos (Condimentos)

Aquí aplicamos la lógica aditiva. Fíjate que llamamos a base.GetCosto() y le sumamos lo nuestro.

public class Leche : DecoradorBebida
{
    public Leche(IBebida bebida) : base(bebida) { }

    public override string GetDescripcion()
    {
        return _bebida.GetDescripcion() + ", con Leche";
    }

    public override double GetCosto()
    {
        return _bebida.GetCosto() + 0.50; // La leche cuesta 50 céntimos
    }
}

public class Chocolate : DecoradorBebida
{
    public Chocolate(IBebida bebida) : base(bebida) { }

    public override string GetDescripcion()
    {
        return _bebida.GetDescripcion() + ", con Chocolate";
    }

    public override double GetCosto()
    {
        return _bebida.GetCosto() + 0.75;
    }
}
Copied!

Cliente: Montando el Café

Aquí es donde se ve la potencia. Podemos combinar ingredientes como queramos en tiempo de ejecución.

class Program
{
    static void Main(string[] args)
    {
        // 1. Pedimos un café solo
        IBebida miCafe = new CafeSolo();
        Console.WriteLine($"{miCafe.GetDescripcion()} -> {miCafe.GetCosto()}€");

        // 2. El cliente quiere leche
        // Envolvemos el café en un decorador de Leche
        miCafe = new Leche(miCafe);
        Console.WriteLine($"{miCafe.GetDescripcion()} -> {miCafe.GetCosto()}€");

        // 3. El cliente es goloso y quiere chocolate TAMBIÉN
        // Envolvemos el (café + leche) en Chocolate
        miCafe = new Chocolate(miCafe);
        Console.WriteLine($"{miCafe.GetDescripcion()} -> {miCafe.GetCosto()}€");
        
        // Resultado final: Café Solo, con Leche, con Chocolate -> 2.25€
    }
}
Copied!

Fíjate que la variable miCafe siempre es de tipo IBebida. Al cliente le da igual si es un café simple o un monstruo de 5 capas; él solo llama a GetCosto().

Decorator en la vida real (.NET)

Si habéis trabajado con Streams en C#, ya habéis usado este patrón.

// FileStream es el componente base (lee bytes de disco)
Stream stream = new FileStream("archivo.txt", FileMode.Create);

// GZipStream es un DECORADOR (añade compresión)
stream = new GZipStream(stream, CompressionLevel.Optimal);

// BufferedStream es OTRO DECORADOR (añade buffer en memoria)
stream = new BufferedStream(stream);

// Escribimos en el 'stream' final.
// Buffered -> GZip -> File -> Disco
stream.Write(...); 
Copied!

Es exactamente el mismo principio: encadenar funcionalidades.

Ventajas y Desventajas

VentajasDesventajas
Extensibilidad: Añade funcionalidad sin tocar el código existente (OCP).Complejidad: Muchas clases pequeñas. Puede ser difícil entender el flujo si hay muchas capas.
Flexibilidad: Permite combinar comportamientos en tiempo de ejecución, algo imposible con herencia.Identidad: Un decorador no es el objeto original. Si tu código depende de comprobar tipos (if (obj is CafeSolo)), el Decorator lo romperá.
SRP: Cada clase tiene una única responsabilidad (el decorador de Leche solo sabe de leche).