El framework de Appleman que los inició a todos (Parte 3)

Las ideas propuestas por Dan Appleman en su libro Advanced Apex para la gestión eficiente de triggers, y han sido la base para varios frameworks aparecidos posteriormente.

De hecho, el título que el autor dio al capítulo de su libro, “One trigger to rule them all” es conocido y mencionado en muchos artículos y sitios web.

Mi objetivo en esta entrada, es explicar de manera llana, los conceptos e ideas de este framework, esencial para cualquier proyecto mediano-grande. Al finalizar el artíuclo, podrás usar el framework directamente mejorar tu actual gestión de triggers.

El contexto de ejecución

Para la creación de un framework eficiente, hay que conocer las características del contexto de ejecución en la plataforma Salesforce.

Al contrario de lo que pueda parecer, hay diferentes tipos de Execution Context, y cada uno de ellos, tiene unos límites diferentes.

Te enumero aquí solo las características e implicaciones (si quieres saber más sobre el Contexto de Ejecución en Salesforce):

  • Un contexto de ejecución se inicia por un evento u operación concreta, y en nuestro caso, es importante cuando se inicia, por el lanzamiento de un trigger
  • Cuando se inicia el contexto por un trigger, todas las operaciones que se deriven de él, incluyendo el lanzamiento de otros triggers, workflows, etc., se mantienen en el mismo contexto de ejecución (esto es crucial!)
  • En un contexto de ejecución se establecen y se mantienen los mismos Governor Limits (también muy importante)
  • En un contexto de ejecución se establecen y se mantienen todos los miembros estáticos, SOLO durante la vida de contexto
  • Es fácil saber cuando se inicia un contexto, pero como puedes ver en la imagen a continuación es difícil saber cuando finaliza, y las bifurcaciones que puede tener
Ejemplo de un contexto de ejecución, con varias operaciones concatenadas – recuerda que en el mismo contexto de ejecución, se mantienen los mismos Governor Limits

Esqueleto completo del Framework

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
  2. Todos los triggers centralizan sus llamadas en un único Dispatcher Global
  3. El Dispatcher realiza 3 funciones:
    • Ejecuta funciones transversales a todos los triggers, tanto al inicio de la ejecución como al finalizar
    • Enruta las peticiones hacía el Handler del objeto adecuado
    • Controla y gestiona las re-entradas (recursividad) de todos los objetos
  4. Todos los Handler de objeto deben implementar una interface, que asegura el tratamiento de la llamada inicial y las re-entradas
  5. La lógica de negocio se implementa fuera del Dispatcher, en bloques funcionales aislados

En el diagrama a continuación, intento explicar estas partes que conforman el framework y sus interacciones de alto nivel:

Nota para el lector (del libro)

Para facilitar el entendimiento y explicar más claramente los conceptos, he modificado el código  que aparece en el libro, y he realizado ciertos ajustes:

  • En el  el libro, verás que no se explicita el concepto de Handler, aunque se menciona algo parecido
  • El (global) dispatcher invoca los bloques funcionales directamente sin pasar por estos Handlers

En mi opinión el uso del Handler, como mediador entre el Dispatcher Global y el Bloque funcional,  refuerza el principio de separación de responsabilidades y disminuye el acoplamiento entre clases (lo que implica menos colisiones entre desarolladores).

Pero, tu decides, qué enfoque te parece más adecuado, o aún mejor, modificarlo y adaptarlo a tus necesidades y a tu equipo.

Veamos el código de cada uno de los componentes.

Invocación del Trigger al global Dispatcher

Como vimos, en el artículo del patrón de Tony Scott, la premisa de tener trigger code-less, es una buena práctica y una premisa también para este framework.

En este caso, tenemos exactamente eso, el trigger realiza una única invocación al Global Dispatcher, para que se espavile. Las diferencias que encontramos con el patrón de Tony Scott son:

  • el trigger está desacoplado del Handler del objeto (delega ese acoplamiento al Dispatcher)
  • se transmiten todos los atributos del trigger al Dispatcher, junto con el nombre del objeto que estamos gestionando

