Domain Layer - wimvelzeboer/fflib-apex-extensions GitHub Wiki

The domain is one of the three core layers of the Separation of Concern design pattern.

It is a wrapper around a list of objects and it contains both data and behavior.

The only concern/context known to the behavior is the List of objects the domain encapsulates. It is not aware of anything outside the domain, so there are no references to other classes or object types besides the classes it is extended from.

The behavior part of the domain only has four types of methods;

  1. Getters - Retrieve information for the objects inside the domain

  2. Setters - Change data on the objects in the domain

  3. Selectors - Select a subset of records based on criteria and returns another domain with the result

  4. Business logic - a collection of calls to its getters, setters and selectors

A graphical overview

 +--- Domain ------+
 | +-------------+ | 
 | | List< ... > | |
 | +-------------+ |
 |                 |
 | Getters   ----- | ----- >   Set<...>   /   List<...>   /   Map<..., ...>
 |                 |
 | Setters   <---- | ------    data/value in the most primitive form
 |                 |
 | Selectors ----- |----------------  >  +--- Domain ------+
 +-----------------+                     | +-------------+ | 
                                         | | List< ... > | |
                                         | +-------------+ | 
                                         |                 |
                                         | Getters         |
                                         | Setters         |
                                         | Selectors       |
                                         +-----------------+ 

Basic rules

  1. The getter, setter and seletor methods always targets (iterate over) all the records the domain contains.

  2. The domain is not aware of other (S)ObjectType than its own,<br/> Even when dealing with relationships, to the domain the relationship is just a field with an Id.

  3. It is not aware of other classes, with the exception for a constant class of the same (S)ObjectType as the domain.

Naming convention

A domain class name is written in the plural form of the (S)ObjectType, e.g. 'Accounts' for the SObjectType Scheme.Account, or 'CustomerDetails' for the data-class CustomerDetail

Domain Template

Interface

public interface IAccounts extends fflib_ISObjects
{
    // all method signatures go here
}

The interface is extended from fflib_ISObjects and inherits the three core methods getRecords, getRecordIds and getSObjectType.

Implementation

public with sharing class Accounts
        extends SObjects
        implements IAccounts
{
    // Class constructor
    @TestVisible
    private Accounts(List<SObject> records)
    {
        super(records, Schema.Account.SObjectType);
    }

    // Method to create new domain instances via record Ids
    public static IAccounts newInstance(Set<Id> ids)
    {
        return (IAccounts) Application.Domain.newInstance(ids);
    }

    // Method to create new domain instances via records
    public static IAccounts newInstance(List<SObject> records)
    {
        return (IAccounts) Application.Domain.newInstance(
                records,
                Schema.Account.sObjectType
        );
    }

    // Getter Methods

    // Selector Methods

    // Setter Methods

    // Sub-class constructor
    public class Constructor implements fflib_IDomainConstructor
    {
        public fflib_IDomain construct(List<Object> objectList)
        {
            return (fflib_IDomain) new Accounts((List<SObject>) objectList);
        }
    }
}

The implementation class is extended from SObjects as this domain contains a SObjectType, if it were to contain a data-class it should be extended from Objects. This class contains three constructor types. One class constructor method accepting a list of SObjects, two methods to create new instances, and a sub-class named Constructor to permit the dynamic creation of the Domain class by the Application class.

Notice that the class constructor method is private, to prevent direct instantiation from outside the domain itself with the exception for unit tests. Having this constructor set to private will force the use of the Application class to create domain instances. This is to make sure that when you register a mock of a domain that it will always be used.

The two static methods newInstance are the main points to create a new instance of the domain class implementation.

Usage

IAccounts domain = Accounts.newInstance(Ids);
// or
IAccounts domain = Accounts.newInstance(recordList);

Getter Methods

The getter methods retrieve information from the list of object the domain contains. They return data in their most basic form with primitive data types, in Sets, Lists or Maps

Getter method naming conventions

Getter name signature Description Example

get FieldName

Get all the values from one particular field.
Use a plural version of the field name.

