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.
Attachment
was being deprecated in favor ofContentDocument
). 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).public class SalesOrderAttachmentHandler extends AttachmentTriggerHandler implements TriggerHandler