4

Note this is not a question about handling trigger events outside of triggers. There's a good number of established frameworks out there that does this already (e.g., trigger handlers/dispatchers, fflib, etc).

This question is about finding a suitable object-oriented design to simplify the usual 2-step logic of qualifying and executing business functions on trigger records. The former usually consists of meeting a set of criteria based on the new state or differences between old and new states (e.g., newRecord.field != oldRecord.field;). And the latter is usually executed if records have met that criteria (e.g., if (!qualifiedRecords.isEmpty()){ businessMethod(); })

What's a good design to abstract this logic when it becomes repetitive?

0

2 Answers 2

11

If you're looking to separate concerns between filters and actions, the Selector framework can be a great tool. It's free and open source. For example:

List<MyObject__c> hasFieldChanged = Select.Field.hasChanged(MyObject__c.Field__c)
    .filter(trigger.new,  trigger.oldMap);

This makes the following pattern effective for me:

  • Trigger delegates records and events to Handler
  • Handler composes Filters and Actions
  • Service defines Filters and Actions
  • Tests hit Service and Trigger

In my experience, it is easier to write unit tests this way. When your only public method composes the filters and actions already, your complexity is higher than it needs to be. This way, you can test filters quite simply (covered in the Selector documentation), and have more freedom to set up your data as you wish for your action methods.


public trigger MyObject on MyObject__c (/*events*/)
{
    MyObjectTriggerHandler handle = new MyObjectTriggerHandler(trigger.new, trigger.oldMap);
    if (trigger.isBefore)
    {
        if (trigger.isInsert) handle.beforeInsert();
        // etc.
    }
    if (trigger.isAfter)
    {
        if (trigger.isInsert) handle.afterInsert();
        // etc.
    }
}

public with sharing class MyObjectTriggerHandler
{
    @TestVisible static Boolean bypassTrigger = false;
    // Turn the whole thing off for unit tests when needed
    // Granularity can be added to taste

    final List<MyObject__c> newRecords;
    final Map<Id, MyObject__c> oldMap;
    // final collections are non-nillable once set

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

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

        MyObjectService.action1(
            MyObjectService.doesMatchCriteria1().filter(newRecords)
        );
    }
    public void afterInsert()
    {
        if (bypassTrigger) return;

        MyObjectService.action2(
            MyObjectService.doesMatchCriteria2().filter(newRecords)
        );
    }

    public void beforeUpdate() { /* etc. */ }
    public void afterUpdate() { /* etc. */ }
}

public with sharing class MyObjectService
{
    public static Select.Filter doesMatchCriteria1()
    {
        return Select.Field.isEqual(MyObject__c.Checkbox__c, true);
    }
    public static Select.Filter doesMatchCriteria2()
    {
        return Select.Field.notEqual(MyObject__c.TextField__c, null);
    }

    public static void action1() { /* manipulate records */ }
    public static void action2() { /* cross-object */ }
}
2
  • 1
    excellent; another use of the fluent style; I'll be adding this to my repetoire
    – cropredy
    May 14, 2016 at 1:10
  • @cropredy It's pretty awesome. Especially since you can write your own predicates.
    – Adrian Larson
    May 14, 2016 at 2:13
2

I have a similar approach to Adrian Larson's with a few minor differences.

There are also a few variations that I would like to try some time, but haven't had the reason or the motivation (I'm pretty lazy okay!) to do so.

A while back I answered a bounty question on a similar topic, basically the Qualify portion of this question. In doing so, I have used the following answer from it throughout many of my recent projects with great results.

So my code bases always have the following:

public with sharing class Filters 
{
    public interface ITriggerComparisonFilter
    {
        Set<sObject> FilterResults(List<sObject> newsObjectList, Map<Id, sObject> oldMapForsObjects);
    }

    public interface IFilterable { }
    
    public with sharing class AllNewRecords implements Filters.IFilterable, Filters.ITriggerComparisonFilter
    {
        public Set<sObject> FilterResults(List<sObject> newsObjectList, Map<Id, sObject> oldMapForsObjects)
        {
            return new Set<sObject>(newsObjectList);
        }
    }   

    public with sharing class AllOldRecords implements Filters.IFilterable, Filters.ITriggerComparisonFilter
    {
        public Set<sObject> FilterResults(List<sObject> newsObjectList, Map<Id, sObject> oldMapForsObjects)
        {
            return new Set<sObject>(oldMapForsObjects.values());
        }
    }   
}

As you can see in the class, I nested two interfaces. A marker interface called IFilterable and a filtering interface for Triggers called ITriggerComparisonFilter. I can have more for different variations of Filter interfaces if need be and place them in here as well.

Here I also implement the generic and simple implementations of the ITriggerComparisonFilter interface. These I use when I am not really interested in filtering, but I still want to remain consistent in my code.

From here, I start implementing a concrete implementation of my interfaces.

So, say I have an sObject called YourMama__c and you want to implement a filter.

I'd do something like this:

