Implementa el Patrón de Diseño Command Dispatcher en LARAVEL

Spread the love

El patrón Command Dispatcher es una mejora del Patrón Command, ya que separa de forma mas efectiva el objeto que representa una acción del objeto que lo procesara o manejara.

Idea del command handler
Command Dispatcher

En otras palabras el command dispatcher introduce un objeto que se encarga de relacionar una orden o acción con un objeto responsable de ejecutarla, con esto la orden y el manejador no tienen conocimiento uno del otro ya que el comman dispatcher sirve como un intermediario entre ellos.

Y esto nos permite crear servicios reutilizables que pueden ser extendidos o modificados sin tener que hacer grandes cambios en ellos, esto lo vas a ver con mas detalle cuando veas el código.

Muy bien vamos a suponer que tenemos un controlador con el siguiente código:

<?php

namespace App\Http\Controllers;

use App\Models\User;
use App\Http\Requests\UpdateUserRequest;
use App\Notifications\UpdatedProfileNotification;

class UserController extends Controller
{
    //...

    public function update(UpdateUserRequest $request, User $user): \Illuminate\Http\RedirectResponse
    {
        $user->update(
            collect($request->validated())->filter()->all()
        );

        $user->notify(new UpdatedProfileNotification());

        return back()->withSuccess('Successful update data');
    }

    //...
}

Si bien este código es simple y sencillo tiene el problema de que no es reutilizable y tu, no quieres eso. Tu quieres tener la opción de usar este mismo código en el controlador de un API Rest o en un comando de consola sin tener que copiar el código.

Además los cambios que se realicen aquí te obligan a modificar todos los lugares donde hallas copiado este código.

Y precisamente el patrón Command Distpacher te ayudara con esta tarea.:

Repositorio

Si quieres ver todos los detalles de la implementación Seba Carrasco Poblete a relaizado un gran trabajo creando un repositorio que recrea este ejemplo basándose en esta explicación 👍🏼

¿Qué es el Patrón Command Distpacher?

El patrón Command Distpacher es un objeto que vincula la solicitud de una acción con un manejador de acción apropiado. Su propósito es separar la operación de un comando de los objetos emisores y receptores para que ninguno tenga conocimiento del otro.

Cuando debes de aplicar el patrón Command Distpacher

Separación por capas: puedes organizar tu código para enviar comandos a una capa interior, a través de la interfaz consistente proporcionada por el command dispatcher.

Separación del cliente y el Framwework: permite mantener tu aplicación manejable a medida que el framework cambia o se actualiza ya que las propiedes necesarias para realizar una tarea, se pasan como comandos, de esta forma tu logica no tiene que acoplarse al framework y esto hace que tu aplicación sea más fácil de probar y mantener.

Separar la intención de la interpretación: El papel del command dispatcher es transportar un comando a su manejador. Esto significa intrínsecamente que la intención de llevar a cabo una acción está separada de la ejecución.

Estructura del Patrón Command Distpacher

Diagrama de clases comand handler
Diagrama de clases

debes notar que debe de haber un manejador de comandos distinto para cada tipo de comando que se pueda invocar. Los manejadores de comandos pueden ser registrados y eliminados en tiempo de ejecución simplemente proporcionando un nombre para el registro, independientemente de la aplicación.

Es importante que tambien notes que la clase invoker puede o no estar y cuando eso sucede el cliene se encarga de pasar directamente el comando que debe manejar la clase CommandDistpacher.

La clase CommandDistpacher debes de implementarla como un repositorio que va a poder registrar y o alminar manejadores de comandos y además se encargara de relacionar un comando con un solo ConcreteCommandHandler.

Ahora veremos que hacen cada una de las clases del diagrama.

