El patrón Command (extract params into class)
Esto podría ser la firma de un caso de uso que podríamos encontrar en producción.
public function updateUser(
int $userId,
string $name,
string $email,
?string $phone = null,
?string $vatId = null,
bool $newsletter = true,
bool $verified = false,
): void
Nadie la habría escrito así de golpe. Es fácil que empezara siendo updateUser($userId, $name) y que fuera engordando parámetro a parámetro, cada vez que negocio pide un dato más. El resultado es una lista que hay que leer entera para entender qué hace el método, y que obliga a cada llamador a saberse de memoria el orden, los tipos y los valores por defecto. Cuando alguien añade un campo nuevo en medio de la lista, cada llamada repartida por la aplicación se convierte en una pregunta de "¿esto iba antes o después del booleano?".
Hay un refactor para esto, y es más viejo y más simple de lo que parece. Se llama Extract Params Into Class, y en el fondo dice algo muy obvio: si un montón de parámetros viajan siempre juntos, es que en realidad son una sola cosa. Júntalos.
De lista de parámetros a objeto
El refactor es exactamente lo que sugiere el nombre. Coges los parámetros sueltos y los metes en una clase.
final class UpdateUserCommand
{
public function __construct(
public readonly int $userId,
public readonly string $name,
public readonly string $email,
public readonly ?string $phone = null,
public readonly ?string $vatId = null,
public readonly bool $newsletter = true,
public readonly bool $verified = false,
) {
}
}
El método que los recibía deja de mirar a una lista y empieza a mirar a un objeto.
public function updateUser(UpdateUserCommand $command): void
{
// ...
}
Fíjate en lo que ha cambiado. Antes la firma declaraba siete cosas que había que recordar. Ahora declara una. El día que negocio pida un octavo dato, lo añades a la clase UpdateUserCommand y nadie que ya llamaba al método tiene que enterarse de nada. El orden de los argumentos desaparece como problema, los valores por defecto viven donde deben y, de regalo, has dado un nombre a todo el bloque: actualizar un usuario ahora es un objeto con identidad, no una serie de valores sueltos que había que combinar bien.
Esto que acabas de ver es, sin más ceremonia, el patrón Command. La diferencia entre un simple DTO y un Command no está en la estructura, está en la intención. Un Command es un objeto que representa una orden: todo lo que hay que saber para ejecutar un caso de uso, metido en un solo paquete. No describe un estado, describe lo que se quiere hacer.
La ganancia que no se ve a simple vista
Juntar los parámetros en una clase no parece gran cosa, y de hecho es de esas mejoras que un júnior mira y dice "¿esto era todo?". Pero el valor real no está en lo que el objeto es, sino en lo que permite hacer con él. En cuanto todos tus casos de uso se expresan como comandos, algo muy bonito ocurre en tu aplicación: todos los puntos de entrada tienen la misma forma.
public function updateUser(UpdateUserCommand $command): void
public function createOrder(CreateOrderCommand $command): void
public function cancelSubscription(CancelSubscriptionCommand $command): void
public function sendInvoice(SendInvoiceCommand $command): void
Una firma única para todas las operaciones. Antes cada caso de uso tenía su propia combinación de argumentos, y no había forma de hablar de "las operaciones de la aplicación" como un conjunto. Ahora sí: cada operación es un objeto Command, y cada objeto Command se maneja igual. Esa uniformidad es la que abre la puerta a todo lo demás.
Los superpoderes
Cuando la entrada a tus casos de uso está estandarizada, puedes hacer cosas que antes requerían tocar cada servicio uno por uno.
Logging y auditoría centralizados. Un único envoltorio puede registrar cada comando que entra al sistema: qué se pidió, quién lo pidió y cuándo. Sin una sola línea de log dentro de la lógica de negocio.
final class LoggingMiddleware
{
public function __construct(
private readonly LoggerInterface $logger,
) {
}
public function __invoke(Command $command, callable $next): void
{
$this->logger->info('Handling ' . $command::class);
$next($command);
}
}
Resiliencia y reintentos. Un comando es un valor serializable. Si algo falla a mitad, puedes guardarlo y reintentarlo más tarde, porque contiene todo lo necesario para volver a ejecutarse desde cero. No dependes de que el proceso original siga vivo.
Colas y procesamiento asíncrono. ¿Una operación tarda demasiado? La conviertes en un comando, lo pones en una cola y un worker lo procesa en segundo plano. El usuario recibe una respuesta inmediata y el trabajo pesado ocurre donde no molesta.
Escalabilidad horizontal. Si los comandos viajan solos y son autocontenidos, puedes repartirlos entre procesos o máquinas distintas. Escalar deja de ser un proyecto de reescritura y pasa a ser una cuestión de infraestructura.
Decoradores y middleware. Añadir validación, transacciones o caché se convierte en envolver el handler con otro objeto. El patrón Decorador funciona de maravilla sobre algo que tiene siempre la misma forma.
Con un Application Service tradicional, con su lista de argumentos a medida, conseguir todo esto exige modificar cada servicio individualmente y duplicar la lógica transversal por toda la aplicación. El Command te da un único punto por el que pasan todas las órdenes, y ese punto es donde pones la infraestructura.
Comandos inmutables
Hay un detalle que merece la pena mirar despacio, y es que el UpdateUserCommand de arriba tiene todas sus propiedades readonly. No es casualidad. Un comando es una orden, y una orden no se edita a mitad de camino. Una vez creado, el comando dice exactamente una cosa, y esa cosa no cambia.
Esto conecta con algo que ya conté cuando hablaba de objetos inmutables: cuando un valor no puede cambiar, puedes pasarlo de un lado a otro sin miedo a que alguien lo modifique por el camino. En un comando, esa garantía es todavía más importante, porque el comando va a viajar. Se serializará, se encolará, se reintentará y quizá viva en una máquina distinta. Que nadie pueda tocarlo por el camino es lo que hace seguro todo ese recorrido.
Y la construcción del comando es el sitio perfecto para volver a usar los named constructors que vimos hace unos artículos. Un comando con un constructor que valida y normaliza los datos de entrada es un comando que ya no puede llegar mal formado a la lógica de negocio. La validación de frontera se hace una vez, en el nacimiento del objeto, y todo lo que hay aguas abajo puede confiar.
El primer paso hacia CQRS
Lo que hemos construido hasta aquí no es un capricho estético. Es el cimiento sobre el que se levanta CQRS, Command Query Responsibility Segregation, del que hablaremos en los próximos artículos.
Fíjate en que toda la conversación de hoy ha girado alrededor de una sola mitad del problema: la de hacer. Un comando es una orden que cambia el estado del sistema, y todo lo que hemos ganado (colas, reintentos, auditoría, escalado) tiene que ver con esa mitad. Pero una aplicación también tiene otra mitad: la de preguntar. Las consultas, las listas, los informes. Esa mitad tiene necesidades muy distintas, y mezclarla con la de hacer es justo lo que CQRS propone dejar de hacer.
No hace falta anticiparse. El camino natural es este: primero dominas el patrón Command para la escritura, y ya tienes la mitad del trabajo hecho. En un próximo artículo montaremos encima el Command Bus, esa capa de abstracción que se queda con cualquier comando y lo encamina hacia su handler sin que el emisor sepa quién lo ejecuta. Y después, con los dos conceptos asentados, CQRS dejará de sonar a arquitectura de gurú y se verá como lo que es: una consecuencia casi inevitable de haber hecho bien el primer paso.
Juntar los parámetros en una clase es un cambio de cinco minutos que cuesta muy poco y no rompe nada. Pero es de esos refactors que, como una llave maestra, abren puertas que ni sabías que estaban cerradas.