Как мы внедряли инверсию зависимостей в Salesforce

в 18:32, , рубрики: Песочница, метки: , , ,

Как мы внедряли инверсию зависимостей в Salesforce - 1
Salesforce.com — популярная CRM-система.
Service Locator — шаблон проектирования, позволяющий инкапсулировать процесс получения сервиса с высоким уровнем абстракции. Шаблон использует центральный реестр, называемый «Service Locator», который по запросу возвращает информацию необходимую для выполнения задачи.

Проблема

Часто наши проекты приходят к тому, что становится необходимым начать использовать принцип инверсии зависимостей. Salesforce не имеет готовых DI контейнеров, а также отсутствует Reflection API для реализации собственного. Поэтому мы решили использовать в своих проектах реализацию шаблона Service Locator. Это позволило нам избавиться от следующих проблем:

  1. сильная связанность
  2. сложность тестирования


Если с первым пунктом всё понятно, то для второго я хотел бы привести несколько примеров из жизни:

  • — Несколько запросов в контексте одного выполнения. В этом случае стандартными средствами Salesforce мы можем создать mock-объект только для одного запроса. В нашем же случае мы не ограничены в количестве. При этом тест не приходится изменять и добавлять в него специфические вещи, такие как Test.setMock(...);
  • — DML/SOQL операции. В контексте выполнения тестов мы можем быть ограничены платформой, иногда же разработка нескольких приложений, связанных с одним объектом, идёт на одном сервере и за чужие validation rules/required fields мы отвечать не можем.

Решение

Наше решение включает в себя класс-локатор, а также фабрики для удобства использования.

Первым делом мы создаем класс MyServiceLocator, который будет отвечать за поиск необходимого сервиса в зависимости от контекста выполнения:

public class MyServiceLocator {

    private static final Map<Type, Type> customTypesMap = new Map<Type, Type> {
        MyIDatabase.class => MyDatabase.class,

        // Services
        MyICustomObjectService.class => MyCustomObjectService.class,
        MyIHttpService.class => MyHttpService.class,

        // DAOs
        MyICustomObjectDao.class => MyCustomObjectDao.class
    };

    private static final Map<Type, Type> testTypesMap = new Map<Type, Type> {
        // Mocks
        MyICustomObjectDao.class => MyTestCustomObjectDao.class,
        MyIHttpService.class = MyTestHttpService.class
    };

    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;
    }

}

В данном решении можно использовать Custom Settings для хранения соответствия типов, при этом можно использовать функцию Type.forName(String typeName).

Обычно данный шаблон мы комбинируем с Factory. Для этого создаются подобные следующему классы, объединяющие сервисы различных слоёв приложения (DaoFactory, ServiceFactory и так далее):

public class MyServiceFactory {
    
    private static Object initService(Type t) {
        Type typeForService = MyServiceLocator.resolve(t);
        return  typeForService.newInstance();
    }

    public static MyIHttpService getHttpService() {
        return (MyIHttpService)initService(MyIHttpService.class);
    }

    public static MyICustomObjectService getCustomObjectService() {
        return (MyICustomObjectService)initService(MyICustomObjectService.class);
    }

}

Использование сводится к вызову одной строчки для инициализации и дальнеешее использование сервисов в остальных методах.

public class MyController {
    
    private MyIHttpService httpService;
    private MyICustomObjectService customObjectService;

    public MyController() {
        initServices();
    }

    private void initServices() {
        httpService = MyServiceFactory.getHttpService();
        customObjectService = MyServiceFactory.getCustomObjectService();
    }

    ...
}

Надеюсь это простое и удобное решение поможет кому-то из русскоязычного salesforce-коммьюнити.

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js