DDD and your database
The introduction of Domain-Driven Design (DDD) to a larger audience has led to a few really damaging ideas among developers, like this one (maybe it's more a sentiment than an idea):
Data is bad, behavior is good. The domain model is great, the database awful.
(We're not even discussing CRUD in this article, which apparently is the worst of the worst.)
By now many of us feel ashamed of using an ORM alongside a "DDD domain model", putting some mapping configuration in it, doing things inside your entities (or do you call them aggregates?) just to make them easily serializable to the database.
Infrastructure code in your entities
We want our domain model to be pure objects, have only "domain concerns", leading us to reinvent assertion libraries just because "those are in /vendor". We think that mapping configuration is infrastructure code, whether you use annotations or write your mapping code yourself.
In a previous article (Is all code in vendor infrastructure code?) I've shared my definition of infrastructure code and we discussed a more practical definition as well: infrastructure code is code that is not sufficiently isolated to test it with a unit test. Let's use this definition for domain models that are prepared for persistence - that are "ORM-ready" so to speak. In order to know if we have properly separated infrastructure code from domain code, we only have to check if we can exercise the model's behavior in a unit test. Let's show Michael Feather's definition of a unit test once more:
A test is not a unit test if:
- It talks to the database
- It communicates across the network
- It touches the file system
- It can't run at the same time as any of your other unit tests
- You have to do special things to your environment (such as editing config files) to run it.
So to test a domain model's behavior with a unit test it shouldn't need an actual database, a network connection, a file system, etc. No "special setup" should be required. You should be able to instantiate the object and call a method on it, just like that.
Entities should be testable in isolation
Let's look at some common cases where people might be worried that they have infrastructure code in their domain model. What about this code:
/**
* @Entity
* @Table(name="todo_items")
*/
final class ToDoItem
{
/**
* @Id
* @Column(type="integer")
* @GeneratedValue
*/
private int $id;
/**
* @Column(type="string")
*/
private string $description;
public function __construct()
{
}
public function setDescription(string $description): void
{
$this->description = $description;
}
// ...
}
Is it true, does this code pass the test? It does: you can run this code without a database, etc. without external dependencies. You can run this code without preparing the context in a special way. In a unit test you can just do this:
$toDoItem = new ToDoItem();
$toDoItem->setDescription('The description');
// ...
So none of this code is infrastructure code, although those annotations are definitely meant to support persistence and you could say this class has "infrastructure concerns".
What about the following example, which has hand-written mapping code in it?
final class ToDoItem
{
private int $id;
private string $description;
public function __construct()
{
}
public function setDescription(string $description): void
{
$this->description = $description;
}
public function getState(): array
{
return [
'id' => $this->id,
'description' => $this->description
];
}
public static function fromState(array $state): self
{
$instance = new self();
$instance->id = (int)$state['id'];
$instance->description = (int)$state['description'];
return $instance;
}
public static function getTableName(): string
{
return 'todo_items';
}
// ...
}
It's not a great model. But again, you can just create an instance of this class, call any method on it, and make some assertions. There are no external dependencies, or special setup needed. So far we haven't seen any example of infrastructure code, even though we saw table names, column names, and column types.
Testing
Truncated by Planet PHP, read more at the original (another 5581 bytes)