Introducción

El patrón de diseño Decorator es un patrón estructural que permite añadir funcionalidades adicionales a un objeto de manera dinámica. Este patrón es útil cuando se desea extender las capacidades de una clase sin modificar su código original, promoviendo así el principio de abierto/cerrado (Open/Closed Principle) de SOLID.

Conceptos Clave

  • Composición sobre herencia: El patrón Decorator favorece la composición en lugar de la herencia para extender funcionalidades.
  • Interfaz común: Tanto el objeto original como los decoradores implementan una interfaz común, lo que permite que los decoradores puedan ser intercambiables.
  • Encapsulación de comportamiento: Los decoradores encapsulan el comportamiento adicional que se desea añadir al objeto original.

Estructura del Patrón Decorator

El patrón Decorator se compone de los siguientes elementos:

  1. Componente (Component): Define la interfaz común para los objetos que pueden tener responsabilidades adicionales.
  2. Componente Concreto (ConcreteComponent): Implementa la interfaz del Componente. Es el objeto al que se le pueden añadir responsabilidades adicionales.
  3. Decorador (Decorator): Mantiene una referencia al Componente y define una interfaz que cumple con la del Componente.
  4. Decorador Concreto (ConcreteDecorator): Extiende el Decorador y añade responsabilidades adicionales al Componente.

Diagrama UML

+------------------+       +------------------+
|   Component      |<------|   Decorator      |
+------------------+       +------------------+
| +operation()     |       | +operation()     |
+------------------+       +------------------+
        ^                          ^
        |                          |
+------------------+       +------------------+
|ConcreteComponent |       |ConcreteDecorator |
+------------------+       +------------------+
| +operation()     |       | +operation()     |
+------------------+       +------------------+

Ejemplo Práctico

Supongamos que estamos desarrollando una aplicación de cafetería donde se pueden pedir diferentes tipos de bebidas y añadirles complementos como leche, azúcar, etc. Utilizaremos el patrón Decorator para añadir estos complementos de manera dinámica.

Paso 1: Definir la Interfaz Componente

public interface Beverage {
    String getDescription();
    double cost();
}

Paso 2: Implementar el Componente Concreto

public class Espresso implements Beverage {
    @Override
    public String getDescription() {
        return "Espresso";
    }

    @Override
    public double cost() {
        return 1.99;
    }
}

Paso 3: Crear la Clase Decorador

public abstract class CondimentDecorator implements Beverage {
    protected Beverage beverage;

    public CondimentDecorator(Beverage beverage) {
        this.beverage = beverage;
    }

    @Override
    public String getDescription() {
        return beverage.getDescription();
    }
}

Paso 4: Implementar los Decoradores Concretos

public class Milk extends CondimentDecorator {
    public Milk(Beverage beverage) {
        super(beverage);
    }

    @Override
    public String getDescription() {
        return beverage.getDescription() + ", Milk";
    }

    @Override
    public double cost() {
        return beverage.cost() + 0.50;
    }
}

public class Sugar extends CondimentDecorator {
    public Sugar(Beverage beverage) {
        super(beverage);
    }

    @Override
    public String getDescription() {
        return beverage.getDescription() + ", Sugar";
    }

    @Override
    public double cost() {
        return beverage.cost() + 0.20;
    }
}

Paso 5: Uso del Patrón Decorator

public class CoffeeShop {
    public static void main(String[] args) {
        Beverage beverage = new Espresso();
        System.out.println(beverage.getDescription() + " $" + beverage.cost());

        beverage = new Milk(beverage);
        System.out.println(beverage.getDescription() + " $" + beverage.cost());

        beverage = new Sugar(beverage);
        System.out.println(beverage.getDescription() + " $" + beverage.cost());
    }
}

Salida Esperada

Espresso $1.99
Espresso, Milk $2.49
Espresso, Milk, Sugar $2.69

Ejercicio Práctico

Ejercicio 1

Implementa un nuevo decorador llamado WhippedCream que añade crema batida a una bebida. El costo adicional de la crema batida es de $0.70.

Solución

public class WhippedCream extends CondimentDecorator {
    public WhippedCream(Beverage beverage) {
        super(beverage);
    }

    @Override
    public String getDescription() {
        return beverage.getDescription() + ", Whipped Cream";
    }

    @Override
    public double cost() {
        return beverage.cost() + 0.70;
    }
}

Ejercicio 2

Crea una nueva bebida llamada Latte que tiene un costo base de $2.50. Luego, utiliza los decoradores Milk, Sugar y WhippedCream para crear un Latte con todos estos complementos y muestra la descripción y el costo total.

Solución

public class Latte implements Beverage {
    @Override
    public String getDescription() {
        return "Latte";
    }

    @Override
    public double cost() {
        return 2.50;
    }
}

public class CoffeeShop {
    public static void main(String[] args) {
        Beverage beverage = new Latte();
        System.out.println(beverage.getDescription() + " $" + beverage.cost());

        beverage = new Milk(beverage);
        System.out.println(beverage.getDescription() + " $" + beverage.cost());

        beverage = new Sugar(beverage);
        System.out.println(beverage.getDescription() + " $" + beverage.cost());

        beverage = new WhippedCream(beverage);
        System.out.println(beverage.getDescription() + " $" + beverage.cost());
    }
}

Salida Esperada

Latte $2.50
Latte, Milk $3.00
Latte, Milk, Sugar $3.20
Latte, Milk, Sugar, Whipped Cream $3.90

Conclusión

El patrón Decorator es una poderosa herramienta para añadir funcionalidades a objetos de manera flexible y dinámica. Al seguir el principio de composición sobre herencia, este patrón permite extender las capacidades de los objetos sin modificar su código original, facilitando así el mantenimiento y la escalabilidad del software.

En el siguiente módulo, exploraremos otros patrones estructurales que también nos ayudarán a organizar y estructurar nuestro código de manera eficiente.

© Copyright 2024. Todos los derechos reservados