I am going to try to extend the answer given by @plalx with an example of the Specification
pattern.
For the sake of the example I am going to use some classes from this ddd library. Specifically the ones that define the interfaces to work with the specification pattern (provided by @martinezdelariva)
First of all, let's forget about the UI and keep the focus on the domain invariants that you must satisfy. So you said that in order to send an email the email needs to:
- Not contain forbidden keywords.
- Contain at least one recipient and body content.
- Be unique, meaning that a similar email was not already sent.
Now let's take a look at the Application Service (use case) to see the big picture before going into the details:
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);
}
}
}
We fetch the email from the repo, check if it can be sent and send it. So let's see how the Aggregate Root (Email) is working with the invariants, here the canBeSent
method:
/**
* @param CanSendEmailSpecification $specification
*
* @return bool
*/
public function canBeSent(CanSendEmailSpecification $specification)
{
return $specification->isSatisfiedBy($this);
}
So far so good, now let's see how easy is to compound the CanSendEmailSpecification
to satisfy our invariants:
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);
}
}
As you can see we say here that, in order an email to be sent, we must satisfy that:
- The email is fully filled (here you can check that body content is not empty and there is at least one recipient)
- And the same email type was NOT already sent.
- And there are NOT forbidden words in the body content.
Find below the implementation of the two first specifications:
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;
}
}
Thanks to the Specification pattern you are ready now to manage as many invariants as your boss asks you to add without modifying the existing code. You can create unit tests very easily for every spec as well.
On the other hand, you can make the UI as complex as you want to let the user know that the email is ready to be sent. I would create another use case ValidateEmailService
that only calls the method canBeSent
from the Aggregate Root when the user clicks on a validate button, or when the user switches from one input (filling the recipient) to another (filling the body)... that is up to you.