At Domain-driven design, we strive to keep our domain model outright bulletproof. In some cases, it is necessary to enforce certain business rules when bringing a new object to life. If the construct is too complex or simply cannot be implemented by the object itself, then you should move the construct of the object to a dedicated class: factory.
Let me give you a quick example. Let’s say we have a meaningful TimeSpan object. Our initial TimeSpan implementation might look like this:
class TimeSpan
{
/** @var \DateTimeImmutable **/
private $from; /** @var \DateTimeImmutable **/
private $until; public function __construct(\DateTimeImmutable $from, \DateTimeImmutable $until)
{
if ( $from >= $until ) {
throw new \InvalidArgumentException('Invalid time span.');
}
$this->from = $from;
$this->until = $until;
} // Some other useful stuff goes in here...
}
Let’s say we don’t want the time interval to be unlimited, so we’ll set the maximum that can be set through the configuration. This is how it looks:
class TimeSpanConfiguration
{
/** @var \DateInterval **/
private $maxTimeSpan; public function __construct(\DateInterval $maxTimeSpan)
{
$this->maxTimeSpan = $maxTimeSpan;
}
}
So, we have a business rule that says: there can be no time span greater than the maximum that we have set. How can this be ensured? We can create instances of TimeSpan however we want, because nothing ever implements such a configuration. Fortunately, there are many solutions to this problem. I want to show you a factory:
timeSpanConfiguration = $timeSpanConfiguration;
} public function createTimeSpan(\DateTimeImmutable $from, \DateTimeImmutable $until): TimeSpan
{
// We just ask the configuration if the given from-until time span is valid.
// That way we don't need any getters on the configuration. Neat.
if ( !$this->timeSpanConfiguration->isValidTimeSpanFromUntil($form, $until) ) {
throw new \DomainException('This time span is too long!');
} return new TimeSpan($from, $until);
}}
Now, when creating a new TimeSpan instance, we simply use the TimeSpanFactory instead of the TimeSpan constructor. Thus, we always get a time interval that does not exceed the configured maximum.
Now you may be asking: So what ?, I can still create an invalid time interval when using the constructor. And yes, you’re right! And this can turn into a problem depending on the development team. If you have a fairly small development team, you can simply decide to always use TimeSpanFactory instead of the constructor directly. But there is a solution that forces you to use the factory pattern and that is Reflection. Using this approach can break something, but I’ll show you anyway;)
First, make the TimeSpan constructor private:
class TimeSpan
{
/** @var \DateTimeImmutable **/
private $from; /** @var \DateTimeImmutable **/
private $until; private function __construct(\DateTimeImmutable $from, \DateTimeImmutable $until)
{
if ( $from >= $until ) {
throw new \InvalidArgumentException('Invalid time span.');
}
$this->from = $from;
$this->until = $until;
} // Some other useful stuff goes in here...
}
Now you cannot create a TimeSpan instance using the new operator. PHP will throw a fatal error when you try to do this. Okay, but if we can’t create it that way, how then can our TimeSpanFactory create it? Reflection rushes to the rescue! Let’s take a look at our new TimeSpanFactory implementation:
timeSpanConfiguration = $timeSpanConfiguration;
} public function createTimeSpan(\DateTimeImmutable $from, \DateTimeImmutable $until): TimeSpan
{
// We just ask the configuration if the given from-until time span is valid.
// That way we don't need any getters on the configuration. Neat.
if ( !$this->timeSpanConfiguration->isValidTimeSpanFromUntil($form, $until) ) {
throw new \DomainException('This time span is too long!');
} return $this->constructTimeSpan($from, $until);
} private function constructTimeSpan(\DateTimeImmutable $from, \DateTimeImmutable $until): TimeSpan
{
$class = new ReflectionClass(TimeSpan::class);
$constructor = $class->getConstructor();
$constructor->setAccessible(true);
$timeSpan = $class->newInstanceWithoutConstructor();
$constructor->invoke($timeSpan, $from, $until); return $timeSpan;
}}// Usage:
$factory = new TimeSpanFactory(new TimeSpanConfiguration(new \DateInterval('PT2D')));$timeSpan = $factory->constructTimeSpan(new \DateTimeImmutable('2019-02-17 17:00:00'), new \DateTimeImmutable('2019-02-17 18:00:00'));// Fails due too to long time span
$timeSpan = $factory->constructTimeSpan(new \DateTimeImmutable('2019-02-17 17:00:00'), new \DateTimeImmutable('2019-02-17 23:00:00'));// Also failes, but due to private constructor ;)
$timeSpan = new TimeSpan(new \DateTimeImmutable('2019-02-17 17:00:00'), new \DateTimeImmutable('2019-02-17 23:00:00'));
I agree, but I’d rather have a bulletproof domain model, and the likelihood that someone will create an invalid time interval and violate a business rule is not so great. It could be a junior developer who doesn’t know you should use a TimeSpanFactory, or even a senior who just forgot about Factory.
But as I said, it depends on your development team.