July 31, 2017

Salesforce Apex Solution Using the Strategy Pattern

By Tom Sullivan

A Centare client has a Salesforce data model that uses several one-to-many and many-to-many relationships between the Account object and many custom objects. Junction objects are used to represent the many-to-many relationships.

This data model presents unique challenges when two Accounts are merged using the standard Salesforce Merge user interface. In many cases, an Account merge results in duplicate junction objects when the junction objects that were related to the losing account are re-parented under the winning account as part of standard merge processing. After the merge, the duplicates are removed by the Administrator who manually examines and deletes them using a related list for the junction object.

I was tasked with designing an automated solution that would delete the duplicate records as part of the merge process.

Here are the assumptions about the automated process:

  • The business could define rules that would determine what constitutes a duplicate.
  • Each junction object could have different rules for what constitutes a duplicate.
  • In the future, there may be additional junction objects created that would have to be de-duped following a merge.
  • When a duplicate was found, the only action is to delete it.
  • If the automated duplicate record removal fails for any reason, the merge should still take place.
  • There already exists an Apex trigger handler for the Account object that performs other processing when a merge takes place.

This seemed to me to be a good situation in which to use the Strategy pattern.

As a quick review, here are the principles of the Strategy pattern:

  1. Define a family of algorithms, encapsulate each one, and make them interchangeable. Strategy lets the algorithm vary independently from the clients that use it.
  2. Capture the abstraction in an interface, bury implementation details in derived classes.

Since each junction object could have its own rules regarding what a duplicate is, it seemed to make sense to encapsulate that in separate concrete classes for each junction object. An interface could be created that would be implemented by each concrete class. A "factory" or "manager" service class could be used to determine what collection of junction objects needed to be examined for duplicates based on what object was being merged, and instantiate the correct class(es) as an instance of the interface. In addition, I wanted the solution to be generic enough that it could be used for removing duplicate children following the merge of records of a different object, such as Contact.

Let's examine the design in detail.

First off, I wanted to make the solution data-driven so the "manager" class would use data to determine what children objects needed to be examined for duplicates when records of a specific object (such as Account) were merged. This would allow the manager class to instantiate the correct classes without having to hard-code it.

For this I turned to Custom Metadata. I created a new Custom Metadata Type with one custom field to hold the name of the object that was being merged:

Merge Duplicate Remover 1

Then I created the records for this new type:

Merge Duplicate Remover 2

The manager class can read these records to know what concrete classes to instantiate (DuplicateRemover_Junction_Object_A, etc.). Each concrete class will look for, and delete, duplicate records of a specific junction object resulting from an Account merge.

Next, here is the interface:

public interface IDuplicateRemover {

List<Database.Error> removeDuplicates(string winnerId);
}

There is just one method, removeDuplicates. This method will encapsulate the unique rules for determining a duplicate record. It takes one argument, which is the Salesforce id of the winning Account record from the merge. Taking in the id instead of the full Account object was done because I decided that the duplicate removal could be handled as part of a future call, since the merge would be allowed to complete whether or not the duplicate removal process encountered errors. Future calls cannot use Sobjects as arguments. The method returns a list of database errors. If the process failed in any way, those errors would become part of the body of an email that would be sent to the person who initiated the merge. The user could then remove the duplicates using the current manual process. Each concrete class implements this interface.

Now, let's take a look at the manager class:

public class Service_DuplicateManager {

@future
public static void removeDuplicateChildren(Set<String> parentIdSet, String parentObjectName, String initiatorEmailAddress) { 

    List<Database.Error> allDeletionErrors = new List<Database.Error>();

    // put custom metadata that drives the process into a list where each entry has a value that is
    // a duplicate remover class name to be instantiated for that parent class
    List<Merge_Duplicate_Remover_Class__mdt> duplicateRemoverClasses = [SELECT Parent_Class_Name__c, DeveloperName FROM Merge_Duplicate_Remover_Class__mdt WHERE Parent_Class_Name__c = :parentObjectName];

    List<SObject> parents = (List<SObject>)Database.query('SELECT Id, Name FROM ' + parentObjectName + ' WHERE Id IN :parentIdSet');

    // for each parent object, invoke the interface method on each duplicate remover class
    for (SObject parent : parents) {
            for (Merge_Duplicate_Remover_Class__mdt classMetadata : duplicateRemoverClasses) {
                List<Database.Error> errors = removeObjectDuplicates(classMetadata.DeveloperName, parent.Id);            
            if (!errors.isEmpty()) {
                allDeletionErrors.addAll(errors);                        
            }
        }
    }

    if (allDeletionErrors.size() > 0) {
        sendErrorNotification(allDeletionErrors, parentObjectName);
    }
}

private static List<Database.Error> removeObjectDuplicates(String className, string parentId) {
    Type t = Type.forName(className);
    IDuplicateRemover duplicateRemover = (IDuplicateRemover)t.newInstance();
    return duplicateRemover.removeDuplicates(parentId);
}

private static void sendErrorNotification(List<Database.Error> allDeletionErrors, string parentObjectName) {
    // TODO: build email notification (check existing utility/service classes)
}
}

