El patrón decorador es un recurso muy utilizado en Angular (@Component, @Injectable…) y sin embargo se explota poco para solucionar problemas. Hoy voy a poner un ejemplo muy práctico que salió recientemente en un proyecto de Ionic en el que participo.

Automatizar analytics con decoradores

El escenario es el siguiente: Imagina que quieres usar Google Analytics (u otro similar) para detectar cuándo entra o sale alguien de cada vista.

Te traigo una solución muy elegante: centralizar la lógica de comunicación con tu API de analytics mediante un decorador que añadirás a tus vistas, para que la información de analítica se envíe automáticamente cada vez que se carga o destruye el componente.

Este decorador está inspirado en un artículo de Netanel Basal y adaptado para funcionar con Angular 5. Desde Angular 4+ podrías prescindir del decorador y utilizar los eventos del router, pero Ionic usa un router diferente, así que para Ionic ésta es la mejor solución.

Servicio de analytics

En primer lugar, voy a abstraer el conocimiento de analytics en un servicio, que llamaré AnalyticsService y que implementará está interfaz tan sencilla:

// services/analytics.service.ts
export  interface  AnalyticsService{
  enter(page:string);
  leave(page:string);
}

Tienes un método al que llamar cuando entras en una página y otro que llamarás al salir de ella. En ambos casos, le pasas un string con el nombre de la página. Internamente, puedes usar Google Analytics, Firebase analytics, o la herramienta que te apetezca, no voy a entrar en esos detalles. Para el artículo, lo importante es que tu decorador va a usar un servicio con esta interfaz.

Decorador de tracking

Ahora que ya tienes el servicio, lo ideal sería llamar a AnalyticsService.enter('some-page') en el método ngOnInit() de cada vista, y hacer lo propio con AnalyticsService.leave('some-page') en el método ngOnDestroy().

El problema es que hacer eso a mano sería muy repetitivo e iría en contra del principio D.R.Y. (Don’t Repeat Yourself), pero tranquilo, aquí es donde entran en juego los decoradores.

Voy a crear un decorador de clase que se encargará justamente de añadir estas llamadas a cada componente que decore. Se usaría así:

// some view component
@Component({
   selector:  'app-first-view',
   /*...*/
})
@PageTrack('first-view')
export  class  FirstViewComponent{
   /*... some stuff ...*/
}

Estructura del decorador de clase (factoría)

En realidad el @PageTrack que acabas de ver no es un decorador, sino una factoría de decoradores. Es una función que recibe un parámetro (el nombre de la página en este caso) y devuelve un decorador personalizado con dicho parámetro.

Para crear esta factoría, necesitas una función que devuelva un objeto ClassDecorator (que no es más que otra función cuyo parámetro representa al constructor). Es más fácil de ver que de leer, así que, aquí tienes:

// decorators/page-track.decorator.ts

export  function  PageTrack(pageName:string):ClassDecorator{
    return  function(constructor:any){}
}

Inyectando dependencias en el decorador

Vale, ya tienes tu factoría de decoradores. Ahora necesitas usar el servicio AnalyticsService en su interior, y para eso tienes que proporcionárselo por inyección de dependencias.

Esto no funciona como los componentes de Angular, donde pasas la DI en el constructor. Aquí tienes que hacerlo de forma manual. Para eso está la clase Injector de Angular.

Fíjate como se usa:

// decorators/page-track.decorator.ts

import { Injector } from  "@angular/core";
import { AnalyticsService } from  "../services/analytics.service";

export  function  PageTrack(pageName:string):ClassDecorator{
    return  function(constructor:any){
        //retrieve analytics service by DI
        const  injector  =  Injector.create([{provide:AnalyticsService, deps:[]}]);
        const  analytics:  AnalyticsService  =  injector.get(AnalyticsService); 
    }
}

Es decir, lo primero que hago es obtener un injector con los providers que necesito para mi(s) servicio(s), y posteriormente, obtengo la instancia del servicio Analytics a través del injector.

Sobreescribiendo métodos de la clase

El último paso sería sobreescribir los métodos ngOnInit() y ngOnDestroy() de la clase que vas a decorar.

Para eso, solo tienes que aprovechar el constructor que recibes de la clase decorada, y modificar su prototype para llamar al servicio analytics. No olvides guardar el método original para llamarlo después de tus modificaciones.

A continuación puedes ver el código completo del decorador.

// decorators/page-track.decorator.ts

import { Injector } from  "@angular/core";
import { AnalyticsService } from  "../services/analytics.service";

export  function  PageTrack(pageName:string):ClassDecorator{
    return  function(constructor:any){
        //retrieve analytics service by DI
        const  injector  =  Injector.create([{provide:AnalyticsService, deps:[]}]);
        const  analytics:  AnalyticsService  =  injector.get(AnalyticsService); 

        //override ngOnInit method
        const  ngOnInit  =  constructor.prototype.ngOnInit;
        constructor.prototype.ngOnInit  =  function ( ...args ){
            analytics.enter(pageName);
            ngOnInit  &&  ngOnInit.apply(this, args);
        }

        //override ngOnDestroy method
        const  ngOnDestroy  =  constructor.prototype.ngOnDestroy;
        constructor.prototype.ngOnDestroy  =  function ( ...args ) {
            analytics.leave(pageName);
            ngOnDestroy  &&  ngOnDestroy.apply(this, args);
        }
    }
}

Fíjate en el detalle de cómo sobreescribo cada método:

  1. Guardo la referencia del método original
  2. Sobreescribo el método del prototype con una firma equivalente (una función que recibe argumentos)
  3. Añado primero la llamada al método correspondiente del servicio analytics
  4. Y finalmente ejecuto el método original con el método apply().

Recuerda que para que funcione, una vez creado el decorador anterior, tienes que usarlo para decorar los componentes de las vistas que quieres analizar. De este modo:

// some view component
@Component({
   selector:  'app-some-view',
   /*...*/
})
@PageTrack('some-view')
export  class  SomeViewComponent{
   /*... some stuff ...*/
}

Reflexiones personales

Con Angular te pasas el día usando el patrón decorador para convertir simples clases en componentes, o en módulos, o en servicios inyectables. Pero… ¿por qué vas a quedarte solo ahí? ¿Por qué no aprovechar esta herramienta tan potente para crear tus propias soluciones?

Espero que con este ejemplo del mundo real hayas visto el potencial que ofrece este patrón y te animes a crear tus propios decoradores en lugar de usar solo los que te proporciona Angular por defecto.

Si te ha gustado este artículo, compártelo 😉