El framework de Hari Krishnan: lo bueno hecho mejor (Parte 4)

Como tercera entrada de la serie de Frameworks para la gestión de Triggers, no podía faltar la propuesta de Hari Krishnan. Este framework no es revolucionario, sinó que cómo explica el mismo autor, fue la culminación de evolucionar las ideas de Tony Scott, Dan Appleman, y mejorarlas.

De los 3 frameworks analizados en esta serie, este es el que ofrece una orientación a objetos y uso de los nuevos recursos del lenguaje Apex más novedosas, y amplía las ideas de los 2 frameworks anteriores para la implementación de buenas prácticas.

Si participas o prevés estar en proyectos de cierto tamaño, conocer este framework es vital, y sobretodo entender su implementación (si tienes poca experiencia como programador, no desistas, dedicando algo de tiempo, no tendrás problema).

Mi intención nuevamente es explicarlo de forma sencilla, a ver si lo consigo.

Evolución no Revolución

Partiendo de los 2 framweworks anteriores, el autor se centró en los siguientes puntos:

  1. Proporcionar una implementación, que afecte a partes muy delimitadas del código por cambios en la lógica de negocio
  2. Profundizar en los conceptos de orientación a objetos y separación de responsabilidades, utilizando las capacidades que han ido apareciendo en el lenguaje Apex (en especial la instanciación de clases dinámica – lo veremos más adelante)
  3. Utilizar el concepto de diseño mediante interfaces

Esqueleto completo del Framework

A continuación, muestro un diagrama para facilitar su entendimiento:

Se puede observar que he incluido un esquema básico de clases, para que se pueda valorar el cambio que no se aprecia a nivel conceptual.

Este framework, cumple las siguientes características:

  1. Solo debe existir un único trigger por objeto (“One trigger to rule them all“, más claro imposible). Esta idea, ya vimos en el patrón de Tony Scott y es una buena práctica indiscutible de la plataforma, y se realiza mediante el uso de una clase Factory (lo veremos en detalle)
  2. Todos los triggers utilizan su propio Dispatcher. Además estos Dispatchers, son creados dinámicamente y especializados por evento
  3. El Dispatcher realiza 2 funciones principales:
    • Enruta las peticiones hacía el Handler del objeto + evento adecuado
    • Controla y gestiona las re-entradas (recursividad) de todos los objetos
  4. Se definen Handlers a nivel de Objeto+Evento, y esto es una gran diferencia con lo que habíamos visto (quizás intuido) hasta ahora
  5. La lógica de negocio se implementa en los Handler, pero además se alienta a proporcionar clases Helper para posibilitar la re-utilización

Veamos cada una de las partes y su código.

Invocación del Trigger a la Trigger Factory

Siguiendo la misma buena práctica que los frameworks anteriores, el trigger invoca con una sola línea de código a una clase a partir de la cual desencadenará todo el flujo.
En este framework esta clase es la TriggerFactory.


trigger AccountTrigger on Account (after insert, 
                                   after update, 
                                   before insert, 
                                   before update) {

	TriggerFactory.createTriggerDispatcher(Account.sObjectType);
}

Podemos ver que la invocación a la TriggerFactory no es a su constructor sinó que se invoca un método estático donde se realiza la creación de la que será la clase Dispatcher.

La TriggerFactory y el Dispatcher

La clase TriggerFactory es una clase muy simple con 2 funciones principales:

  1. Crear una instancia de la clase Dispatcher para objeto sobre el que se ha generado el trigger
  2. En función del evento (before/insert, delete/insert/update) del trigger enrutar hacia el método del Dispatcher del objeto adecuada

Veamos el código comentado:

