1

Hoping to tap into the wisdom of the Apex design pattern wizards out there. We have recently moved to a trigger framework in order to decouple and standardize our wildly disparate trigger code developed over several years. For better or worse, the framework we chose is theLightweight Trigger Framework.

Here's an implementation of the trigger handler class:

public class AttachmentTriggerHandler implements TriggerHandler {

    public static Boolean triggerDisabled = false;

    public Boolean isDisabled() {
        return triggerDisabled;
    }

    public void beforeInsert(List<SObject> newItems) {
        preProcessAttachment((List<Account>)newitems);
    }

    public void beforeUpdate(Map<Id, SObject> newItems, Map<Id, SObject> oldItems) {}

    public void beforeDelete(Map<Id, SObject> oldItems) {}

    public void afterInsert(Map<Id, SObject> newItems) {}

    public void afterUpdate(Map<Id, SObject> newItems, Map<Id, SObject> oldItems) {}

    public void afterDelete(Map<Id, SObject> oldItems) {}

    public void afterUndelete(Map<Id, SObject> oldItems) {}
}

Now that we have the framework in place, over time we will be gradually converting pre-existing code to use the framework. Presently, we are refactoring four Attachment triggers written independently over time to do specific, unrelated tasks depending on the parent object type the attachment is being added to. In each trigger, we first detect for parent object type and then if it's found, execute some function. For example, in one trigger, if the parent object type is a Sales Order, set a flag on the parent record to send an email during the next email batch run. In another, separate trigger, if the parent object is a Purchase Order, set some other flag related to batch notifications.

So given the new trigger framework, I'm looking for a way to use another interface or bridge so that I can handle different attachment types in a decoupled manner.

I could write a bunch of if else clauses within AttachmentTriggerHandler and just keep adding new methods and bloat that class, however, it seems like it would be more elegant if I could write a new/separate class for each Parent Object for which I want custom behaviors whenever they get an attachment. If a new use case comes up, all I need to do is write a new parent object attachment class and never touch AttachmentTriggerHandler.

So the trouble I'm having is conceptualizing what that bridge or dispatcher class that sits between AttachmentTriggerHandler and the specific object attachment classes might look like.

5
  • 2
    I'm not really sure if there's enough information here to give a decent answer. Why do you think that different attachments need to be handled differently? It's all stored as base64 encoded binary data (Also, I think I remember hearing somewhere that Attachment was being deprecated in favor of ContentDocument). For now, I think that you're getting ahead of yourself. I'd suggest altering your question to focus more on the problem (need to handle files with differences x, y, and z) instead of your preferred approach/solution (use some magical design pattern).
    – Derek F
    Sep 25, 2018 at 18:57
  • I think I understand what you're going for, but do me a favor and give us an example of how you might handle different attachment types differently. (I love this stuff!) Sep 25, 2018 at 20:19
  • @DerekF -- thanks for pressing me. I've provided some more color in the edit above. Probably still too vague and broad stroked -- but let me know if that helps. Sep 25, 2018 at 23:09
  • @ShaneSteinfeld -- provided a bit more color. Hopefully that helps. Sep 25, 2018 at 23:10
  • What do you think about this?: public class SalesOrderAttachmentHandler extends AttachmentTriggerHandler implements TriggerHandler Sep 25, 2018 at 23:56

2 Answers 2

1

Forgive me if I cover things that you already know. I'll be doing so in the interest of making this answer more accessible for others who may not have as much experience.

Analysis

So, the general idea here is you have a List<Attachment>, and each Attachment in that list can have a parentId that points to a different object.

You'll want to handle attachments to different objects differently, but the input should likely stay the same (just a List<Attachment>). This fits rather nicely into the concept of an Interface.

Interfaces are nice here because we can do things like this...

public Interface IMyInterface{
    public Integer add(List<Integer> integerList);
}

public class AddAllIntegers implements IMyInterface{
    public Integer add(List<Integer> integerList){
        // Some implementation here.
        // Doesn't particularly matter what it is
    }
}

public class AddEveryOtherInteger implements IMyInterface{
    public Integer add(List<Integer> integerList){
        // A different implementation
    }
}

public class DoWork{
    public void mainMethod(){
        List<IMyInterface> strategies = new List<IMyInterface>{
            // Classes that implement an interface are a subtype of that interface
            // Thus, we can store these class instances in a List of the interface's type
            // Salesforce takes care of the "up-casting" implicitly
            new AddAllIntegers(),
            new AddEveryOtherInteger()
        };

        List<Integer> someIntList = new List<Integer>{1, 2, 3, 4};

        for(IMyInterface strategy :strategies){
            // All instances of the IMyInterface type/interface must implement
            //   the add() method, and this method must take a List<Integer>.
            // So, even though we're using different concrete implementations
            //   of IMyInterface, we don't need to treat those implementations
            //   differently in the "client" code (the code that uses the implementation)
            strategy.add(someIntList);

            // The first strategy should return 10
            // The second strategy should return either 4 or 6 (depending on if "skip every 
            //   other" means we skip the first or second element of the list)
        }
    }
}

