Pull to refresh

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


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-коммьюнити.
Tags:
Hubs:
You can’t comment this publication because its author is not yet a full member of the community. You will be able to contact the author only after he or she has been invited by someone in the community. Until then, author’s username will be hidden by an alias.