Teaching and Learning Domain-Driven Design without Patterns
First published at Friday 26 February 2016
Warning: This blog post is more then 8 years old – read and use with care.
Teaching and Learning Domain-Driven Design without Patterns
When development teams start to use Domain-Driven Design (DDD) in work projects, by focussing on patterns such as Entity, Repository, Services and Value Objects, then there is trouble ahead. In my experience with teams using DDD the pattern-thinking can be very distracting and dangerous. An approach without patterns can bring the essentials across much better. Especially, less experienced developers have a hard time to follow DDD by pattern because the "Why and How are we doing this" is not easy to teach.
For this post, I am assuming you are starting to use DDD as a developer team without real backing of a domain expert – which seems to be the widespread default. Don't forget to remind yourself that you are missing a critical part, the domain expert to help you get the critical nuances of your code right.
In my experience, when you apply very strict DDD in this scenario, the domain expert will change or specify the requirements at some point and leave you helpless with the wrong model. This can be a huge setback and cause a lot of friction and code changes. CRUD (Anemic) models are much more resilient here, because you can easily adapt or work around all the already existing business logic.
So lets say you are using a mostly CRUD-centric style to application development and still want to benefit from some of the achievements of DDD, that is - you would like business rules to be:
using the language of the domain.
enforced and translated into code as stricly as possible.
independent of technical details and frameworks.
The technical DDD patterns are just one means to those ends. They are not an end on their own, just applying the patterns doesn't make any sense.
We can try to bring those properties to live by relentlessly applying the SOLID principles or the four rules of simple design.
Take the following (extremely simplified) code example performing some money calculations and then storing the result in a database table. It is written from a CRUD perspective using the transaction script pattern to implement the "discount" use-case:
<?php
namespace MyShop\Domain;
class OrderService
{
/**
* @param int $orderId
* @param float $percent (between 0 and 1)
*/
public function discount($orderId, $percent)
{
if ($percent < 0 || $percent > 1) {
throw new \InvalidArgumentException("Percent must be float between 0 and 1");
}
$orderTotal = $this->selectOrderTotal($orderId);
$orderTotal = (1 - $percent) * $orderTotal;
$sql = 'UPDATE order SET total = ? WHERE id = ?';
$this->connection->executeQuery($sql, [$orderTotal, $orderId]);
}
}
Please ignore the consistency problems here – order items and taxes are simply ignored in this simple example.
The business rules in this snippet cannot live up to the DDD practices:
Business rules are not strictly enforced for Percent and Money values, they are implemented only when they are needed, operating on primitive tpyes like floats and integers.
No independence of technical details, the business rules about discount handling, percent and money calculations are mixed with SQL statements.
Without DDD patterns, we start with applying SOLID principles. The single responsibility principle teaches us, that no class should have more than one responsibility, but here OrderService
knows how to validate percentage values and how to multiply money with percentages.
So lets refactor and introduce a class Percent and a class Money:
<?php
namespace MyShop\Domain;
class Percent
{
private $value;
public function __construct($value)
{
if ($value < 0 || $value > 1) {
throw new \InvalidArgumentException("Percent must be float between 0 and 1");
}
$this->value = $value;
}
public function asFloat()
{
return $this->value;
}
}
class Money
{
private $value;
public function __construct($value)
{
$this->value = $value;
}
public function reduceBy(Percent $percent)
{
return new Money($this->value * (1 - $percent->asFloat()));
}
public function asFloat()
{
return $this->value;
}
}
class OrderService
{
public function discount($orderId, Percent $percent)
{
$orderTotal = $this->selectOrderTotal($orderId);
$orderTotal = $orderTotal->reduceBy($percent);
$sql = 'UPDATE order SET total = ? WHERE id = ?';
$this->connection->executeQuery($sql, [$orderTotal->asFloat(), $orderId]);
}
}
Much better! We have separated the responsibilities. Please ignore the fact Percent and Money should probably not use floats internally, there is much more to improve in the code example, which is outside of this blog post.
But we are not done with using SOLID as an improvement method to refactor towards DDD principled code. The dependency inversion principle states that high level code should never depend on low level code, but our high level business logic depends on low level database code here.
Let's introduce an interface that abstracts the data access:
<?php
namespace MyShop\Domain;
interface OrderTotalGateway
{
public function fetch($orderId);
public function update($orderId, Money $total);
}
class OrderService
{
public function discount($orderId, Percent $percent)
{
$orderTotal = $this->totalGateway->fetch($orderId);
$orderTotal = $orderTotal->reduceBy($percent);
$this->totalGateway->update($orderId, $orderTotal);
}
}
Now we can implement this interface outside the MyShop\Domain
namespace, where only high level (business logic) is located, not database code.
namespace MyShop\Implementations\Doctrine;
use MyShop\Domain\Money;
use MyShop\Domain\OrderTotalGateway;
class DbalOrderTotalGateway implements OrderTotalGateway
{
public function fetch($orderId) { // ... }
public function update($orderId, Money $total)
{
$sql = 'UPDATE order SET total = ? WHERE id = ?';
$this->connection->executeQuery($sql, [$total->asFloat(), $orderId]);
}
}
Now only high-level code is located in the OrderService
and details such as the money being stored as a float in the database has been pushed down into a low level implementation.
Without ever knowing about DDD patterns we have introduced Value Objects (Percent
and Money
) and a class loosely resembling Repository (OrderTotalGateway
). The business logic is independent from technical details and enforced in a much stricter way then before using encapsulation.
The similar result can be achieved by applying the four rules of simple design, in this case "No duplication" and "Reveals intent" can help achieving the same result without knowing about the SOLID principles.
Another approach to achieve yet the same result is to view this problem from an anti-pattern or code-smell perspective: The original OrderService class exhibits a severe case of feature-envy, drawing too much logic onto itself. One reason for this is the primitive obsession using floats for Money and Percent. Getting rid of this problem also leads to introduction of the Money
and Percent
classes.
Don't make the mistake to implement DDD by thinking by the blue books building blocks and technical patterns too much. Using a refactoring based approach combined with SOLID principles or the Four Rules of Simple Design lets you move towards DDD in much smaller and sustainable way. If you want to implement technical DDD practices in your team, start with SOLID and Refactoring instead. This is much easier to teach and learn. Gradually showing team members how the code improves and how simple these small steps goes a long way to convince the team to start using more challenging DDD patterns.
Subscribe to updates
There are multiple ways to stay updated with new posts on my blog: