Capítulo 3. Lo que aprendí de DDD. Entidades
Ahora que ya sabemos cómo trabajar con Value Objects, veamos el otro concepto fundamental de nuestro dominio: las entidades
En el capítulo anterior repasé el patrón Value Object para modelar los elementos de nuestro dominio que representan valores, como un precio o un rango de fechas.
Sin embargo, otros elementos de nuestro dominio pueden tener un carácter único, como una persona, una orden de compra o una factura, es decir, representan entidades únicas de nuestra aplicación.
Simplificando mucho esta idea, una entidad vendría a ser una tabla de nuestra base de datos (si estamos empleando un sistema relacional). Yo prefiero entender el concepto entidad como un elemento que posee identidad propia por sí mismo junto con una serie propiedades que pueden variar a lo largo del tiempo.
Identidad
Supongamos que tenemos definida la siguiente clase básica de PHP para representar a la entidad Person
de nuestro dominio:
class Person
{
public function __construct(
private int $id,
private string $name,
private string $surname
) {}
// ...
}
Lo que diferencia a un objeto de la clase Person
de otro es su campo $id
, el cual tiene un valor único a lo largo de toda la aplicación. Da igual que este campo sea, como en el ejemplo, un número, lo importante es que posee un carácter único que sirve para identificar inequívocamente al objeto que representa.
❗️ Importante. Puede que estemos acostumbrados a especificar los identificadores como números, pero también pueden ser sin problema cadenas de texto como uuids o cosas más concretas como el ISBN de un libro o el DNI de una persona. Lo crucial es que estemos seguros de que no se producirán colisiones.
Y ahora que conocemos el poder de los Value Objects, ¿por qué no representarlos mediante ellos?
class Person
{
public function __construct(
private PersonId $id,
private string $name,
private string $surname
) {
}
// getters and setters
}
De este modo, será el Value Object PersonId
el encargado de:
Proveer de la unicidad de cada identificador generado (segregación de responsabilidades).
Encapsular la idea de “identificador de una persona” dentro de un objeto.
Recuerda, con DDD trataremos de explotar el paradigma de la programación orientada a objetos.
Generación de identificadores
Para generar estos identificadores existen 3 formas diferentes de hacerlo. Vamos a verlas.
Delegación en la base de datos
Probablemente esta forma sea la más común y habitual en la mayoría de aplicaciones. Nuestras entidades son construidas sin identificador y no es hasta el momento en que se insertan en base de datos cuando reciben su identificador único (el típico número incremental de MySQL, por ejemplo).
Sin embargo esta aproximación presenta dos problemas:
Algo tan fundamental de nuestro dominio como la generación de identificadores para nuestras entidades lo hemos delegado en un servicio externo sobre el cual no tenemos control.
Además, nuestras entidades son construidas en un estado intermedio o incompleto. Necesitamos insertarlas en base de datos para que sean realmente entidades de nuestro dominio.
class Person
{
public function __construct(
// 🙁 no sé mi identificador private int $id,
private string $name,
private string $surname
) {}
// getters and setters
}
Sé que este punto puede resultar demasiado filosófico, pero, si estamos adoptando un enfoque DDD, no tiene sentido que a las primeras de cambio nuestro dominio pase a girar en torno a la infraestructura (concretamente nuestra base de datos) cuando debería ser al revés.
Delegación en el cliente
Otro enfoque es delegar en el cliente que consume nuestra aplicación la generación de esos identificadores.
Es decir, el identificador procede de fuera de nuestra aplicación, llegándonos por medio de la petición (por ejemplo):
{
"id" => "a8325530-c925-11eb-b8bc-0242ac130003",
"name" => "Gerardo",
"surname" => "Fernández"
}
Ahora sí podríamos construir nuestro objeto de la clase Person
en un estado “completo”, pues dispondríamos de todos los campos para crearlo:
class Person
{
public function __construct(
private PersonId $id,
private string $name,
private string $surname
) {}
// getters and setters
}
$person = new Person(
new PersonId($post['id']),
$post['name'],
$post['surname']
);
Sin embargo, a mí personalmente me resulta raro delegar esta tarea en el cliente. Es cierto que en contextos muy concretos como por ejemplo el de modelar un “libro” podemos emplear el ISBN proporcionado por el cliente de nuestra aplicación, pero no sé si es aplicable en todo tipo de aplicaciones.
La aplicación genera el identificador
La tercera y última opción es que sea la propia aplicación quien se encargue de generar dicho identificador. En mi opinión es la opción que tiene más sentido para la mayoría de casos.
Aquí es donde cobran especial importancia los “identificadores únicos universales”, también conocidos por su abreviatura “uuid”, los cuales nos van a garantizar (casi casi casi al 100% de probabilidad) que cada entidad tendrá un identificador único dentro de nuestra aplicación.
// Ejemplo de uuid
123e4567-e89b-12d3-a456-426614174000
Es decir, en vez de generar números aleatorios para asociarlos a nuestras entidades como identificadores (lo cual podría provocar a la larga colisiones), los “uuids” nos van a garantizar esa unicidad.
Si trabajas con PHP te recomiendo que eches un vistazo a las siguientes librerías para generarlos:
https://github.com/ramsey/uuid
https://symfony.com/doc/current/components/uid.html
Sabiendo esto, podemos modelar el Value Object PersonId
de la siguiente forma:
<?php
namespace App\Entity;
use Symfony\Component\Uid\Uuid;
class PersonId
{
public function __construct(
public readonly Uuid $value
);
public static function generate(): self
{
return new self(Uuid::v4());
}
public function equals(UserId $userId): bool
{
return $this->value->equals($userId->value);
}
}
Ahora, al construir nuestro objeto Person
lo haremos del siguiente modo:
$person = new Person(
PersonId::generate(),
$post['name'],
$post['surname']
);
Si lo deseas, puedes ver cómo migro un proyecto de ids generados en base de datos a uuids en mi curso gratuito de Symfony:
Validación
Otro punto muy interesante sobre las entidades es su validación. Es decir, ¿cómo asegurarnos de que creamos entidades válidas para nuestro dominio?
Os hablaré de los dos tipos de validación que podemos considerar.
Validación de valor
Este tipo de validación es la que realizamos para asegurarnos de que no nos “cuelan” un email inválido o un teléfono de 3 dígitos.
Como su propio nombre indica, una validación valor debería ser acometida dentro del “Value Object” que representa dicho valor. ¿Tiene sentido no? Esto es lo que más me gusta del enfoque DDD: encontrar las cosas allí donde se las espera sin rebuscar por distintas carpetas o clases.
Por tanto, la validación de si un email es válido la realizaremos dentro del Value Object Email
, de forma que impidamos crear emails que no son válidos:
class Email
{
public function __construct(private string $value)
{
$this->ensureValid($value);
}
private function ensureValid(string $value): void
{
// lanza una excepción si es inválido
}
}
Aplicando esto, terminaremos con un constructor similar a éste para nuestra entidad Person
en el que cada propiedad es representada por un ValueObject:
class Person
{
// ...
public function __construct(
private PersonId $id,
private Name $name,
private Surname $surname,
private Email $email
) {}
// ...
}
Validación de integridad
Por otra parte, puede haber validaciones que requieran comprobar propiedades relacionadas entre sí. Por ejemplo, una regla de negocio que impida que un usuario pueda completar su teléfono si ya tiene un email establecido.
Este tipo de validaciones las podríamos realizar en dos lugares:
Dentro del constructor de la propia entidad.
En un servicio de dominio que se encargue de realizar dicha validación.
Ambos enfoques tienen sus ventajas e inconvenientes.
Si validamos dentro del constructor, es posible que terminemos con una clase gigantesca según el número de validaciones que haya que realizar. Sin embargo, nos estaremos asegurando de que cualquier objeto que construyamos lo hará en un estado válido.
Por otra parte, si lo hacemos en un servicio externo, nos veremos obligados a recordar que cada vez que construyamos nuestras entidades, deberemos invocar ese servicio para asegurarnos de que son realmente válidas.
¿Mi consejo? Yo apostaría por validar dentro del constructor siempre y cuando podamos mantener dicha complejidad.
Eventos de dominio
Hablemos finalmente de los eventos de dominio, otro punto clave del enfoque “Domain Driven Design” y que nos permitirá reaccionar a los sucesos que tengan lugar dentro de nuestra aplicación.
Pese a que dedicaré un capítulo completo a este concepto, me parece interesante reseñarlo ahora que estamos hablando de entidades.
La idea clave de los eventos de dominio es publicar todo lo que sucede con respecto a nuestras entidades.
Por ejemplo, cuando construyamos nuestra entidad Person
, no sólo crearemos el objeto sino que registraremos un evento que simbolice la creación de este objeto:
class Person
{
// ...
private array $domainEvents = []
public function __construct(
private PersonId $id,
private Name $name,
private Surname $surname,
private Email $email
) {
// ...
$this->recordDomainEvent(
new PersonCreated($id)
);
}
public function recordDomainEvent(DomainEvent $event): self
{
$this->domainEvents[] = $event;
return $this;
}
public function pullDomainEvents(): array
{
$domainEvents = $this->domainEvents;
$this->domainEvents = [];
return $domainEvents;
} // ...
}
Sabiendo esto, el servicio o clase que creó este objeto Person
deberá publicar (empleando por ejemplo un Event Dispatcher como el que proporciona Symfony) los eventos que la entidad haya registrado:
<?php
namespace App\Service\Person\Application;
...
class CreatePerson
{
public function __construct(
private EventDispatcherInterface $eventDispatcher,
private PersonRepository $personRepository
) {}
public function __invoke(array $post): Person
{
$person = new Person(
PersonId::generate(),
$post['name'],
$post['surname']
);
$this->personRepository->save($person);
foreach ($person->pullDomainEvents() as $event) {
$this->eventDispatcher->dispatch($event);
}
return $person;
}
}
🙏🏻 Gracias a Garaje de Ideas
Los amigos de Garaje de ideas patrocinan Latte and Code y buscan talento: http://bit.ly/garaje-tech-talento
Repositorios de entidades
Si te fijas en el código anterior, estoy empleando la clase PersonRepository
para persistir en base de datos el recién creado objeto $person
.
En el próximo capítulo repasaré el concepto de “repositorio” para concluir con la gestión de entidades de nuestro proyecto.
Hasta entonces, espero que hayas disfrutado este capítulo.
¡Muchas gracias por leerme!