Participantes en el Modelo

  • Invoker – Contiene una colección de Comandos que van a ser ejecutados
  • Command – representa la solicitud o cambios de estado a ser procesada, encapsulando todos los parámetros que utilizara un manejador para procesar el comando.
  • Command Handler – especifica la interfaz que cualquier manejador de comandos debe implementar
  • Concrete Command Handler – implementa la solicitud o el cambio de estado
  • Command Dispatcher – Permite el registro dinámico de Manejadores de Comandos y busca manejadores para los comandos, haciendo coincidir el comando y la clave del manejador.
  • Receiver – Es la clase que realmente realiza la acción y recibe los parámetros provistos por el command
  • Client – registra los comandos con el despachador de comandos.

Cómo funciona

El ciclo de vida del patrón Command Distpacher es el siguiente:

  1. La aplicación recibe una solicitud(Request).
  2. Se crea un comando utilizando la información de esa entrada.
  3. El comando se pasa a un despachador.
  4. El despachador encuentra el manejador para ese comando.
  5. El comando se pasa al manejador.
  6. El manejador ejecuta ese comando.
  7. Se puede devolver un resultado o no.

Siguiendo los pasos esto se ve de la siguiente forma desde un controlador:

$command = new Command($request->all());
$dispatcher = new CommandDispatcher(new ConcreteHandler());
//El Dispatcher resuelve el Concrete handler y lo ejecuta con el comando
$result = $dispatcher->dispatch($command);

Soluciona el problema

Lo primero que vamos a determinar son los actores que se presentan en nuestro problema.

Asi que tomando en cuenta el código de nuestro ejemplo, podemos ver que nuestro comando es la petición que llega mediante la clase Request de Laravel para actualizar el modelo User.

Tendras que tener un manejador para cada comando, que en este caso solo sera para la actualización del modelo.

El modelo User representa el receiver en nuestro diagrama y finalmente es el que procesa los datos enviados mediante una petición a tu controlador.

El invoker sera el servicio que recibe los comandos y los delega al CommandDispatcher, tambien observa que el invoker debe de recibir los comandos y manejadores que usara.

Finalmente el cliente sera nuestro controlador y solo tendra la tarea de ejecutar nuestro servicio y pasarle un comando.

con esta información vamos a actualizar nuestro diagrama de clases.

Solución usando el command dispatcher
Solución dragrma de clases

Crea los comandos

En este patrón los comandos son clases simples que contienen todos los datos necesarios para realizar una tarea especifica, por lo general llevan nombres que representan las operaciones que van a realizar como RegisterUser, CreatePost, etc.

Un DTO es un objeto que transporta datos entre procesos para reducir el número de llamadas a métodos.

P of EAA

Como los comandos deben de ser simples es muy común implementarlos como DTO.

Para este ejemplo los DTO o Command van a extender de una clase abstracta que se encargara de proporcionarnos acceso a sus propiedades de una forma muy similar a como lo hace Eloquent.

Esto es para que no tengas que estar escribiendo clases con variables y métodos publicos para cada comando.

<?php

namespace App\Services\Commands;


abstract class Command
{
    protected array $parameters;

    public function __construct(array $parameters)
    {
        $this->parameters = $parameters;
    }

    public function __get($name)
    {
        return $this->parameters[$name];
    }

    public function __set($name, $value)
    {
        $this->parameters[$name] = $value;
    }

    public function toArray(): array
    {
        return $this->parameters;
    }
}

Ahora solo tenemos que crear el comando UpdateCommand para desarrollar nuestro ejemplo:

<?php

namespace App\Services\Commands\Users;

use App\Services\Commands\Command;

class UpdateCommand extends Command
{
    public static function new(array $parameters)
    {
        return new static($parameters);
    }
}

Crear los Command Handler

Todos los manejadores de comandos tendrán la misma interfaz y esto nos sirve para crear nuevos manejadores que despues podemos inercambiar en tiempo de ejecucióno solo cambiando el comando.

<?php

namespace App\Services\Commands;

interface CommandHandler
{
    public function handle(Command $command);
}

Cada manejador recibe un command como argumento y además debe de haber tantos manejadores como comandos necesites manejar, en una relación uno a uno.

Para nuestro ejemplo crearemos el manejador para nuestro UpdateCommand

