Wednesday, December 15, 2021

Apex Design Patterns

 Apex allows you to build just about any custom solution on the Force.com platform. But what are the common design patterns and associated best practices for Apex development, and how can you leverage these patterns and best practices to avoid reinventing the wheel?

This article describes how to leverage common design patterns to optimize your code and ensure reusability, maintainability and performance. This is a wiki version of the Dreamforce 2012 presentation 'Apex Design Patterns'.

Contents

Common Design Patterns

The following are a list of design patterns, some of which are standard object-oriented patterns in a Force.com context, and some of which are specific Force.com patterns.

  • Singleton - minimizing object instantiation for improved performance and to mitigate impact of governor limits
  • Strategy - defining a family of algorithms, enscapsulating each one and making them interchangeable and selectable at runtime
  • Decorator - extending the functionality of an sObject in Apex
  • Facade - simplifying the execution of classes with complex interfaces (e.g. web service callouts)
  • Composite - treating a group of objects in a similar manner to a single instance of that object
  • Bulk State Transition - efficiently tracking the change of a field value in a trigger and executing functionality based on this change

Singleton

The Singleton pattern attempts to solve the issue of repeatedly using an object instance, but only wishing to instantiate it once within a single transaction context. Common uses for this pattern include:

  • Global variables - whilst Apex does not support global variables across execution contexts, this pattern allows you to create an object instance that will only ever be instantiated once within an execution context.
  • Limiting Impact of Governor Limits - certain system objects and methods, such as Apex Describes, are subject to governor limits. The Singleton pattern allows repeated reference to these without breaching governor limits.
  • As an implementation to other patterns - other design patterns, such as Facade, are often implemented as Singletons.

However, it's most common use is to create an object instance that's instantiated only once for the lifetime of that execution context.

Problem

Developers often write inefficient code that can cause repeated instantiation of objects. This can result in inefficient, poorly performing code, and potentially the breaching of governor limits. This most commonly occurs in triggers, as they can operate against a set of records.

The following code shows an example of repeated code invocation that can result in a breach of governor limits:

The Trigger

1
2
3
4
5
6
trigger AccountTrigger on Account (before insert, before update) {
    for(Account record : Trigger.new){
        AccountFooRecordType rt = new AccountFooRecordType();
        ....
    }
}


The Class

1
2
3
4
5
6
7
8
9
public class AccountFooRecordType {
    public String id {get;private set;}
    public AccountFooRecordType(){
        // This could breach the governor limits on describes
        // if a trigger is executed in bulk
        id = Account.sObjectType.getDescribe()
            .getRecordTypeInfosByName().get('Foo').getRecordTypeId();
    }
}

The trigger will cause a repeated execution of the sObject getDescribe() method, resulting in a breach of the total number of describes governor limit if the trigger operates against more than 100 records.

Unified Modeling Language

Singleton.png

Implementation

In order to implement a Singleton pattern in Apex, the class must instantiate only a single instance and be globally accessible. It is implemented by:

  • Creating a class with a method that creates a new instance of the class if it doesn't already exist
  • If it already exists, then simply return a reference to that object

The following code sample demonstrates an implementation of the Singleton pattern for returning a record type describe within a trigger:

The Trigger

1
2
3
4
5
6
7
trigger AccountTrigger on Account (before insert, before update) {
    for(Account record : Trigger.new){
        // Instantiate the record type using the singleton class
        AccountFooRecordType rt = AccountFooRecordType.getInstance();
        ....
    }
}

The Singleton Class

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class AccountFooRecordType {
    // private static variable referencing the class
    private static AccountFooRecordType instance = null;
    public String id {get;private set;} // the id of the record type
 
    // The constructor is private and initializes the id of the record type
    private AccountFooRecordType(){
        id = Account.sObjectType.getDescribe()
            .getRecordTypeInfosByName().get('Foo').getRecordTypeId();
    }
    // a static method that returns the instance of the record type
    public static AccountFooRecordType getInstance(){
        // lazy load the record type - only initialize if it doesn't already exist
        if(instance == null) instance = new AccountFooRecordType();
        return instance;
    }
}

