El patrón Decorator permite agregar responsabilidades adicionales a un objeto de manera dinámica, en tiempo de ejecución, sin alterar la estructura de clases existentes.
Es especialmente útil para extender funcionalidades de manera flexible y reutilizable.
La necesidad de extender la funcionalidad de una clase de manera dinámica, sin recurrir a la herencia, que genera una jerarquía de clases rígida y menos flexible.
La solución que propone el Decorator es envolver el objeto original en un objeto “decorador”, que agregue la funcionalidad deseada. Esto permite la composición de comportamientos en lugar de heredarlos.
Este patrón es recomendable cuando:
Mayor Flexibilidad: Permite agregar o remover responsabilidades de manera dinámica.
Evita Clases con Muchas Responsabilidades: Reduce la necesidad de clases que están sobrecargadas con funcionalidades que no siempre son necesarias.
Combinación y Reutilización: Facilita la combinación y reutilización de comportamientos.
Complejidad Aumentada: Introduce complejidad al agregar múltiples capas pequeñas.
Dificultad en la Configuración: Puede ser difícil configurar un sistema con múltiples capas de decoradores.
Problemas de Identificación: Puede ser difícil identificar el objeto base cuando hay muchos decoradores.
Estamos desarrollando un sistema para una cadena de cafeterías que necesita ofrecer una amplia gama de cafés personalizables.
Los clientes pueden elegir diferentes tipos de café y agregarles una variedad de ingredientes, como leche, azúcar, crema, saborizantes y más.
El desafío está en el cálculo del costo de estas bebidas personalizadas.
Una solución podría ser crear una clase separada para cada posible combinación de café e ingredientes, pero esto generaría un número muy alto de clases, lo que hace que el sistema sea difícil de mantener y ampliar.
Además, queremos evitar alterar las clases existentes cada vez que necesitemos agregar un nuevo ingrediente o tipo de café.
Vamos a usar el patrón Decorator para abordar este problema, ya que nos permite agregar dinámicamente responsabilidades adicionales (ingredientes) a los objetos (cafés) en tiempo de ejecución.
Con un decorador podemos comenzar con un café simple y luego “decorarlo” con varios ingredientes, cada uno agregará su costo y descripción al café base.
Esto nos da flexibilidad para crear una variedad casi ilimitada de combinaciones de café sin necesidad de crear clases adicionales para cada variante.
Definimos la interfaz del objeto que queremos extender su funcionalidad Coffee.
La clase SimpleCoffee representa un café sin adornos.
Creamos la clase abstracta CoffeeDecorator, que nos va a permitir decorar Coffee.
Creamos los decoradores concretos, WithMilk y WithSugar, que agregan características y costos adicionales.
El cliente interactúa con el objeto Coffee, le va agregando fácilmente los ingredientes.
Codificamos en Java lo que preparamos en el diagrama.
Definimos la Interfaz Café (Component):
interface Coffee {
double getCost();
String getDescription();
}
Implementamos las clases Café Simple, el componente básico:
class SimpleCoffee implements Coffee {
public double getCost() {
return 2.0;
}
public String getDescription() {
return "Café simple";
}
}
Creamos la clase abstracta para decorar café:
abstract class CoffeeDecorator implements Coffee {
protected Coffee decoratedCoffee;
public CoffeeDecorator(Coffee coffee) {
this.decoratedCoffee = coffee;
}
public double getCost() {
return decoratedCoffee.getCost();
}
public String getDescription() {
return decoratedCoffee.getDescription();
}
}
Implementamos un decorador concreto, el que agrega leche al café:
class WithMilk extends CoffeeDecorator {
public WithMilk(Coffee coffee) {
super(coffee);
}
public double getCost() {
return super.getCost() + 0.5;
}
public String getDescription() {
return super.getDescription() + ", con leche";
}
}
Implementamos otro decorador concreto, el que agrega azúcar al café:
class WithSugar extends CoffeeDecorator {
public WithSugar(Coffee coffee) {
super(coffee);
}
public double getCost() {
return super.getCost() + 0.2;
}
public String getDescription() {
return super.getDescription() + ", con azúcar";
}
}
El cliente crea un café básico y le agrega leche y azúcar:
public class Client {
public static void main(String[] args) {
Coffee myCoffee = new SimpleCoffee();
myCoffee = new WithMilk(myCoffee);
myCoffee = new WithSugar(myCoffee);
System.out.println("Costo: " + myCoffee.getCost());
System.out.println("Descripción: " + myCoffee.getDescription());
// Resultado de la ejecución:
// Costo: 2.7
// Descripción: Café simple, con leche, con azúcar
}
}
Los participantes que vimos antes son: Component, ConcreteComponent, Decorator, ConcreteDecorator, Client:
El patrón Decorator da una solución flexible y escalable al problema de la personalización del café. Podemos extender el comportamiento de manera dinámica y transparente, ideal para un sistema en el que las necesidades del cliente pueden variar.
Este enfoque además facilita la adición de nuevos ingredientes en el futuro, sin modificar el código existente.
Un Decorator permite cambiar el exterior de un objeto, un Strategy el interior.
Podemos ver a un Decorator como un composite que solo tiene un componente. Sin embargo, un Decorator no está pensado para agregación de objetos, sino para agregar responsabilidades.
Un Decorator cambia las responsabilidades de un objeto, no su interfaz, mientras que el Adapter le da a un objeto una interfaz completamente nueva.