<?php

namespace App\Services\Commands\Handlers;

use App\Models\User;
use App\Notifications\UpdatedProfileNotification;
use App\Services\Commands\Command;
use App\Services\Commands\CommandHandler;

class UpdateCommandHandler implements CommandHandler
{
    public function handle(Command $command): ?User
    {
        $user = User::findOrFail($command->user);

        $user->update(
            collect($command->toArray())->filter()->all()
        );

        $user->notify(new UpdatedProfileNotification());

        return $user;
    }
}

La logica que antes estaba en nuestro controlador ahora estara en el UpdateCommandHandler observa que el command se encarga de pasar todos los inputs que fueron enviados al controlador mediante la clase Request.

Crear el CommandDispatcher

Ya que tienes tus comandos y tus manejadores ahora requieres crear la pieza que comunica cada comando con su manejador.

Pero no te preocupes es muy sencillo implementarlo ya que basicamente es como un respositorio que busca manejadores usando los comandos como llaves.

Veamos como queda el tuyo para este ejemplo:

<?php

namespace App\Services\Commands;

use Illuminate\Support\Collection;

class CommandDispatcher
{
    private Collection $handlers;

    public function __construct(array $handlers = [])
    {
        $this->handlers = collect($handlers);
    }

    public static function new(array $handlers = []): CommandDispatcher
    {
        return new static($handlers);
    }

    public function add($command, $handler)
    {
        $this->handlers->put($command, $handler);
    }

    public function has($command): bool
    {
        return $this->handlers->has(get_class($command));
    }

    public function removeBy(Command $command) {
        $this->handlers->forget(get_class($command));
    }

    public function dispatch(Command $command)
    {
        $handler = $this->findBy($command);

        return $handler->handle($command);
    }

    protected function findBy(Command $command) : CommandHandler
    {
        $handler = $this->handlers->get(get_class($command));

        return new $handler;
    }

    public function toArray() : array
    {
        return $this->handlers->all();
    }
}

Puedes tener un solo CommandDispatcher ya que esta implementación puede ejecutar sin problema otros comandos, per si necesitas tener diferentes implementaciones entonces podrias crear una clase abstracta y que cada despachador de comandos extienda de esa clase.

Crear el servicio para el modelo User

Para finalizar solo nos queda crear el servicio que se encargara de manejar las peticiones desde el controlador.

<?php


namespace App\Services;


use App\Services\Commands\Command;
use App\Services\Commands\CommandDispatcher;
use App\Services\Commands\Handlers\StoreCommandHandler;
use App\Services\Commands\Handlers\UpdateCommandHandler;
use App\Services\Commands\Users\StoreCommand;
use App\Services\Commands\Users\UpdateCommand;

class UserService
{
    private CommandDispatcher $dispatcher;

    private array $handlers = [
        UpdateCommand::class => UpdateCommandHandler::class,
        StoreCommand::class => StoreCommandHandler::class,
    ];

    public function __construct()
    {
        $this->dispatcher = app(CommandDispatcher::class, [
            'handlers' => $this->handlers
        ]);
    }

    public function handle(Command $command)
    {
        return $this->dispatcher->dispatch($command);
    }

    public static function new(): UserService
    {
        return new static();
    }
}

El servicio tiene la ventaja que puede usarse para ejecutar mas operaciones como la creación de usuario, el listado de usuario, el borrado ,etc. Sin tener que hacer cambios adicionales 😊

Actualizar el controlador

Ya solo nos queda un paso mas, y es actualizar nuestro controlador:

<?php

namespace App\Http\Controllers;

use App\Models\User;
use App\Services\UserService;
use App\Http\Requests\UserStoreRequest;
use App\Http\Requests\UpdateUserRequest;

class UserController extends Controller
{
    //...

    public function store(UserStoreRequest $request): \Illuminate\Http\RedirectResponse
    {
        UserService::new()
            ->handle(StoreCommand::new($request->validated()));

        return back()->withSuccess('User created');
    }

