Регистрация  |  Вход

Паттерны проектирования бизнес приложений на Salesforce - Слой служб (Service Layer).

Продолжаю серию переводов замечательных статей  Andrew Fawcett.

Service layer (Слой служб) (SL) - будет отныне сердцем вашего приложения. На любой вопрос «Как это сделать» должен отвечать именно SL. Не контроллер вашей страницы, как было при использовании структуры MVC. Теперь контроллер будет отвечать только за вызов нужного функционала из SL, никаких обработок данных, никаких обращений к базе данных в контроллере быть не должно.

Таким образом SL поможет инкапсулировать код отвечающий за бизнес логику вашего приложения и сделать его доступным из разных мест. Особенно это будет полезно при разработке приложений для которые будут обслуживать и стандартную работу через браузер и работу с мобильными клиентами и предоставлять доступ к API.



Кто может использовать Service layer? Диаграмма (в начале статьи) показывает всех клиентов, откуда могут производиться обращения к Service layer в Salesforce. Список поистине внушительный, но можно заметить что Apex Triggers отсутствуют. Это потому что логика внутри триггеров относится к Domain Layer (о котором мы поговорим позже), который тесно связан непосредственно с объектами базы данных. Особенностью такой структуры кода является его адаптивность к изменяющемуся окружению. Технологии на схеме расположены слева направо в том порядке, в котором они появлялись в Salesforce в течении последних 5 лет. Что произошло с приложением, если бы вы постепенно внедряли данные технологии? Появилось бы много дублирующих участков кода.



Основные соглашения в ходе проектирования Service Layer



- именование. SL должен быть достаточно абстрактным, чтобы его мог использовать любой клиент. Необходимо давать имя службе, понятное и логично для любого обращающегося к ней клиента: Правильно: InvoiceService.calculateTax(...) Неправильно: InvoiceService.handleTaxCodeTabout(...)



- адаптация. Salesforce выдвигает ряд требований к построению кода, которые в SL также необходимо придерживаться. Основным требованием в Salesforce является bulkificaiton (массовая обработка данных), поэтому на вход службы должен приходить массив данных, а не одиночное значение: Правильно: InvoiceService.calculateTax(List<TaxCalulation> taxCalulations) Неправильно: InvoiceService.calculateTax(Invoice invoice, TaxInfo taxCodeInfo)



- ответственность. SL должен содержать код, выполняющий только задачи связанные с общей обработкой множества объектов. Его можно сравнить с оркестром. В то время код, который относится только к определенному объекту (отдельный музыкант) должен находиться уже в Domain Layer, о котором мы поговорим позже.



- очередность. Так как несколько клиентов могут выполнить запрос в одно и тоже время, это может привести к ошибкам, особенно что касается работы с базой данных. Служба должна уметь обрабатывать такую ситуацию. В лучшем случае должен обеспечиваться сбор всех запросов и их последовательная обработка. Это требование особенно важно в случае оптимизации службы в условиях ограниченного количества SQOL и DML операций.



- управление транзакциями. SL должен обеспечить сохранность данных при возникновении исключительных ситуаций. Одним из примеров является такая ситуация – вы сохраняете объект A и B в рамках одной транзакции, сохранение объекта B приводит к ошибке, состояние базы данных откатывается на состояние до начала транзакции, но объект A уже будет содержать заполненное поле ID. Это может привести к ошибкам в логике приложения. Как решение можно использоваться объекты-клоны и работать уже с ними. В случае успешной транзакции объекты-клоны становятся оригинальными объектами.



- настройка. Служба должна обеспечить изменение поведения при передаче определенных параметров. Например, если в службе присутствует отправка email, то может быть предусмотрена возможность отключения данной функции. Или может потребоваться ситуация, когда логика должны выполняться без внесения изменений в базу данных.



ПРИМЕЧАНИЕ: В Apex транзакции базы данных автоматически выполняются при завершении запроса без ошибок и откатываются при возникновении исключения. Во многих случаях разработчики перехватывают эти исключения для того, чтобы обработать ошибку и передать ее пользователю в «красивом» виде. Для Salesforce это означает что запрос выполнился удачно и все изменения записываются в базу данных. В этом случае ручное управление транзакциями на уровне Service Layer принимает большое значение при проектировании.



Создание службы



Простейшая реализация Service Layer – это правильно названный класс со статическими методами. Методы выполняют определенные задачи бизнес логики, для чего получают данные из окружения и входящих параметров, производят определенные манипуляции и возвращают ответ необходимого типа. Вот пример службы:




public class OpportunityService
{
public static void applyDiscounts(Set<ID> opportunityIds, Decimal discountPercentage)
{
if(opportunityIds==null || opportunityIds.size()==0)
throw new OpportunityServiceException('Opportunities not specified.');
if(discountPercentage<0 || discountPercentage>100)
throw new OpportunityServiceException('Invalid discount to apply.');

// Query Opportunities and Lines
List<Opportunity> opportunities = // ...

// Update Opportunities and Lines (if present)
List<Opportunity> oppsToUpdate = ....
List<OpportunityLineItem> oppLinesToUpdate = ....
for(Opportunity : opportunities)
{
// Apply to Opportunity Amount or Product Lines?
// ...
}

// Update the database
Savepoint sp = Database.setSavePoint();
try
{
update oppLinesToUpdate;
update oppsToUpdate;
}
catch (Exception e)
{
// Rollback
Database.rollback(sp);
// Throw exception on to caller
throw e;
}
}

public class OpportunityServiceException extends Exception
{
// Add members and methods here to communicate data
// relating to exceptions
}
}


Применяя соглашения, описанные выше, можно к параметрам вызова добавить объект настроек. Например если необходимо сделать в приложении предварительный просмотрт изменений перед непосредственной записью их в базу данных:




public static List<Opportunity> applyDiscounts(Set<ID> opportunityIds, Decimal discountPercentage, Options config)


Можно заметить что метод принимает на вход Set<ID>, но всего лишь один параметр discountPercentage. Это не соответствует соглашению о массовой обработке данных в Salesforce. Службы будет выглядеть лучше, если переписать ее таким образом:




public class OpportunityService
{
public class ApplyDiscountInfo
{
public Id OpportunityId;
public Decimal DiscountPercentage;
}

public static void applyDiscounts(List<ApplyDiscountInfo> discInfos)


Использование службы Ниже приведены примеры как можно использовать OpportunityService из различных мест в приложении.



StandardController:




public PageReference applyDiscount()
{
try
{
// Apply discount entered to the current Opportunity
OpportunitiesService.applyDiscounts(
new Set<ID> { standardController.getId() }, DiscountPercentage);
}
catch (Exception e)
{
ApexPages.addMessages(e);
}
return ApexPages.hasMessages() ? null : standardController.view();
}


StandardSetController:




public PageReference applyDiscounts()
{
try
{
// Apply discount entered to the selected Opportunities
OpportunitiesService.applyDiscounts(
new Map<Id,SObject>(standardSetController.getSelected()).keyValues(),
DiscountPercentage);
}
catch (Exception e)
{
ApexPages.addMessages(e);
}
return ApexPages.hasMessages() ? null : standardController.view();
}


Batch Apex:




public with sharing class DiscountOpportunities
implements Database.Batchable<sObject>
{
public Decimal DiscountPercentage {get;set;}

public Database.QueryLocator start(Database.BatchableContext BC) { ... }

public void execute(Database.BatchableContext BC, List<sObject> scope)
{
try
{
OpportunitiesService.applyDiscounts(
new Map<Id,SObject>(scope).keyValues(),DiscountPercentage);
}
catch (Exception e)
{
// Email error, log error, chatter error etc..
}
}

public void finish(Database.BatchableContext BC) { ... }
}


ПРИМЕЧАНИЕ: В execute методе Batch используются только ID, которые передаются в службу, где повторно происходит выборка записей по этим ID. Это пример затронул еще одно соглашение, принятое в Salesforce. В соответствии с Best Practice при работе с Batch при каждом проходе execute необходимо повторно запрашивать данные из базы, несмотря на то, что они приходит в составе scope. Это связано с тем, что при инициализации Batch результаты запроса, которые возвращает метод start кешируются и могут устареть.



JavaScript Remoting:




public class OpportunityController
{
@RemoteAction
public static String applyDiscount(Id opportunityId, Decimal discountPercent)
{
// Call service
OpportunitiesService.applyDiscounts(new Set<ID> { opportunityId }, discountPercent);
}
}


Можно предоставить доступ к Service Layer для REST клиентов. Это будет выглядеть следующим образом:




@RestResource(urlMapping='/opportunity/*/applydiscount')
global with sharing class OpportunityResource
{
@HttpGet
global static void applyDiscount()
{
// Parse context
RestRequest req = RestContext.request;
String[] uriParts = req.requestURI.split('/');
Id opportunityId = uriParts[2];
Decimal discountPercentage = Decimal.valueOf(req.parameters.get('discountPercentage'));

// Call service
OpportunitiesService.applyDiscounts(
new Set<ID> { opportunityId }, DiscountPercentage);
}
}


Подводя итоги можно сказать, что применения данного подхода значительно упростит разработку приложения и его последующее развитие. Кроме того разделение кода на отдельные классы для контроллеров страниц и service layer позволит организовать параллельную работу двух и более разработчиков. Отделение бизнес логики из структуры MVC от Salesforce также дает возможность создать API на базе вашего приложения, которое позволит использовать внутренний функционал сторонними клиентами напрямую.



Оригинал статьи Следующая статья будет посвящена Domain Layer. Это слой позволит инкапсулировать код, который, отвечает за валидацию, инициализацию значений по-умолчанию и другую логику отдельно взятого объекта базы данных. Domain Layer отвечает за выполнение GRUD операций и используется совместно с Apex Triggers. Существует ряд особенностей во взаимодействии Service Layer и Domain Layer, в которых попробуем разобраться в следующей статье.