Using interfaces, for me at least, helps to change my frame of mind from
"I need to write this class to do x, y, and z, so I can use it in [other class]" (programming to an implementation)
to
"I need to provide some input, and I expect X to be returned" (programming to a contract).

Programming to a contract has benefits, including making it easier to swap out implementations (if needed). It tends to make code more decoupled (a fancy way of saying that, if we're calling a piece of code "X" in a method of class "Y", "X" itself does not make any alterations to "Y").

Last bit of analysis... to be able to handle attachments to different objects in a different way, we'll need some way to figure out which SObject type the attachment is being related to.

Information we know so far

  • Our input is a List<Attachment>
  • We want to figure out which SObject each Attachment is being related to
  • We want to perform different work based on the SObject from the point above
  • An Interface is probably a good fit
  • Adding processing for a new SObject should not require changes to our solution (beyond creating a new class to handle that processing)

Working on the solution

One of the things that will likely be involved in any implementation here will be the Type class, which contains the forName(String) method. This allows us to generate an instance of a class, dynamically, at run-time (as opposed to statically at compile-time).

I think a good first step would be to separate your incoming List<Attachment> into different lists according to the SObject type of the Attachment's parent Id. I think a Map<Schema.SObjectType, List<Attachment>> is probably the most appropriate structure here because we need to retain information about the attachment's parent object and we want to allow for future expansion with as few modifications as possible.

Map<Schema.SObjectType, List<Attachment>> parentObjectTypeToAttachmentsMap = new Map<Schema.SObjectType, List<Attachment>();

for(Attachment att :attachments){
    // parentId is an Id field, and the Id class provides us a getSObjectType() method
    Schema.SObjectType parentObjectType = att.parentId.getSObjectType();

    // Just some standard map initialization.
    // If the map doesn't contain our key, put the key in the map along with
    //   a new list
    if(!parentObjectTypeToAttachmentsMap.containsKey(parentObjectType)){
        parentObjectTypeToAttachmentsMap.put(parentObjectType, new List<Attachment>());
    }

    // At this point, we're guaranteed to have the parent object's type in the map.
    // So, we can simply fetch the list stored for that key, and add our attachment to it
    parentObjectTypeToAttachmentsMap.get(parentObjectType).add(att);
}

If you haven't done so already, you'll want to create your processing classes. The (probably) easiest way to make your trigger handler be able to handle attachments to new parent objects is to make your processing classes implement an interface (which allows us to make the code to call the processing method in your processing class in the client of the interface (i.e. your trigger handler) the same for all parentIds).

Probably the easiest way to dynamically instantiate each processing class is to follow a naming convention. If all of your attachment processing classes are named ProcessAttachment[SObject name here] (ProcessAttachmentAccount, ProcessAttachmentContract, etc...), then we'll only need the name of the parent SObject to complete the class name. You can put the name of the SObject anywhere in the class name (prefix, postfix, infix), but prefix or postfix is marginally easier to handle. The point is that you just need a consistent naming scheme so that when you add a processor for Lead (for example), your existing code will be able to find that new class without any modifications.

Next, we write the code to instantiate your processing classes based on the SObject type.

Map<Schema.SObjectType, IProcessAttachment> sobjectTypeToProcessingClassMap = new Map<Schema.SObjectType, IProcessAttachment>();

for(Schema.SObjectType sobjType :setOrListOfSObjectTypes){
    IProcessAttachment processingClass;

    // This doesn't need to be on a different line, I'm just doing that to
    //   make it easier to comment.
    // Type.forname() returns an instance of the Type class, which also has a
    //   newInstance() method (which does precisely what you think it would).
    // newInstance() returns a plain Object though, so we do need to cast the result
    // This is one of the major limitations of the Type class, and a good part of
    //   why an interface is required.
    processingClass = (IProcessAttachment)type.forName('ProcessAttachment' + sobjType.getDescribe().getName()).newInstance();

    sobjectTypeToProcessingClassMap.put(sobjType, processingClass);
}

So now we have a Map<Schema.SObjectType, List<Attachment>>, and a Map<Schema.SObjectType, IProcessAttachment>. I hope you can figure out from here how to wire those together to call the appropriate IProcessAttachment implementation.

Not really sure what this pattern would be called. Maybe "dynamic strategy"?

Some closing advice

  • I'd strongly consider adding a check to make sure that ProcessAttachment[type name here] actually exists before calling newInstance()
  • Having a default attachment processing class that does no work is probably a good idea. If you get an attachment for an object you're not handling, you can assign it this no-work class so the Map<Schema.SObjectType, IProcessAttachment> has something in it for all types you encounter.
  • Failing the above, the minimum protection I'd add is to check to see whether or not the Map<Schema.SObjectType, IProcessAttachment> has a value for the SObjectType prior to trying to use the IProcessAttachment
  • The code to generate the Map<Schema.SObjectType, IProcessAttachment> is probably good to keep as a helper method in your trigger handler. I don't think there's justification for making it its own class, and breaking it out into a separate method keeps the number of responsibilities in your trigger event methods low. Also makes it extremely easy to test that you generate the appropriate map.
3
  • Wow. Thank you. What an amazing answer. For now, gonna mark as best as I'm certain I can make it work for my needs. FWIW, I'll try to implement what you suggest -- while also extending it to account for the other methods in the TriggerHandler. Will report back, probably not shortly. Sep 26, 2018 at 21:09
  • curious regarding this line for(Schema.SObjectType sobjType :setOrListOfSObjectTypes) -- do you see setOrListOfSObjectTypes as being a list I maintain manually, or a dynamically generated list of all SOjectTypes. We have hundreds... Sep 26, 2018 at 22:14
  • @mpaler I imagine this as a dynamically-generated collection, but not of all SObject types. You only need to pass in the SObject types that you've extracted from the ParentIds of the attachments you're processing in any given transaction.
    – Derek F
    Sep 26, 2018 at 22:27
0

Thanks again @Derek F for your generous and instructive suggestions. Very helpful. In case it might help others, thought I would share how I implemented your suggestions for my specific case.

First, the IAttachmentProcessor interface. I felt it simplest to just use the same methods that would be triggered from the Trigger handler:

public interface IAttachmentProcessor {
    void beforeInsert(List<Attachment> newAttachments);
    void beforeUpdate(Map<Id, Attachment> newAttachments, Map<Id, Attachment> oldAttachments);
    void beforeDelete(Map<Id, Attachment> oldAttachments);
    void afterInsert(List<Attachment> attachments);
    void afterUpdate(Map<Id, Attachment> newAttachments, Map<Id, Attachment> oldAttachments);
    void afterDelete(Map<Id, Attachment> oldAttachments);
    void afterUndelete(Map<Id, Attachment> oldAttachments);
}

Then, I rewrote AttachmentTriggerHandler as follows:

public inherited sharing class AttachmentTriggerHandler implements TriggerHandler {

    public static Boolean triggerDisabled = false;

    public Boolean isDisabled() {
        return triggerDisabled;
    }

    public void beforeInsert(List<Attachment> attachments) {

        // Create a map of parent object type to attachments.
        Map<Schema.SObjectType, List<Attachment>> parentObjectTypeToAttachmentsMap = getParentObjectTypeToAttachmentsMap(attachments);

        // Create a set of object types that we can use to dynamically
        // instantiate a AttachmentProcessor implementation if one exsits.
        Set<Schema.SObjectType> objectTypeSet = parentObjectTypeToAttachmentsMap.keySet();
        Map<Schema.SObjectType, IAttachmentProcessor> sobjectTypeToProcessorClassMap = getSobjectTypeToProcessorClassMap(objectTypeSet);

        // Now loop through the sobjectTypeToProcessorClassMap and use the SobjectType key
        // to access the attachments contained in parentObjectTypeToAttachmentsMap and pass these 
        // back to the instantiated AttachmentProcessor contained in sobjectTypeToProcessorClassMap.
        if (!sobjectTypeToProcessorClassMap.keySet().isEmpty()) {
            for (Schema.SObjectType key : sobjectTypeToProcessorClassMap.keySet()) {
                List<Attachment> objAttachments =  parentObjectTypeToAttachmentsMap.get(key);
                IAttachmentProcessor attachmentProcessor = sobjectTypeToProcessorClassMap.get(key);
                attachmentProcessor.beforeInsert(objAttachments);
            }
        }
    }

    public void beforeUpdate(Map<Id, SObject> newAttachments, Map<Id, SObject> oldAttachments) {}
    public void beforeDelete(Map<Id, SObject> oldAttachments) {}
    public void afterInsert(Map<Id, SObject> newAttachments) {}
    public void afterUpdate(Map<Id, SObject> newAttachments, Map<Id, SObject> oldAttachments) {}
    public void afterDelete(Map<Id, SObject> oldAttachments) {}
    public void afterUndelete(Map<Id, SObject> oldAttachments) {}

    /**
    * Return a map of parent object type to a list of attachments.
    *
    * @param attachments a raw list of attachments passed to the AttachmentTriggerHandler.
    * @return parentObjectTypeToAttachmentsMap Map of parent object type and attachments.
    */
    public static Map<Schema.SObjectType, List<Attachment>> getParentObjectTypeToAttachmentsMap(List<Attachment> attachments) {

        Map<Schema.SObjectType, List<Attachment>> parentObjectTypeToAttachmentsMap = new Map<Schema.SObjectType, List<Attachment>>();

        for(Attachment att : attachments){
            // parentId is an Id field, and the Id class provides us a getSObjectType() method
            Schema.SObjectType parentObjectType = att.parentId.getSObjectType();

            // Just some standard map initialization.
            // If the map doesn't contain our key, put the key in the map along with
            // a new list.
            if (!parentObjectTypeToAttachmentsMap.containsKey(parentObjectType)){
                parentObjectTypeToAttachmentsMap.put(parentObjectType, new List<Attachment>());
            }

            // At this point, we're guaranteed to have the parent object's type in the map.
            // So, we can simply fetch the list stored for that key, and add our attachment to it
            parentObjectTypeToAttachmentsMap.get(parentObjectType).add(att);
        }

        return parentObjectTypeToAttachmentsMap;
    }


    /**
    * Return a map of object type to an instantiation of the AttachmentProcessor
    * for that object if it exists.
    *
    * @param setOfSObjectTypes A set of parent object types to the attachments being handled by the AttachmentTriggerHandler.
    * @return sobjectTypeToProcessorClassMap A map of object type and an instantiation of AttachmentProcessor.
    */
    public static Map<Schema.SObjectType, IAttachmentProcessor> getSobjectTypeToProcessorClassMap(Set<Schema.SObjectType> setOfSObjectTypes) {

        Map<Schema.SObjectType, IAttachmentProcessor> sobjectTypeToProcessorClassMap = new Map<Schema.SObjectType, IAttachmentProcessor>();

        for(Schema.SObjectType sobjType :setOfSObjectTypes){

            IAttachmentProcessor attachmentProcessor;

            // Prepare the name of the class we would expect for an object AttachmentProcessor.
            // In our case, for custom object, we keep any namespace prefix, get rid of the
            // __c and change any double underscores to single. For example:
            // KNDY4__Sales_Order__c should be KNDY4_Sales_Order. The AttachmentProcessor
            // would be named AttachmentProcessorKNDY4_Sales_Order
            String sobjectClassName = sobjType.getDescribe().getName().removeEnd('__c').replace('__', '_');

            // Type.forname() returns an instance of the Type class, which has a
            // newInstance() method (which does precisely what you think it would).
            Type processor = Type.forName('AttachmentProcessor' + sobjectClassName);
            if (processor != null) {
                // newInstance() returns a plain Object, so we need to cast the result.
                attachmentProcessor = (IAttachmentProcessor)processor.newInstance();
                sobjectTypeToProcessorClassMap.put(sobjType, attachmentProcessor);
            }
        }

        return sobjectTypeToProcessorClassMap;
    }
}

And finally, a sample implementation of IAttachmentProcessor. Do one of these any parent object type that requires custom handling when attachments are doing their thing. Be sure to give it the right class name.

/**
* Implementation of IAttachmentProcessor
*
* Only instantiated for Attachments when parent object is a Sales Order.
*/
public inherited sharing class AttachmentProcessorKNDY4_Sales_Order implements IAttachmentProcessor {

    public void beforeInsert(List<Attachment> attachments){

        // Rename print only attachments before they are inserted.
        salesOrderPrintOnly(attachments);
    }

    public void afterInsert(List<Attachment> newAttachments){

        // Create a new Sales Order contract when attachment is a pure Sales Order.
        createSalesOrderContract(newAttachments);
    }

    public void beforeUpdate(Map<Id, Attachment> newAttachments, Map<Id, Attachment> oldAttachments){}
    public void beforeDelete(Map<Id, Attachment> oldAttachments){}
    public void afterUpdate(Map<Id, Attachment> newAttachments, Map<Id, Attachment> oldAttachments){}
    public void afterDelete(Map<Id, Attachment> oldAttachments){}
    public void afterUndelete(Map<Id, Attachment> oldAttachments){}


    public void salesOrderPrintOnly(List<Attachment> attachments) {
        // Do stuff.
    }

    public void createSalesOrderContract(List<Attachment> attachments) {
        // Do stuff.
    }
}

You must log in to answer this question.

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