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, siendo mejoradas.
De los 3 frameworks analizados en esta serie, este es el que ofrece una orientación a objetos y uso de Apex más potente, ampliando las ideas de los 2 frameworks anteriores.
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).
Evolución no Revolución
Partiendo de los 2 frameworks anteriores, el autor se centró en los siguientes puntos:
- Proporcionar una implementación, que permitiera la mínima colisión entre desarrolladores.
- 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).
- Utilizar el concepto de diseño mediante interfaces.
Esqueleto completo del Framework
A continuación, muestro un diagrama para facilitar su entendimiento:
He incluido un esquema básico de las clases e interfaces usadas, para facilitar la comprensión.
Este framework, cumple las siguientes características:
- Solo debe existir un único trigger por objeto (“One trigger to rule them all“, más claro imposible). Esta idea, ya la vimos en el patrón de Tony Scott y es una buena práctica indiscutible, y se realiza mediante el uso de una clase Factory (lo veremos en detalle).
- Todos los triggers utilizan su propio Dispatcher. Además estos Dispatchers, son creados dinámicamente y especializados por evento.
- El Dispatcher (TriggerDispatcherBase) 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.
- Se definen Handlers a nivel de (objeto+evento), y esto es una gran diferencia con lo que habíamos visto, aunque seguro intuido.
- 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.
Invocar 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 sino que invoca un método estático para la creación del Dispatcher.
La TriggerFactory y el Dispatcher
La clase TriggerFactory es una clase muy simple con 2 funciones principales:
- Crear una instancia de la clase Dispatcher para el objeto sobre el que se ha generado el Trigger.
- En función del evento (before/insert, delete/insert/update) enruta hacia el método del Dispatcher adecuado. Mediante el uso de la Type API, se construye el nombre del Dispatcher y se crea utilizando ese nombre (en este punto radica el uso de las nuevas funcionalidades que utilizó Hari).
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 implemento este modelo:
- Usar la interface ITriggerDispatcher, que contiene los métodos que todo Dispatcher contendrá.
- 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.
- Todos los Dispatcher (evento+objeto) siguen una convención de nombres, que consiste en
<nombreObjeto)TriggerDispatcher
, por ejemplo:AccountTriggerDispatcher
,miCustomObjectTriggerDispatcher
(es decir sin el sufijo __c para los Custom Object).
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 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:
- Permite inyectar código en la clase base, que todos los Dispatchers obtendrán. De hecho la gestión de las re-entradas se gestiona con este enfoque, en el método
execute.
- Permite solo implementar los métodos para los eventos que necesitemos en los (objeto+evento) Dispatcher concretos, pero la TriggerDispatcherBase los contiene todos preparados.
- Minimiza drásticamente las colisiones de código entre desarrolladores, ya que los desarrolladores no trabajan sobre esta clase, sino sobre las instanciaciones de (objeto+evento).
- 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); } } }
Cómo 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 y así detectar las re-entradas. 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
.
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 del Handler
El patrón jerárquico para los Handler es muy parecido:
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() &amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;amp;gt; 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:
- 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
- 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
- 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
- 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
- Activar/Desactivar un trigger en caliente en cualquier entorno: no propone nada al respecto, pero es muy fácil introducirlo
- 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í