/*
* Clase TriggerFactory, con 2 funciones principales:
* 1. Creación del Dispatcher del objeto
* 2. Invocar el método adecuado del Dispatcher en 
* función del evento recibido
*/
public with sharing class TriggerFactory
{
    /* 
	* Este es el método de entrada a la TriggerFactory
    * Invoca la creación del Dispatcher para el objeto 
    * y en caso positivo, ejecuta el enrutador
	*/
    public static void createTriggerDispatcher(Schema.sObjectType soType)
    {
        ITriggerDispatcher dispatcher = getTriggerDispatcher(soType);
        if (dispatcher == null)
            throw new TriggerException('No Trigger dispatcher registered for Object Type: ' + soType);
        execute(dispatcher);
    }

    /* 
	* Este método enruta hacia el método adecuado del Dispatcher 
    * Toda la información del trigger (maps, lists y booleans) se almacena en una
    * nueva instancia de clase que los métodos enrutados reciben. 
    * 
    * Al inicio de cada bloque (before triggers y after triggers) se invocan
    * métodos donde podremos llevar a cabo funciones de carácter transversal
    * (bulkBefore() y bulkAfter()) 
    *
    * Finalmente, tenemos un método de ejecución final (andFinally) donde
    * realizar funciones de housekeeping, logging, etc.
	*/ 
    private static void execute(ITriggerDispatcher dispatcher)
    {
    	TriggerParameters tp = new TriggerParameters(Trigger.old, Trigger.new, Trigger.oldMap, Trigger.newMap,
									Trigger.isBefore, Trigger.isAfter, Trigger.isDelete, 
									Trigger.isInsert, Trigger.isUpdate, Trigger.isUnDelete, Trigger.isExecuting);
        
        if (Trigger.isBefore) {

            //Tratamiento del bulk y ejecución de funciones transversales 
            dispatcher.bulkBefore();
            
            if (Trigger.isDelete)
                dispatcher.beforeDelete(tp);
            else if (Trigger.isInsert)
                dispatcher.beforeInsert(tp);
            else if (Trigger.isUpdate)
                dispatcher.beforeUpdate(tp);         
        }
        else	// After trigger events
        {
            //Tratamiento del bulk y ejecución de funciones transversales
            dispatcher.bulkAfter();
            
            if (Trigger.isDelete)
                dispatcher.afterDelete(tp);
            else if (Trigger.isInsert)
                dispatcher.afterInsert(tp);
            else if (Trigger.isUpdate)
                dispatcher.afterUpdate(tp);
        }

        //Función de Housekeeping
        dispatcher.andFinally();
    } 

    /*
    * Este método es de los más interesantes, y donde el autor introduce novedades:
    * 1. Utiliza una convención de nombres para invocar las clases Dispatcher 
    * 2. Con esta convención y con las capacidades de Apex de instanciar clases
    * mediante el nombre, se crean los Dispatcher y se desacopla por completo 
    * la TriggerFactory del Dispatcher 
	*/
    private static ITriggerDispatcher getTriggerDispatcher(Schema.sObjectType soType)
    {
    	String originalTypeName = soType.getDescribe().getName();
    	String dispatcherTypeName = null;
    	if (originalTypeName.toLowerCase().endsWith('__c')) {
    		Integer index = originalTypeName.toLowerCase().indexOf('__c');
    		dispatcherTypeName = originalTypeName.substring(0, index) + 'TriggerDispatcher';
    	}
    	else
    		dispatcherTypeName = originalTypeName + 'TriggerDispatcher';

        //Instanciación mediante el nombre de la clase, que se obtiene en tiempo de ejecución
        //Observar la creación de tipo y la creación de la instancia con el cast 
		Type obType = Type.forName(dispatcherTypeName);
		ITriggerDispatcher dispatcher = (obType == null) ? null : (ITriggerDispatcher)obType.newInstance();
    	return dispatcher;
    }
}

Por tanto, hemos visto como la Trigger Factory invoca el Dispatcher con la instanciación en runtime.

Pero esto implica que el Dispatcher debe cumplir ciertas características, que veremos de inmediato.

Jerarquía de Clases del Dispatcher

Para la elaboración del Dispatcher, Hari se basó en las siguientes puntos:

  1. Todos los Dispatcher siguen una convención de nombres, que consiste en <nombreObjeto)TriggerDispatcher, por ejemplo: AccountTriggerDispatcher, miCustomObjectTriggerDispatcher (es decir sin el sufijo __c para los objetos custom)
  2. Usar una interface que indica todos los métodos que se esperarán de cualquier Dispatcher. Esta interface se denomina ITriggerDispatcher
  3. Una clase base (TriggerDispatcherBase) que implementa los métodos indicados en la interface, y que puede proporcionar código base para cada método, y en la que se lleva a cabo la gestión de las re-entradas.

Veamos el código comentado de las diferentes partes que conforman el concepto de Dispatcher.

La interface ITriggerDispatcher