List<String> getNames()

get FieldName By FieldName

Get a map with the values of the given first field name grouped by the values of the second field name

Map<Id, String> getNameById()

getBy FieldName

Get a map of domains grouped by the given field value

Map<String, Accounts> getByShippingCountry()

getRecords

Get a list of all the records of the domain

// for all records
public List<Account> getRecords();

// for a subset of records (used by filter methods)
private List<Account> getRecords(fflib_Criteria criteria);

Take a good look at the naming convention as this is very important if you want to increase code reuse. Not adhering to this naming convention might result in having multiple methods doing the same thing.
Imagine you have the following methods; fetchAccountName, getAccountName, getName all doing the same thing.
Too bad and a big waste of your time, so make sure you get it right!

Selector Methods

The selector methods filter the list of objects contained in the domain based on certain criteria. It returns a new instance of a domain containing the objects meeting the criteria.
Try to keep it simple and only have one condition per selector method, and chain them if you need more complexity.

List<Account> records =
    AccountsSelector.newInstance(accountRecords)
        .selectByShippingCountry('Holland')
        .selectByRating('Hot')
        .selectByNumberOfEmployeesGreaterThan(50);

Selector method naming conventions

Filter name signature Description Example

selectBy FieldName ( value )

Create a domain with a subset of records where the given field name had the provided value

Accounts selectByShippingCountry(String countryName);

selectBy FieldName Condition ( value )

Create a domain with a subset of records where the value of the field meets the criteria condition of the provided value.

Accounts selectByNumberOfEmployeesGreaterThan(Integer numberOfEmployees);

selectWith FieldName

Get a domain with values for the given field name

Accounts selectWithShippingCountry()

selectWithBlank FieldName

Get a domain with blank values for a given field name

Accounts selectWithBlankShippingCountry();

selectWithNonBlank FieldName

Get a domain with non-blank values for a given field name

Accounts selectWithNonBlankShippingCountry();

Setter Methods

Setter methods change data in the (S)Objects on the list the domain contains. These methods accept data or values in the most primitive form.

It is important to remember the first rule, to always target all the records in the domain. So, these setter methods changes the data an all objects contained by the domain. If data only needs to be modified on just a sub-set of (S)Objects, then you need to chain the setter method onto a selector method.

List<Account> records =
    AccountsSelector.newInstance(accountRecords)
        .selectByShippingCountry('Holland')
        .selectByNumberOfEmployeesGreaterThan(50)
        .setRating('Hot');
.

Setter method naming conventions

Setter name signature Description Example

set FieldName ( value )

Change all the values of the given field into the provided value

Accounts setShippingCountry(String countryName)

set FieldName By FieldName (Map<Id, String> values)

Change the values of the first field name into the provided value which is grouped by the second field name

Accounts setMailingCountryByAccountId(Map<Id, String> countryNameByAccountId)

Avoiding God classes

Domains for objects with many fields can become very large. One way of addressing this is to create separate classes for each concern inside the domain.

  • Selectors

  • Accessors

or

  • Selectors

  • Getters

  • Setters

public abstract class AccountGetters extends fflib_sObjects { ... }
public abstract class AccountSetters extends AccountsGetters { ... }
public abstract class AccountSelectors extends AccountsSetters { ... }
public class Accounts extends AccountSelectors implements IAccounts { ... }

The domain interface would look like:

public interface IAccounts extends fflib_ISObjects, IAccountGetters, IAccountSetters, IAccountSelectors
{
  ...
}

Multiple Implementations

When working with multiple implementations, we still use the same structure and add a alternative implementation.

In the following example we have a domain for the object Parcel. A parcel contains, among many other things, a track-and-trace code.

public class Parcel
{
    String trackAndTraceCode;
    ...

    public String getTrackAndTraceCode()
    {
        return this.trackAndTraceCode;
    }
    ...
}

The domain has an Interface, defining the method signatures for all domain implementations of the object Parcel.