Esto permite, a diferencia del patrón anterior, no acoplar el Dispatcher con un Handler especifico (.class), consiguiendo así, que el código del trigger se mantenga mínimo y sin cambios, desacoplando completamente  Trigger y Handler, guai!!.

En esta imagen podemos ver la primera etapa de ejecución del framework:

En el código a continuación, dejo comentada la invocación del patrón de Tony Scott, para que puedas comparar las diferencias entre ambos frameworks (observa como el Trigger conoce el .class del Handler – un cambio de Handler implica un cambio en el Trigger).

Esto que puede parecer, una mejora estética, tiene implicaciones importantes. Si decidimos tener diferentes Handlers en función de ciertos valores de ciertas variables, en el framework de Appleman es completamente posible, dado que lo implementaremos en el Dispatcher, mientras que en el de Scott, requiere de la modificación de Trigger y de las clases dependientes posteriores.

/*
 * @author Esteve Graells
 * @date: junio 2018
 * @description: Trigger para Account que invoca al Dispatcher
 */

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

    // Llamada al dispatcher del patron de Tony Scott
    // TriggerFactory.createAndExecuteHandler(AccountHandler2.class);

    // Llamada al dispatcher del framework de Dan Appleman
    GlobalDispatcher.run('Account',
            trigger.isBefore, trigger.isAfter,
            trigger.isInsert, trigger.isUpdate,
            trigger.isDelete, trigger.isExecuting,
            trigger.new, trigger.newMap,
            trigger.old, trigger.oldMap);
}

Ejecución del Dispatcher

Después de estudiar el framework, creo que Appleman debió dudar entre el título “One trigger to rule them all” y “One Dispatcher to manage them all“, yo me hubiera decantado por el segundo :-).

Digo esto, porque el Dispatcher en este framework, es un punto de entrada único para todos los triggers de nuestro proyecto. En él, se realizan las tareas siguientes:

  1. (opcional) Ejecución de funciones transversales como Logging, Control de cuotas, etc
  2. (opcional) Definición de miembros estáticos comunes al contexto, por ejemplo, para albergar objetos con los que realizar DMLs únicas
  3. Detección de la re-entrada y enrutamiento al método del Handler en curso
  4. Invocación del Handler que controlará el flujo de negocio asociado
  5. (opcional) Operaciones de DML únicas, para evitar DML intermedias innecesarias
  6. (opcional) Otra vez, ejecución de funciones transversales al salir como Logging, Control de cuotas, notificaciones, etc.

Veamos cada una de las actividades.

Funciones transversales al inicio y final

Permiten realizar funciones de control, logging, notificaciones, etc. Cada uno implementamos las que necesitamos, pero atención, teniendo en mente, el rendimiento, y los límites asociados al contexto (CPU y Heap especialmente).

Si leíste el artículo anterior, ¿recuerdas el método andFinally() de la clase Handler? Claro, Scott ya vió la necesidad de tener funciones transversales de logging, housekeeping, etc., en su patrón.

Declaración de miembros estáticos

Esta sección permite definir aquellas variables que estarán disponibles durante todo el contexto de ejecución. En el framework hay 2 usos fundamentales de los miembros estáticos:

  1. Declaración de la variable que nos indicará si hay una re-entrada (contiene la instancia del Handler en curso)
  2. Declaración de la variable que contiene los elementos del objeto, que se van creando/modificando (según queramos) y poder ejecutar así una única DML al final, eficientando las operaciones sobre BD

Recordar, que el tamaño del Heap del Contexto de Ejecución es limitado, y hay que diseñar con cuidado que variables serán incluidas en el contexto.

Gestión de la re-entrada

La problemática es la siguiente: ¿qué sucede si las operaciones DML de los bloques funcionales,  provocan invocaciones a otros triggers, que a su vez, ejecutan operaciones DML, que acaban provocando la invocación del mismo trigger en qué se inició la lógica?

Esto produce lo que se denomina una re-entrada en el trigger, y entramos en recursividad de llamadas (en la imagen del apartado inicial del Contexto de Ejecución, ya lo viste).