The above code demonstrates the following:

  • The getInstance() static method will only instantiate an instance of the class if it doesn't already exist in a lazy-initialization manner
  • The constructor and the instance variable for the class is private to make sure that it cannot be instantiated outside of the getInstance() method
  • The class defines a private, static instance of itself that can only be referenced via the getInstance() static method
  • The ID member stores the record ID of the record type and is initialized in the constructor

This allows the trigger to obtain a reference to the record type without breaching governor limits.

The following code sample shows how to use eager-initialization so that a new instance is always created when the class is instantiated.

The Singleton Class - Eager Initialization variant

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class AccountFooRecordType {
    // a static, final variable that initializes an instance of the class
    // as it's final, it will only be initialized once
    private static final AccountFooRecordType instance = new AccountFooRecordType();
    public String id {get;private set;}
    private AccountFooRecordType(){
        id = Account.sObjectType.getDescribe()
            .getRecordTypeInfosByName().get('Foo').getRecordTypeId();
    }
    public static AccountFooRecordType getInstance(){
        // eager load of the class
        return instance;
    }
}

The instance of the class is instantiated as a final, static variable, which means that only one instance ever exists. This method is typically used if the cost of creating the instance is small.

Conclusion

The Singleton design pattern allows Apex code to repeatedly reference an object instance in an optimal manner, whilst mitigating the impact of governor limits.

Strategy

The Strategy pattern (aka the policy pattern) attempts to solve the issue where you need to provide multiple solutions for the same problem so that one can be selected at runtime.

Problem

You need to provide a geographical-based search engine solution where the implementing code can choose the search provider at runtime.

1
2
geocode('moscone center')
//=> 37.7845935,-122.3994262

Unified Modeling Language

Apex Design Patterns Strategy.png

Implementation

In order to implement a Strategy pattern in Apex, you need to define a family of algorithms, encapsulate each one, make them interchangeable, and selectable at runtime. It is implemented by:

  • Creating an interface class (the Strategy) with methods that will be implemented by other classes
  • Creating a class for each concrete Strategy that implements the methods defined in the Strategy interface

The following code demonstrates an implementation of the Strategy pattern for the underlying GeocodeService strategy and implementations.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public interface GeocodeService{
    List<Double> getLatLong(String address);
}
 
public class GoogleMapsImpl implements GeocodeService{
    public List<Double> getLatLong(String address){
        // Web service callout
        return new List<Double>{0,0};
    }
}
 
public class MapQuestImpl implements GeocodeService{
    public List<Double> getLatLong(String address){
        // Web service callout
        return new List<Double>{1,1};
    }
}

So the specific Unified Modeling Language (UML) for this implementation is

Apex Design Patterns Strategy Geocoder Example.png

where:

  • Context => Geocoder
  • operation() => getLatLong()
  • Strategy => GeocodeService
  • ConcreteStrategyA => GoogleMapsImpl
  • ConcreteStrategyB => MapQuestImpl

Now that we have our core strategy interface and implementations, we need to make use of them. There are a couple of ways you might be tempted to do this, however, in order to decouple as much as possible, consider the following:

The Class

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public class Geocoder {
  public class NameException extends Exception{}
 
  public static final Map<String,GeocodeService> strategies;
 
  static{
      // Retrieve comma delimited list of strategies from a Custom Setting
      GlobalVariable__c gv = GlobalVariable__c.getInstance('strategies');
 
      // Populate a List Collection of strategy names e.g., googleMaps, mapQuest etc
      List<String> strategyNames = new List<String>();
      if(gv != null && gv.value__c != null) strategyNames = gv.value__c.split(',');
 
      // Populate a map of strategy names to concrete implementations
      // using Type.forName for each strategy string
      strategies = new Map<String,GeocodeService>();
      for(String name : strategyNames){
          try{strategies.put(name, (GeocodeService)Type.forName(name + 'impl').newInstance());}
          catch(Exception e){continue;} //skip bad name silently
      }
  }
 
  private GeocodeService strategy;
 
  public Geocoder(String name){
      if(!strategies.containsKey(name)) throw new NameException(name);
      strategy = strategies.get(name);
  }
 
  public List<Double> getLatLong(String address){
      return strategy.getLatLong(address);
  }
}

