Trigger Frameworks – Un patrón de triggers simple y potente (Parte 2)

Uno de los primeros patrones/frameworks que aparecieron fue en un Force.com Cookbook, y después mejorado en su propio blog, por su propio autor, Tony Scott.

Utilizaré la nomenclatura de Patrón en lugar de Framework, porque realmente el autor no propuso, ni era su propósito, facilitar un framework, sino unas buenas prácticas que dieran respuesta a las problemáticas de su propia experiencia.

Los principios de este patrón de gestión de triggers son:

  1. El trigger no contine código, solo invoca a una clase Factory que implementa la lógica en su evento respectivo
  2. El Handler contiene la lógica de negocio de todos los eventos. Sus métodos son la implementación de una Interface.
  3. Una clase Factory, instancia el Handler y enruta las acciones en función del evento del trigger (before, insert…)
  4. Se incorporan funciones de BULK para eficientar el código

Estos principios, son pilares que los frameworks que vinieron a continuación tomaron como pilares,  y por lo tanto, vale la pena entender detenidamente lo que propuso Tony Scott .

Trigger sin código

El trigger solo realiza una acción, que es, invocar a la Factory proporcionando el Type de la clase del Handler que tiene la lógica del Trigger, implementando la interface (vaya frase más horrenda por dios!).

Veamos el código del trigger, donde observamos su simplicidad:

/* 1 única invocación a la Factory */

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

// Se invoca a la clase factoria, que construirá la clase
// handler de forma dinámica

TriggerFactory.createAndExecuteHandler(AccountHandler.class);
}

Diseño de la Interface

Antes de ver el código de la Factory, veamos el código de la interface que cada trigger debe implementar, y que contiene todas las funciones que la parte del enrutador de la Factory utiliza.

Aquí es donde empieza a brillar el patrón reglando como debe implementarse el código del trigger, proporcionado la separación entre conceptos.

Creo que con los comentarios que he incluido en el código, podemos seguir lo que realiza el código:

/*
* Para cada uno de los eventos del Trigger la interface
* implementa un método.
* Además implementa 2 métodos adicionales para caché de
* los registros, y un método de finalización, donde por
* ejemplo, concentrar las operaciones DML, y así evitar
* ineficiencias y superar límites en el Execution Context
*
*/

public interface ITrigger
{

// La característica de todos estos métodos, es que son
// invocados para cada registro involucrado

void beforeInsert(SObject so);
void beforeUpdate(SObject oldSo, SObject so);
void beforeDelete(SObject so);
void afterInsert(SObject so);
void afterUpdate(SObject oldSo, SObject so);
void afterDelete(SObject so);

// Los 2 métodos Bulk, son invocados para cachear los
// registros en variables tipo Map, y así eficientar
// su consulta

void bulkBefore();
void bulkAfter();

// Este método será invocado al finalizar toda la lógica del
// trigger. Puede servir para realizar operaciones DML,
// de limpieza, audit/log, etc.

void andFinally();
}

Código de la Factory

Las funciones de esta clase:

  • crear la instancia del Handler del objeto que estemos gestionando (y de ahí su nombre)
  • enrutar el código hacia la función adecuada del Handler dependiendo de la naturaleza del trigger
public with sharing class TriggerFactory{

// Función principal donde se realiza la
// creación dinámica del Handler y el enrutamiento
public static void createAndExecuteHandler(Type t){
ITrigger handler = getHandler(t);

if (Handler == null)
throw new TriggerException('No Trigger Handler: ' +
t.getName());

// Ejecutar el enrutado hacia la función que
// implementa el evento generado

execute(handler);
}

/*
* Ejecución del enrutado
*/
private static void execute(ITrigger handler){

if (Trigger.isBefore){

// Función de caché
handler.bulkBefore();

if (Trigger.isDelete){
for (SObject so : Trigger.old){
handler.beforeDelete(so);
}
}

else if (Trigger.isInsert){
for (SObject so : Trigger.new){
handler.beforeInsert(so);
}
}

else if (Trigger.isUpdate){
for (SObject so : Trigger.old){
handler.beforeUpdate(so,
Trigger.newMap.get(so.Id));
}
}
}

//Repetimos el esquema hacia las funciones after*

else{

handler.bulkAfter();

if (Trigger.isDelete){
for (SObject so : Trigger.old){
handler.afterDelete(so);
}
}

else if (Trigger.isInsert){
for (SObject so : Trigger.new){
handler.afterInsert(so);
}
}

else if (Trigger.isUpdate){
for (SObject so : Trigger.old){
handler.afterUpdate(so,
Trigger.newMap.get(so.Id));
}
}
}

// Post-proceso y operaciones DML
handler.andFinally();
}

//Función para la creación del handler

private static ITrigger getHandler(Type t){
Object o = t.newInstance();

// Comprobación de seguridad
if (o instanceOf ITrigger)return (ITrigger)o;
else return null;
}

/*
* El código de la Excepcion, puede ser el que deseemos
*/
public class TriggerException extends Exception {}
}