Estas re-entradas pueden ser necesarias, y por tanto, no es objetivo eliminarlas para que no se produzcan o ignorarlas, sino tratarlas según sea necesario.

El framework realiza un control de las re-entradas de la siguiente manera:

  1. Detecta la re-entrada y la entrada inicial (fácil dirás, las re-entradas comparten Execution Context – Correcto! y por ahí va la solución mediante las variables estáticas)
  2. Fuerza a incluir en el código  gestión de las re-entradas, mediante la implementación de una interface

La implementación pasa nuevamente por el uso de variables estáticas al contexto (nota: recuerda que en Apex, una variable estática se mantiene durante la vida del  Execution Context, no durante toda la ejecución de la aplicación como sucede en .net ó  Java).

El enfoque es muy sencillo:

  • Una variable contiene la instancia del Handler, se denomina  activeFunction
  • Al invocar el trigger, el Dispatcher, detecta si la variable ha sido inicializada previamente, lo que implica que ya hubo una invocación anterior del Handler, con lo que esto supone una re-entrada

Por tanto, hemos pasado de desconocer y no controlar lo que sucedía en las re-entradas, a controlarlas y aprovecharlas según necesitemos. Veremos un poco más adelante como estamos forzados a considerarlo, guai!!

Gestión de la entrada inicial

La entrada inicial, es el código con el que pensamos en la lógica de negocio. Su ejecución se lleva a cabo dentro del Handler, y accedemos a él, siempre que no hayamos detectado una re-entrada. No mucho más que añadir.

Invocación del Handler

El Dispatcher invoca a los Handler, para ejecutar la lógica de negocio. Lo hace, sabiendo que implementan la interface iTriggerEntry. Esto es fundamental, porque así invoca a sus 2 métodos:

  • una para la entrada inicial
  • otro para la re-entrada

El Dispatcher invoca el Handler, con todos los atributos del trigger y  el nombre del objeto.

Este útimo parámetro, no es estrictamente necesario, ya que el Dispatcher ya invoca al Handler del objeto concreto (Opportunity, Account, etc.) pero sirve para realizar una comprobación de seguridad, y validar que no estamos instanciando un Handler erróneo.

Operación DML única

Esta idea, es parecida a la que vimos en el patrón de Tony Scott, pero aquí Appleman la explicita de manera muy clara.

Estas variables, son variable de tipo Map, que contienen los objetos que la lógica de negocio va generando y/o modificando.

Por tanto, dado que las variables son estáticas y  serán visibles durante la vida del contexto, definimos un Map (por ejemplo de Account), donde se insertan/los registros (las Accounts), y en lugar de ejecutar operaciones DML parciales en los bloques funcionales, antes de finalizar el trigger, se realiza una única operación DML (insert, update ó upsert, delete, undelete) con el Map.

Este patrón es realmente interesante por lo simple y útil que es, y lo puedes mejorar con estructuras de datos más complejas (como veremos en el framework de Scott Wells), la idea sigue siendo la misma.


/*
 * @author Esteve Graells
 * @date: junio 2018
 * @description: Dispatcher que centraliza todas las llamadas de triggers,
 * gestiona las re-entradas y realiza enrutamiento hacia el Handler
 */