Calling googleMaps

1
2
3
Geocoder geocoder = new Geocoder('googlemaps');
System.debug(geocoder.getLatLong('moscone center'));
//=> 13:56:36.029 (29225000)|USER_DEBUG|[29]|DEBUG|(0.0, 0.0)

Calling MapQuest

1
2
3
Geocoder geocoder = new Geocoder('MapQuest');
System.debug(geocoder.getLatLong('moscone center'));
//=> 13:56:36.129 (29225000)|USER_DEBUG|[29]|DEBUG|(0.0, 0.0)

As you can see above, the calling code has a choice of implementations and the only change is the string passed to the Strategy interface.

Conclusion

The Strategy design pattern uses aggregation instead of inheritance, allowing better decoupling between the behavior and the class that uses the behavior. This allows the behavior to be changed without breaking the classes that use it, and the classes can switch between behaviors by changing the specific implementation used without requiring any significant code changes.

Decorator

The Decorator pattern attempts to solve the issue where you need temporary fields for processing (typically in Visualforce) but do not need to add these fields to the sObject.

Common uses for this pattern include:

  • Selection Checkbox – a list of records that the user selects and applies behavior to; the selection checkbox is not saved
  • Calculated fields – a complex read-only value that cannot be easily done in a formula field (e.g. calculation of a check digit)
  • Proxy fields – a field, that when updated, converts to a different value on the record (e.g. temperature figures presented to the user in C, but stored as F)

This pattern is primarily for Visualforce use cases.

Problem

You need to obtain or display temporary information on a Visualforce page that is not needed beyond the context of the interaction.

Unified Modeling Language

Apex Design Patterns Decorator.png

Implementation

In order to implement the Decorator pattern in Apex, we need to be aware that this is not a true OO implementation, but the intent is to add behavior at runtime rather than via inheritance at compile time. It is implemented by:

  • Understanding the existing sObject superclass – the actual sObject class.
  • Understanding the existing concrete sObject – our underlying sObject (e.g. Account, Opportunity) – this class is not extensible in Apex
  • Creating an "sObject Decorator" class that wraps the sObject with a pointer to the concrete sObject, with additional operations and properties extending the behavior of the concrete sObject at runtime
  • Creating a Visualforce controller/class that acts as a client to the decorator

Note that this is not a “true” implementation of the OO Decorator pattern:

  • The decorator class does not implement an interface to sObject (you can’t do it)
  • The sObject pointer in the class is public to simplify access to its behaviors (unless you want to recreate all the sObject methods in the class)
  • It does have the intent of decorator, which is to add functionality at runtime

In our example scenario, we have a Weather sObject with City__c and TempInFahrenheit__c fields, and our Visualforce requirements are:

  • the stored temperature in Fahrenheit should be displayed in Celsius
  • when the user enters the temperature in Celsius it is stored as Fahrenheit

Decorator Weather Example.png

The Code - Decorated sObject Class

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class DecoratedWeather {
 
  public Weather__c weather { getprivate set; }
 
  public DecoratedWeather (Weather__c weather) {
            this.weather = weather; 
  }
 
  public Decimal tempInCelcius {
    get {
      if (weather != null && tempInCelcius == null )
        tempInCelcius = new Temperature().FtoC(weather.TempInFahrenheit__c);
                     
      return tempInCelcius; 
    }  
    set {
      if (weather != null && value != null )
        weather.TempInFahrenheit__cnew Temperature().CtoF(value);
 
            tempInCelcius = value;
    }
  }
}

