Apply Domain Layer Principles in Apex

Learning Objectives

After completing this unit, you’ll be able to:

  • Create a Domain Apex class.
  • Reflect your defaulting and validation code in a Domain class.
  • Map methods in the Domain class to Apex trigger events.
  • Control the application of security enforcement at runtime.

Creating Domain Classes

The fflib_SObjectDomain class used in the earlier trigger code is actually a base class for all domain classes, providing some useful functionality, such as object security.

fflib_SObjectDomain class

Note: Most methods are provided as virtual methods (those with a v next to them). Overriding them is optional. The handle*XXXX* methods in this base class are responsible for ensuring that the on*XXXX* methods are called at the appropriate times. For more advanced cases in which you need direct handling, you can choose to override handler methods. But be aware that, unless you call the super base class version of the handler method, the on*XXXX* methods aren’t called.

The base class uses the template method pattern to provide standard hooks to implement common domain logic for validation of records via onValidate and defaulting field values via onApplyDefaults.

In addition, there are methods for placing logic relating to specific Apex trigger events. Finally, the constructor (for which all classes that extended the fflib_SObjectDomain class must also expose) takes a list of sObjects as per the bulkification design goal described in the previous unit.

Here is a basic implementation of the Opportunities domain class.

public class Opportunities extends fflib_SObjectDomain {

    public Opportunities(List<Opportunity> sObjectList) {
        super(sObjectList);
    }

    public class Constructor implements fflib_SObjectDomain.IConstructable {
        public fflib_SObjectDomain construct(List<SObject> sObjectList) {
            return new Opportunities(sObjectList);
        }
    }
}

Note: The Constructor inner class allows the base class method SObjectDomain.triggerHandler used in the Apex trigger sample shown in the previous unit to create a new instance of the domain class passing in the sObject list (for example, Trigger.new). This is a workaround to the present lack of full reflection in Apex.

Implementing Field Defaulting Logic

To provide a place for field defaulting logic, the base class exposes the onApplyDefaults method. This method is called from the handleBeforeInsert base class method during a trigger invocation.

Placing logic here ensures that defaulting occurs consistently across the application when records are added. You can also call it explicitly, if needed, from a service that helps present default record values to a user accessing a custom UI via a Visualforce page or Lightning component, for example.

The base class exposes the sObject list provided during the constructor call to all methods via the Records property. Although we’re not strictly in a trigger scenario here, it’s still strongly encouraged to consider bulkification, as per the Domain design goals in the previous unit.

public override void onApplyDefaults() {

    // Apply defaults to Opportunities
    for(Opportunity opportunity : (List<Opportunity>) Records) {
        if(opportunity.DiscountType__c == null) {
            opportunity.DiscountType__c = OpportunitySettings__c.getInstance().DiscountType__c;
        }               
    }
}

Note: The above example is also possible through using a formula expressed as part of the DiscountType__c field definition. Of course, defaulting logic could span multiple fields or other records. In that case, you must resort to Apex coding.

Implementing Validation Logic

Although you can override any of the above trigger methods to implement validation logic, it’s platform best practice to do this only in the after phase of the Apex trigger invocation. By overriding one of the two onValidate methods, you can implement this logic in a clearly defined place.

public override void onValidate() {

    // Validate Opportunities
    for(Opportunity opp : (List<Opportunity>) Records) {
        if(opp.Type.startsWith('Existing') && opp.AccountId == null) {
            opp.AccountId.addError('You must provide an Account when ' +
                'creating Opportunities for existing Customers.');
        }
    }
}

The above validate method is called from the base class when records are inserted on the object. If you require validation logic that is sensitive to data changing during record updates, you can override the following variant.

public override void onValidate(Map<Id,SObject> existingRecords) {

    // Validate changes to Opportunities
    for(Opportunity opp : (List<Opportunity>) Records) {
        Opportunity existingOpp = (Opportunity) existingRecords.get(opp.Id);
        if(opp.Type != existingOpp.Type) {
            opp.Type.addError('You cannot change the Opportunity type once it has been created');
        }
    }
}

