20 марта 2015

Гибкая архитектура Rest сервисов для разработки на платформе Salesforce.com

CRM-системы
Из песочницы
Salesforce.com — популярная зарубежом CRM система. В России и странах СНГ разботчиков, специализаирующихся на данной платформе не так много, но на рынке труда постоянно появляются вакансии и люди медленно, но верно, переходят на эту платформу.

В рунете не так много ресурсов, посвященный разработке на платформе Force.com, хотелось бы написать ряд статей, касающихся разработки, чтобы увеличить популярность платформы.

Первая статья касается гибкой архитектуры для написания Rest сервисов.

Проблема


Платформа Force.com спроектирована на использование шаблона MVC для разработки приложений и кастомизации существующего функционала. Преимущества серверного рендеринга неоспоримы, но зачастую требуется добавить больше динамичности в приложение, и в данном случае без Rest сервисов не обойтись.

В Salesforce «из-коробки» есть:

Также платформа известна своими лимитами. И в данном случае — это API Usage Limit. Чтобы избавиться от проблем и незатрагивать лимиты было разработано следующее решение.

Решение


Решение заключается в том, чтобы использовать обычную VisualForce страничку с контроллером для получения результата запроса.

В итоге мы имеем страничку со следующим содержимым:

<apex:page controller="GS_RestApiController" action="{!execute}" contenttype="text/javascript" showHeader="false" sidebar="false">
<apex:outputText value="{!response}" escape="false"/>
</apex:page>


При загрузке странички будет выполняться метод execute из контроллера GS_RestApiController. А результат будет биндиться в outputText .

Код контроллера:

public class GS_RestApiController {

    private static final String COMMAND_NAME_PARAM = 'command';

    private Map<String, String> commandAliasNameMap =  new Map<String, String>{
        'test' => 'FirstTest'
    };

    public String response {get; private set;}

    public GS_RestApiController() { }

    public void execute() {
        this.response = getCommand().execute().toJSON();
    }

    private GS_CommandContainer.GS_Command getCommand() {

        Map<String, String> params = ApexPages.currentPage().getParameters();

        String commandName = params.get(COMMAND_NAME_PARAM);

        if (commandAliasNameMap.containsKey(commandName)) {
            commandName = commandAliasNameMap.get(commandName);
        }
        
        params.remove(COMMAND_NAME_PARAM);

        return GS_CommandFactory.create(commandName, params);
    }
}

В контроллере мы используем параметр command для выполнения нужной команды, а также храним соответствие между командой и ее алиасом, если это необходимо.

Все команды хранятся в классе-контейнере — GS_CommandContainer

public class GS_CommandContainer {

    public abstract class GS_Command {

        private Map<String, String> params = new Map<String, String>();

        public void setParams(Map<String, String> params) {
            this.params = params;
        }

        public GS_RestResponse execute() {
            try {
                Object resultObject = perform();
                return new GS_RestResponse(GS_StatusCode.OK, getMessage(), resultObject);
            } catch (GS_Exception exp) {
                String message = exp.getMessage() + exp.getStackTraceString();
                return new GS_RestResponse(GS_StatusCode.ERROR, message);
            } catch (Exception exp) {
                String message =  exp.getMessage() + exp.getStackTraceString();
                return new GS_RestResponse(GS_StatusCode.ERROR, message);
            }
        }

        public abstract Object perform();

        public virtual String getMessage() {
            return null; 
        }
    }

    public class GS_DefaultCommand extends GS_Command {

        public override Object perform() {
            return 'This is defult result.';
        }

        public override String getMessage() {
            return 'This is default message.';
        }
    }

Таким образом, чтобы добавить новую команду, необходимо просто расширить базовый класс GS_Command и реализовать метод perform(), где будет присутствовать логика выполнения.

Для создания экземпляра класса GS_Command предназначена фабрика — GS_CommandFactory.

public class GS_CommandFactory {

    private static final String DOT = '.';
    private static final String COMMAND_CONTAINER_NAME = 'GS_CommandContainer';
    private static final String DEFAULT_COMMAND_NAME = 'GS_DefaultCommand';
    private static final String COMMAND_NAME_TEMPLATE = 'GS_{0}Command';

    private static final String COMMAND_NAME_TEMPLATE_WITH_CONTAINER = COMMAND_CONTAINER_NAME + DOT + COMMAND_NAME_TEMPLATE;
    private static final String DEFAULT_COMMAND_NAME_TEMPLATE_WITH_CONTAINER = COMMAND_CONTAINER_NAME + DOT + DEFAULT_COMMAND_NAME;

    public static GS_CommandContainer.GS_Command create() {
        Type commandType = Type.forName(DEFAULT_COMMAND_NAME_TEMPLATE_WITH_CONTAINER);
        GS_CommandContainer.GS_Command command = (GS_CommandContainer.GS_Command)commandType.newInstance();
        return command;
    }

    public static GS_CommandContainer.GS_Command create(String commandName, Map<String, String> params) {
        if(String.isBlank(commandName)) {
            create();
        }

        String commandClassName = String.format(COMMAND_NAME_TEMPLATE_WITH_CONTAINER, new String[] {commandName});
        Type commandType = Type.forName(commandClassName);
        if(commandType == null) {
            commandType = Type.forName(DEFAULT_COMMAND_NAME_TEMPLATE_WITH_CONTAINER);
        }
        GS_CommandContainer.GS_Command command = (GS_CommandContainer.GS_Command)commandType.newInstance();
        command.setParams(params);
        return command;
    }
}

Она создает экземпляр требуемой команды, в зависимости от переданного параметра, либо создает экземпляр класса по умолчанию, если такая команда не найдена.

Пример использования достаточно прост:
var result = $.post('{!Page.RestApi}', {command : 'test'}); 

Результат:
{"result":"FirstTestResult + Params : {}","message":"FirstTestMessage","code":200}

При запросе без параметров выполняется команда по умолчанию. Имя команды должно соответствовать шаблону COMMAND_NAME_TEMPLATE, описанному в GS_CommandFactory, также можно добавить соотвествие алиаса и имени команды в commandAliasNameMap класса GS_RestApiController.

На мой взгляд, архитектура удобная и легко расширяемая.

Исходный код проекта можно найти на GitHub.

P.S. Хотелось бы получить обратную связь от читателей, стоит ли продолжать писать статьи по поводу разработки на данной платформе.

Спасибо.
Теги:salesforce.comforce.comapexvisualforcerest api
Хабы: CRM-системы
+4
6,3k 17
Комментарии 5
Похожие публикации
Back-end разработчик (Laravel, REST API)
от 100 000 до 140 000 ₽GBMSМожно удаленно
Разработчик CRM
от 170 000 ₽ТакскомМосква
CRM менеджер
от 50 000 до 150 000 ₽BezlimitМосква
Системный аналитик
от 140 000 до 240 000 ₽МТСМоскваМожно удаленно
Разработчик MS Dynamics CRM
от 60 000 ₽GMCSТулаМожно удаленно
Лучшие публикации за сутки