The above code demonstrates how the decorator class extends or wraps the Weather sObject with new functionality.

  • Reference the weather sObject as a public property, but make it a private set. (This is different to the true decorator pattern, which makes this private and delegates behavior instead)
  • Constructor – is passed in the weather object
  • Property for tempInCelcius
  • Getter – converts the temp from F to C and returns it (assuming we have another class to convert temperature)
  • Setter – uses the value entered to convert from C to F and a side effect to store into the weather.tempInF. The last line makes sure that the value is stored back on the property.

The Code - Custom Controller

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class Weather_Controller {
 
  public List<DecoratedWeather> listOfWeather {
 
    get {
      if (listOfWeather == null) {
        listOfWeather = new List<DecoratedWeather>();
 
        for (Weather__c weather : [select name, temperature__c from Weather__c]) {       
            listOfWeather.add(new DecoratedWeather(weather));
        }
      }
      return listOfWeather;
    }
 
    private set;
  }
}

The above code demonstrates how the Weather Controller is the client

  • Has a property that is a list of DecoratedWeather (our wrapper class)
  • Has a getter that lazy initializes the list by selecting from the Weather__c table
  • Private setter – only this class can set it

The Code - Visualforce Page

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<apex:page controller="weather_controller">
  <!-- VF page to render the weather records with Ajax that only rerenders
     the page on change of the temperature
  -->
  <apex:form id="theForm">
    <apex:pageBlock id="pageBlock">
      <apex:pageBlockTable value="{!listOfWeather}" var="weather">
        <apex:column value="{!weather.weather.name}"/>
        <apex:column headerValue="Temperature (C)">
          <apex:actionRegion >
            <apex:inputText value="{!weather.tempInCelcius}">
              <apex:actionSupport event="onchange"
                      reRender="pageBlock"/>
            </apex:inputText>
          </apex:actionRegion>
        </apex:column>
        <apex:column headerValue="Temperature (F)"
              value="{!weather.weather.Temperature__c}"
              id="tempInF"/>
      </apex:pageBlockTable>
    </apex:pageBlock>
  </apex:form>
</apex:page>

The above code demonstrates that the Visualforce page only needs to implement a little AJAX action support to rerender the page if the temperature changes. There is no real client side logic and no other fancy tricks, just a simple rerender. The getter and setter in the decorator class takes care of all the work.

Conclusion

So what we have is an example of how to extend sObject functionality in Apex with behavior at runtime, rather than through inheritance using a pseudo decorator pattern. Most of you will have seen this done with selection checkboxes, but it’s something that can be applied with other use-cases as well.

Facade

The primary purpose of the Facade pattern is to provide a simpler interface to a complex class. This avoids repeated code and increases maintainability. Common uses include:

  • Simplifying the execution of an Apex Web Service Proxy class
  • Simplifying the execution of custom Apex classes with complex interfaces
  • Providing a single interface to execute methods in multiple classes (e.g. multiple web service callouts)

This pattern effectively abstracts one or more complex classes, simplifying their execution for the rest of the application.

Problem

Often times, the execution of a particular class method requires multiple lines of code or is complex in nature. If the same code is repeated multiple times across different parts of the application, this degrades maintainability.

In Force.com, one of the biggest examples of this is the execution of Web Service callouts. The generated Apex code often times requires complex code, such as setting timeout values, setting the target host, as well as setup of the various inputs and parsing of the callout results.

The following demonstrates the issue of repeated code when executing Web Service callouts, especially if the business process requires the execution of multiple web services.

Facade2.png

In the above example, multiple clients attempt to create an account and contact in a target system. This results in repeated code to setup two web service proxies, degrading maintainability.

Unified Modeling Language

Facade.png

1
2
3
4
5
6
7
8
public FooFacade {
  public String LotsOfFoo() {
    Foo1 f1 = new Foo1();
    Foo2 f2 = new Foo2();
   
    return f1.Foo1() + f2.Foo2();
  }
}

Implementation

To implement a facade class, simply create another class that abstracts the implementation of the complex class(es). This facade class usually contains a simpler interface and in some cases, orchestrates the execution of multiple complex classes.

