10

I am getting my head around really using a single trigger approach and I appreciate that minimal logic should be in the trigger. I'm already using utility classes but I'm used to including filtering in my trigger (such as below) so that only relevant records are based to my utils class...is this still keeping to the minimal logic approach or should this be done in the utils class as well?

trigger OptyTrigger on Opportunity (after insert, after update) 
{
        List<Opportunity> lostOptys = new List<Opportunity>();
        For (Opportunity o:Trigger.New){
            If(o.StageName == 'Closed Lost'){
                lostOptys.add(o);           
            }
        }    
        OptyUtils.optyLostReason(lostOptys);

}

4 Answers 4

8

As with most things SF, there are numerous ways of doing this. One way is to to use a trigger that calls a trigger dispatcher class for the trigger object. Using Dan Appleman's model as described in his book Advanced Apex Programming for Salesforce.com and Force.com, (Note: sample code available from App Exchange as an unmanaged package). In his model, a very simple trigger, calls a Main trigger class for an object which holds all the logic for calling sub classes that handle the logic for both before and after insert, update and delete classes, along with regression and post DML operations, etc.

The basic trigger looks something like this:

trigger OnOpportunity3 on Opportunity (after delete, after insert, after update, 
before delete, before insert, before update) {
    DiagnosticsMain.MainEntry('Opportunity', trigger.IsBefore, trigger.IsDelete, trigger.IsAfter, 
    trigger.IsInsert, trigger.IsUpdate, trigger.IsExecuting, trigger.new, trigger.newmap, trigger.old, trigger.oldmap);
}

The Main Entry class, which is where you'd do your primary sorting logic for each trigger you'd normally have that you'd then call a subclass to process, looks something like this:

public class TriggerArchitectureMain1 {
    public static Map<ID, Opportunity> opstoupdate = new Map<ID, Opportunity>();

    public static void Entry2(List<Opportunity> newlist, Map<ID, Opportunity> newmap, List<Opportunity> oldlist, Map<ID,Opportunity> oldmap)
    {   
        TriggerArchitectureClass1.Entry2(newlist, newmap, oldlist, oldmap);
        TriggerArchitectureClass2.Entry2(newlist, newmap, oldlist, oldmap);
        if(opstoupdate.size()>0) update opstoupdate.values();
    }

    public static Boolean InClass1 = false;
    public static Boolean InClass2 = false;
    public static void Entry3(List<Opportunity> newlist, Map<ID, Opportunity> newmap, List<Opportunity> oldlist, Map<ID,Opportunity> oldmap)
    {   
        if(!InClass1)
        {
            InClass1= true;
            TriggerArchitectureClass1.Entry3(newlist, newmap, oldlist, oldmap);
            InClass1 = false;
        }
        InClass2 = true;
        TriggerArchitectureClass2.Entry3(newlist, newmap, oldlist, oldmap);
        InClass2 = false;
        if(opstoupdate.size()>0) update opstoupdate.values();
    }

    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);
    }

    public static ITriggerEntry activefunction = null;

    public static void Entry4(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)
    {   
        if(activefunction != null) 
        {
            activefunction.InProgressEntry(TriggerObject, IsBefore, IsDelete, IsAfter, IsInsert, IsUpdate, IsExecuting, newlist, newmap, oldlist, oldmap);
            return;
        }

        if(TriggerObject == 'Opportunity' && IsAfter && IsUpdate)
        {
            activefunction = new TriggerArchitectureClass1();
            activefunction.MainEntry(TriggerObject, IsBefore, IsDelete, IsAfter, IsInsert, IsUpdate, IsExecuting,  newlist, newmap, oldlist, oldmap);

            activefunction = new TriggerArchitectureClass2();
            activefunction.MainEntry(TriggerObject, IsBefore, IsDelete, IsAfter, IsInsert, IsUpdate, IsExecuting,  newlist, newmap, oldlist, oldmap);

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

        }
    }

}

The above class detects both main entry from the trigger and re-entry caused by recurrent triggers. As such, it has a built-in method for triggers that fire an additional time under the same execution context. You'll also notice that this architecture implements an ITrigger interface.

An example of a sub class this architecture would call follows below:

public class TriggerArchitectureClass2  implements TriggerArchitectureMain1.ITriggerEntry {

    public static void Entry1(List<Opportunity> newlist, Map<ID, Opportunity> newmap, List<Opportunity> oldlist, Map<ID,Opportunity> oldmap, Map<ID, Opportunity> objectstoupdate)
    {
        // Do some processing here
        // Add entries to the objectstoupdate map if they need to be updated
    }

