Salesforce.com — популярная CRM-система.
Service Locator — шаблон проектирования, позволяющий инкапсулировать процесс получения сервиса с высоким уровнем абстракции. Шаблон использует центральный реестр, называемый «Service Locator», который по запросу возвращает информацию необходимую для выполнения задачи.
Проблема
Часто наши проекты приходят к тому, что становится необходимым начать использовать принцип инверсии зависимостей. Salesforce не имеет готовых DI контейнеров, а также отсутствует Reflection API для реализации собственного. Поэтому мы решили использовать в своих проектах реализацию шаблона Service Locator. Это позволило нам избавиться от следующих проблем:
- сильная связанность
- сложность тестирования
Если с первым пунктом всё понятно, то для второго я хотел бы привести несколько примеров из жизни:
- — Несколько запросов в контексте одного выполнения. В этом случае стандартными средствами 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-коммьюнити.