    public function update(UpdateUserRequest $request, $user): \Illuminate\Http\RedirectResponse
    {
        UserService::new()
            ->handle(UpdateCommand::new(
                $request->validated() + ['user' => $user]
            ));

        return back()->withSuccess('Successful update data');
    }

    //..
}

Aunque no lo vimos en el ejemplo, puedes notar que agregue la creación de usuario, que funciona de forma similar a la operación de actualización, solo tienes que crear el comando y su manejador y listo.

¿Qué mas puedo hacer?

Vamos a seguir con un ejemplo mas para que se pueda apreciar mejor el uso de este patrón

Vamos a suponer que ahora quieres tener la opción de cambiar el nombre del usuario desde la consola, para lo cual tienes que crear un comando.

Pero tienes el inconveniente que desde consola no puedes pasar el id de usuario como sucede desde el navegador, pero puedes usar el E-mail del usuario para encontrarlo y luego de ahi actualizar el nombre.

Asi que para resolver esto; puedes usar el comando UpdateUser para usarlo con un nuevo manejador que tenga la loagica para buscar el usuaro mediante su E-mail.

Veamos como queda esto:

<?php

namespace App\Services\Commands\Handlers;

use App\Models\User;
use App\Notifications\UpdatedProfileNotification;
use App\Services\Commands\Command;
use App\Services\Commands\CommandHandler;

class ConsoleCommandHandler implements CommandHandler
{
    public function handle(Command $command)
    {
        $user = User::firstWhere('email', $command->email);

        $user->update(
            collect($command->toArray())->filter()->all()
        );

        $user->notify(new UpdatedProfileNotification());

        return $user;
    }
}

Con este nuevo manejador solo tienes que crear tu comando para la consola con el siguiente código.

<?php

namespace App\Console\Commands;

use App\Services\Commands\CommandDispatcher;
use App\Services\Commands\Handlers\ConsoleCommandHandler;
use App\Services\Commands\Users\UpdateCommand;
use Illuminate\Console\Command;

class UpdateUserCommand extends Command
{
    //...

    /**
     * Execute the console command.
     *
     * @return int
     */
    public function handle(): int
    {
        $email = $this->ask('what is the email address?');
        $name = $this->ask('What is the new name?');

        CommandDispatcher::new(
            [UpdateCommand::class => ConsoleCommandHandler::class]
        )->dispatch(UpdateCommand::new([
            'name' => $name,
            'email' => $email,
        ]));


        $this->info('The command was successful!');

        return self::SUCCESS;
    }
}

Y asi de sencillo tienes la funcionalidad lista para trabajar.

Si no te gusta la idea de un nuevo manejador, siempre puedes agregar una condición en el manejador que usamos en la clase UserService, para que busque el usuario por el Email en el caso de que no este disponible el id. Pero en ambos casos el cambio es minimo 👍🏼.

Conclusión

Este patrón aumenta la flexibilidad de las aplicaciones al permitir cambiar sus servicios, añadiendo
sustituyendo o eliminando cualquier gestor de comandos en cualquier momento sin tener que modificar la aplicación.

En resumen el patrón command dispatcher tiene las siguientes ventajas:

  • Desacopla las clases que invocan una operación del objeto que sabe cómo ejecutar esa operación.
  • Agregar o cambiar comandos es mas fácil y pueden hacerse sin cambiar el código existente
  • Agregar o cambiar manejadores es sencillo y puede hacerse sin cambiar el código existente

A este patrón se le confunde con el patrón Chain of Responsibility (COR) pero es muy diferente ya que el Command Dispatcher conecta un comando con un solo manejador, y en el patrón COR el comando o mensaje puede ser procesado de forma secuencial por mas de un manejador.

También en algunos artículos se le menciona como Command Bus este ultimo es una mejora de este patrón ya que es una integración con el patrón COR. Lo que permite que puedas realizar acciones adicionales sobre el comando antes de que sea entregado a su manejador.