Design Patterns - Decorator

Propósito

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.

Problema

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.

Solución

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.

  • Decoración de Objetos: En lugar de usar la herencia, extiende la funcionalidad de los objetos “envolviéndolos” con clases decoradoras.
  • Combinación Flexible de Comportamientos: Permite combinar funcionalidades adicionales, de forma flexible, al “decorar” los objetos con múltiples decoradores.

Estructura

singleton

Participantes

  • Component: Define la interfaz para objetos a los que se le quiere agregar responsabilidades.
  • ConcreteComponent: Objeto que se va a decorar, o sea, agregarle responsabilidades.
  • Decorator: Clase abstracta que envuelve un objeto Component.
  • ConcreteDecorator: Añade funcionalidades al Component.

Cuándo Usarlo

Este patrón es recomendable cuando:

  • queremos agregar responsabilidades a objetos individuales dinámicamente y de manera transparente.
  • la extensión mediante la herencia no es viable o práctica.

Ventajas

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.

Desventajas

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.

Ejemplo: Sistema de cafetería personalizable

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.

Problema

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

Solución planteada

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.

singleton

Código Java

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

Mapeo (del ejemplo a Participantes)

Los participantes que vimos antes son: Component, ConcreteComponent, Decorator, ConcreteDecorator, Client:

  • Coffee (Component): Interfaz común para objetos y decoradores.
  • SimpleCoffee (ConcreteComponent): Implementación del componente concreto a decorar.
  • CoffeeDecorator (Decorator): Clase abstracta que envuelve un objeto Component.
  • WithMilk, WithSugar (ConcreteDecorators): Añade funcionalidades al Component.
  • Client (Client): Usa los decoradores para crear un café con múltiples ingredientes.

Conclusiones

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.

Patrones relacionados

  • Strategy

Un Decorator permite cambiar el exterior de un objeto, un Strategy el interior.

  • Composite

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.

  • Adapter

Un Decorator cambia las responsabilidades de un objeto, no su interfaz, mientras que el Adapter le da a un objeto una interfaz completamente nueva.