Éste fue el error de modelado más caro que cometimos, y es instructivo porque el razonamiento que lleva a él es correcto hasta el último paso.
El razonamiento
Un pedido tiene artículos. Un pedido sin artículos no es un pedido. Eso es una invariante, y las invariantes van en el validador del agregado:
export class OrderItemsValidator extends AbstractRuleValidator<Order> {
public addRules(): void {
if (this.subject.props.items.length === 0) {
this.addBrokenRule(\'props.items\', \'El pedido debe tener al menos un artículo\');
}
}
}
Impecable. Y rompe la aplicación entera.
Lo que pasa
const order = Order.create(cliente, direccion);
order.isValid; // false ← recién creado por su propia fábrica
Order.create() construye un pedido con items: [], porque un pedido nace vacío y se le van añadiendo cosas. Y como la fábrica comprueba isValid antes de devolver, crear un pedido pasó a ser imposible.
El agregado rechazaba el objeto que él mismo acababa de construir.
Dónde está el fallo del razonamiento
«Un pedido tiene artículos» es verdad para un pedido confirmado. Es falsa para un carrito. Y son el mismo agregado en dos momentos distintos de su vida.
La tentación es partir el modelo en dos clases, Cart y Order. A veces es lo correcto. Pero si comparten identidad —el carrito se convierte en el pedido, conservando su id— no son dos cosas: son una con estados.
La corrección
export class OrderItemsValidator extends AbstractRuleValidator<Order> {
public addRules(): void {
// Un borrador puede estar vacío; a partir de CONFIRMED, no.
if (this.subject.isDraft()) return;
if (this.subject.props.items.length === 0) {
this.addBrokenRule(\'props.items\', \'El pedido debe tener al menos un artículo\');
}
}
}
La invariante no desaparece: se fecha. Empieza a aplicarse en el momento del ciclo de vida en que es cierta.
La pregunta que ahorra el error
Cada vez que escribas una invariante en un agregado con estados, pregúntate:
¿Esto es cierto desde el instante en que el objeto nace?
Si la respuesta es «bueno, en cuanto el usuario haya…», entonces no es una invariante del agregado: es una precondición de una transición, y su sitio es el método que la ejecuta:
confirm(): void {
if (this.props.items.length === 0) {
throw new InvalidOperationException(\'No se puede confirmar un pedido sin artículos\');
}
// ...
}
Que además devuelve un 409 en lugar de un 422, y eso es lo correcto: nada está mal formado y ningún valor es inválido — el pedido simplemente no está en un estado que permita la operación. La distinción está desarrollada en Estructura contra significado.
La regla
Una invariante de agregado tiene que ser cierta en todos los estados en los que el agregado puede existir. Si sólo es cierta a partir de cierto punto, o es condicional al estado, o pertenece a la transición y no al agregado.