    public static void Entry2(List<Opportunity> newlist, Map<ID, Opportunity> newmap, List<Opportunity> oldlist, Map<ID,Opportunity> oldmap)
    {
        // Do some processing here
        // Add entries to the dispatcher static variable if they need to be updated
    }

    public static void Entry3(List<Opportunity> newlist, Map<ID, Opportunity> newmap, List<Opportunity> oldlist, Map<ID,Opportunity> oldmap)
    {
        // Do some processing here
        // Add entries to the dispatcher static variable if they need to be updated
    }

    public 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)
    {
        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;

        // Do some processing here
        // Add entries to the dispatcher static variable if they need to be updated or do direct DML

    }

    public 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)
    {
        // Be sure to detect for the objects you actually want to handle.
        // Can dispatch to other classes is necessary

        // Here's how:

        if(TriggerObject == 'Opportunity' && IsAfter)
        {
            TriggerArchitectureMain1.activefunction = new TriggerArchitectureClass1();
            TriggerArchitectureMain1.activefunction.MainEntry(TriggerObject, IsBefore, IsDelete, IsAfter, IsInsert, IsUpdate, IsExecuting, newlist, newmap, oldlist, oldmap);
            TriggerArchitectureMain1.activefunction = this;
        }

    }

}

Hari Krishnan has a similar, but different architecture that he describes in his blog article titled An architecture framework to handle triggers in the Force.com platform. I'm quite fond of this architecture and think it has much to offer. You might say that I've used variants of it. In my opinion, it is simpler than Appleman's architecture to work with.

Like Appleman, he uses a single trigger to call a Dispatcher class for each single object.

Below is Hari's dispatcher class for Accounts which is built up from a virtual class that implements an ITriggerDispatcher and ITriggerHandler class similar to Appleman's architecture only without the need for an ITriggerEntry class. This class is not well suited for implementing logic to sort records for processing to fit a particular trigger's needs. Instead, that is best left to the sub-class that would be called from it. You can see that like Appleman's architecture, it does include reentrant code handling.

/**
* @author Hari Krishnan
* @date 07/17/2013
* @description This class extends the TriggerDispatcherBase to provide the dispatching functionality for the trigger actions 
*               on the Account object. The event handlers support allowing and preventing actions for reentrant scenarios. 
*               This is controlled by the flag isBeforeXxxxx and isAfterXxxxx member variables. These variables need to be set
*               to true before invoking the handlers and set to false after the invocation of the handlers. Resetting is MUST
*               as otherwise unit tests MAY fail. The actual actions should be placed in the handlers (in a separate class).
*/
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; 

    /** 
    * @author Hari Krishnan
    * @date 07/17/2013
    * @description Called by the trigger framework to carry out the actions before the records are inserted. If there is an
    *               existing call running on the same context, the rentrant call will utilize the handler that was created
    *               in the original call.
    * @param TriggerParameters Contains the trigger parameters which includes the records that is getting inserted.
    */
    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);
    }

    /** 
    * @author Hari Krishnan
    * @date 06/20/2013
    * @description Called by the trigger framework to carry out the actions before the records are updated. If there is an
    *               existing call running on the same context, the rentrant call will utilize the handler that was created
    *               in the original call.
    * @param TriggerParameters Contains the trigger parameters which includes the records that is getting updated.
    */
    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);
    }

    /** 
    * @author Hari Krishnan
    * @date 07/17/2013
    * @description Called by the trigger framework to carry out the actions after the record(s) are created. If there is an
    *               existing call running on the same context, the rentrant call will utilize the handler that was created
    *               in the original call.
    * @param TriggerParameters Contains the trigger parameters which includes the record(s) that got created.
    */
    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);
    }

    /** 
    * @author Hari Krishnan
    * @date 07/17/2013
    * @description Called by the trigger framework to carry out the actions after the records are updated. If there is an
    *               existing call running on the same context, the rentrant call will utilize the handler that was created
    *               in the original call.
    * @param TriggerParameters Contains the trigger parameters which includes the record(s) that got updated.
    */  
    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);
    }
}

Below is an example of one of the classes the above Dispatcher class would call. Using Hari's framework, the actual record sorting and logic for handling the record processing would be handled in this class:

public class AccountAfterUpdateTriggerHandler extends TriggerHandlerBase {
    public override void mainEntry(TriggerParameters tp) {
        process((List<Account>)tp.newList);
    }

    private void process(List<Account> 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...
    }
}

There are other trigger class frameworks that exist which have been proposed by various coders in our community. The most well known would be the Trigger Pattern for Tidy, Streamlined, Bulkified Triggers by Tony Scott.

