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

Паттерны проектирования бизнес приложений на Salesforce — Доменный слой (Domain Layer).

В прошлой статье я рассказал о слое Слое служб как инструменте для вынесения бизнес логики из привычной структуры страница-контроллер. Это позволило создать код, доступный для разных клиентов в Salesforce (Visualforce Controllers, Batch Apex, API) и значительно улучшить процесс разработки самого приложения. В этой статье я опишу новый слой - Domain Layer.



Доменный слой открывает возможности Предметно-ориентированного программирования для Salesforce приложений, написанных на Apex. Domain (software engineering) – набор требований, понятий и функциональности в приложении предназначенный для решения задачи определённой предметной области. В Salesforce данную задачу выполняют Standard и Custom объекты (Account, Contact, Task и т.д), которые позволяют быстро спроектировать и создать рабочую базу данных для вашего приложения.



Но что можно сказать про их функциональность? Проверку, обработку и изменение данных. В случае необходимости реализации сложной логики для отдельного объекта с использованием Apex кода, лучшим решение будет использовать предметно-ориентированного подход и выделить в приложении Domain Layer.



Domain model – объектная модель, которая содержит данные и бизнес логику, предназначенную для проверки, обработки и изменения этих данных. Далее я покажу как данный подход можно реализовать в Salesforce.



Кто использует Domain Layer? Domain Layer будет использоваться с следующих ситуациях: - Манипуляции с базой данных. CRUD операции, как результат работы пользователя с объектами через стандартный интерфейс Salesforce, а также обращение к объектам напрямую через API Salesforce. - Вызов службы (разработкой которых мы занимались в прошлой статье).



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



Важно: Помните, что код, относящийся к Visualforce Controller, Batch Apex, API классам должен использовать бизнес логику из Слоя Служб. Таким образом использование логики из Domain Layer напрямую исключено.



Требования к Domain Layer:



- Массовая обработка данных. Как и для Слоя служб это требование является ключевых в рамках приложения для Salesforce, и подразумевает, что код в Доменном слое должен на вход принимать и обрабатывать список значений.



- Расширение за счет вложенности. Как упоминалось выше, Доменный объект должна объединять в себе и сами данные и логику для их обработки. В Salesforce данные представлены в виде SObjects, которые нельзя наследовать и расширять. Но SObject можно обернуть в класс, который будет содержать дополнительные методы для выполнения задач бизнес логики.



- Объектно-ориентированное программирование. В Salesforce один Custom Object не может наследовать другой в отличии от Apex классов. Поэтому после создания необходимого количества похожих классов со всеми полями и связями в Domain Layer общую логику необходимо вынести в интерфейс и затем наследовать его для расширения и изменения поведения объектов.



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



Использование доменных классов: Для примера приведу код сервиса applyDiscounts из прошлой статьи, но уже с использованием Domain Layer:




global static void applyDiscounts(Set<ID> opportunityIds, Decimal discountPercentage)
{
// Create unit of work to capture work and commit it under one transaction
SObjectUnitOfWork uow = new SObjectUnitOfWork(SERVICE_SOBJECTS);

// Query Opportunities and construct domain class
Opportunities opportunities =
new Opportunities([select ... from Opportunity where Id in :opportunityIds]);

// Apply discount via domain class behaviour
opportunities.applyDiscount(discountPercentage, uow);

// Commit updates to opportunities
uow.commitWork();
}


Доменным объектом здесь является opportunities, который объединяет в себе и данные (List<Opportunity>) и логику по работу с этими данными opportunities.applyDiscount(). Использованием Domain Layer подразумевает, что вся логика связанная с объектом будет находиться в domain class.



В Salesforce из коробки предусмотрены триггеры – инструмент для добавления к объекам дополнительной логики, который срабатывает в ходе DML операций (Insert, Update, Delete). С приходом Domain Layer мы должны вынести всю логику из триггеров в доменные классы, оставив только правильно сформированные вызовы. Такой подход к написанию триггера освещается во многих best practice подходах, потому что триггер сам по себе не является apex классом и имеет упрощенную структуру (нет методов, нельзя наследовать другие классы). Триггер является плохим контейнером для хранения сложного кода. Поэтому salesforce сообщество давно рекомендует выносить логику из триггера в отдельный класс, в чем нам Domain Layer собственно и поможет.



Вот как должен выглядеть триггер:




trigger OpportunitiesTrigger on Opportunity (after delete, after insert, after update, before delete, before insert, before update)
{
// Creates Domain class instance and calls appropriate methods
SObjectDomain.triggerHandler(Opportunities.class);
}


Чтобы упростить разработку доменного объекта, нам предлагают использовать базовый класс SObjectDomain, который будет диктовать структуру и поведение вашего доменного класса.



SObjectDomainMethods



Как использовать базовый класс SObjectDomain на примере доменного класса Opportunities.



Вот пример создания доменного класса Opportunities




public with sharing class Opportunities extends SObjectDomain
{
public Opportunities(List<Opportunity> sObjectList)
{
super(sObjectList);
}

public class Constructor implements SObjectDomain.IConstructable
{
public SObjectDomain construct(List<SObject> sObjectList)
{
return new Opportunities(sObjectList);
}
}
}