Implementación de una clase de ejemplo

Por tanto, hemos visto el código de la interface, hemos visto el de la Factory creando y enrutando la lógica, veamos pues un ejemplo de implementación de trigger sobre el objeto Account, que he simplificado levemente el código para evitar un código extenso:


/*
* Ejemplo de trigger trivial para el objeto Account
*
*/
public without sharing class AccountHandler
implements ITrigger
{
// Variable miembro que almacena los identificadores
// de Account con los q trabaja nuestra lógica de negocio
private Set<ID> m_inUseIds = new Set<ID>();

public void bulkBefore() {

// Función auxiliar que obtiene los Id account
// con los que realiza cierto tratamiento
if (Trigger.isDelete){
Set<ID> accIds = Trigger.oldMap.keySet();

//En el código de Bitbucket he utilizado Contacts,
//pero en esta versión. dejo la que utiliza Tony Scott
//para que no te pierdas si consultas su artículo
for (Account[] accounts : [
SELECT p.Id,
(SELECT Id FROM Opportunities LIMIT 1)
FROM Account p
WHERE p.Id in : accIds]){

for (Account acc : accounts){
if (acc.Opportunities.size() &amp;amp;amp;gt; 0)
m_inUseIds.add(acc.id);
}
}
}
}

// Se realizan ciertas comprobaciones de negocio sobre
// los IDs que hemos obtenido previamente

public void beforeDelete(SObject so) {

Account myAccount = (Account) so;

if (m_inUseIds.contains(myAccount.Id))
so.addError('Esta Account no se puede eliminar');
}

public void bulkAfter(){}
public void beforeInsert(SObject so){}
public void beforeUpdate(SObject oldSo, SObject so){}
public void afterInsert(SObject so){}
public void afterUpdate(SObject oldSo, SObject so){}
public void afterDelete(SObject so){}

public void andFinally(){
System.Debug ('Trigger ejecutado');
}
}

Facilidad de uso

¿Como es el uso de este patrón de uso de triggers en el día a día? Esa es una de sus grandes virtudes. Añadir nueva funcionalidad a un trigger existente implementado con este patrón, supone exclusivamente añadir o modificar el código en la función del Handler adecuada.

Si el trigger no existiera, creamos el trigger, 5 líneas de código, e implementamos su Handler (solo aquellas funciones que necesitemos).

Es decir, el patrón no requiere mantenimiento y posee una ciclotomía muy baja, lo que ayuda a su mantenibilidad y reducción de errores.

¿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: el patrón mediante el uso del Handler utiliza una única clase para la gestión del trigger.
  2. Crear el trigger sin código, externalizando todo su código en clases externas: el trigger realizar una única llamada a la Factory, para registrarse y que se inicie el tratamiento.
  3. Controlar el orden de ejecución: el orden de la ejecución lo marca el código del Handler, con lo que estamos controlando en todo momento el flujo de ejecución.
  4. Detección de la recursividad / Control de re-entradas: no propone nada al respecto. Como veremos más adelante en el framework de Dan Appleman, mediante el uso de variables estáticas, esto sería fácilmente controlable.
  5. Activar/Desactivar un trigger en caliente en cualquier entorno: no propone nada al respecto.
  6. Crear una estructura de código que facilite un entorno multi-programador: la separación del handler, de la factory y del trigger, un equipo de programadores obtendrá pocas colisiones en sus modificaciones del código.

Conclusiones

Enmi opinión, este es un patrón que presenta las principales buenas prácticas, aunque no dio respuesta al control de re-entradas y recursividad.

Para un equipo que no esté utilizando ningún patrón ó framework, este puede ser un punto de partida perfecto, para realizar pruebas, y adop/adap-tarlo rápidamente.

En la siguiente entrada veremos el framework de Dan Appleman, el que ha sido el framework de referencia durante mucho tiempo, que gestiona la recursividad y la re-entrada.

Espero que te haya sido de ayuda.

Enlaces Interesantes