Saltar al contenido

Trigger Handler, ¿cuál es su propósito?

Solución:

TL; DR

El código que se le pide que “documente” sigue un Service Layer patrón. Los métodos en este enfoque tienden a ser static void y actuar sobre una entrada. Otro nombre común que he visto es de hecho Util o Utility, como mencionas en tu pregunta.

Escribí bastante a continuación sobre lo que creo que es un Trigger Handler debiera ser. ¿Cómo se relaciona con la tarea que le han encomendado? Bueno, por un lado, es la base de mi afirmación de que debería no seguir el patrón establecido por esta clase. Mi principal problema es que la combinación de Handler y Service capas solo conduce a la confusión en su modelo mental y suite de pruebas.

Normalmente, documentar el Trigger y Handler capas es casi trivial. Y de hecho, si se le da bien la denominación descriptiva, puede resultar bastante sencillo traducir el Handler implementación en documentación sensible.

Rara vez debería necesitar escribir una línea 700 Apex Class. Yo diría que nunca, pero la API de metadatos de Salesforce de Apex Wrapper * es un ejemplo que me ha hecho suavizar mi postura y alejarme de los absolutos.


Beneficios del patrón de controlador de gatillo

Lectura relacionada: Trigger Frameworks y Apex Trigger Best Practices

Si bien mi patrón preferido difiere ligeramente, el artículo anterior expone muchas de las ventajas. Personalmente, encuentro una implementación de controlador imprescindible porque:

  • Facilita disparadores sin lógica y One Trigger Per Object, que en sí mismo tiene muchos beneficios
    • (incluido un control detallado sobre el orden de ejecución)
  • mejora la legibilidad y Separation of Concerns
    • (esta legibilidad mejorada también hace que el control de versiones sea más fructífero)
  • opcionalmente facilita la desactivación selectiva

Separación de intereses

Las preocupaciones se pueden dividir simplemente en tres preguntas básicas:

  1. Cuando (capa de activación) – ¿Cuándo se actuará sobre los registros?
  2. Que cual (capa de controlador) – ¿Qué acciones se realizarán? ¿Sobre qué registros se actuará?
  3. Cómo (capa de servicio) – ¿Cómo se realizarán esas acciones? ¿Cómo se definen esos criterios?

Desencadenar

los desencadenar puede simplemente preocuparse por cuando:

trigger MyObject on MyObject__c (/*events*/)
{
    TriggerHandler handle = new TriggerHandler(trigger.new, trigger.oldMap);
    if (trigger.isBefore)
    {
        if (trigger.isInsert) handle.beforeInsert();
        if (trigger.isUpdate) handle.beforeUpdate();
    }
    if (trigger.isAfter)
    {
        if (trigger.isInsert) handle.afterInsert();
        if (trigger.isUpdate) handle.afterUpdate();
    }
}

Una cosa que realmente me gusta de este patrón es que puedes mirar rápidamente tu disparador en el objeto e identificar qué eventos tienen lógica. También serán muy estables en su repositorio de código a lo largo del tiempo.

Manipulador

los manipulador puede preocuparse por qué/cuales:

public with sharing class MyObjectTriggerHandler
{
    @TestVisible static Boolean bypassTrigger = false;

    final List<MyObject__c> newRecords;
    final Map<Id, MyObject__c> oldMap;
    public MyObjectTriggerHandler(List<MyObject__c> newRecords, Map<Id, MyObject__c> oldMap)
    {
        this.newRecords = newRecords;
        this.oldMap = oldMap;
    }

    public void beforeInsert()
    {
        if (bypassTrigger) return;
        MyObjectService.validate(
            MyObjectService.isInvalid().filter(newRecords)
        );
    }
    public void afterInsert()
    {
        if (bypassTrigger) return;
        MyObjectService.alterRelatedData(
            MyObjectService.shouldAlterRelatedData().filter(newRecords)
        );
    }

    public void beforeUpdate()
    {
        if (bypassTrigger) return;
        MyObjectService.transform(newRecords);
    }
    public void afterUpdate()
    {
        if (bypassTrigger) return;
        MyObjectService.alterRelatedData(
            MyObjectService.shouldAlterRelatedData().filter(newRecords, oldMap)
        );
    }
}

Las principales ventajas que veo en lo anterior son que nuevamente se vuelve fácil identificar rápidamente los efectos de cualquier evento desencadenante dado y diferir la comprensión de la cómo (la pregunta más complicada) a unidades de código más pequeñas. También tiene el beneficio adicional de hacer que la desactivación selectiva sea mucho más fácil de implementar. Por lo general, solo hago posible omitir todo el disparador para fines de prueba unitaria (sobre lo cual hablaremos más adelante).

Servicio

los Servicio Entonces finalmente puedo responder a esos molestos cómo preguntas:

public with sharing class MyObjectService
{
    public static Select.Filter shouldAlterRelatedData()
    {
        // some filter
    }
    public static Select.Filter isInvalid()
    {
        // some filter
    }
    public static void validate(List<MyObject__c> records)
    {
        for (MyObject__c record : records) record.addError('message');
    }
    public static void transform(List<MyObject__c> records)
    {
        // modify records
    }
    public static void alterRelatedData(List<MyObject__c> records)
    {
        // modify parent/sibling/child records
    }
}

La principal ventaja que veo en este patrón de servicio es que las pruebas unitarias se vuelven mucho más atómicas. La biblioteca Selector realmente ayuda a evitar que esta capa crezca demasiado y también tiene una gran explicación de los beneficios de las pruebas en su documentación.

Al probar el Service Layer, a veces necesito insertar registros para poder probar métodos que deben volver a consultarse. En tales casos, puedo obtener una imagen más clara sobre el comportamiento del servicio desactivando los activadores, que pueden llamar a los mismos métodos cuando están activos.

* Los MetadataService la clase tiene más de 11K LOC

¡Haz clic para puntuar esta entrada!
(Votos: 0 Promedio: 0)



Utiliza Nuestro Buscador

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *