El patrón decorador en los frameworks modernos

Problema: ¿Cómo añadir funcionalidad a un objeto sin modificar la clase generadora o extenderla?.

interface ActionInterface 
{
  public function trigger();
}
 

class Accion1 implements ActionInterface 
{
   private function performAction() 
   {
       return 'Accion 1 realizada';
   }
 
  public function trigger() 
  {
      return $this->performAction();
  }
   
}

$miAccion = new Accion1();
$miAccion->trigger();

Dependiendo del contexto podriamos querer ampliar la funcionalidad de $miAccion (enviar un mail, guardar un log, cachearlo ….). Tendriamos varias opciones:

  • Modificar la clase inicial: Esta no es una buena idea, ya que constantemente tendremos que modificar o crear métodos cada vez que queramos añadir  nuevas funionalidades y eso si la clase no pertenece a librerías externas.
  • Extender la clase inicial: Mejoramos, en comparación a nuestra primera opción. De esta forma no modificamos la clase inicial. Pero igualmente tendríamos que implementar diferentes subclases por cada nueva funcionalidad.

El patrón decorador soluciona este problema encapsulando el objeto dentro de otro que le añade la funcionalidad extra (por ejemplo enviarMail):

class NuevaAction implements ActionInterface 
{
 
  private $action;
 
  public function __construct(ActionInterface $unaAccion) {
    $this->action = $unaAccion;
  }
 
  public function enviarMail() {
    // Logica para enviar un mail
  }
 
  public function trigger() {
    $resultadoOriginal = $this->action->trigger(); //trigger de $miAccion
    $this->enviarMail();
    return $resultadoOriginal;
  }
 
}$action

$accion2 = new NuevaAction($miAccion);
$accion2->trigger();

O de una forma más explicita, podemos encadenar las acciones

$accion2 = new NuevaAction(new Accion1());
$accion2->trigger();

La razón por la que el decorador debe implementar la misma interfaz es para asegurarse que los objetos son más o menos intercambiables, como comenté en un post anterior.

Lo intereante es que a partir de un objeto podemos encadenar una serie de acciones sobre él. Imaginemos que tenemos un texto extraido de una pagina web con tags, negritas, enlaces … y por otra parte tenemos una serie de clases que implementan la interface ActionInterface: EliminarNegritas, SangrarTexto, EliminarTags . El trigger de cada una de ellas acepta un texto y devuelve otro texto con la accion de describe el nombre de la clase.

De este modo obtenemos nuestro texto formateado de la forma:

$texto = " .... ";
$acciones = new EliminarNegritas(new SangrarTexto(new  EliminarTags()));
$textoFormateado = $acciones->trigger($texto);

Pudiendo, según el caso, encadenar unas u otras acciones.