The purpose of the facade class is to simplify the execution of one or more complex classes with a simpler interface, increasing maintainability.

The following code sample shows a facade class that orchestrates the creation of an account and contact in an external system by calling multiple web services:

Facade Class

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
public class CreateCustomerFacade {
 
    // Inner class representing the response from the web service
    public class CreateCustomerResponse {
        public String accountNumber; 
        public String contactNumber;
 
        public CreateCustomerResponse(String accountNumber, String contactNumber) {
            this.accountNumber = accountNumber;
            this.contactNumber = contactNumber;
        }
    }
 
    // Method to call the two external web services
    public String CreateCustomerExternal(String Name, String LastName, String FirstName) {
        // Setup the web service proxy class to create an account in the target system
        CreateAccount_Service.CreateAccount stubCA = new CreateAccount_Service.CreateAccount();
        CreateAccount_Service.Inputs inputCA = new CreateAccount_Service.Inputs();
 
        // Set timeout and endpoint
      stubCA.timeout_x = 60000;
      stubCA.endpoint_x = 'https://www.foo.com/ca';
       
        // Default other values
        inputCA.userid = Userinfo.getUserName();
        inputCA.timestamp = datetime.now();
        inputCA.Name = name.toUpperCase();
     
        // Call the web service and retrieve the account number
        String accountNumber = inputCA.CreateAccount(input);
 
        /* REPEAT FOR CONTACT - Code not shown */
   
        // Generate and return a response
        return new CreateCustomerResponse (accountNumber, contactNumber);              
    }
}

In the above facade class:

  • The CreateCustomerExternal method wraps the execution of the two web service calls to create an account and contact in the external system. Repeated code, such as setting timeout values and endpoints, user IDs, timestamps, etc., is enscapsulated within this method rather than repeated elsewhere.
  • The CreateCustomerResponse inner class contains the account and contact numbers generated by the external system.

The facade class is then called in other parts of the application as follows:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class FooController{
 
    public Account account { get; set; }
    public Contact contact { get; set; }
    
    public void CallCreateCustomerWS() {
        CreateCustomerFacade ca = new CreateCustomerFacade();
 
        CreateCustomerFacade.CreateCustomerResponse resp = ca.CreateCustomerExternal(account.name, contact.LastName, contact.FirstName);
 
  account.accountNumber = resp.accountNumber;
  contact.contactNumber__c = resp.contactNumber;
    }
}

Conclusion

The Facade design pattern increases the maintainability of Apex code by simplifying the execution of one or complex classes with a facade class.

Composite

The Composite Design Pattern allows for representation of expressions, such as;

  • 1 AND 2
  • 1 OR (2 AND 3)
  • (1 AND 2) OR ((3 OR 4) AND 5)

 

Problem

Our developer has a requirement to create a custom version of the Create/Edit List View screen. However, he is really struggling with how to represent an expression (outlined in red). The difficulty is due to the level of recursion involved.

ExpressionScreen.jpg

Note: This discussion does not cover developing the actual screen or parsing the expression - it is limited to representing an expression in Apex.

Unified Modeling Language

Composite uml.jpg

Implementation

To implement the Composite Design Pattern, deploy the following interface and classes in your environment.

Expression Interface

1
2
3
4
5
public interface Expression {
    Expression add(Expression expr);
    Expression set(String name, Boolean value);
    Boolean evaluate();
}

Composite class

1
2
3
4
5
6
7
8
9
10
11
12
13
public abstract class Composite implements Expression{
    public List<Expression> children {getprivate set;}
    public Composite(){ this.children = new List<Expression>(); }
    public Expression add(Expression expr){
        children.add(expr); return this;
    }
    public Expression set(String name, Boolean value){
        for(Expression expr : children) expr.set(name,value);
        return this;
    }
    public abstract Boolean evaluate();
    public Boolean hasChildren{getreturn !children.isEmpty(); }}
}

AndComposite Class