The "removeDuplicateChildren" method of the Manager is called by the Account trigger in the onAfterUpdate event, because at that point in the merge, all the child records have been re-parented under the winning Account record. The method takes in the following arguments:

  • Set of Salesforce id values for the merge winners (only one when using the standard Merge UI - but all code should be bulkified)
  • A string that is the name of the object that was merged ("Account")
  • The email address of the user who initiated the merge (for sending an error notification)

The method does the following:

  1. Reads in the set of custom metadata records for the object being merged. This will be the three custom metadata records for the Account object shown in the above screen snippet.
  2. Gets the Account record that was the merge winner (again, only one under normal circumstances).
  3. For each winning record the private removeObjectDuplicates method is invoked for each custom metadata record retrieved for the merged Account object. Here the code uses some runtime keywords and methods to generically instantiate an instance of a class whose name is in a variable from the metadata record. The class that is instantiated is cast to the interface, and the interface method is called.

Here's an unfinished sample of one of the concrete classes that implements the interface (all the classes would follow a similar pattern):

public with class DuplicateRemover_Junction_Object_A implements IDuplicateRemover {

public List<Database.Error> removeDuplicates(string winnerId) {

    List<Database.Error> deletionErrors = new List<Database.Error>();

    List<Junction_Object_A__c> recordsToDelete = new List<Junction_Object_A__c>();

    List<Junction_Object_A__c> junctionObjectList = 
        [SELECT Id, Name, Role FROM Junction_Object_A__c WHERE AccountId = :winnerId];

    // TODO: check for duplicates; add any to recordsToDelete

    Database.DeleteResult[] resultList = Database.delete(recordsToDelete, false);

    for (Database.DeleteResult result : resultList) {
        if (!result.isSuccess()) {
            deletionErrors.addAll(result.getErrors());
        }
    }

    return deletionErrors;
}
}

The class implements the removeDuplicates method required by the interface. It queries the list of junction objects associated to the winning Account object. The actual duplicate identification logic is omitted, but that logic would be specific to what defines a duplicate for Junction_Object_A. Any identified duplicate junction object records are added to a collection; then the collection is deleted. If the deletion results in any errors, then the error messages are added to a Database.Error collection and returned by the method. When all duplicate processing is done for the merged object, the Manager class will send an email if any errors were returned from the deletion of the duplicates in any of the invoked classes.

Each concrete class would be separately unit testable.

To conclude, using the principles of the Strategy pattern provides a design that is flexible and easily extensible. For example:

  • If the rules change for determining what a duplicate is for any junction object, that logic is encapsulated in one class and that's all that would need to be changed.

  • If a new junction object is introduced for Account, only two things need to change:

    1. Add a new custom metadata record that contains the name of the duplicate remover class that should be invoked for that junction object.
    2. Create the new duplicate remover class with the specific logic needed for that junction object, along with its corresponding unit test class.
  • If duplicates need to be removed for other objects that are merged, such as Contact, existing logic is untouched and only these changes are needed:

    1. Add one or more custom metadata records for the Contact object.
    2. Create new duplicate remover classes for each child junction object that needs duplicates removed (and their unit test classes).
    3. Add code to invoke the manager class from the appropriate place in the Contact trigger handler class.

Happy Apex coding!


Tom Sullivan - Developer

About Tom Sullivan

When not playing with his grandchildren or following baseball, Tom enjoys doing software development in an agile environment. He possesses a good mix of soft skills that enables him to successfully work directly with business stakeholders, while keeping his hands dirty with technology. Salesforce is quickly becoming a passion that Tom will be able to specialize in for years to come.