Design Patterns - Singleton applied to Apex w/o describe call limits
Singleton designs are never as efficient as static variables, which are not as efficient as local (method) variables. Do feel free to use static variables to cache common queries, but do not use them to cache unmetered results, such as describe calls. Depending on the object being described, it can actually increase the CPU time used.
As an example, I give you this:
public class AccountSObjectType {
static DescribeSObjectResult instance;
public static DescribeSObjectResult getDescribe() {
if(instance == null) instance = Account.SObjectType.getDescribe();
return instance;
}
}
In my org, I ran this for a baseline:
Long start = DateTime.now().getTime();
for(Integer i = 0; i < 100000; i++) {
DescribeSObjectResult recordTypeId = Account.SObjectType.getDescribe();
}
Long stop = DateTime.now().getTime();
System.assert(false, stop-start);
The result was about 1900 milliseconds. I figured that using the singleton should improve my performance, right?
Long start = DateTime.now().getTime();
for(Integer i = 0; i < 100000; i++) {
DescribeSObjectResult recordTypeId = AccountSObjectType.getDescribe();
}
Long stop = DateTime.now().getTime();
System.assert(false, stop-start);
The first run result of that was about 3800 ms (subsequent runs got no lower than about 2400 ms). That's right. 50-100% more CPU time than the direct call. The Singleton actually failed to outperform a describe called directly from a Schema object.
So, use the Singleton as a convenience tool (e.g. gather data all in one place), but do not always expect it to give you a boost in performance.
Aside from lower limit consumption, I find another benefit of the Singleton Pattern
is that it simplifies serialization of the involved state. For example, you may want to pass this state to Javascript
for use in a Single Page Application
. Having serializable state made projects involving angular
development way easier for me to manage. I guess that's not in the context of Apex
but it can be a compelling argument.
Two key Limits
still make the Singleton Pattern
effective: Limits.getQueries()
and Limits.getCpuTime()
. In my opinion, this pattern is just a different flavor of lazy-load.
The following is a vanilla example where a Singleton
is still useful. If called many times in a loop, caching would save a large amount of CPU Time
.
public with sharing class RecordTypeCache
{
static RecordTypeCache instance;
public RecordTypeCache getInstance()
{
if (instance == null)
instance = new RecordTypeCache();
return instance;
}
final Map<String, Map<String, RecordType>> cache;
public RecordTypeCache()
{
cache = new Map<String, Map<String, RecordType>>();
for (RecordType rt : /*get all RecordType data*/)
{
if (!cache.containsKey(rt.SObjectType))
cache.put(rt.SObjectType, new Map<String, RecordType>());
cache.get(rt.sObjectType).put(rt.DeveloperName, rt);
}
return cache;
}
public Map<String, RecordType> get(SObjectType schemaType)
{ // Never return a null collection
String sObjectType = String.valueOf(schemaType);
return cache.containsKey(sObjectType) ?
cache.get(sObjectType) : new Map<String, RecordType>();
}
public RecordType get(SObjectType sObjectType, String developerName)
{
return this.get(sObjectType).get(developerName);
}
}
If you don't care about preserving state, having an instance at all seems almost foolish, using static
properties tends to be much more concise.
public with sharing class RecordTypeCache
{
static final Map<String, Map<String, RecordType>> cache;
{
cache = new Map<String, Map<String, RecordType>>();
for (RecordType rt : /*get all RecordType data*/)
{
if (!cache.containsKey(rt.SObjectType))
cache.put(rt.SObjectType, new Map<String, RecordType>());
cache.get(rt.sObjectType).put(rt.DeveloperName, rt);
}
}
public static Map<String, RecordType> get(SObjectType schemaType)
{ // Never return a null collection
String sObjectType = String.valueOf(schemaType);
return cache.containsKey(sObjectType) ?
cache.get(sObjectType) : new Map<String, RecordType>();
}
public static RecordType get(SObjectType sObjectType, String developerName)
{
return this.get(sObjectType).get(developerName);
}
}
Though it should be very rare that you ever need to worry about Limits.getHeapSize()
, you could go so far as to clear out the instance when you are done using it. This sort of manual "garbage collection" should only be used as a last resort, I would think. But if you have many properties, you could clear out heap faster with a singleton than clearing each property one at a time.
Here is an Execute Anonymous
snippet you can use to demonstrate this heap benefit:
class Obj
{
final List<Integer> collection;
Obj() { collection = new List<Integer>(); }
}
static Obj instance;
static Obj getInstance()
{
if (instance == null) instance = new Obj();
return instance;
}
static void clear() { instance = null; }
for (Integer i = 0; i < 100000; i++)
{
getInstance().collection.add(i);
}
system.debug(Limits.getHeapSize()); // 1201092
clear();
system.debug(Limits.getHeapSize()); // 1080