1
2
3
4
5
6
public class AndComposite extends Composite{
    public override Boolean evaluate(){
        for(Expression expr : children) if(!expr.evaluate()) return false;
        return true;
    }
}

OrComposite class

1
2
3
4
5
6
public class OrComposite extends Composite{
    public override Boolean evaluate(){
        for(Expression expr : children) if(expr.evaluate()) return true;
        return false;
    }
}

Variable class

1
2
3
4
5
6
7
8
9
10
11
12
public class Variable implements Expression{
    public String  name  {get;private set;}
    public Boolean value {get;private set;}
    public Variable(String name){ this.name = name; }
    public Expression add(Expression expr){ return this; }
    public Expression set(String name, Boolean value){
        if(this.name != null && this.name.equalsIgnoreCase(name))
            this.value = value;
        return this;
    }
    public Boolean evaluate(){ return value; }
}

Examples/Usage

The examples below illustrate how to use the Expression interface.

Example: 1 AND 2

1
2
3
4
//1 AND 2
Expression expr = new AndComposite();
expr.add(new Variable('1'));
expr.add(new Variable('2'));

Example: 1 OR (2 AND 3)

1
2
3
4
5
6
7
//1 OR (2 AND 3)
Expression expr = new OrComposite();
expr.add(new Variable('1'));
Expression expr2 = new AndComposite();
expr.add(expr2);
expr2.add(new Variable('2'));
expr2.add(new Variable('3'));

Example: Method Chaining

1
2
3
4
5
6
7
8
//no need for expr2 var if using "method chaining"
//last line of add method: return this;
Expression expr = (new OrComposite())
    .add(new Variable('1'))
    .add((new AndComposite())
        .add(new Variable('2'))
        .add(new Variable('3'))
    );

Example: 1 OR (2 AND 3)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//1 OR (2 AND 3)
Expression expr = (new OrComposite())
    .add(new Variable('1'))
    .add((new AndComposite())
        .add(new Variable('2'))
        .add(new Variable('3'))
    )
    .set('1',false)
    .set('2',true)
    .set('3',false);
 
System.debug(expr.evaluate());
//FALSE OR (TRUE AND FALSE) => FALSE
 
expr.set('3',true);
 
System.debug(expr.evaluate());
//FALSE OR (TRUE AND TRUE) => TRUE

Conclusion

The Composite design pattern can be used to represent an expression in Apex regardless of expression complexity, whilst mitigating the impact of governor limits that can result from recursions.

Bulk State Transition

The Bulk State Transition design pattern is a general pattern used for performing bulk actions in Apex based on the change of state of one or more records.

Problem

Our developer has written a trigger to create an order upon the close of an opportunity, however, he has noted that:

  • It always creates a new order every time the closed opportunity is updated
  • When loading via Data Loader, not all closed opportunities result in an order

The Offending Code

1
2
3
4
5
6
7
8
trigger OpptyTrigger on Opportunity (after insert, after update) {
 
  if (trigger.new[0].isClosed) {
    Order__c order new Order__c();
    
    insert order
  }
}

The above code has more than a few issues. Namely;

  • Occurs regardless of prior state - What if a closed opportunity was updated?
  • No bulk handling - What if more than one opportunity was closed?
  • Poor reusability - What if there are other places that we needed to create orders from an opportunity?


Our developer has rewritten the offending code, this time using a trigger and class.

Attempt 2: Trigger

1
2
3
4
5
trigger OpptyTrigger on Opportunity (after insert, after update) {
 
  new OrderClass().CreateOrder(trigger.new);
 
}

Attempt 2: Class

1
2
3
4
5
6
7
8
9
10
11
12
13
public class OrderClass {
   
  public void CreateOrder(List<Opportunity> opptyList) {
    for (Opportunity oppty : opptyList) {
      if (oppty.isClosed) {
        Order__c order new Order__c();
        ...
        insert order;
      }
    }
     
  }
}

While his second attempt improves reusability, there are still the issues:

  • Occurs regardless of prior state
  • No bulk handling


Our developer makes yet another attempt at addressing these concerns.