public with sharing class YourMamaFilters 
{
    public with sharing class YourMamaSoFatJokeNeededOnChangeFilter implements Filters.IFilterable, Filters.ITriggerComparisonFilter
    {
        public Set<sObject> FilterResults(List<sObject> newsObjectList, Map<Id, sObject> oldMapForsObjects)
        {
            List<YourMama__c> updatedYourMamaRecords = (List<YourMama__c>)newsObjectList;
            Map<Id, YourMama__c> oldYourMamaMap = (Map<Id, YourMama__c>)oldMapForsObjects;
            Set<sObject> filteredYourMamaRecords = new Set<sObject>();

            for(YourMama__c yourMamasRecord : updatedYourMamaRecords)
                if(NeedsAYourMamaJoke(yourMamasRecord, (YourMama__c)oldYourMamaMap.get(newClient.Id)))
                    filteredYourMamaRecords.add(newYourMamaRecord);

            return filteredYourMamaRecords;
        }

        private Boolean NeedsAYourMamaJoke(YourMama__c newYourMamasRecord, YourMama__c oldYourMamasRecord)
        {
            return oldYourMamasRecord.NeedsJoke == false && newYourMamasRecord.NeedsJoke == true;
        }
    }
    
    public with sharing class YourMamaSoFatJokeNeededOnCreationFilter implements Filters.IFilterable, Filters.ITriggerComparisonFilter
    {
        public Set<sObject> FilterResults(List<sObject> newsObjectList, Map<Id, sObject> oldMapForsObjects)
        {
            Set<sObject> filteredYourMamaRecords = new Set<sObject>();
            for(YourMama__c newYourMamasRecord : (List<YourMama__c>)newsObjectList)
                if(NeedsAYourMamaJoke(newYourMamasRecord))
                    filteredYourMamaRecords.add(newYourMamasRecord);

            return filteredYourMamaRecords;
        }

        private Boolean NeedsAYourMamaJoke(YourMama__c newYourMamasRecord)
        {
            return newYourMamasRecord.NeedsJoke == true;
        }
    }
}

Now, I have a more object oriented way of implementing my qualifying logic.

So in a trigger, you may see different ways you can use this.

if(Trigger.isBefore && Trigger.isInsert)
{
   Set<sObject> yourMamaRecordsNeedingJokes = 
             new Set<sObject>((new YourMamaFilters.YourMamaSoFatJokeNeededOnCreationFilter().FilterResults(Trigger.New, Trigger.OldMap)));
   YourMamaTriggerHandler.SendYourMamaJoke(yourMamaRecordsNeedingJokes); 
}

Where your handler delegates to some service:

public with sharing class YourMamaTriggerHandler
{
    public void SendYourMamaJoke(List<YourMama__c> yourMamaRecordsNeedingJokes)
    {
        (new YourMamaService()).SendYourMamaSoFatJokeRandom((List<YourMama__c>)new List<sObject>(yourMamaRecordsNeedingJokes));
    }
}

public with sharing class YourMamaService 
{
    public void SendYourMamaSoFatJokeRandom(List<YourMama__c> yourMamaRecordsNeedingJokes)
    {
        List<Messaging.SingleEmailMessage> youMamaJokeEmails = new List<Messaging.SingleEmailMessage>();
        Interger randomHelper = 0;
        
        for(YourMama__c singleYourMamaRecord : yourMamaRecordsNeedingJokes)
        {
            Messaging.SingleEmailMessage newYourMamaEmail = new Messaging.SingleEmailMessage();
            newYourMamaEmail.setSubject('Your mama\'s so fat...');
            if(Math.Mod(randomHelper, 0) == 0)
                newYourMamaEmail.setPlainTextBody('Your mama\'s so fat, her weight causes an arithmetic overflow on a Double value');
            /*
                ... Other variations here.
                Yes, this isn't scalable by the way.
            */
            randomHelper++;
            newYourMamaEmail.setToAddresses(new List<String> { singleYourMamaRecord.Email__c });
            youMamaJokeEmails.add(newYourMamaEmail);
        }

        Messaging.sendEmail(youMamaJokeEmails); 
    }
}

Now this is just one way to do it. A very simple way. I have been doing this recently because the projects I have been working on are rather small scale.

There are other things I'd like to try this though:

  • You could make a map that maps Filters to Service implementations and loop through the action as they occur
  • You can create a utility so you can hold onto cached/static variable results
  • You have the option of implementing static variable in the filter to stop recursion as opposed to handling it in the trigger handler (which I prefer)
  • Embed filters within each other to further filter your results or make new combinations

Basically as soon as you decouple the filtering and execution logic, it makes implementing features so much easier. Explore what feels right to you and your team. Look at other frameworks like FinancialForce's or simple trigger frameworks like Trigger Pattern for Tidy, Streamlined, Bulkified Triggers. I can't forget the grandfather of all these, Dan Appleman's triiger design pattern in his Advanced Apex Programming book (Some may disagree, but it also helped with Adam Purkiss' version at Plursight which in my opinion is a little better).

That's the beauty of programming, you don't have stick with a single design, or any at all. Gain inspiration from other languages and software design patterns - experiment. That's what makes the job sort of fun, right ;) .

2
  • Nitpick here, but using Set<SObject> can lead to unpredictable results.
    – Adrian Larson
    Jul 9, 2016 at 15:05
  • 1
    Yeah, I should have put a note that you can alter the interface to Lists. But, I will definitely take your advice on this one. Jul 9, 2016 at 15:08

You must log in to answer this question.

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