- Get link
- X
- Other Apps
- Get link
- X
- Other Apps
When you are in domain driven design you probably have heard of a guy called Martin Fowler. He is the domain driven design guru and he has a hate for so called anemic domain objects. So what is an anemic domain object? You've probably seen one before as basically it is an object that looks like a domain object but actually is just an object.
class AnemicDomainObject
{
private string $phoneNumber;
public function setPhoneNumber(string $phoneNumber)
{
$this->phoneNumber = $phoneNumber;
}
public function getPhoneNumber(): string
{
return $this->phoneNumber;
}
}
If I see this object any programmer can not figure out some business logic or even some programming logic:
- It crashes if I call getPhoneNumber() without calling setPhoneNumber() first. Is that intentional or a bug?
- What format is the phone number? Does it support national numbers? Does it support international numbers?
The problem is that the logic about the phone number is probably written somewhere else in the codebase, probably in some validator service. If domain driven design is followed more carefully the above example should contain the business logic about the phone number property and your fellow developer should not search for an other class where this business logic is found. It should also be clear whether the object is required or not for a domain object.
If a phone number is required we could rewrite it like this:
final class AnemicDomainObject
{
public function __construct(
private string $phoneNumber,
) {
}
public function setPhoneNumber(string $phoneNumber)
{
$this->phoneNumber = $phoneNumber;
}
public function getPhoneNumber(): string
{
return $this->phoneNumber;
}
}
By adding the final keyword I tell people this class should not be extended. By providing the constructor I tell that you need a phone number. With the setter I tell you that phoneNumber is mutable.
If the phone number is not required we could rewrite it like this:
final class AnemicDomainObject
{
private ?string $phoneNumber = null;
public function setPhoneNumber(string $phoneNumber)
{
$this->phoneNumber = $phoneNumber;
}
public function hasPhoneNumber(): bool
{
return $this->phoneNumber !== null;
}
public function getPhoneNumber(): string
{
if (!$this->hasPhoneNumber()) {
throw new \LogicException("This domain object has no phone number!');
}
return $this->phoneNumber;
}
}
We tell other developers that the phone number is optional. If getPhoneNumber() is called without a phone number we throw an exception. We could also have the exception message have a reference to the id of the domain object to have better debugging information. Also once we set a phone number, we can no longer unset this.
So is this enough to not be a anemic domain object? Sadly no. There is no validation in our phone number property so I can enter '911', '112' or 'Lorem ipsum' as phone number. We could add the phone validation in the setter so our property can only contain invalid values if hacky methods are used, like the reflection API or hacked serialized php strings.
final class AnemicDomainObject
{
private ?string $phoneNumber = null;
public function setPhoneNumber(string $phoneNumber)
{
$phoneUtil = \libphonenumber\PhoneNumberUtil::getInstance();
$phoneObject = $phoneUtil->parse($phoneNumber, "NL"); // this method throws an exception on invalid input
$this->phoneNumber = $phoneUtil->format($phoneObject, \libphonenumber\PhoneNumberFormat::NATIONAL);
}
public function hasPhoneNumber(): bool
{
return $this->phoneNumber !== null;
}
public function getPhoneNumber(): string
{
if (!$this->hasPhoneNumber()) {
throw new \LogicException("This domain object has no phone number!');
}
return $this->phoneNumber;
}
}
For this example we use the composer package giggsey/libphonenumber-for-php to parse a Dutch phone number and store it in the Dutch national format.
Great! We now show in our domain object that the phone number is a Dutch format, but the problem now is that we can not reuse it. Every time we need a Dutch phone number we need the same setter logic.
So you may think to move it to a trait or a base class then? Wrong! That only works if every domain object names their phone number 'phone number' and also enforces that no domain object can have more than one phone number.
People who are not into value objects would probably introduce a PhoneNumberUtils class to reuse this logic:
public function setPhoneNumber(string $phoneNumber)
{
$this->phoneNumber = PhoneNumberUtils::formatToDutchPhoneNumber($phoneNumber);
}
This looks great at first and all programmers get it but the risk here is when we get variations in every domain object. Maybe some domain objects want Dutch or Belgian phone numbers instead or want to store it in a different format then the national format? It could even be possible the domain object creates multiple getters for the same phone number property just to offer multiple formats.
/**
* This example is deliberately bad and you should not use it like this
*/
final class AnemicDomainObject
{
private ?string $phoneNumber = null;
public function setPhoneNumber(string $phoneNumber)
{
try {
$this->phoneNumber = PhoneNumberUtils::formatToDutchPhoneNumber($phoneNumber);
} catch (Throwable) {
$this->phoneNumber = PhoneNumberUtils::formatToBelgianPhoneNumber($phoneNumber);
}
}
public function hasPhoneNumber(): bool
{
return $this->phoneNumber !== null;
}
public function getNationalPhoneNumber(): string
{
return PhoneNumberUtils::formatToNationalNumber($this->getPhoneNumber());
}
public function getInternationalPhoneNumber(): string
{
return PhoneNumberUtils::formatToInternationalNumber($this->getPhoneNumber());
}
public function getPhoneNumber(): string
{
if (!$this->hasPhoneNumber()) {
throw new \LogicException("This domain object has no phone number!');
}
return $this->phoneNumber;
}
}
That's where value objects come in! They provide all information needed as a value object is perfectly fine having multiple display options as getters. In combination with union types you can even tell other programmers I expect a Dutch or Belgian phone number.
final class AProperDomainObject
{
public function __construct(
private DutchPhoneNumber|BelgianPhoneNumber $phoneNumber,
) {
}
public function setPhoneNumber(DutchPhoneNumber|BelgianPhoneNumber $phoneNumber)
{
$this->phoneNumber = $phoneNumber;
}
public function getPhoneNumber(): DutchPhoneNumber|BelgianPhoneNumber
{
return $this->phoneNumber;
}
}
$object->getPhoneNumber()->getNationalNumber();
$object->getPhoneNumber()->getInterationalNumber();
Apie has the apie/country-and-phone-number package for phone number value objects.The issue with MVC frameworks and use of full domain objects
If value objects are so awesome and expressive, why do we not use them all the time? The real problem is how all frameworks handle validation of the HTTP request.
For example Laravel handles this with form requests having a rules array for validations. It works on the raw array input you get from the request and you need manual code or a serializer to convert the array into the domain object. Of course you can write your own logic to map your domain object to a validation rules array so there is only one way. But as far as I know there is no such package that does this. The closest you can find is spatie's laravel-data package. It could work as a CQRS-like architecture but you will end up with probably manual code to convert a domain object in a form request or in a Laravel resource for display.
Symfony works with validating the object properties, so is basically recommending you to write anemic domain objects and be stateful as you can not add validations to non-properties or validate before the object is created. The validation is written in a yaml file or as PHP8 attribute. The only benefits of PHP8 attributes is configuration reuse in traits and that the validation rules will be removed automatically if the properties are removed too. It is also visible in the domain object for readability.
class AnemicDomainObject
{
// if I remove the phoneNumber property it will automatically remove the validation too
#[ValidPhoneNumber]
#[NotBlank]
private string $phoneNumber;
public function setPhoneNumber(string $phoneNumber)
{
$this->phoneNumber = $phoneNumber;
}
public function getPhoneNumber(): string
{
return $this->phoneNumber;
}
}
The API Platform, that only runs in Symfony and an inspiration for my Apie library also falls flat here: If I write an anemic domain object with attributes to tell they are required, it will map the properties as required in the OpenAPI spec and throw a 422 validation error when they are missing. If I try to use a proper domain object with requird properties in the constructor, it will not map the properties as required and throw a 400 bad request instead when they are missing.
It's for these reasons why we are so used to anemic domain objects in our projects as they do not map easily with the current frameworks. Of course if you use my Apie library, you do not need to manually make validations for your requests or serializers for your responses. Just make sure you use value objects instead!
Comments
Post a Comment