/**
* Partiendo de los frameworks vistos anteriormente, esta interface
* no tiene mucho misterio, explicita los métodos habituales para
* el enrutado de la ejecución en función del evento y además
* incluye 2 métodos para el tratamiento bulk y el método
* para la ejecución de DML únicas, housekeeping, logging, etc.
*/
public interface ITriggerDispatcher {
void bulkBefore();
void bulkAfter();
void andFinally();
void beforeInsert (TriggerParameters tp);
void beforeUpdate (TriggerParameters tp);
void beforeDelete (TriggerParameters tp);
void afterInsert  (TriggerParameters tp);
void afterUpdate  (TriggerParameters tp);
void afterDelete  (TriggerParameters tp);
void afterUnDelete(TriggerParameters tp);
}

He eliminado todos los comentarios que Hari, incluye en su código (sorry!) para simplificar la entrada.

La clase base TriggerDispatcherBase

Para construir el Dispatcher de un objeto, el autor ya proporciona una clase base con la implementación de los métodos de la interface (TriggerDispatcherBase). Por tanto, los desarrolladores se centran en extender esta clase.

Esto es en en mi opinión muy buena idea, ¿por qué? pues porque al proporcionar la clase TriggerDispatcherBase, el desarrollador solo implementará en su Dispatcher aquellos métodos y clases Handler de los eventos , a medida que los requerimientos se vayan ampliando.

Esto supone:

  • Permite inyectar código en la clase base, que todas los Dispatchers obtendrán. De hecho la gestión de las re-entradas se gestiona con este enfoque, en el método execute
  • Solo implementar los métodos para los eventos que necesitemos
  • Minimizar así, las colisiones de código entre desarrolladores
  • Simplifica los despliegues, ya que solo estamos modificando una clase

Veamos pues el código de TriggerDispatcherBase con el mínimo código:

/**
* Clase base del Dispatcher, implementa la interface de Dispatcher,
* facilitando así la herencia, y permitiendo la inyección de código.
* Ver que todos los métodos de gestión de eventos, son virtual,
* con lo que los Dispatchers iran implementando los métodos en 
* función de los requerimientos.
*
* La gestión de las re-entradas se lleva a cabo en el método 
* execute, que es un método protected, lo que permite modificar 
* su implementación en las clases hijas.
*
*/
public virtual class TriggerDispatcherBase implements ITriggerDispatcher { 
   
   private static ITriggerHandler beforeInserthandler;
   private static ITriggerHandler beforeUpdatehandler;
   private static ITriggerHandler beforeDeleteHandler;
   private static ITriggerHandler afterInserthandler;
   private static ITriggerHandler afterUpdatehandler;
   private static ITriggerHandler afterDeleteHandler;
   private static ITriggerHandler afterUndeleteHandler;

    public virtual void bulkBefore() {}
    public virtual void bulkAfter() {}
    public virtual void beforeInsert(TriggerParameters tp) {}
    public virtual void beforeUpdate(TriggerParameters tp) {}
    public virtual void beforeDelete(TriggerParameters tp) {}
    public virtual void afterInsert(TriggerParameters tp) {} 
    public virtual void afterUpdate(TriggerParameters tp) {}
    public virtual void afterDelete(TriggerParameters tp) {}
    public virtual void afterUnDelete(TriggerParameters tp) {}
    public virtual void andFinally() {}
   
      protected void execute(ITriggerHandler handlerInstance, TriggerParameters tp, TriggerParameters.TriggerEvent tEvent) {
       if(handlerInstance != null) {
          if(tEvent == TriggerParameters.TriggerEvent.beforeInsert)
             beforeInsertHandler = handlerInstance;
          if(tEvent == TriggerParameters.TriggerEvent.beforeUpdate)
             beforeUpdateHandler = handlerInstance;
          if(tEvent == TriggerParameters.TriggerEvent.beforeDelete)
             beforeDeleteHandler = handlerInstance;
          if(tEvent == TriggerParameters.TriggerEvent.afterInsert)
             afterInsertHandler = handlerInstance;
          if(tEvent == TriggerParameters.TriggerEvent.afterUpdate)
             afterUpdateHandler = handlerInstance;
          if(tEvent == TriggerParameters.TriggerEvent.afterDelete)
             afterDeleteHandler = handlerInstance;
          if(tEvent == TriggerParameters.TriggerEvent.afterUnDelete)
             afterUndeleteHandler = handlerInstance;
          handlerInstance.mainEntry(tp);
          handlerInstance.updateObjects();
       }
       else {
          if(tEvent == TriggerParameters.TriggerEvent.beforeInsert)
             beforeInsertHandler.inProgressEntry(tp);
          if(tEvent == TriggerParameters.TriggerEvent.beforeUpdate)
             beforeUpdateHandler.inProgressEntry(tp);
          if(tEvent == TriggerParameters.TriggerEvent.beforeDelete)
             beforeDeleteHandler.inProgressEntry(tp);
          if(tEvent == TriggerParameters.TriggerEvent.afterInsert)
             afterInsertHandler.inProgressEntry(tp);
          if(tEvent == TriggerParameters.TriggerEvent.afterUpdate)
             afterUpdateHandler.inProgressEntry(tp);
          if(tEvent == TriggerParameters.TriggerEvent.afterDelete)
             afterDeleteHandler.inProgressEntry(tp);
          if(tEvent == TriggerParameters.TriggerEvent.afterUnDelete)
             afterUndeleteHandler.inProgressEntry(tp);
       }
    }
}

