avatarGerardo Fernández

Free AI web copilot to create summaries, insights and extended knowledge, download it at here

5721

Abstract

ass="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">__construct</span>(<span class="hljs-params">UserId <span class="hljs-variable">userId</span></span>) </span>{ <span class="hljs-variable language_">this</span>->userId = <span class="hljs-variable">userId</span>; <span class="hljs-variable language_">this</span>->ocurredOn = <span class="hljs-keyword">new</span> <span class="hljs-title class_">DateTimeImmutable</span>(); }</pre></div><div id="6141"><pre> <span class="hljs-keyword">public</span> <span class="hljs-keyword">function</span> <span class="hljs-title function_">getUserId</span><span class="hljs-params">()</span>: UserId { <span class="hljs-keyword">return</span> <span class="hljs-keyword">this</span>-&gt;userId; }</pre></div><div id="ec26"><pre> <span class="hljs-keyword">public</span> <span class="hljs-keyword">function</span> <span class="hljs-title function_">getOcurredOn</span><span class="hljs-params">()</span>: DateTimeImmutable { <span class="hljs-keyword">return</span> <span class="hljs-keyword">this</span>->ocurredOn; } }</pre></div><p id="5eb1">Como veis, el evento de dominio cumple con todas las características que mencionábamos anteriormente:</p><ul><li>Es inmutable (no contiene <i>setters).</i></li><li>Todos sus campos quedan inicializados en el constructor.</li><li>Las entidades involucradas aparecen referenciadas mediante su identificador.</li><li>Disponemos del campo <code>ocurredOn</code> para saber en qué momento se produjo el evento.</li></ul><h1 id="545a">Creando el evento de dominio</h1><p id="fc19">Ahora que ya disponemos de un evento de dominio, lo siguiente será crearlo. Aquí existen diferentes alternativas.</p><ul><li>Algunos desarrolladores son partidarios de crear el evento de dominio en el caso de uso.</li><li>Otros prefieren que sea la entidad la que cree el evento de dominio, permitiendo a los servicios recuperar los eventos registrados mediante un método <i>getter </i>en la entidad.</li></ul><p id="4719">A mí personalmente me gusta mucho más esta segunda opción, pues me da la sensación de que es más segura. Si un usuario puede ser creado desde diferentes servicios / casos de uso, la entidad queda como responsable de crear el evento de dominio de modo que no tendremos que preocuparnos de acordarnos de crear el evento de dminio en cada servicio.</p><p id="f0e8">Además, para mí está muy arraigada la idea de que bajo el enfoque DDD debemos acercar lo máximo posible la lógica a las entidades, por lo que desde mi punto de vista tiene sentido que sean ellas las encargadas de crear los eventos de dominio siempre que sea posible.</p><p id="b6ef">De este modo, añadiremos lo siguiente a nuestra entidad <code>User</code> :</p><div id="9fc1"><pre><span class="hljs-meta"><?php</span></pre></div><div id="944f"><pre><span class="hljs-keyword">class</span> <span class="hljs-symbol">User</span> { <span class="hljs-comment">// other properties</span></pre></div><div id="6964"><pre> <span class="hljs-keyword"> protected</span><span class="hljs-built_in"> array </span>domainEvents = [];</pre></div><div id="c57e"><pre> <span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <span class="hljs-title function_"><span class="hljs-keyword">function</span> <span class="hljs-title">create</span></span>(<span class="hljs-comment">/* arguments */</span>): <span class="hljs-type">self</span> { <span class="hljs-variable">user</span> = <span class="hljs-keyword">new</span> <span class="hljs-type">self</span>(<span class="hljs-comment">/* arguments */</span>); <span class="hljs-variable">uer</span>-&gt;recordDomainEvent( <span class="hljs-keyword">new</span> <span class="hljs-type">UserCreatedEvent</span>(<span class="hljs-variable">user</span>->getId()) ); <span class="hljs-keyword">return</span> <span class="hljs-variable">user</span>; }</pre></div><div id="a120"><pre> <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">recordDomainEvent</span>(<span class="hljs-params">DomainEvent <span class="hljs-variable">event</span></span>): <span class="hljs-title">self</span> </span>{ <span class="hljs-variable language_">this</span>-&gt;domainEvents[] = <span class="hljs-variable">event</span>; <span class="hljs-keyword">return</span> <span class="hljs-variable language_">this</span>; }</pre></div><div id="871a"><pre> <span class="hljs-keyword"> public</span> function pullDomainEvents():<span class="hljs-built_in"> array </span> { domainEvents = this-&gt;domainEvents; this->domainEvents = []; <span class="hljs-built_in"> return </span>$domainEvents; }</pre></div><p id="2349">Aquí destacan varias cosas.</p><p id="e8c5">La primera es que la entidad dispone de un array llamado <code>domainEvents</code> donde se registrarán todos los eventos de dominio generados durante la ejecución del caso de uso.</p><p id="7fc9">El método <code>pullDomainEvents</code> nos permitirá recuperar los eventos de dominio de cara a publicarlos y vaciar el array para evitar que se publiquen varias veces.</p><p id="74c3">Finalmente y posiblemente lo que más te haya llamado la atención. En vez de crear el evento de dominio en el constructor, añadiremos un método estático <code>create</code> dentro de la clase <code>User</code> donde construiremos la entidad y añadiremos el evento de dominio pertinente.</p><p id="444f">Esto nos evitará crear un evento de dominio siempre que hagamos un <code>new User</code> , algo que puede que no qu

Options

eramos que suceda en determinadas circunstancias (por ejemplo, cuando la entidad es recuperada desde Doctrine).</p><h1 id="ba59">Publicando el evento de dominio</h1><p id="f71e">Finalmente, será el servicio o caso de uso quien se encargue de publicar los eventos de dominio generados dentro de la entidad.</p><p id="ae1d">En Symfony disponemos de los componentes <code>EventDispatcher</code> y <code>Messenger</code> para publicar eventos y escucharlos (el segundo permite además encolar los eventos para procesarlos de forma asíncrona):</p><p id="11af">Por tanto, nuestro servicio / caso de uso <code>CreateUser</code> puede tener la siguiente forma:</p><div id="062f"><pre><span class="hljs-meta"><?php</span></pre></div><div id="ce1a"><pre>namespace App\Service\User\Application;</pre></div><div id="31b9"><pre><span class="hljs-keyword">use</span> <span class="hljs-keyword">App</span>\Entity\User; <span class="hljs-keyword">use</span> <span class="hljs-keyword">App</span>\Entity\User\UserId; <span class="hljs-keyword">use</span> <span class="hljs-keyword">App</span>\Repository\UserRepository; <span class="hljs-keyword">use</span> Symfony\Contracts\EventDispatcher\EventDispatcherInterface;</pre></div><div id="c0c6"><pre><span class="hljs-keyword">class</span> <span class="hljs-symbol">CreateUser</span> {</pre></div><div id="e1d0"><pre> <span class="hljs-keyword">private</span> UserRepository <span class="hljs-variable">userRepository</span>; <span class="hljs-keyword">private</span> EventDispatcherInterface <span class="hljs-variable">eventDispatcher</span>;</pre></div><div id="033a"><pre> <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">__construct</span>(<span class="hljs-params"> EventDispatcherInterface <span class="hljs-variable">eventDispatcher</span>, UserRepository <span class="hljs-variable">userRepository</span> </span>) </span>{ <span class="hljs-variable language_">this</span>-&gt;userRepository = <span class="hljs-variable">userRepository</span>; <span class="hljs-variable language_">this</span>-&gt;eventDispatcher = <span class="hljs-variable">eventDispatcher</span>; }</pre></div><div id="d2d3"><pre> <span class="hljs-keyword">public</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">__invoke</span>(<span class="hljs-params"><span class="hljs-comment">/* args */</span></span>): <span class="hljs-title">User</span> </span>{ <span class="hljs-variable">user</span> = <span class="hljs-title class_">User</span>::<span class="hljs-title function_ invoke__">create</span>( <span class="hljs-title class_">UserId</span>::<span class="hljs-title function_ invoke__">generate</span>(), <span class="hljs-comment">/* args */</span> ); <span class="hljs-variable language_">this</span>->userRepository-><span class="hljs-title function_ invoke__">save</span>(<span class="hljs-variable">user</span>); <span class="hljs-keyword">foreach</span> (<span class="hljs-variable">user</span>-><span class="hljs-title function_ invoke__">pullDomainEvents</span>() <span class="hljs-keyword">as</span> <span class="hljs-variable">event</span>) { <span class="hljs-variable language_">this</span>->eventDispatcher-><span class="hljs-title function_ invoke__">dispatch</span>(<span class="hljs-variable">event</span>); } <span class="hljs-keyword">return</span> <span class="hljs-variable">user</span>; } }</pre></div><p id="2c0b">Como veis, tras ejecutar el código correspondiente al caso de uso, recuperaremos los eventos de dominio generados y los publicaremos para que quien quiera pueda actuar en consecuencia.</p><p id="eff1">Creo que la ventaja que suponen los eventos de dominio se aprecia muy bien en este caso. Por ejemplo, en vez de tener toda la lógica para enviar un email de confirmación dentro del caso de uso, esta queda delegada a otro servicio.</p><h1 id="7af7">💛 Apóyame en Patreon</h1><p id="b36c">Si te ha gustado este o cualquier otro de mis artículos, puedes unirte al resto de mis <i>patreons</i> para seguir apoyándome en la creación de contenidos.</p><div id="869a" class="link-block"> <a href="https://www.patreon.com/latteandcode"> <div> <div> <h2>Gerardo Fernández Moreno is creating courses about developing with React, Javascript and Symfony |…</h2> <div><h3>Soy Gerardo. Tengo 33 años y desde pequeño me han encantado los ordenadores y la programación. Ya ha pasado mucho…</h3></div> <div><p>www.patreon.com</p></div> </div> <div> <div style="background-image: url(https://miro.readmedium.com/v2/resize:fit:320/0*roHGXaHvnVsVht0l)"></div> </div> </div> </a> </div><p id="56c0">Este artículo ha sido posible gracias a Alex, Jorge, Joseba, Óscar, Sergio, Karolin, Taig y Daniel.</p><p id="57fe">¡GRACIAS!</p><h1 id="cf49">Conclusión</h1><p id="ee69">Los eventos de dominio son una pieza fundamental del enfoque <b>Domain Driven Design </b>gracias a que nos permiten publicar las acciones que tienen lugar dentro de nuestras entidades para que otros servicios o componentes de nuestra aplicación reaccionen a ellas.</p><p id="04b8">Además, su implementación es muy sencilla gracias a las herramientas que nos proporcionan los frameworks de desarrollo, como es el caso de Symfony con sus componentes <code>EventDispatcher</code> y <code>Messenger</code> .</p></article></body>

Capítulo 5. Lo que aprendí de DDD. Eventos de dominio

Los eventos de dominio cumplen la función de notificar al resto de componentes de nuestra aplicación de cambios en nuestro dominio

Photo by Possessed Photography on Unsplash

Otro de los conceptos básicos que aparecen dentro del enfoque Domain Driven Design es el de los eventos de dominio.

Los eventos de dominio representan cambios en nuestro dominio con el objetivo de comunicarlos a otros elementos de nuestra aplicación y, llegado el caso, incluso mantener un histórico de todo lo acontecido desde que lanzamos nuestra aplicación.

Por supuesto, los eventos de dominio también son la puerta para introducir asincronicidad en nuestro código.

¡Comencemos!

La idea detrás de los eventos de dominio

Para entender bien los eventos de dominio supongamos una aplicación web que gestiona una tienda online.

Cuando un cliente adquiere un producto lo más habitual será que además de registrar el pedido, realicemos una serie de acciones secundarias como enviar un email de confirmación, avisar al almacén o generar un PDF con la factura.

Los eventos de dominio nos permiten lograr esto de una forma muy sencilla y, lo más importante, desacoplada 100% del servicio principal encargado de registrar el pedido.

Para ello, el servicio CreateOrder tras guardar el pedido, publicará un evento OrderCreatedEvent que será recibido por los diferentes “listeners” u “observers”, los cuales llevarán a cabo las acciones pertinentes. En este ejemplo, definiríamos 3 listeners, uno para el email de confirmación, otro para notificar al almacén y el último para generar la factura.

Estas acciones podrán ser ejecutadas tanto síncronamente como asíncronamente en el caso de que nuestro sistema permita publicar los eventos dentro de colas que sean procesadas por procesos del servidor corriendo en segundo plano.

Ventajas de trabajar con eventos de dominio

Trabajar con eventos de dominio nos proporcionará varias ventajas:

  • La primera es que será mucho más sencillo extender la funcionalidad de nuestra aplicación (el principio SOLID Open-Closed) ya que podremos escuchar los eventos que se lancen para definir nuevo comportamiento en vez de tener que modificar el código de los servicios / casos de uso.
  • La segunda es una consecuencia de la primera y es que todo quedará mucho más desacoplado y el tamaño de nuestros casos de uso se verá reducido, pues implementarán tan solo la funcionalidad de la que son responsables.

Además, y como ya comentaba antes, los eventos de dominio recogen todos los cambios que han sucedido en nuestro dominio, por lo que podríamos loggearlos de cara a tener un histórico de las acciones sucedidas dentro de nuestra aplicación.

Características de los eventos de dominio

Los eventos de dominio no son más que objetos planos que encapsulan la información de lo sucedido.

Por tanto, conviene que sean inmutables (no queremos que nadie los modifique por el camino) y serializables, es decir, no deberían contener dentro de ellos objetos que no puedan ser serializados directamente (como entidades de Doctrine). Por tanto, es recomendable que contengan el identificador de las entidades de dominio modificadas en vez del objeto completo.

Finalmente, puesto que los eventos de dominio representan cosas que han sucedido dentro de nuestra aplicación, su nombre deberá incluir el verbo de la acción sucedida en pasado, por ejemplo, UserEmailChanged o ProductCreated .

Nota. También es deseable incluir dentro de los eventos de dominio un campo con el timestamp del evento, de forma que podamos recuperar la fecha de cuando sucedió el evento.

Modelando un evento de dominio

Ahora que ya sabemos qué son los eventos de dominio y cómo podemos usarlos, veamos cómo podemos modelarlos.

Como siempre, emplearé PHP para los ejemplos, pero recordad que esto es fácilmente exportable a otros lenguajes y frameworks.

Lo primero que haremos será modelar el evento de dominio UserCreated :

<?php
namespace App\Event\User;
use App\Entity\User\UserId; 
use DateTimeImmutable;
use Symfony\Contracts\EventDispatcher\Event;
class UserCreatedEvent extends Event
{
    protected DateTimeImmutable $ocurredOn;
    protected UserId $userId;
    public function __construct(UserId $userId)
    {
        $this->userId = $userId; 
        $this->ocurredOn = new DateTimeImmutable();
    }
    public function getUserId(): UserId
    {
        return $this->userId;
    }
    public function getOcurredOn(): DateTimeImmutable
    {
        return $this->ocurredOn;
    }
}

Como veis, el evento de dominio cumple con todas las características que mencionábamos anteriormente:

  • Es inmutable (no contiene setters).
  • Todos sus campos quedan inicializados en el constructor.
  • Las entidades involucradas aparecen referenciadas mediante su identificador.
  • Disponemos del campo ocurredOn para saber en qué momento se produjo el evento.

Creando el evento de dominio

Ahora que ya disponemos de un evento de dominio, lo siguiente será crearlo. Aquí existen diferentes alternativas.

  • Algunos desarrolladores son partidarios de crear el evento de dominio en el caso de uso.
  • Otros prefieren que sea la entidad la que cree el evento de dominio, permitiendo a los servicios recuperar los eventos registrados mediante un método getter en la entidad.

A mí personalmente me gusta mucho más esta segunda opción, pues me da la sensación de que es más segura. Si un usuario puede ser creado desde diferentes servicios / casos de uso, la entidad queda como responsable de crear el evento de dominio de modo que no tendremos que preocuparnos de acordarnos de crear el evento de dminio en cada servicio.

Además, para mí está muy arraigada la idea de que bajo el enfoque DDD debemos acercar lo máximo posible la lógica a las entidades, por lo que desde mi punto de vista tiene sentido que sean ellas las encargadas de crear los eventos de dominio siempre que sea posible.

De este modo, añadiremos lo siguiente a nuestra entidad User :

<?php
class User
{
  // other properties
  protected array $domainEvents = [];
  public static function create(/* arguments */): self
  {
    $user = new self(/* arguments */);
    $uer->recordDomainEvent(
       new UserCreatedEvent($user->getId())
    ); 
    return $user;
  }
  public function recordDomainEvent(DomainEvent $event): self
  {
      $this->domainEvents[] = $event;
      return $this;
  }
  public function pullDomainEvents(): array
  {
      $domainEvents = $this->domainEvents;
      $this->domainEvents = [];
      return $domainEvents;
  }

Aquí destacan varias cosas.

La primera es que la entidad dispone de un array llamado domainEvents donde se registrarán todos los eventos de dominio generados durante la ejecución del caso de uso.

El método pullDomainEvents nos permitirá recuperar los eventos de dominio de cara a publicarlos y vaciar el array para evitar que se publiquen varias veces.

Finalmente y posiblemente lo que más te haya llamado la atención. En vez de crear el evento de dominio en el constructor, añadiremos un método estático create dentro de la clase User donde construiremos la entidad y añadiremos el evento de dominio pertinente.

Esto nos evitará crear un evento de dominio siempre que hagamos un new User , algo que puede que no queramos que suceda en determinadas circunstancias (por ejemplo, cuando la entidad es recuperada desde Doctrine).

Publicando el evento de dominio

Finalmente, será el servicio o caso de uso quien se encargue de publicar los eventos de dominio generados dentro de la entidad.

En Symfony disponemos de los componentes EventDispatcher y Messenger para publicar eventos y escucharlos (el segundo permite además encolar los eventos para procesarlos de forma asíncrona):

Por tanto, nuestro servicio / caso de uso CreateUser puede tener la siguiente forma:

<?php
namespace App\Service\User\Application;
use App\Entity\User;
use App\Entity\User\UserId;
use App\Repository\UserRepository;
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
class CreateUser
{
    private UserRepository $userRepository;
    private EventDispatcherInterface $eventDispatcher;
    public function __construct(
        EventDispatcherInterface $eventDispatcher,
        UserRepository $userRepository
    ) {
        $this->userRepository = $userRepository;
        $this->eventDispatcher = $eventDispatcher;
    }
    public function __invoke(/* args */): User
    {
        $user = User::create(
            UserId::generate(),
            /* args */
        );
        $this->userRepository->save($user);
        foreach ($user->pullDomainEvents() as $event) {
            $this->eventDispatcher->dispatch($event);
        }
        return $user;
    }
}

Como veis, tras ejecutar el código correspondiente al caso de uso, recuperaremos los eventos de dominio generados y los publicaremos para que quien quiera pueda actuar en consecuencia.

Creo que la ventaja que suponen los eventos de dominio se aprecia muy bien en este caso. Por ejemplo, en vez de tener toda la lógica para enviar un email de confirmación dentro del caso de uso, esta queda delegada a otro servicio.

💛 Apóyame en Patreon

Si te ha gustado este o cualquier otro de mis artículos, puedes unirte al resto de mis patreons para seguir apoyándome en la creación de contenidos.

Este artículo ha sido posible gracias a Alex, Jorge, Joseba, Óscar, Sergio, Karolin, Taig y Daniel.

¡GRACIAS!

Conclusión

Los eventos de dominio son una pieza fundamental del enfoque Domain Driven Design gracias a que nos permiten publicar las acciones que tienen lugar dentro de nuestras entidades para que otros servicios o componentes de nuestra aplicación reaccionen a ellas.

Además, su implementación es muy sencilla gracias a las herramientas que nos proporcionan los frameworks de desarrollo, como es el caso de Symfony con sus componentes EventDispatcher y Messenger .

Ddd
Domain Driven Design
Hexagonal Architecture
Symfony
PHP
Recommended from ReadMedium