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

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. Хотелось бы получить обратную связь от читателей, стоит ли продолжать писать статьи по поводу разработки на данной платформе.

Спасибо.

Комментарии 5

    +3
    Имел несчастье ввязаться в разработку API для SPA-приложения на этой платформе. К сожалению, ничего позитивного в этой платформе, порожденной самим Вельзевулом, я не нашёл.
      –1
      Платформа занимает определенную нишу, решает конкретные задачи и делает это неплохо, несвойственные же ей задачи превращаются в набор «костылей».
        0
        Полностью несогласен. Мой опыт совершенно другой. G12ES очень правильно подметил, про нишу.
        0
        Можно ли обрабатывать с такой системой PUT и DELETE методы?
          0
          Конечно, можно.

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

        Самое читаемое