- Get link
- X
- Other Apps
- Get link
- X
- Other Apps
I've seen a lot of programmers or even software architects working on an application that they call is using domain-driven design. As I've written in my blog article about the value of value objects already that often the existing frameworks and libraries actually hinder us in writing actual domain-driven design and we often end up with anemic domain objects as a trade-off. Sadly this also means many inexperienced developers that see these architectures think that this is how you should do domain-driven design. So let's see at a few very common misconceptions.
Splitting your code in multiple namespaces is not domain-driven design
The most common thing I see is that many developers think that splitting your source code in multiple logical namespaces is domain-driven design. While it's true that domain-driven design separates parts of the application, the problem is that in real domain-driven design you can actually split it in bounded contexts and/or in subdomains and they are not the same terminology, though very similar. For example all bounded contexts should have their own User object with only the fields relevant to the bounded context and not have a global User object. But in practive nobody does this.
Domain objects have no setters
Yes, I'm not kidding. Domain objects have no setters is considered good practice if you do domain-driven design. Thinking in setters would end you up in thinking in CRUD only. A domain object has methods, but every non-getter method should tell a capability of a domain object, for example an Animal class can walk, so there is a walk method. Using one of these methods will change it's internal state, which you can use the getters to get the current value/state. So if a full CRUD is correct for a domain object, then you can add a modify() method that expects a DTO as input (with validation).
Also adding setters is not productive if properties have individual constraints with each other as I will show you in this example:
final class ObjectWithDateRange {
public function __construct(
private readonly DateTimeInterface $startDate,
private readonly DateTimeInterface $endDate
) {
if ($endDate < $startDate) {
throw new LogicException('End date should be before start date');
}
}
public function setStartDate(DateTimeInterface $startDate): self
{
if ($startDate > $this->endDate) {
throw new LogicException('Start date can not be after end date');
}
$this->startDate = $startDate;
return $this;
}
public function setEndDate(DateTimeInterface $endDate): self
{
if ($endDate < $this->startDate) {
throw new LogicException('End date should be before start date');
}
$this->endDate = $endDate;
return $this;
}
}
So what is the problem? There are two problems even though they are not quite obvious at start. First one is that we check the start date and end date restriction in 3 places. Of course we could move this to a private static method that does the validation to get around this. But the second one is not so obvious at first, until you attempt to update both the start and end date at the same time.
$object = new ObjectWithDateRange(
new DateTime('2024-01-01'),
new DateTime('2024-01-01')
);
$object->setStartDate(new DateTime('2024-12-01'))
->setEndDate(new DateTime('2024-12-31'));
Yes, this call throws an exception. If I call setStartDate first it will throw an error for the start date being after the end date, but there are also cases where you first have to call setEndDate before setStartDate. So the caller should think whether he should call setStartDate or setEndDate first to not throw an exception. A hack would be to first set the start date on January the 1st 1970 and the end date on December the 31 2100 and then do your calls to set the start- and end date.
A better solution would be to make a DateRange object that contains the start- and end date and is immutable and have a setDateRange method in ObjectWithDateRange. But the moment your domain object gets more and more restrictions you probably end up with fields having multiple constraints and any setter would be myriad of consistency checks.
Domain objects are entities, but entities are not domain objects
All domain objects are entities. But not all entities are domain objects. An entity is nothing more than an object with an identifier. that is always in a stable state. In domain driven design there is a concept called root aggregates. For example if we have a Webshop, we probably end up with a domain object for orders. An order consists of one or more order lines. It's very common to make the order lines their own entities for having an identifier, but since they are part of order, they are not domain objects. The order should also maintain stability. In fact the order lines should not have functions that can modify itself (if the method does, you can get around by returning an immutable object or a clone of the original object).
Domain objects can not contain other domain objects.
I've seen quite a lot that people will map domain objects to domain objects as this is also the case inside the database or their application, but this is wrong in domain-driven design. For example an order is created by an user, so it's very common to return the User object with Order::getCreatedBy(), except since User contains everything a user can do you can do some things which make no sense from a domain-driven design perspective.
$order = $orderRepository->find(42);
// these calls would not be logical in domain-driven design, but we
// can make calls like these:
$order->getCreatedBy()->resetPassword();
$order->getCreatedBy()->getCompany()->setName('Acme BV');
These calls make no sense in domain objects(why would an order reset the creator's password?), but they could make sense in a service layer.
When designing the Apie library making the assumption a domain object can not contain other domain objects made it actually possible to generate a database and a CMS without running in some complex issues, so when I realised this I actually enforce users of Apie to not use domain objects in their domain objects.
So what should you do to reference an other domain object? The answer is simple: value objects! A value object with a name of UserReference that only allows uuid's as content should explain quite easily that is referencing an other domain object.
So a more domain-driven design approach in case you ever need to reset a password of someone who created an order would be:
$orderId = new OrderId(42);
$order = $datalayer->find($orderId);
// getCreatedBy returns a value object referencing the user instead.
$createdBy = $datalayer->find($order->getCreatedBy());
$createdBy->resetPassword();
$company = $datalayer->find($createdBy->getCompany());
$company->setName('Acme BV');
In this example we have an Order, a User and a Company domain object and a service can ask the datalayer all 3 of them.
Using a data mapper as ORM is not domain-driven design.
In PHP most people using a data mapper as ORM will probably use Doctrine. Doctrine even calls everything Doctrine entities, so it's natural to think that they are entities like they are in domain-driven design. Sadly Doctrine entities do not follow all guidelines how a domain object should be handled. For example if we take the previous example of an Order with order lines: in this case the one to many is made with an order_id in the order_line table, so Doctrine requires a property on OrderLine to store order lines correctly. Doctrine forces you to make a circular reference:
#[Entity]
class Order
{
#[OneToMany(targetEntity: OrderLine::class, mappedBy: 'order')]
private Collection $orderLines;
}
#[Entity]
class OrderLine
{
#[ManyToOne(targetEntity: Order::class, inversedBy: 'orderLines')]
private Order;
}
In domain-driven design Order is a root aggregate and OrderLine a child entity of it. The id of OrderLine should also only be unique in the context of the order. None of this applies here as OrderLine knows his parent object and the id is unique in the entire application instead.
Matthias Noback wrote a nice article about working around Doctrine to make it more like domain objects, but again this is like a workaround to make a Doctrine entity look like a domain object. The circular reference is unavoidable.
Matthias Noback wrote a nice article about working around Doctrine to make it more like domain objects, but again this is like a workaround to make a Doctrine entity look like a domain object. The circular reference is unavoidable.
In my Apie library the default datalayer is the DoctrineEntityDataLayer. After a lot of rewriting and refactoring, as I wrote about in the past, I made a package that converted the domain object into a Doctrine entity. I did make the Doctrine entity a DTO as much as possible as it should clearly not be used as an entity here (the only downside is you have to be aware of Doctrine proxies not having loaded the properties yet).
Using services in domain objects.
So if I say domain objects should not have direct references to services, many programmers already apply this on their entities. Sometimes a programmer will use things like singletons or Laravel facades in domain objects, but these are forbidden as they are 'hidden' dependencies because the interface does not tell they exist.
Now the funny thing is: services can be used inside domain objects, but only when calling a method on a domain object. For example let's say we have an Address domain object. It will have a zipcode field. We probably need some external API call to find out if an entered zipcode is valid and we probably have a service that does this API call and tell us if a zipcode is valid.
You also don't save whether it's valid or not as a boolean, but you store when it was being validated as valid by storing the current time. If you avoid using services in your domain object, you will end up with methods like setZipcodeValidatedAt(DateTimeInterface $date) or markZipcodeAsValid(). And it becomes even larger of a problem if you fill in street name or city and not providing a method to mark manually entered addresses without a zipcode check (for example in Holland there are no zipcodes in the Netherlands Antilles, but officially they are part of Holland)
If we do use services in our domain object as just a method argument we can avoid opening up everything in the interface:
class Address {
private string $zipcode;
private ?Street $street = null;
private >StreetNumber $streetNumber = null;
private bool $manualAddress = true;
private ?DateTimeInterface $zipcodeValidatedAt = null;
private ?DateTimeInterface $zipcodeCheckFailedAt = null;
public function setZipcode(string $zipcode) {
$this->zipcode = $zipcode;
$this->street = null;
$this->streetNumber = null;
$this->manualAddress = true;
$this->zipcodeValidatedAt = null;
$this->zipcodeCheckFailedAt = null;
}
public function revalidateZipcodeCheck(ZipcodeApi $zipcodeApi): self {
try {
$result = $zipcodeApi->doApiCheck($this->zipcode);
$this->zipcodeValidatedAt = new DateTimeImmutable();
$this->street = new Street($result->street);
$this->streetNumber = new StreetNumber($result->street_number);
$this->manualAddress = false;
} catch (Error) {
$this->zipcodeCheckFailedAt = new DateTimeImmutable();
}
}
}
So in this example we have a method on our domain object that updates the fields again. If we would change the setZipcode() method our address is a manually entered address again and our domain object stays in a stable status without exposing too many methods. Also if the call fails (which could also be the case the street name is not accepted by the Street value object here), we log the zipcode check failed (but we still keep the address as marked valid if a previous check marked is valid)
Validation does not exist in domain objects.
One of the properties of a domain object is that a domain object is robust and can't be in an invalid state, so adding validation in your domain object is the classic example of an anemic domain object and makes no sense. While I normally think Laravel has a worse design over Symfony, Laravel is closer to domain-driven design as it's normal to use form requests for validating your object and not integrate it in your database model.
In Symfony applications it's very common to add validation attributes to your domain object and use the Validator service to validate if your object is valid. The correct solution would be to make a data transfer object as input and use the Symfony validator there and then convert your DTO through the domain object:
class ExampleDto {
#[Assert\Pattern('/^06[0-9]{8}$/')]
#[Assert\Required()]
public string $mobilePhoneNumber;
}
class ExampleDomainObject {
public function __construct(private readonly DutchPhoneNumber $mobilePhoneNumber) {
}
public static function createFromDto(ExampleDto $input): self {
return new self($input->mobilePhoneNumber);
}
}
class ExampleController {
public function create(ValidatorInterface $validator, ExampleDto $input): Response
{
$errors = $validator->validate($input);
if (count($errors) > 0) {
// create response for errors
return $response
}
$domainObject = ExampleDomainObject::createFromDto($input);
}
}
The trade-off here is obvious. The correct solution gives you more objects and methods and an annoying conversion method to convert a DTO in a domain object. Many programmers take the 'shortcut' so they end up with less number of objects at the cost of being 'less' domain-driven. But it's also possible this architecture is not known to a new developer in which case he will think it's better to get rid of the ExampleDto object and just put the validation in the ExampleDomainObject to avoid so much boilerplate code.
Conclusion
So what can we conclude here? Domain-driven design can be very confusing, because there is a lot of terminology and to be honest most frameworks and libraries just do domain-driven design a little, because like in the last example most people do not want so much boiler plate and it's very confusing for new programmers to figure out why adding a property to a resource forces you to add them in 3 or 4 places at the same time.
Of course whenever I work on an application I try to design an application my entire team would find well constructed and not follow the terminology 1 on 1 if that would end up in a more confusing application. Domain-driven design can help very well in writing a robust system with proper data integrity, but it can lead in large discussions how to apply it. If anemic domain objects really were an anti-pattern, you could ask the question why so many people apply it in their codebase. I'm also quite sure experienced programmers will have some opinions about all these things mentioned above whether you should have these in your applications or not.
Comments
Post a Comment