Como podemos ver en el código el autor, define una variable estática para cada evento, que contiene el valor de la instanciación de la clase. De esta manera se redirije hacia el método mainEntry cuando es la primera vez que se invoca el evento en el contexto en curso, y si hay re-entrada se enruta hacia inProgressEntry.

Es una manera elegante y limpia de ejecutar la re-entrada, proporcionar una clase base ampliable y por consiguiente estructurar el código eficientemente.

Por tanto ya únicamente nos queda mostrar un ejemplo de implementación de un Dispatcher de un objeto concreto:


/*
* En este ejemplo se implementan 4 eventos.
* Cada función de Dispatcher gestiona la re-entrada de la misma forma:
* La varible estática contiene true si se estava ejecutando ese 
* Handler, y false si es la primera vez en este contexto
*
* Cada evento invoca a su Handler específico, dedicado a ese evento
* 
*/
public class AccountTriggerDispatcher extends TriggerDispatcherBase {
   private static Boolean isBeforeInsertProcessing = false;
   private static Boolean isBeforeUpdateProcessing = false;
   private static Boolean isAfterInsertProcessing = false;
   private static Boolean isAfterUpdateProcessing = false; 
   
   public virtual override void beforeInsert(TriggerParameters tp) {
      if(!isBeforeInsertProcessing) {
         isBeforeInsertProcessing = true;
         execute(new AccountBeforeInsertTriggerHandler(), tp, TriggerParameters.TriggerEvent.beforeInsert);
         isBeforeInsertProcessing = false;
      }
      else execute(null, tp, TriggerParameters.TriggerEvent.beforeInsert);
   }
   
   public virtual override void beforeUpdate(TriggerParameters tp) {
      if(!isBeforeUpdateProcessing) {
         isBeforeUpdateProcessing = true;
         execute(new AccountBeforeUpdateTriggerHandler(), tp, TriggerParameters.TriggerEvent.beforeUpdate);
         isBeforeUpdateProcessing = false;
      }
      else execute(null, tp, TriggerParameters.TriggerEvent.beforeUpdate);
   }

   public virtual override void afterInsert(TriggerParameters tp) {
      if(!isAfterInsertProcessing) {
         isAfterInsertProcessing = true;
         execute(new AccountAfterInsertTriggerHandler(), tp, TriggerParameters.TriggerEvent.afterInsert);
         isAfterInsertProcessing = false;
      }
      else execute(null, tp, TriggerParameters.TriggerEvent.afterInsert);
   }

   public virtual override void afterUpdate(TriggerParameters tp) {
      if(!isAfterUpdateProcessing) {
         isAfterUpdateProcessing = true;
         execute(new AccountAfterUpdateTriggerHandler(), tp, TriggerParameters.TriggerEvent.afterUpdate);
         isAfterUpdateProcessing = false;
      }
      else execute(null, tp, TriggerParameters.TriggerEvent.afterUpdate);
   }
}

Veamos ahora la estructura como es la implementación en detalle para los Handler de Eventos.

Jerarquía de clases para el Handler

Habiendo visto la estructura para los Dispatcher, tenemos medio camino andado para los Handler, dado que siguen el mismo patrón:

La interface definida ITriggerHandler implementa 3 métodos, los 2 destinados a la re-entrada y la actualización de objetos.

/**
* Interface para la definición del Handler
*/
public interface ITriggerHandler {
   
   void mainEntry(TriggerParameters tp);
   void inProgressEntry(TriggerParameters tp);
   
   void updateObjects();
}

La implementación de la clase base del Handler (TriggerHandlerBase) es muy sencilla, proporciona el código únicamente para la actualización de los objetos para realizar una única DML sobre el objeto.

/**
* Clase base para Trigger Handler
*/
public abstract class TriggerHandlerBase implements ITriggerHandler {
   
   protected Map sObjectsToUpdate = new Map();
      
   public abstract void mainEntry(TriggerParameters tp);
   
   public virtual void inProgressEntry(TriggerParameters tp) {
      
   }
   
   public virtual void updateObjects() {
      if(sObjectsToUpdate.size() > 0)
         update sObjectsToUpdate.values();
   }
}

Finalmente tenemos la implementación de ejemplo de un Handler específico, en este caso vemos AccountAfterUpdateTriggerHandler:

 

public class AccountAfterUpdateTriggerHandler 
                             extends TriggerHandlerBase {
	
   public override void mainEntry(TriggerParameters tp) {
      process((List)tp.newList);
   }
   
   private void process(List listNewAccounts) {
      for(Account acct : listNewAccounts) {
         Account newAccount = new Account();
         newAccount.Id = acct.Id;
         newAccount.Website = 'www.salesforce.com';
         sObjectsToUpdate.put(newAccount.Id, newAccount);
      }
   }
   
   public override void inProgressEntry(TriggerParameters tp) {
      System.debug('This is an example for reentrant code...');
   }
   
   public override void updateObjects() {
      // for demonstration purposes, don't do anything here...
   }
}

Y con esto y un bizcocho…

¿Consigue los objetivos?

Veamos si esta propuesta consigue los objetivos que nos habíamos propuesto:

  1. Crear un único trigger por objeto y evitar duplicidades: si, tenemos centralizada la ejecución del trigger con la invocación a la clase factory
  2. Crear el trigger sin código, externalizando todo su código en clases externas: esta es la  implementación que más elegantemente realiza la externalización de la clases
  3. Controlar el orden de ejecución: tenemos un Dispatcher que gestiona el orden de ejecución de los bloques funcionales y por tanto, el flujo está claramente bajo nuestro control
  4. Detección la recursividad / Control de re-entradas: cada uno de los Handlers, está obligado a implementar métodos para la re-entrada, y el Dispatcher de cada objeto implementa la re-entrada
  5. Activar/Desactivar un trigger en caliente en cualquier entorno: no propone nada al respecto, pero es muy fácil introducirlo
  6. Crear una estructura de código que facilite un entorno multi-programador: para mi esta es la implementación más orientada a objetos, la que mejor utilización realiza de Apex (sin usar aún el switch 😉 ) y la que permite a un equipo de desarrolladores minimizar las colisiones a medida que avanza el proyecto.

Conclusiones

Después del análisis de los 3 frameworks, en mi opinión este debe ser el framework de gestión  de triggers del cual deberías partir para la implementación en un proyecto que pueda llegar a ser mediano-grande.

La curva de aprendizaje es moderada, lo dominarás de inmediato y las capacidades del lenguaje utilizadas  pueden ayudarte a mejorar su uso.

Finalmente comentar que siguen apareciendo nuevas apuestas muy interesantes, y que si estás interesado valdría la pena que dieras un vistazo. Las 2 que te recomendaría son:

Espero que esta serie de artículos te hayan servido, para mejorar la gestión de triggers y así mejorar tu implementación y el futuro de tus proyectos.

Enlaces interesantes

  • Código disponible directamente del autor aquí

Por cierto, por qué ropa? Porque empiezan las rebajas ya!!!

Responder

Introduce tus datos o haz clic en un icono para iniciar sesión:

Logo de WordPress.com

Estás comentando usando tu cuenta de WordPress.com. Cerrar sesión /  Cambiar )

Google+ photo

Estás comentando usando tu cuenta de Google+. Cerrar sesión /  Cambiar )

Imagen de Twitter

Estás comentando usando tu cuenta de Twitter. Cerrar sesión /  Cambiar )

Foto de Facebook

Estás comentando usando tu cuenta de Facebook. Cerrar sesión /  Cambiar )

Conectando a %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.