Я попытаюсь продлить ответ, данный @plalx, на примере шаблона Specification
.
Для примера я собираюсь использовать некоторые классы из this ddd library. В частности, те, которые определяют интерфейсы для работы с шаблоном спецификации (предоставлены @martinezdelariva)
Прежде всего, давайте забудем о пользовательском интерфейсе и сосредоточимся на инвариантах домена, которые вы должны удовлетворить. Поэтому вы сказали, что для того, чтобы отправить электронное письмо, необходимо:
- Не содержат запрещенных ключевых слов.
- Содержит по крайней мере одно содержимое получателя и тела.
- Будьте уникальны, что означает, что подобное письмо еще не отправлено.
Теперь давайте посмотрим на Application Service (прецеденту), чтобы увидеть большую картину, прежде чем в деталях:
class SendEmailService implements ApplicationService
{
/**
* @var EmailRepository
*/
private $emailRepository;
/**
* @var CanSendEmailSpecificationFactory
*/
private $canSendEmailSpecFactory;
/**
* @var EmailMessagingService
*/
private $emailMessagingService;
/**
* @param EmailRepository $emailRepository
* @param CanSendEmailSpecificationFactory $canSendEmailSpecFactory
*/
public function __construct(
EmailRepository $emailRepository,
CanSendEmailSpecificationFactory $canSendEmailSpecFactory,
EmailMessagingService $emailMessagingService
) {
$this->emailRepository = $emailRepository;
$this->canSendEmailSpecFactory = $canSendEmailSpecFactory;
$this->emailMessagingService = $emailMessagingService;
}
/**
* @param $request
*
* @return mixed
*/
public function execute($request = null)
{
$email = $this->emailRepository->findOfId(new EmailId($request->emailId()));
$canSendEmailSpec = $this->canSendEmailSpecFactory->create();
if ($email->canBeSent($canSendEmailSpec)) {
$this->emailMessagingService->send($email);
}
}
}
Мы принести письмо от репо, проверьте, может быть отправлено и отправлено.Итак, давайте посмотрим, как совокупный Root (электронная почта) работает с инвариантами, здесь canBeSent
метод:
/**
* @param CanSendEmailSpecification $specification
*
* @return bool
*/
public function canBeSent(CanSendEmailSpecification $specification)
{
return $specification->isSatisfiedBy($this);
}
До сих пор так хорошо, теперь давайте посмотрим, как легко это усугубит CanSendEmailSpecification
, чтобы удовлетворить наши инварианты:
class CanSendEmailSpecification extends AbstractSpecification
{
/**
* @var Specification
*/
private $compoundSpec;
/**
* @param EmailFullyFilledSpecification $emailFullyFilledSpecification
* @param SameEmailTypeAlreadySentSpecification $sameEmailTypeAlreadySentSpec
* @param ForbiddenKeywordsInBodyContentSpecification $forbiddenKeywordsInBodyContentSpec
*/
public function __construct(
EmailFullyFilledSpecification $emailFullyFilledSpecification,
SameEmailTypeAlreadySentSpecification $sameEmailTypeAlreadySentSpec,
ForbiddenKeywordsInBodyContentSpecification $forbiddenKeywordsInBodyContentSpec
) {
$this->compoundSpec = $emailFullyFilledSpecification
->andSpecification($sameEmailTypeAlreadySentSpec->not())
->andSpecification($forbiddenKeywordsInBodyContentSpec->not());
}
/**
* @param mixed $object
*
* @return bool
*/
public function isSatisfiedBy($object)
{
return $this->compoundSpec->isSatisfiedBy($object);
}
}
Как вы можете видеть, мы говорим здесь, что для того, письмо будет отправлено, мы должны удовлетворять, что:
- Электронная почта полностью заполнена (здесь вы можете проверить, что Конте тела nt не пуст и есть хотя бы один получатель)
- И тот же тип электронной почты НЕ был отправлен.
- И в содержании тела нет запрещенных слов.
Найти ниже реализации двух первых спецификаций:
class EmailFullyFilledSpecification extends AbstractSpecification
{
/**
* @param EmailFake $email
*
* @return bool
*/
public function isSatisfiedBy($email)
{
return $email->hasRecipient() && !empty($email->bodyContent());
}
}
class SameEmailTypeAlreadySentSpecification extends AbstractSpecification
{
/**
* @var EmailRepository
*/
private $emailRepository;
/**
* @param EmailRepository $emailRepository
*/
public function __construct(EmailRepository $emailRepository)
{
$this->emailRepository = $emailRepository;
}
/**
* @param EmailFake $email
*
* @return bool
*/
public function isSatisfiedBy($email)
{
$result = $this->emailRepository->findAllOfType($email->type());
return count($result) > 0 ? true : false;
}
}
Благодаря шаблон спецификации вы готовы теперь управлять как много инвариантов, как ваш босс просит вас, чтобы добавить без изменения существующего кода. Вы можете создавать модульные тесты очень легко для каждой спецификации.
С другой стороны, вы можете сделать интерфейс настолько сложным, насколько хотите, чтобы пользователь знал, что письмо готово к отправке. Я бы создал другой вариант использования ValidateEmailService
, который вызывает только метод canBeSent
из Агрегированного корня, когда пользователь нажимает кнопку , чтобы подтвердить действие кнопки или когда пользователь переключается с одного входа (заполняя получателя) на другой (заполняя тело). Это зависит от вас.
Разве вы не можете реализовать эту логику в модели чтения? Проблема с этими типами проверок заключается в том, что они не являются государственными, они основаны на состоянии. Пытаясь сохранить эти проверки, возникает проблема, связанная с тем, что они постоянно синхронизируются, но это также усложняет ситуацию, когда вам приходится менять бизнес-логику. На данный момент я просто решил поместить эти правила в считываемую модель, например. 'SELECT CASE WHEN some_state THEN 1 ELSE 0 END AS can_be_sent'. Это не идеально, но это работает. – plalx
Один из подходов, который я придерживаюсь для сохранения логики в домене, но повторного использования его в модели запросов - это создание спецификаций, которые могут быть преобразованы в абстрактные деревья выражений, которые, в свою очередь, могут быть преобразованы в SQL-выражения или что-то еще, но Я еще этого не делал. В основном 'domain.EmailReadyToBeSentSpecification -> domain.Expression -> query.SqlPredicate'. – plalx
Я сделал что-то похожее на то, что вы говорите, и он работает: эти абстрактные деревья являются агностиками базы данных, и это хорошая абстракция: можно легко заменить SQL с помощью NoSQL. Итак, реальный вопрос заключается в том, что эта бизнес-логика должна оставаться в командной модели (агрегате) или в модели чтения. Где @ greg-young, когда он вам нужен? :) –