0

У меня есть проект по электронной почте в виде совокупного корня с помощью следующих команд: addToRecipient, addCcRecipient, addBccRecipient, updateBodyText, uploadAttachment, removeAttachment и в пользовательском интерфейсе я хочу отключите кнопку SEND, если черновик не готов к отправке (т. е. есть, по крайней мере, для получателя, а тело имеет текст). Я знаю, что мне не разрешено запрашивать агрегат, но это единственный, кто может сказать мне, что я могу или не могу отправить электронное письмо.Как «запрос» в совокупности, чтобы увидеть, если команда может быть выполнена

Если я применить то, что я знаю о поиске событий и CQRS, то агрегат будет излучать EmailIsReadyToBeSent события и моя UserEmailDrafts модели чтения будет выбирать, что и обновление пользовательского интерфейса так или иначе, но тогда, я бы проверить после каждой команды и отправить событие отмены, т.е. EmailIsNotReadyToBeSent.

Это кажется очень сложным, что вы думаете?

+0

Разве вы не можете реализовать эту логику в модели чтения? Проблема с этими типами проверок заключается в том, что они не являются государственными, они основаны на состоянии. Пытаясь сохранить эти проверки, возникает проблема, связанная с тем, что они постоянно синхронизируются, но это также усложняет ситуацию, когда вам приходится менять бизнес-логику. На данный момент я просто решил поместить эти правила в считываемую модель, например. 'SELECT CASE WHEN some_state THEN 1 ELSE 0 END AS can_be_sent'. Это не идеально, но это работает. – plalx

+0

Один из подходов, который я придерживаюсь для сохранения логики в домене, но повторного использования его в модели запросов - это создание спецификаций, которые могут быть преобразованы в абстрактные деревья выражений, которые, в свою очередь, могут быть преобразованы в SQL-выражения или что-то еще, но Я еще этого не делал. В основном 'domain.EmailReadyToBeSentSpecification -> domain.Expression -> query.SqlPredicate'. – plalx

+0

Я сделал что-то похожее на то, что вы говорите, и он работает: эти абстрактные деревья являются агностиками базы данных, и это хорошая абстракция: можно легко заменить SQL с помощью NoSQL. Итак, реальный вопрос заключается в том, что эта бизнес-логика должна оставаться в командной модели (агрегате) или в модели чтения. Где @ greg-young, когда он вам нужен? :) –

ответ

4

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

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

Как и вы, вы не шокированы, когда видите, что клиентская логика делает обязательную проверку поля на форме. Это совершенно правильный и приемлемый компромисс, поскольку логика проста и универсальна.

Обратите внимание, что это не мешает вам также иметь эти правила в совокупности, отвергая любую команду, которая их не удовлетворяет.

+0

Но что, если я не хочу посылать дважды «похожее» электронное письмо (похоже, это те же получатели и одно и то же тело)? Как я могу проверить это?Что мне приходит в голову - спросить некоторую SentEmailsReadModel, вне какого-либо агрегата, но где именно поставить эту логику? –

+0

Это ретривер, верно? Потому что в реальном приложении я не вижу, что может оправдать запрет пользователю отправлять аналогичное электронное письмо дважды. Теперь, если вы говорите о случайном двойном щелчке по кнопке «Отправить», я бы справился с этим также на уровне пользовательского интерфейса - это в основном ошибка ввода. – guillaume31

+0

Но что, если я не хочу, чтобы пользователи отправляли электронные письма, содержащие некоторые запрещенные ключевые слова, взятые из базы данных или внешней службы? –

1

Я попытаюсь продлить ответ, данный @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 из Агрегированного корня, когда пользователь нажимает кнопку , чтобы подтвердить действие кнопки или когда пользователь переключается с одного входа (заполняя получателя) на другой (заполняя тело). Это зависит от вас.

+0

Какой подход вы использовали для интеграции репозиториев и спецификаций? В идеале вы не хотите дублировать эту логику при запросе языков. – plalx

+0

Извините @plalx, ​​но не понимаю, что вы имеете в виду. – mgonzalezbaile

+0

Например, если вы хотите получить все экземпляры электронной почты, которые могут быть отправлены из хранилища. Вы не можете просто загрузить их все в памяти для выполнения спецификации. Поэтому вы должны либо дублировать правило, либо иметь механизм для перевода его на другой язык. – plalx