public Interface IParcels extends fflib_IObjects
{
   IParcels selectWithValidTrackAndTraceCode();
}

There is one main implementation for Parcels domain, containing the default constructors and in this example a filter method to only return a domain with parcels with a valid track-and-trace code.
In this implementation the validation is quite simple as it looks only for a non-blank track-and-trace code.

public class Parcels extends fflib_Objects implements IParcels
{
    ...
    public IParcels selectWithValidTrackAndTraceCode()
    {
        List<Parcel> result = new List<Parcel>();
        for (Parcel object : (List<Parcel>) getObjects())
        {
            if (String.isBlank(object.getTrackAndTraceCode()) continue;

            result.add(object);
        }
        return object;
    }
    ...
}

One day management decides that they want to be able to easily switch between DHL and TNT, just depending on the best contract they can get. Both parcel service companies use a different format for their track-and-trace code, that should be reflected in the implementation.

public class DHLParcels extends fflib_Objects implements IParcels
{
    ...
    public IParcels selectWithValidTrackAndTraceCode()
    {
        List<Parcel> result = new List<Parcel>();
        for (Parcel object : (List<Parcel>) getObjects())
        {
            String trackingCode = object.getTrackAndTraceCode();
            if (String.isBlank(trackingCode) || trackingCode.length() != 10) continue;

            result.add(object);
        }
        return object;
    }
    ...
}

Notice the change in the condition, the tracking code is now only valid when it has a length of 10 characters.

TNT has a more complex tracking code. The validation is also extracted into its own methods, to simplify the condition of the if statement.

public class TNTParcels extends fflib_Objects implements IParcels
{
    ...
    public IParcels selectWithValidTrackAndTraceCode()
    {
        List<Parcel> result = new List<Parcel>();
        for (Parcel object : (List<Parcel>) getObjects())
        {
            String trackingCode = object.getTrackAndTraceCode();
            if (validTrackingCode(trackingCode) == false) continue;

            result.add(object);
        }
        return object;
    }

    private static validTrackingCode(String trackingCode)
    {
        return String.isNotBlank(trackingCode)
               && trackingCode.startWith('GE')
               && trackingCode.endsWith('WW')
               && trackingCode.length() == 13;
    }
    ...
}

The Application class can be used to easily manage which implementation is used. Force-Di can also be utilized here to dynamically switch implementations without making code-changes.

Writing Unit Tests

A unit test for a domain class is one of the easiest to create, as it doesn’t involve any database interaction or mocking. It is only using data created in memory, and therefore these unit-test are the quickest to run.

In most cases we don’t even need valid records, like records with an Id. We just only require the fields that we are using in the method that we want to test. Like in the following example we want to write a test for a domain method that is interacting only with the ShippingCountry field on Accounts.

Account record = new Account(ShippingCountry = 'USA');  // this is enough of mimic an existing record.

When testing with relationships we typically do not even need the related record, only an Id.

Contact record =
      new Contact(
            AccountId = fflib_IDGenerator.generate(Account.SObjectType),
            MailingCountry = 'USA'
      );

Notice that the mandatory field LastName is missing.

The following example contains a unit test for a getter method that returns the values of a field. Notice that only the ShippingCountry field is provided, we do not need anything else.

@IsTest
private class AccountsTest
{
    @IsTest
    static void itShouldGetTheShippingCountryValues()
    {
        // GIVEN a domain with accounts containing ShippingCountry values
        AccountsImp domain = new Accounts(
                new List<Account>
                {
                        new Account(ShippingCountry = 'USA'),
                        new Account(ShippingCountry = 'Ireland'),
                        new Account(ShippingCountry = 'Holland')
                });

        // WHEN we get the ShippingCountry values for the domain
        System.Test.startTest();
        Set<String> result = domain.getShippingCountries();
        System.Test.stopTest();

        // THEN the values should be returned
        System.assert(
            result.containsAll(
                new Set<String> { 'USA', 'Ireland', 'Holland' }
            )
        );
   }
}
⚠️ **GitHub.com Fallback** ⚠️