Attempt 3: Trigger

1
2
3
4
5
6
7
trigger OpptyTrigger on Opportunity (before insert, before update) { 
  if (trigger.isInsert) {
    new OrderClass().CreateOrder(trigger.newMap, null);
  else
    new OrderClass().CreateOrder(trigger.newMap, trigger.oldMap);
  }
}

Attempt 3: Class

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class OrderClass {
   
  public void CreateOrder(Map<Opportunity> opptyMapNew
              Map<Opportunity> opptyMapOld) {
    List<Order__c> orderList = new List<Order__c>();
    for (Opportunity oppty : opptyMapNew.values()) {
      if (oppty.isClosed && (opptyMapOld == null ||
          !opptyMapOld.get(oppty.id).isClosed)) {
        Order__c order new Order__c();
        ...
        orderList.add(order);
      }
    }
    insert orderList ;
  }
}

This last attempt is slightly better. At least it now handles state transition (i.e. only if the opportunity is closed). Also, it's now bulkified – separate list to keep track of orders, and a single bulk insert. However, the order class is highly coupled and is very hard to reuse outside of the trigger context. You can pass in null into the second argument, but that relies on you knowing the inner workings of the class.

Unified Modeling Language

BulkTransition uml.jpg

In the Bulk State Transition design pattern:

  • The trigger checks for eligible records that have changed state
  • Calls a utility class to perform the work, only passing in the eligible records
  • The utility class has a method that does the work in bulk

Implementation

We will implement the Bulk State Transition pattern utilizing two components:

  • A trigger that which creates a filtered list of eligible records that have changed state
  • A utility class that performs the logic for the list of eligible records in bulk

OpptyTrigger

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
trigger OpptyTrigger on Opportunity (after insert, after update) {
  if (trigger.isAfter && (trigger.isInsert || trigger.isUpdate)) {
    List<Opportunity> closedOpptyList = new List<Opportunity>();
     
    //loop through opportunities and check if closed
    for (Opportunity oppty : trigger.new) {
      /* Note: for insert, check current state,
          for update, check current state and prior state */
      if (oppty.isClosed && (trigger.isInsert ||
          (trigger.isUpdate &&
          !trigger.oldMap.get(oppty.id).isClosed)))
        closedOpptyList.add(oppty);
    }
     
    //send eligible records to OrderClass
    if (!closedOpptyList.isEmpty())
      new OrderClass().CreateOrderFromOpptys(closedOpptyList); 
  }
}

Above, we have the trigger, OpptyTrigger:

  • Checks for the type of trigger (IsBefore, IsAfter, etc.) and the DML type (IsInsert, IsUpdate, etc.).
  • Instantiates a list, closedOpptyList, that keeps track of our eligible records.
  • Loops through the trigger.new.
    • If the opty is eligible (IsClosed), we add it to the list of eligible records. The important thing here is that for inserts, no check is made for the state change (since if the criteria is met, it’s always treated as a state change) and if it's an update, we check the oldMap to obtain the prior record’s value. Only if it’s changed will we make it eligible.
  • It is important that the trigger checks for eligibility only and has no logic.

OrderClass

1
2
3
4
5
6
7
8
9
10
11
public class OrderClass {
  public void CreateOrdersFromOpptys(List<Opportunity> opptyList) {
    List<Order__c> orderList = new List<Order__c>();
    for (Opportunity oppty : opptyList) {
      Order__c order new Order__c();
      //more logic here
      orderList.add(order);
    }
    insert orderList ;
  }
}

Above we have the utility class, OrderClass:

  • OrderClass is passed the list of eligible opportunities from our OpptyTrigger.
  • We loop through the list of opportunities, creating a new Order object for each, and adding them to a list of orders.
  • We then insert the list of Orders in one operation to ensure the operation is bulk safe.

Conclusion

With the above example, we've demonstrated implementing the Bulk State Transition design pattern as a means to perform bulk actions in Apex based on the change of state in one or more records.

 

No comments:

Post a Comment