public with sharing class GlobalDispatcher {

    public static Map<ID, Account> acsToUpdate = new Map<ID, Account>();
    public static ITriggerEntry activeFunction = null;

    public static void run(
            String triggerObject,
            Boolean isBefore,
            Boolean isAfter,
            Boolean isInsert,
            Boolean isUpdate,
            Boolean isDelete,
            Boolean isExecuting,
            List<SObject> newList, Map<ID, SObject> newMap,
            List<SObject> oldList, Map<ID, SObject> oldMap) {

        // Ejecución de Funciones Transversales como logging, 
        // control de excepciones etc.
        transversalFunctionsAtBeginning();

        //Detección de la re-entrada en la clase en curso
        if (activeFunction != null) {
            activeFunction.inProgressEntry(triggerObject,
                    isBefore, isAfter, isInsert, isUpdate, isDelete, 
                    isExecuting, newList, newMap, oldList, oldMap);

            System.debug('-ege- Re-entrada para objeto ' + triggerObject);

            return;
        }

        ////Routing para el objeto ACCOUNT a su Handler

        if (triggerObject == 'Account') {
            activeFunction = new AccountEntryHandler();

            activeFunction.mainEntry(triggerObject,
                    isBefore, isAfter, isInsert, isUpdate, isDelete, 
                    isExecuting, newList, newMap, oldList, oldMap);

            if (acsToUpdate.size() > 0) update acsToUpdate.values();

            activeFunction = null;
        }

        //Routing para el objeto OPPORTUNITY a su Handler

        if (triggerObject == 'Opportunity') {
            activeFunction = new OpportunityEntryHandler();

            //Invoación a la función del Handler de primera entrada
            activeFunction.mainEntry(triggerObject,
                    isBefore, isDelete, isAfter, isInsert,
                    isUpdate, isExecuting,
                    newList, newMap, oldList, oldMap);

            activeFunction = null;
        }

        // Ejecución de Funciones Transversales como logging, 
        //control de excepciones etc.

        transversalFunctionsAtEnding();
    }

    //Código de la funcional transversal de inicio
    private static void transversalFunctionsAtBeginning(){}

    //Código de la funcional transversal de cierre
    private static void transversalFunctionsAtEnding(){}


}</pre>

Handler y los bloques funcionales

El Handler implementa la interface iTriggerHander lo que le obliga a proporcionar código para la entrada inicial y las re-entradas.

El código de la entrada inicial, se segrega a nivel conceptual en Bloques Funcionales, que es la lógica de negocio que queremos ejecutar.

Veamos el código de la interface y un par de ejemplos sencillos de Handler (Account y Opportunity):

/*
 * @author Esteve Graells
 * @date: junio 2018
 * @description:
 *
 */

public interface iTriggerEntry {
    void mainEntry(String triggerObject, Boolean isBefore,
            Boolean isDelete, Boolean isAfter,
            Boolean isInsert, Boolean isUpdate,
            Boolean isExecuting,
            List<SObject> newList, Map<ID, SObject> newMap,
            List<SObject> oldList, Map<ID, SObject> oldMap);

    void inProgressEntry(String triggerObject, Boolean isBefore,
            Boolean isDelete, Boolean isAfter,
            Boolean isInsert, Boolean isUpdate,
            Boolean isExecuting, List<SObject> newList,
            Map<ID, SObject> newMap,
            List<SObject> oldList, Map<ID, SObject> oldMap);
}

A continuación, el Handler para el objeto Account completo, en este caso, los bloques funcionales evitan que se eliminen Accounts que tengan Oportunidades

/*
 * @author Esteve Graells
 * @date: junio 2018
 * @description: Implantación de la lógica de negocio para la gestión de los
 * triggers de Account
 *
 */

public class AccountEntryHandler implements iTriggerEntry {

    static Set<Id> accIdsWithOpps = new Set<Id>();