Note: Code in the base class methods handleAfterInsert and handleAfterUpdate ensures that security best practice is enforced by calling this method only during the after part of the Apex trigger (after all Apex triggers on this object have completed). This behavior is most important to AppExchange package developers (see the Resource section).

Implementing Apex Trigger Logic

It’s not always the case that all domain logic you implement falls into the above methods. Indeed, it’s not a strict requirement to implement all defaulting or validation logic in these methods, as per the Separation of Concerns guidelines. It’s purely a consideration. If you prefer, you can put it all in the methods below.

To implement Apex trigger-related code that invokes behaviors via other domain objects, the following is a slightly contrived example of overriding the onAfterInsert method to update the Description field on related Accounts whenever new Opportunities are inserted.

public override void onAfterInsert() {
    // Related Accounts
    List<Id> accountIds = new List<Id>();
    for(Opportunity opp : (List<Opportunity>) Records) {
        if(opp.AccountId!=null) {
            accountIds.add(opp.AccountId);
        }
    }

    // Update last Opportunity activity on related Accounts via the Accounts domain class
    fflib_SObjectUnitOfWork uow =
      new fflib_SObjectUnitOfWork(
        new Schema.SObjectType[] { Account.SObjectType });
    Accounts accounts = new Accounts([select Id from Account
      where id in :accountIds]);
    accounts.updateOpportunityActivity(uow);
    uow.commitWork();              

}

Some things to note about the above example.

  • An instance of the Accounts Domain class is initialized using an inline SOQL query. The next unit in this module introduces a pattern that helps encapsulate query logic for better reuse and consistency around the resulting data, which is important to the Domain class logic.

  • The fflib_SObjectUnitOfWork method is used in a Apex trigger context rather than a service context as per the SOC module. In this case, its scope is a trigger event or method. These methods are directly called by the platform, not the service layer. As such a Unit of Work is created and given to the Accounts method for it to register updates to the Account records. Although not shown here, it’s typically good practice to have a single place to initialize the Unit of Work to avoid duplication.

  • Delegation to the Accounts domain class is appropriate here because updating activity based on Accounts is more a behavior of the Account object rather than Opportunity. This type of SOC between Domain classes is also illustrated later in the next section.

For reference, here is the Accounts.updateOpportunityActivity method.

public class Accounts extends fflib_SObjectDomain {

    public Accounts(List<Account> sObjectList) {
        super(sObjectList);
    }

    public void updateOpportunityActivity(fflib_SObjectUnitOfWork uow) {

        for(Account account : (List<Account>) Records) {
            account.Description = 'Last Opportunity Raised ' + System.today();
            uow.registerDirty(account);
        }
    }
}

Implementing Custom Logic

You are not restricted to implementing only methods that are overridable from the base class. Recall our revised Service layer shown in the previous unit.

public static void applyDiscounts(Set<Id> opportunityIds, Decimal discountPercentage) {
    // Unit of Work
    // ...

    // Validate parameters
    // ...

    // Construct Opportunities domain class
    Opportunities opportunities = new Opportunities( ... );

    // Apply discount via domain class behavior
    opportunities.applyDiscount(discountPercentage, uow);

    // Commit updates to opportunities     
    uow.commitWork();                      
}

This code uses a Domain class method that has the ability to apply a discount to an Opportunity, basically further encapsulating this logic in a place that’s associated with the Domain object.

When required, the code delegates to the OpportunityLineItems domain class to apply line-level discounts. For the sake of this discussion, assume that logic is different for opportunities that leverage product lines.

