EDIT: Added usage example in the modified TimeSpanFactory.
In Domain-Driven design we want our domain model to be bullet proof. Sometimes it is necessary to ensure some business rules while bringing a new object into life. If this construction is too complicated or simply cannot be ensured by the object itself, than you should move the construction of this object in a dedicated class: A factory.
Let's make a quick example. Let's say we have a TimeSpan value object. Our initial implementation of a TimeSpan may look like this:
<?php
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 TimeSpan to be unlimited, there is a maximum that can be specified by some user through a Configuration that looks like this:
<?php
class TimeSpanConfiguration
{
/** @var \DateInterval **/
private $maxTimeSpan;
public function __construct(\DateInterval $maxTimeSpan)
{
$this->maxTimeSpan = $maxTimeSpan;
}
}
So now we have a business rule that states: There can be no TimeSpan longer than the configured maximum. How do we enforce that? We can construct TimeSpan instances as we like, nothing will ever enforce that configuration. Well there are - as always - multiple solutions to this problem. The one that I want to show you is the Factory:
<?php
class TimeSpanFactory
{
/** @var TimeSpanConfiguration **/
private $maxTimeSpan;
public function __construct(TimeSpanConfiguration $timeSpanConfiguration)
{
// For the sake of simplicity we just give the factory the configuration directly.
$this->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 constructing a new TimeSpan, we just use the TimeSpanFactory instead of the constructor of TimeSpan directly. This way we always get a valid TimeSpan that is not longer than the configured maximum.
You may ask now: Well, I could still construct an invalid TimeSpan when using the constructor directly. And YES, you are right! This may be a problem depending on your team. If your team is small enough, you could just agree on always using the TimeSpanFactory instead of the constructor directly. But there is a solution that enforces you to use the Factory: Reflection. This may introduce some side effects you don't want, but I will show you anyway ;)
First, make the constructor of TimeSpan private
:
<?php
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, there is no way to construct a TimeSpan with a simple new
. PHP will throw a fatal error when you try to do that. Well but if you can't construct it anymore, how can our TimeSpanFactory construct it then? Reflection to the rescue! Let's look at our new implementation of TimeSpanFactory:
<?php
class TimeSpanFactory
{
/** @var TimeSpanConfiguration **/
private $maxTimeSpan;
public function __construct(TimeSpanConfiguration $timeSpanConfiguration)
{
// For the sake of simplicity we just give the factory the configuration directly.
$this->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'));
Wa...
I agree, but I rather have a bullet proof domain model than the chance that someone constructs an invalid TimeSpan that breaks the business rule. This may be a junior developer that doesn't know that you should use the TimeSpanFactory, or even a senior that just has forgotten about the Factory.
But - as I said - that depends on your team.
Happy DDD'ing!
PS: I wrote this post in less than 10 minutes, all samples are written from scratch within the markdown editor of dev.to, so please: If you find any bug, typo etc., let me know :)
Top comments (6)
Is there any reason we use a factory class instead over a factory function?
I've not dealt much with factory classes, but naïvely a function seems cleaner to me -- That being said, I've noticed that the class pattern is very popular, so I was wondering if you could explain some of the reasoning behind it.
You are absolutely right! It actually depends what you are constructing. Sometimes it's not that simple like in this example, sometimes you may have multiple dependencies that you need to create a class, that's where a own dedicated factory may be more maintainable than passing multiple dependencies to your constructor/factory method. Also, dependency injection is way easier if you have a dedicated factory class :)
In languages like Java or C# you can declare a certain method (including the constructor) to be only visible to the same package (Java) or project (C#). This makes sure you don't have to jump through the hoops of reflection to let the Factory use the constructor without making it available to the rest of the world. This makes it a lot cleaner.
The main reason you would want a separate class is separation of concerns: one class that deals with the logic of being a
Thing
(aTimeSpan
, in this case), one class that deals with the logic of creating a thing (theTimeSpanFactory
).If the business logic for creating a Thing is more involved than a simple check, this is easier to contain in a
ThingFactory
class than in theThing::factory(...)
function.If you need multiple different ways to create a
Thing
, for example if you have different types of configuration or if you want to mock the factory for testing, it's easier to have that in separateThingFactory
classes than to have it in theThing
.There are a couple more reasons why you would want this separation, and most of them have to do with rising complexity of having it all in one class. I will concede that it's not always worth it in PHP, where it's a bit harder to separate the classes, but it does pay off as complexity increases.
Because the factory gets its dependencies through the constructor - the most used way to inject dependencies. You can see the factory more as a service. On the other hand, the factory method gets both dependencies and data. This is hard or even impossible to configure in a standard dependency injection container.
Thanks so much for sharing, I really like DDD examples because this is a pattern that I am not used to. Very interesting!
Hi,
just 2 questions:
What are the side effects? and what and why is illegal?
Thanks in advance.