    public void mainEntry(
            String triggerObject,
            Boolean isBefore,
            Boolean isAfter,
            Boolean isInsert,
            Boolean isUpdate,
            Boolean isDelete,
            Boolean isExecuting,
            List<SObject> newList, Map<ID, SObject> newMap,
            List<SObject> oldList, Map<ID, SObject> oldMap) {

        List<Account> accNewList = (List<Account>) newList;
        List<Account> accOldList = (List<Account>) oldList;
        Map<ID, Account> accNewMap = (Map<ID, Account>) newMap;
        Map<ID, Account> accOldMap = (Map<ID, Account>) oldMap;

        if (triggerObject == 'Account') {

            if (isBefore){

                if (isInsert){
                    //// Ejecución de Bloque funcional correspondiente
                    System.debug('-ege- BeforeInsert ' + triggerObject);}
                if (isUpdate){
                    //FunctionalBlock2()
                    System.debug('-ege- BeforeUpdate ' + triggerObject);}
                if (isDelete) {
                    //// Ejecución de Bloque funcional correspondiente
                    System.debug('-ege- BeforeDelete ' + triggerObject);
                    beforeDeleteFunctionalBlock(isBefore, isAfter, accNewList, accNewMap, accOldList, accOldMap);
                }

            }else if (isAfter){
                if (isInsert){
                    //// Ejecución de Bloque funcional correspondiente
                    System.debug('-ege- AfterInsert ' + triggerObject);}
                if (isUpdate){
                    //// Ejecución de Bloque funcional correspondiente
                    System.debug('-ege- AfterUpdate ' + triggerObject);}
                if (isDelete) {
                    //// Ejecución de Bloque funcional correspondiente
                    System.debug('-ege- AfterDelete ' + triggerObject);
                    afterDeleteFunctionalBlock(isBefore, isAfter, accNewList, accNewMap, accOldList, accOldMap);
                }
            }
        }
    }

    //Gestión de la re-entrada para Account
    public void inProgressEntry(
            String triggerObject,
            Boolean isBefore,
            Boolean isAfter,
            Boolean isInsert,
            Boolean isUpdate,
            Boolean isDelete,
            Boolean isExecuting,
            List<SObject> newList, Map<ID, SObject> newMap,
            List<SObject> oldList, Map<ID, SObject> oldMap) {

        // Be sure to detect for the objects you actually want to handle.

        if (TriggerObject == 'Account' && IsAfter) {
            // Do some processing here
            // Can dispatch to other classes is necessary
        }
    }

    // No permitir el borrado de Accounts con Oportunidades
    // Es un ejemplo de Bloque funcional

    private void beforeDeleteFunctionalBlock(
            Boolean isBefore,
            Boolean isAfter,
            List<Account> newList, Map<ID, Account> newMap,
            List<Account> oldList, Map<ID, Account> oldMap){

        // Dependiendo de la Naturaleza del trigger los valores
        // de las Accounts vienen en un Map u otro

        Set<Id> accIds = oldMap.keySet();

        //Obtener las Accounts con sus Oportunidades
        for (Account[] accounts : [
                SELECT a.Id, (SELECT Id FROM Opportunities LIMIT 1)
                FROM Account a
                WHERE a.Id in :accIds]) {

            for (Account acc : accounts) {

                if (acc.Opportunities.size() > 0) {
                    accIdsWithOpps.add(acc.Id);
                    acc.addError('Esta account no se puede eliminar, tiene oportunidades asociadas');
                }
            }
        }
    }


    // No permitir el borrado de Accounts con Oportunidades
    // Es otro ejemplo de Bloque funcional
    private void afterDeleteFunctionalBlock(
            Boolean isBefore,
            Boolean isAfter,
            List<Account> newList, Map<ID, Account> newMap,
            List<Account> oldList, Map<ID, Account> oldMap){

        // No permitir el borrado de los Accounts que tienen Opportunities

        for ( Id accId : oldMap.keySet() ){

            if (accIdsWithOpps.contains(accId)){
                oldMap.get(accId).addError('Esta account no se puede eliminar, tiene oportunidades asociadas');
            }
        }
    }
}

El Handler para el objeto Opportunity completo, con bloques funcionales vacíos:

/*
 * @author Esteve Graells
 * @date: junio 2018
 * @description: Implantación de la lógica de negocio para
 * la gestión de los triggers de Opportuntiy
 *
 */

public class OpportunityEntryHandler implements iTriggerEntry{