public void applyDiscount(Decimal discountPercentage, fflib_SObjectUnitOfWork uow) {

    // Calculate discount factor
    Decimal factor = 1 - (discountPercentage==null ? 0 : discountPercentage / 100);

    // Opportunity lines to apply discount to
    List<OpportunityLineItem> linesToApplyDiscount = new List<OpportunityLineItem>();

    // Apply discount
    for(Opportunity opportunity : (List<Opportunity>) Records) {

        // Apply to the Opportunity Amount?
        if(opportunity.OpportunityLineItems.size()==0) {
            // Adjust the Amount on the Opportunity if no lines
            opportunity.Amount = opportunity.Amount * factor;
            uow.registerDirty(opportunity);
        } else {
            // Collect lines to apply discount to
            linesToApplyDiscount.addAll(opportunity.OpportunityLineItems);
        }
    }      

    // Apply discount to lines
    OpportunityLineItems lineItems = new OpportunityLineItems(linesToApplyDiscount);
    lineItems.applyDiscount(this, discountPercentage, uow);
}

Note: You can view the code for the OpportunityLineItems domain class here.

The fflib_SObjectUnitOfWork method is taken as an argument so that the caller (in this case, the OpportunitiesService.applyDiscount method) can pass it in for the Domain code to register work against it. It also gets passed onto the OpportunityLineItems domain class applyDiscount method.

Business Logic in the Domain Class vs. Service Class

Sometimes it can feel less than obvious where to place code. Let’s go back to the SOC principle and think about what the Service and Domain layers are concerned with.

Type of Application Concern Service or Domain Example
Making sure that fields are validated and defaulted consistently as record data is manipulated. Domain Apply default discount policy to products as they are added.
Responding to a user or system action that involves pulling together multiple pieces of information or updating multiple objects. Mostly provides actions that can occur to a collection of records and coordinates everything that is needed to complete that action (potentially with other supporting services). Service Create and calculate invoices from work orders. Might pull in price book information.
Handling changes to records that occur in the application as part of other related records changing or through the execution of a user or system action. For example, defaulting values as required. If changing one field affects another, it’s also updated. Domain How the Account object reacts when an Opportunity is created or how the Discount is applied when the Opportunity discount process is run. Note: This sort of logic might start in the Service layer, but be better served in the Domain layer to manage the size and complexity of the service method size or improve reuse.
Handling of common behavior that applies across a number of different objects. Domain Calculate price on the opportunity product or work order product lines. Note: You could place this in a shared Domain base class, overriding the fflib_SObjectDomain method to hook into the Apex trigger events, with concrete Domain classes extending this class in turn with their behaviors.

Controlling Security Enforcement

By default, the fflib_SObjectDomain base class enforces Salesforce object CRUD security. However, it is invoked for all types of access to the object, whether through a controller or service. Service logic might want to access an object on behalf of the user without requiring permissions to the object.

If you prefer to disable this default behavior and enforce it yourself in your service code, you can use a configuration feature of the base class. The following example shows how to do this in each constructor. Another way is to create your own base class with this code in, and then extend that class for all Domain classes.

public Opportunities(List<Opportunity> sObjectList) {

    super(sObjectList);        

    // Disable default Object Security checking    
    Configuration.disableTriggerCRUDSecurity();
}  

Note: The base class doesn’t offer generic support for enforcing field-level security for updates. This enforcement is still your responsibility.

Testing Domain Classes

Factoring logic into smaller, more encapsulated chunks is beneficial for test-driven development (TDD), because you can more easily construct the domain classes in your tests and invoke the methods directly. This does not mean you should not test your service layer, but it does allow you to apply a more incremental test and develop approach.

Preparation for the Challenges

To complete these challenges, you need to deploy some open source libraries unless you have already deployed them as part of the Apex Enterprise Patterns: Service Layer module. You'll need to install the ApexMocks library first and then Apex Commons library. You can read more about both of these libraries and their respective open source license agreements in their repos.

To install these libraries into your org, simply use the "Deploy" buttons below.

Deploy the ApexMocks open source library.

Deploy to Salesforce

Deploy the Apex Common open source library.

Deploy to Salesforce

Resources

retargeting