The primary class that does the processing for Tony's Architecture is a Class SObject Handler. Below is an example showing the Account Handler Class which is where one would implement their selection logic before calling any sub classes to handle logic specific to a trigger context. This is probably the most commonly used class based trigger framework. It's based on the Trigger Factory Class:

/**
 * Class AccountHandler
 *
 * Trigger Handler for the Account SObject. This class implements the ITrigger
 * interface to help ensure the trigger code is bulkified and all in one place.
 */

public with sharing class AccountHandler
    implements ITrigger
{   
    // Member variable to hold the Id's of Accounts 'in use'
    private Set<Id> m_inUseIds = new Set<Id>();

    // Member variable to record Audit records
    private List<Audit__c> m_audits = new List<Audit__c>();

    // Constructor
    public AccountHandler()
    {
    }

    /**
     * bulkBefore
     *
     * This method is called prior to execution of a BEFORE trigger. Use this to cache
     * any data required into maps prior execution of the trigger.
     */
    public void bulkBefore()
    {
        // If this a delete trigger Cache a list of Account Id's that are 'in use'
        if (Trigger.isDelete)
        {
            // pre load all the in use projects passed to this trigger
            m_inUseIds = AccountGateway.findAccountIdsInUse(Trigger.oldMap.keySet());
        }
    }

    public void bulkAfter()
    {
    }

    public void beforeInsert(SObject so)
    {
    }

    public void beforeUpdate(SObject oldSo, SObject so)
    {
    }

    /**
     * beforeDelete
     *
     * This method is called iteratively for each record to be deleted during a BEFORE
     * trigger.
     */
    public void beforeDelete(SObject so)
    {   
        // Cast the SObject to an Account
        Account myAccount = (Account)so;

        // Examine the Set and if the account is in use don't allow it to be deleted.   
        if (m_inUseIds.contains(myAccount.Id))
        {
            // Add the error to the offending object
            so.addError('You cannot delete an Account that is in use.');
        }
        else
        {
            // Add an audit record to the list
            Audit__c myAudit = new Audit__c();
            myAudit.Description__c = 'Account Name: ' + myAccount.Name + ' (Id: ' + myAccount.Id + ') was deleted';

            m_audits.add(myAudit);
        }
    }

    public void afterInsert(SObject so)
    {
    }

    public void afterUpdate(SObject oldSo, SObject so)
    {
    }

    public void afterDelete(SObject so)
    {
    }

    /**
     * andFinally
     *
     * This method is called once all records have been processed by the trigger. Use this 
     * method to accomplish any final operations such as creation or updates of other records.
     */
    public void andFinally()
    {
        // insert any audit records
        if (!m_audits.isEmpty())
        {
            insert m_audits;
        }
    }
}

As you can see from these three example architectures, one can do their filtering of records either in their primary "landing class" or in their initial "sub class" that handles the isInsert, isUpdate or isDelete, as well as the isBefore and isAfter logic. Regardless of which logic step you take on first, I'd recommend you be consistent in applying that logic with whatever framework you choose to use.

Since this is rather advanced material, I hope you find it more helpful than confusing. :)

4
  • Thanks for going to all that effort I will have a good read :)
    – Girbot
    Mar 9, 2015 at 16:40
  • You're welcome. I know this is heady material from having spent many hours working to try and wrap my head around it initially myself. Our Dallas SF DUG will be fortunate to have @DanAppleman presenting later this Month (Mar 25th, 2015) with new material planned for release in the upcoming 3rd edition of his book. Needless to say, I'm immensely looking forward to his presentation.
    – crmprogdev
    Mar 9, 2015 at 16:51
  • 2
    I'd add one more - the trigger pattern in Andrew Fawcett's Force.com Enterprise Architecture: github.com/financialforcedev/fflib-apex-common. The nice thing about this one is it is integrated with all the other great work on design patterns
    – cropredy
    Mar 9, 2015 at 18:56
  • There's actually at least one or two more out there. One in particular that come to mind is Kevin O'Hara's sfdc trigger framework that's available on Github. I only discussed what I was most familiar with, but thanks for the reminder on Andrew's. Is that the "generic" sObject install trigger pattern? If not, then there's another trigger pattern to add to the list too.;)
    – crmprogdev
    Mar 9, 2015 at 19:20
6

My typical pattern is to use a Handler and a Service, and my only logic in the trigger body is to delegate to the Handler. The trigger body sets state and defines WHEN we are in the execution context. The Handler then defines WHAT actions to perform with WHICH subset of records (filtering). The Service then defines HOW to act or filter. I find that it is pretty easy to read and understand when I am working with code someone else wrote using this pattern.