    //Función de gestión del trigger en primera entrada
    public void mainEntry(
            String triggerObject,
            Boolean isBefore,
            Boolean isAfter,
            Boolean isInsert,
            Boolean isUpdate,
            Boolean isDelete,
            Boolean isExecuting,
            List<SObject> newList, Map<ID, SObject> newMap,
            List<SObject> oldList, Map<ID, SObject> oldMap) {

        List<Opportunity> opNewList = (List<Opportunity>) newList;
        List<Opportunity> opOldList = (List<Opportunity>) oldList;
        Map<ID, Opportunity> opNewMap = (Map<ID, Opportunity>) newMap;
        Map<ID, Opportunity> opOldMap = (Map<ID, Opportunity>) oldMap;

        //Implementación de la logica de negocio que se desea
        // para cada evento
        if (triggerObject == 'Opportunity'){

            if (isBefore){

                if (isInsert){
                    //FunctionalBlock1()
                    System.debug('-ege- BeforeInsert ' + triggerObject);}
                if (isUpdate){
                    //FunctionalBlock2()
                    System.debug('-ege- BeforeUpdate ' + triggerObject);}
                if (isDelete){
                    //FunctionalBlockn()
                    System.debug('-ege- BeforeDelete ' + triggerObject);}
            }else if (isAfter){
                if (isInsert){
                    //FunctionalBlockn()
                    System.debug('-ege- AfterInsert ' + triggerObject);}
                if (isUpdate){
                    //FunctionalBlockn()
                    System.debug('-ege- AfterUpdate ' + triggerObject);}
                if (isDelete){
                    //FunctionalBlockn()
                    System.debug('-ege- AfterDelete ' + triggerObject);}
            }
        }
    }

    //Método que gestiona las re-entradas de Oportunidades
    public void inProgressEntry(
            String triggerObject,
            Boolean isBefore,
            Boolean isAfter,
            Boolean isInsert,
            Boolean isUpdate,
            Boolean isDelete,
            Boolean isExecuting,
            List<SObject> newList, Map<ID, SObject> newMap,
            List<SObject> oldList, Map<ID, SObject> oldMap) {

        if (TriggerObject == 'Opportunity') {

            //Logica de negocio
            System.debug('-ege- Re-entrada del trigger para ' + triggerObject);
            //FunctionalBlockn()

        }
    }
}

La implementación de un Handler por Objeto, permite separar y ordenar nuestro código.

Además, cabe la posibilidad de implementar los bloques funcionales en clases separadas (separation of concerns), para conseguir así aún más desacople y re-utilización de esas clases.

Qué hemos conseguido

Casi ya hemos acabado, pero además desde el punto de vista de Arquitectura de desarrollo, con este enfoque hemos conseguido:

  • Tenemos triggers con código sin cambios desacoplando Handler y Trigger
  • Tenemos un Dispatcher central, que permite ejecuciones tranversales, DML eficientes con miembros estáticos, detección y gestión de la re-entradas
  • “Forzar” a la implementación de la gestión de las re-entradas
  • Tenemos bloques funcionales en el Handler o en clase separadas para su re-utilización posterior y de esta manera disminuir las colisiones entre los desarrolladores

¿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, y además podemos optar por centralizar todas las ejecuciones de todos los triggers
  2. Crear el trigger sin código, externalizando todo su código en clases externas: hemos exter
    nalizado toda la funcionalidad en 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 un método para la re-entrada, y por tanto, al menos plantearse que debe hacer
  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: con todos los puntos anteriores, y con una curva de aprendizaje muy pequeña, se proporciona un entorno para que todo el equipo de desarrollo colabore, minimizando las colisiones de código.

Conclusiones

Este framework, propone patrones e ideas que basándose en las buenas prácticas de APEX mejoran enormemente la gestión de triggers en proyectos mediano-grandes.

El control de la recursividad, fue de los primeros en tratarlo con detalle y proponer la idea de incluirlo dentro del framework. Como comenta el autor, este es un esqueleto básico de gestión de triggers, para implementar nuestro propio framework y veremos en la siguiente entrada, como partiendo de las ideas de Appleman y Scott, otr framework proporciona nuevas ideas e intenta mejorar ambos.

Por supuesto, recomiendo la lectura del libro de Appleman, que aborda diversos temas muy interesantes.

Espero que te sea de ayuda.

Enlaces Interesantes

  1. Enlace al repositorio Bitbucket con las clases del proyecto completo

One response to “El framework de Appleman que los inició a todos (Parte 3)

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.