Patrones de Diseño: Decorador

Patrones de Diseño: Decorador

Tabla de Contenido

Objetivo del patrón decorador

Note

El patrón decorador agrega dinamismo a la extensión de funcionalidades.

El objetivo del patrón decorador es crear una clase abstracta, la cual usaremos para crear (a través de herencia) los objetos base. Los decoradores serán clases especiales que extienden las funcionalidades de la clase abstracta, agregando un campo que contendrá una instancia de la clase abstracta, o en su defecto, un hijo de la misma.

Veamos un ejemplo de cómo los decoradores pueden funcionar. Supongamos que estamos creando una aplicación para llevar el control de costos de bebidas, donde el costo de estas bebidas depende de los ingredientes adicionales que le añadamos, como por ejemplo, chocolate rallado, crema, etc.

Ejemplo de uso

Ahora, supongamos que tenemos una clase base café a la cual queremos agregar dos ingredientes nuevos: chocolate y crema chantilly. Gráficamente, se vería algo como

Imagen guia del contenido de la página

De este ejemplo, podemos ver que para calcular el costo total de la bebida debemos sumar el valor de costo de cada uno de los decoradores o de las clases decoradoras. Para ello, la clase base debe tener la capacidad de identificar si tiene un hijo para retornar su valor de costo por defecto y así encadenar la suma.

Antes de pasar al código de implementación, veamos algunas características que debemos tener en cuenta para este patrón.

Características del patrón decorador

  • El decorador debe tener el mismo supertipo que el objeto que están decorando; es decir, ambas clases, la clase decoradora y la que se está decorando, deben heredar de la misma clase.
  • Puedes usar uno o más decoradores para encapsular un objeto, lo que significa que no hay límite en la cantidad de objetos decoradores que se utilicen sobre un objeto.
  • Cuando se le da el mismo supertipo al decorado y a la clase que está decorando, podemos pasar dicho elemento decorado en lugar del objeto original encapsulado.
  • El decorador agrega su propio comportamiento antes o después de que el objeto decorado realice su trabajo.
  • Los objetos pueden ser decorados en cualquier momento, lo que permite que sean decorados de manera dinámica en tiempo de ejecución.

Note

🚀 El patrón decorador adiciona responsabilidades de manera dinámica a un objeto, ofreciendo flexibilidad y una alternativa al uso de subclases.

Implementación del ejemplo en código

Teoría

Para la implementación de nuestro ejemplo, veamos como funcionaría el diagrama de clases

Imagen guia del contenido de la página

Tendremos una clase base (nuestra clase abstracta) que en el diagrama se llama Component. De esta clase creamos dos clases: una es la clase ConcreteComponent, que es la que usaremos para instanciar nuestros objetos, y otra clase llamada Decorator, que es la que usaremos para crear todos los decoradores que podemos usar con nuestro objeto ConcreteComponent.

Decorator es una clase abstracta que será usada para crear nuestros decoradores específicos. Esta clase definirá los métodos y demás lógica que debe ser implementada para que otras clases sean decoradores válidos.

Ejemplo

Aplicando el anterior esquema a un ejemplo, podemos suponer que queremos crear tipos de bebidas que varían su combinación.

Imagen guia del contenido de la página

La implementación del anterior diagrama se haría de la siguiente manera (en Python) empezaremos por la implementación de las clases abstractas

from abc import ABC


class Beverage(ABC):
    def cost(self) -> float:
        pass

    def description(self) -> str:
        pass


class IngredientDecorator(ABC):
    def __init__(self, beverage: Beverage):
        self.beverage = beverage

    def cost(self) -> float:
        pass

    def description(self) -> str:
        pass

Ahora haciendo uso de nuestras clases abstractas, podemos crear nuestros objetos y decoradores concretos, nuestros objetos lucirían de la siguiente forma:

from abc_classes import Beverage


class Coffee(Beverage):
    def cost(self) -> float:
        return 1.99

    def description(self) -> str:
        return "Coffee"


class Decaf(Beverage):
    def cost(self) -> float:
        return 1.25

    def description(self) -> str:
        return "Decaf"


class HouseBlend(Beverage):
    def cost(self) -> float:
        return 0.89

    def description(self) -> str:
        return "House Blend"

y nuestros decoradores

from abc_classes import IngredientDecorator


class Milk(IngredientDecorator):
    def cost(self) -> float:
        return self.beverage.cost() + 0.10

    def description(self) -> str:
        return self.beverage.description() + ", Milk"


class Mocha(IngredientDecorator):
    def cost(self) -> float:
        return self.beverage.cost() + 0.20

    def description(self) -> str:
        return self.beverage.description() + ", Mocha"


class Soy(IngredientDecorator):
    def cost(self) -> float:
        return self.beverage.cost() + 0.15

    def description(self) -> str:
        return self.beverage.description() + ", Soy"


class Chantilly(IngredientDecorator):
    def cost(self) -> float:
        return self.beverage.cost() + 0.25

    def description(self) -> str:
        return self.beverage.description() + ", Chantilly"


class Chocolate(IngredientDecorator):
    def cost(self) -> float:
        return self.beverage.cost() + 0.30

    def description(self) -> str:
        return self.beverage.description() + ", Chocolate"

y su uso dentro de nuestro código sería

from concrete_classes import Coffee, Decaf, HouseBlend
from decorators import Milk, Mocha, Soy, Chantilly, Chocolate

if __name__ == "__main__":
    
    chocolate_blend = Chantilly(Chocolate(HouseBlend()))
    soy_decaf = Soy(Decaf())
    moccachino = Mocha(Chocolate(Milk(Coffee())))
    
    print(chocolate_blend.cost())
    print(chocolate_blend.description())
    
    print(soy_decaf.cost())
    print(soy_decaf.description())
    
    print(moccachino.cost())
    print(moccachino.description())

cuando ejecutemos este código podremos ver que en nuestro resultado obtendremos

1.44
House Blend, Chocolate, Chantilly
1.4
Decaf, Soy
2.59
Coffee, Milk, Chocolate, Mocha

Consideraciones del patrón decorador

  • la herencia es una forma de extensión, pero no necesariamente la mejor forma de alcanzar la flexibilidad de nuestros diseños
  • En nuestros diseños debemos de habilitar la extensión de nuevos comportamientos evitando la modificación del código actual
  • la composición y la delegación pueden ser usados para agregar nuevos comportamientos en tiempo de ejecución
  • El patrón decorador provee una alternativa al uso de subclases para extender el comportamiento
  • El patrón decorador involucra el uso de clases decoradores que son usadas para encapsular componentes del mismo tipo en concreto
  • Las clases decoradoras tienen el mismo tipo de la clase que decoran o encapsulan
  • Los decoradores agregan nuevos comportamientos antes o después de que el objeto encapsulado realiza su trabajo
  • Los decoradores normalmente son transparentes al cliente del componente
  • Los decoradores pueden resultar en muchos objetos pequeños en nuestros diseños y el sobre uso del mismo puede derbar en crear código complejo y difícil de mantener
comments powered by Disqus