How to develop flexible Apex code for Salesforce


Day-to-day developer work related not only with writing new functionality but updating existing ones. When a developer tasked with adding a new feature to an existing application, the goal is to extend the functionality of that application with new behaviors. Extending software is the introduction of a new behavior by the addition of code. Some applications are flexible to this kind of change, whereas others may fight you tooth and nail!

Flexibility is how easily software can adapt to shifting requirements.

In an ideal extensible system, adding new behavior involves strictly adding new code without changing existing code. Adding new behavior to an extensible system means adding new classes, methods, functions, or data that encapsulate the new behavior. But because real systems are rarely ideal, you’ll still find yourself needing to make changes to existing code regularly.

One of the things that help developers write clean, well-structured, and easily-maintainable code is SOLID principles.

SOLID Principles

SOLID is an acronym for a set of five software development principles. It helps developers create flexible and clean code. The five principles are:

  1. Single-responsibility principle – A class should only have a single responsibility, that is, only changes to one part of the software’s specification should be able to affect the specification of the class.
  2. Open–closed principle – “Software entities … should be open for extension, but closed for modification.
  3. Liskov substitution principle – “Objects in a program should be replaceable with instances of their subtypes without altering the correctness of that program.”
  4. Interface segregation principle – “Many client-specific interfaces are better than one general-purpose interface.”
  5. Dependency inversion principle – One should “depend upon abstractions, [not] concretions.”

Dependency Injection

It is a technique in which an object receives other objects that it depends on. Other objects are called dependencies. In the typical “using” relationship the receiving object is called a client and the passed (that is, “injected”) object is called a service. The code that passes the service to the client can be many kinds of things and is called the injector. Instead of the client specifying which service it will use, the injector tells the client what service to use. The “injection” refers to the passing of dependency (a service) into the object (a client) that would use it.

We can’t implement Dependency Injection using Apex, because we don’t have a Reflection, but we can use the Service Locator Pattern to substitute implementation during runtime.

Service Locator

The service locator pattern is a design pattern used in software development to encapsulate the processes involved in obtaining a service with a strong abstraction layer.

This pattern uses a central registry known as the “service locator”, which on request returns the information necessary to perform a certain task. Proponents of the pattern say the approach simplifies component-based applications where all dependencies are listed at the beginning of the whole application design, consequently making traditional dependency injection a more complex way of connecting objects. Critics of the pattern argue that it is an anti-pattern which obscures dependencies and makes software harder to test.

Benefits of using Service Locator

  1. Helps in Unit testing.
  2. Extending the application becomes easier.
  3. Helps to enable loose coupling

Disadvantages of Service Locator

  1. It’s a bit complex.
  2. It’s a GOD object because every class needs to interact with it to set the dependencies.

Below you can find an example of Service Locator implementation.

public with sharing class ServiceLocator {

    static final Map<Type, Type> customTypesMap = new Map<Type, Type> {};

    static final Map<Type, Type> testTypesMap = new Map<Type, Type> {};

    static Map<String, Object> typeNameToInstance = new Map<String, Object>();

    public static Type resolve(Type t) {
        if (Test.isRunningTest()) {
            if (testTypesMap.containsKey(t)) {
                return testTypesMap.get(t);
            }
        }

        if (customTypesMap.containsKey(t)) {
            return customTypesMap.get(t);
        }

        return t;
    }

    public static Object getInstance(Type t) {
        if (typeNameToInstance.containsKey(t.getName())) {
            return typeNameToInstance.get(t.getName());
        }
        Type theType = resolve(t);
        Object requiredInstance = theType.newInstance();
        typeNameToInstance.put(t.getName(), requiredInstance);
        return requiredInstance;
    }

    public static void setMock(Type originalType, Type mockType) {
        testTypesMap.put(originalType, mockType);
        typeNameToInstance.clear();
    }

    public static void setMockInstance(Type originalType, Object instance) {
        typeNameToInstance.put(originalType.getName(), instance);
    }

    public static void removeMock(Type originalType) {
        testTypesMap.remove(originalType);
        typeNameToInstance.clear();
    }
}

The class contains several Map objects to override the implementation of the specific type:

  1. customTypesMap – during runtime
  2. testTypesMap – during test execution
  3. typeNameToInstance – for caching and to override specific instances

The main used public method is getInstance. It helps to create a new instance of the types based on below Maps and already created instances.

Below you can find some examples of its usages. All classes should create an instance through ServiceLocator.

public with sharing class AccountService {

    public static AccountService getInstance() {
        return (AccountService) ServiceLocator 
            .getInstance(AccountService.class);
    }
}

public with sharing class AccountCnt extends ApexServiceLibraryCnt {
    AccountService myAccountService = AccountService.getInstance();
}

setMock, setMockInstance, removeMock methods will be helpful during test class implementation. It will be used for the substitution of real implementation with a fake one. For example, if you need to mock callout data.

Use this pattern or no, it’s up to you. But extensibility helps you deliver value more quickly and more maintainable. If you find some code that isn’t extensible, first work to make it so before adding the desired behavior. This will pay dividends well into the lifetime of your code.

If you have any questions or problems related to Salesforce Development – let us know. We always glad to help you.