Pages

Thursday, February 4, 2016

The Singleton Pattern And "Commonly Used Data"

If you hadn't noticed, Salesforce stored-procedure & trigger programming is basically using Java wrapped around crippled SQL. Which means it's a good idea to be up on your object-oriented design patterns.

One of those that I just used to avoid copying the same SOQL query into a 5th trigger-handler's source code is the Singleton pattern.
(I'll be going back and fixing the 4 older trigger-handlers. Yes, I let it get that bad before I had time to fix it.)


Quick refresher for newer programmers: in object-oriented programming, a "class" is a "cookie cutter" and an "object" is each individual cookie you cut. So, a "car cookie cutter" would say, "Make sure that every car has a number of wheels, a number of doors, a make, a model, a paint color, and a unique serial number. It also needs to be driveable." A "car object" would be the "the 4-wheel 4-door red Ford Taurus with serial #XYZABC12345" - these are the values stored in its "class-level" variables. Like all cars, it would be driveable (this is a "method" that's part of the definition of all "cars" and it comes with this particular car just because it's a car).

When you're defining Apex triggers, you typically don't actually do object-oriented programming with classes. Typically, you define & use "static" methods for the "car cookie cutter" and directly invoke the "drive()" method. Which is a little hard to imagine as a metaphor - driving the car factory instead of the car - so sorry about that. But just trust that, well, if it's not a method that actually needs a particular car to exist, such methods can be defined and used and that's probably what you're actually used to doing as an Apex trigger programmer.


But ... sometimes it IS useful to manufacture a particular car!

And, more specifically, if what you'd like to do is put a single car in a museum and call it "best car ever made" and have all of your code just copy its color and number of doors, you want a Singleton. The reason to do this "copy whatever's in the museum" pattern is that this keeps you from having to hard-code values like "silver" or "4-door." If you ever change your mind about these details, you just change the car in the museum.

Think of a "Singleton" class as the museum itself. It's not open to the public - all you can do is ask the guard, "What color is the car that's in there right now?" You don't even get to know whether they really have a car in there or not. You just know that the sign on the front door says you can ask the guard what color the car is, how many doors it has, etc.

(There's an extension on the "Singleton" programming pattern where, for efficiency, the museum doesn't even exist until the first visitor asks a tourist-info-kiosk for directions to it - at which point the city quickly scrambles to build a museum complete with "4-wheel 2-door silver Ford Taurus" inside before she gets there. Designing the museum this way is called "lazy instantiation." In my code below, I took this pattern a bit further for rare questions & named it "lazy data fetch" in my comments. Think of it like not bothering to paint the car until the first time someone asks the guard what color it is.)


So here's my code for accessing "commonly used info" like "the default Account ID for new Contacts," "the default Owner for records," "the Record Type ID of a given object & record-type-name," etc.

To call the "Singleton" code and ask it "the Record Type ID of a given object & record-type-name," I just write:

Id rtID = UtilityDefaultInfoOftenNeeded.getInstance().getRecordTypeId('Admissions', 'Opportunity');

The call to "getInstance()" asks for directions to the "useful settings" museum (at which point the city scrambles to build one, complete with answers to questions I can ask the guard, if it's not built yet) and gives my code directions to that museum.

For legibility if I'm asking the guard at the door lots of questions, I might instead write:

UtilityDefaultInfoOftenNeeded useful = UtilityDefaultInfoOftenNeeded.getInstance();
Id rtID = useful.getRecordTypeId('Admissions', 'Opportunity');
// (etc.)

Note that I don't say "new UtilityDefaultInfoOftenNeeded()." I made the "constructor" private on purpose to prevent that. Instead, I say "UtilityDefaultInfoOftenNeeded.getInstance()."

In fact, the inability to use "new ..." is really what makes it a "Singleton." You're not allowed to demand that a new "best car ever" or "default settings" museum be built. You can only ask for directions to it via a "public static" method like "getInstance()" and trust that one single museum will exist by the time "getInstance()" returns directions to the museum.


Here's how "UtilityDefaultInfoOftenNeeded" is written (as a "lazy-instantiation singleton"):

public class UtilityDefaultInfoOftenNeeded {
    
    // Please note that many values returned by the "getter" methods of this class could return null,
    // so be sure to check returned values for "== null" if that is important to your code calling these methods.
    
    private static UtilityDefaultInfoOftenNeeded instance = null;
    
    private Id defaultAccountId; // Fallback "Account" for new "Contact" records where not specified
    private Id defaultOwnerID; // Fallback record owner ID for records in the database
    private Id defaultLeadNurturerID; // The User ID of the "default" "Lead Nurturer" staff member

    private Map rtIDs = new Map(); // For holding data from the "Record Type" object


   
    // Private constructor - this is a Singleton class
    private UtilityDefaultInfoOftenNeeded() {
        // In this constructor, we do any computationally expensive or limit-worrisome computations
        // that should be done as soon as the object is instantiated (rather than when the data
        // is requested through a public object-level "getter" method).
        // Checking for "null" is not necessary because this is a constructor - all variables are null so far.
        // We will not populate every object-level variable in this constructor.
        // Some object-level variables' values are rarely needed and more expensive to compute, so we will "lazy data fetch"
        // them in their getter methods.
            if (Schema.getGlobalDescribe().keySet().contains('default_settings__c')) {
                // Set default Contact Account ID, Owner ID, and Lead Nurturer User ID
                // First, grab the "Default Settings" custom setting:
                Default_Settings__c cs = Default_Settings__c.getInstance();
                System.debug('Default Settings custom setting consists of:  ' + cs);
                // Next, initialize Default IDs from this setting:
                defaultAccountId = String.isBlank(cs.Account_ID__c) ? null : cs.Account_ID__c;
                defaultOwnerID = String.isBlank(cs.Owner_ID__c) ? null : cs.Owner_ID__c;
                defaultLeadNurturerID = String.isBlank(cs.Lead_Nurturer_User_ID__c) ? null : cs.Lead_Nurturer_User_ID__c;
            }
    }
    
    // There should be just 1 public method in this class that is STATIC:  "getInstance()."
    public static UtilityDefaultInfoOftenNeeded getInstance() {
        // Lazy instantiation
        if (instance == null) instance = new UtilityDefaultInfoOftenNeeded();
        return instance;
    }
    
    // These three methods may return null Id-typed values, so be sure to check the return value before using.
    // (Developer note - no need to "lazy-data-fetch" these values, as they "lazy-fetched" upon instantiation in the constructor.)
    public Id getDefaultAccountId() {return defaultAccountId;}
    public Id getDefaultOwnerID() {return defaultOwnerID;}
    public Id getDefaultLeadNurturerID() {return defaultLeadNurturerID;}
    
    // This method may return a null Id-typed value, so be sure to check the return value before using.
    public Id getRecordTypeId(String devName, String sObjName) {
        if (rtIDs.isEmpty()) {
            // Lazy data fetch of entire "RecordType" table into this object's "rtIDs" private variable
            for (RecordType rt : [SELECT Id, DeveloperName, SObjectType FROM RecordType]) {
                rtIDs.put((rt.DeveloperName + ';' + rt.SObjectType), rt.Id);
            }
        }
        // Grab the relevant ID and return it (or "null" if not found)
        Id idToReturn = null;
        if (rtIDs.containsKey(devName + ';' + sObjName)) {idToReturn = rtIDs.get(devName + ';' + sObjName);}
        return idToReturn;
    }
    
}

(If you see "string string" end tags at the end of this code, ignore them - my code formatter is inserting them.)


And here's its test class:

@isTest
private class UtilityDefaultInfoOftenNeededTest {

    static testMethod void testInfoOftenNeeded () {
        
        // Set up default custom settings (these are data in a table, so they don't exist in seeAllData=false test classes & need to be made in the test)
        DefaultTestDataAccountFactory.makeAndSetADefaultTestingAccount();
        DefaultTestDataOwnerFactory.makeAndSetADefaultTestingOwner();
        DefaultTestDataLeadNurturerFactory.makeAndSetADefaultTestingLeadNurturer();

        UtilityDefaultInfoOftenNeeded useful = UtilityDefaultInfoOftenNeeded.getInstance();
        
        Test.startTest();
        Test.stopTest();
        
        System.assertEquals(TRUE, useful.getDefaultAccountId() != null, 'getDefaultAccountId() is null.');
        System.assertEquals(TRUE, useful.getDefaultOwnerID() != null, 'getDefaultOwnerID() is null.');
        System.assertEquals(TRUE, useful.getDefaultLeadNurturerID() != null, 'getDefaultLeadNurturerID() is null.');
        System.assertEquals([SELECT Id, DeveloperName, SObjectType FROM RecordType WHERE DeveloperName = 'Admissions' AND SObjectType = 'Opportunity'].Id, useful.getRecordTypeId('Admissions', 'Opportunity'), 'getRecordTypeId(...) is null.');
    }
    
}

No comments:

Post a Comment