Note: The Constructor inner class allows the base class method SObjectDomain.triggerHandler used in the Apex Trigger sample above, to create a new instance of the domain object passing in the SObject list (e.g Trigger.new). Once Apex fully supports reflection, this will not be required and can be removed.



Добавление в доменный класс логики инициализации Для добавления логики инициализации в базовом классе предусмотрен метод onApplyDefaults. Этот метод вызывается из метода handleBeforeInsert в случае срабатывания триггера. Разместив код в этом методе, вы обеспечите его выполнение перед срабатыванием кода в других методах. Также данный метод можно вызывать вручную в случае необходимости. Базовый класс предоставляет доступ всем внутренним методам к списку записей через свойство Records, которое инициализируется в конструкторе.




public override void onApplyDefaults()
{
// Apply defaults to Opportunities
for(Opportunity opportunity : (List<Opportunity>) Records)
{
if(opportunity.DiscountType__c == null)
opportunity.DiscountType__c = OpportunitySettings__c.getInstance().DiscountType__c;
}
}


Добавление логики для проверки Несмотря на то что вы можете добавить логику проверки в любой из методов обработки триггера, лучшей практикой является выполнять проверку после срабатывания триггера. Переназначая метод onValidate  из базового класса вы можете быть уверены, что ваша проверка сработает в нужное время. Метод onValidate вызывается из методов handleAfterInsert и handleAfterUpdate.




public override void onValidate()
{
// Validate Opportunities
for(Opportunity opp : (List<Opportunity>) Records)
{
if(opp.Type.startsWith('Existing') && opp.AccountId == null)
{
opp.AccountId.addError('You must provide an Account when ' +
'creating Opportunities for existing Customers.');
}
}
}


Данный пример проверки подходит для использования в случае если не важно как изменились данные в ходе выполнения логики доменного класса. Если же необходимо проверить как изменились данные после записи их в базу данных в ходе операции update, то предыдущий пример можно переписать в таком виде:




public override void onValidate(Map<Id,SObject> existingRecords)
{
// Validate changes to Opportunities
for(Opportunity opp : (List<Opportunity>) Records)
{
Opportunity existingOpp = (Opportunity) existingRecords.get(opp.Id);
if(opp.Type != existingOpp.Type)
{
opp.Type.addError('You cannot change the Opportunity type once it has been created');
}
}
}


Переназначение методов обработки событий триггера. Чтобы добавить логику для обработки событий триггера, необходимо переназначить методы on… из базового класса. В следующем примере в доменный класс Opportunities добавлена логика для обновления информации в связанных с opportunity объектах Account.




public override void onAfterInsert()
{
// Related Accounts
List<Id> accountIds = new List<Id>();
for(Opportunity opp : (List<Opportunity>) Records)
if(opp.AccountId!=null)
accountIds.add(opp.AccountId);

// Update last Opportunity activity on related Accounts via the Accounts domain class
SObjectUnitOfWork uow = new SObjectUnitOfWork(new Schema.SObjectType[] { Account.SObjectType });
Accounts accounts = new Accounts([select ... from Account where id in :accountIds]);
accounts.updateOpportunityActivity(uow);
uow.commitWork();
}


В данном примере Accounts тоже является доменным объектом, который инициализируется с помощью SOQL запроса.




public class Accounts extends SObjectDomain
{
public void updateOpportunityActivity(SObjectUnitOfWork uow)
{
for(Account account : (List<Account>) Records)
{
account.Description = 'Last Opportunity Raised ' + System.today();
uow.registerDirty(account);
}
}
}


Добавление в доменный класс кастомной логики. Используя базовый доменный класс вы не ограничены только переназначением стандартный методов. Также можно добавлять свои методы для реализации бизнес логики характерной для данного доменного объекта. Вот пример такого метода, добавленного к доменный класс Opportunities:




public void applyDiscount(Decimal discountPercentage, SObjectUnitOfWork uow)
{
// Calculate discount factor
Decimal factor = Util.calculateDiscountFactor(discountPercentage);

// Opportunity lines to apply discount to
List<OpportunityLineItem> linesToApplyDiscount = new List<OpportunityLineItem>();

// Apply discount
for(Opportunity opportunity : (List<Opportunity>) Records)
{
// Apply to the Opportunity Amount?
if(opportunity.OpportunityLineItems.size()==0)
{
// Adjust the Amount on the Opportunity if no lines
opportunity.Amount = opportunity.Amount * factor;
uow.registerDirty(opportunity);
}
else
{
// Collect lines to apply discount to
linesToApplyDiscount.addAll(opportunity.OpportunityLineItems);
}
}

// Apply discount to lines
OpportunityLineItems lineItems = new OpportunityLineItems(linesToApplyDiscount);
lineItems.applyDiscount(this, discountPercentage, uow);
}


Источник - Apex Enterprise Patterns - Domain Layer Длинная и тяжелая получилась статья. Возможно здесь много воды, но общий смысл заставляет о многом задуматься. Поработав с темой паттернов проектирования на Salesforce я заметил, что в повседневной работе стал по другому смотреть на код. И это радует :). В одной из следующих статей я расскажу про Selector Layer (еще подумаю как эго правильно перевести). Для обсуждения данной темы приглашаю пообщаться на форуме