One HUGE ADVANTAGE to this approach is that your action methods no longer need to conflate filtering with whatever task they are assigned to perform. So setting up data for your tests is much more straightforward. The Selector documentation covers this pretty well, but you can pretty easily test all of your filter edge cases and then not worry so much about which records you are passing to your action test.

trigger Opportunity on Opportunity(before insert, after insert, before update)
{
    OpportunityTriggerHandler handle = new OpportunityTriggerHandler(
        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();
    }
}

Now in my handler I just specify WHAT actions to perform on WHICH records. This is where the Selector library really helps.

public with sharing class OpportunityTriggerHandler()
{
    @TestVisible static Boolean bypassTrigger = false; // handy for testing
    final List<Opportunity> newRecords;
    final Map<Id, Opportunity> oldMap;

    public OpportunityTriggerHandler(List<Opportunity> newRecords, Map<Id, Opportunity> oldMap)
    {
        this.newRecords = newRecords;
        this.oldMap = oldMap;
    }

    public void beforeInsert()
    {
        if (bypassTrigger) return;

        OpportunityServices.pullAccountData(
            OpportunityServices.needsAccountData().filter(newRecords)
        );
    }
    public void afterInsert()
    {
        if (bypassTrigger) return;

        OpportunityServices.pushAccountData(
            OpportunityServices.hasAccountUpdates().filter(newRecords)
        );
    }

    public void beforeUpdate()
    {
        if (bypassTrigger) return;

        OpportunityServices.pullAccountData(
            OpportunityServices.needsAccountData().filter(newRecords)
        );
    }
}

Then I have a service that describes all of my HOW questions. How do I perform an action or filter?

public with sharing class OpportunityServices
{
    public static final String CLOSED_WON = 'Closed Won';

    public Select.Filter needsAccountData()
    {
        return Select.Field.isEqual(Opportunity.Name, null);
    }
    public Select.Filter hasAccountUpdates()
    {
        return Select.Field.hasChanged(Opportunity.StageName).andX(
            Select.Field.isEqual(Opportunity.StageName, CLOSED_WON)
        );
    }

    public static void pullAccountData(List<Opportunity> filteredOpportunities)
    {
        // set some fields on your filter results
    }

    public static void pushAccountData(List<Opportunity> filteredOpportunities)
    {
        // set some fields on accounts relevant to your filter results
    }
}
2
  • Seriously no explanation on the down vote?
    – Adrian Larson
    Mar 9, 2015 at 21:45
  • Hey Adrian, I am interested in better understanding your approach as it shares some of the philosophies I have been working towards but looks like you are much further along. I am close to following you but I think I am missing a few key points to get me through your design.
    – Daryn
    Mar 24, 2020 at 21:01
4

In general, I create methods like yours above within my trigger utility class, or another utility class to grab the records to pass into your method. So I basically take the logic in your trigger and move it to a utility class and the trigger now looks something like this

trigger OptyTrigger on Opportunity (after insert, after update) {     
     OptyUtils.optyLostReason(OptyUtils.getLostOpps(trigger.new));
}

You don't necessarily need to use the same utility class, but you can. This basically just assumes the logic you have in your trigger is moved to this utility method.

Same effect in the end, but it keeps the logic out of the trigger. Another big advantage of this is the portability. A method to retrieve all lost opps from a list it is passed seems like something that might be useful in other areas. Now you wont have to rewrite that logic, you can reuse the method in your utility.

6
  • 1
    What's the benefit of doing it in the utility class? I feel like if I do the filtering in the utility class instead I'm potentially calling a class unnecessarily if no records are relevant.
    – Girbot
    Mar 9, 2015 at 13:15
  • The benefits are keeping the trigger logic-less and the portability/reusability of the code. Mar 9, 2015 at 13:22
  • Id argue with that. A method that requires a trigger is less reusable then a method which takes a list. Mar 9, 2015 at 13:27
  • 2
    This is a method that takes a list. How does this method require a trigger? It takes a list. Trigger.new is just a list. Its not any special or particular type of list. Its just a list. This method on the utility just takes any list, therefore can be used in many other places besides this trigger. The method in no way requires a trigger. Mar 9, 2015 at 13:31
  • I think DashboardReport typed "argue" instead of "agree". Mar 9, 2015 at 18:17
2

I realize this is not directly an answer to your question, but this overview of single trigger development was very helpful for me, and a good way to manage things as your organization grows in complexity: Table-Driven Trigger Management.

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .