¿Dónde debe validar el estado de "otros" agregados?

8

Guión:

Un cliente realiza un pedido y luego, después de recibir el producto, proporciona comentarios sobre el proceso del pedido.

Suponga las siguientes raíces agregadas:

  • Cliente
  • Orden
  • Retroalimentación

Aquí están las reglas de negocio:

  1. Un cliente solo puede proporcionar comentarios sobre su propio pedido, no el de otra persona.
  2. Un cliente solo puede proporcionar comentarios si el pedido ha sido pagado.

    class Feedback {
        public function __construct($feedbackId,
                                    Customer $customer,
                                    Order $order,
                                    $content) {
            if ($customer->customerId() != $order->customerId()) {
                // Error
            }
            if (!$order->isPaid()) {
                // Error
            }
            $this->feedbackId = $feedbackId;
            $this->customerId = $customerId;
            $this->orderId = $orderId;
            $this->content = $content;
        }
    }
    

Ahora, suponga que el negocio quiere una nueva regla:

  1. Un cliente solo puede proporcionar comentarios si la Suppliermercancía de la orden todavía está en funcionamiento.

    class Feedback {
        public function __construct($feedbackId,
                                    Customer $customer,
                                    Order $order,
                                    Supplier $supplier,
                                    $content) {
            if ($customer->customerId() != $order->customerId()) {
                // Error
            }
            if (!$order->isPaid()) {
                // Error
            }
            // NEW RULE HERE
            if (!$supplier->isOperating()) {
                // Error
            }
            $this->feedbackId = $feedbackId;
            $this->customerId = $customerId;
            $this->orderId = $orderId;
            $this->content = $content;
        }
    }
    

Coloqué la implementación de las dos primeras reglas dentro del Feedback agregado mismo. Me siento cómodo haciendo esto, especialmente dado que el Feedbackagregado hace referencia a todos los otros agregados por identidad. Por ejemplo, las propiedades del Feedbackcomponente indican que sabe de la existencia de los otros agregados, por lo que me siento cómodo al tener que conocer también el estado de solo lectura de estos agregados.

Sin embargo, en función de sus propiedades, el Feedbackagregado no tiene conocimiento de la existencia del Supplieragregado, entonces, ¿debería tener conocimiento del estado de solo lectura de este agregado?

La solución alternativa para implementar la regla 3 es mover esta lógica a la apropiada CommandHandler. Sin embargo, parece que está alejando la lógica del dominio del "centro" de mi arquitectura basada en la cebolla.

Descripción general de mi arquitectura de cebolla

Magnus
fuente
Las interfaces de repositorio son parte del dominio. Por lo tanto, una lógica de construcción (que en sí misma se considera un servicio en el libro DDD) puede llamar al repositorio de Order para preguntar si el proveedor de Order todavía está operando.
Eufórico
En primer lugar, Supplierel estado operativo de un agregado no se consultaría a través de un Orderrepositorio; Suppliery Orderson dos agregados separados. En segundo lugar, había una pregunta en la lista de correo DDD / CQRS acerca de pasar raíces y repositorios agregados a otros métodos de raíz agregada (incluido el constructor). Hubo una variedad de opiniones, pero Greg Young mencionó que pasar raíces agregadas como parámetros es común, mientras que otra persona dijo que los repositorios están más estrechamente relacionados con la infraestructura que con el dominio. Por ejemplo, los repositorios "abstractos en colecciones de memoria" y no tienen lógica.
Magnus
¿No está relacionado el proveedor con el pedido? ¿Qué sucede cuando se pasa un proveedor que no está relacionado con el pedido? Bueno, "está funcionando el proveedor" no es una lógica. Es simple consulta. Además, hay una razón por la cual es común: sin él, su código se vuelve mucho más complejo y requiere pasar información donde pueden ocurrir errores. Además, la "interfaz del repositorio" no es infraestructura. La implementación del repositorio es.
Eufórico
Tienes razón. Al igual que a Customersolo puede proporcionar comentarios sobre uno de sus propios pedidos ( $order->customerId() == $customer->customerId()), también tenemos que comparar la ID del proveedor ( $order->supplierId() == $supplier->supplierId()). La primera regla protege contra el usuario que proporciona valores incorrectos. La segunda regla protege contra el programador que proporciona valores incorrectos. Sin embargo, la verificación de si el proveedor está operando debe realizarse en la Feedbackentidad o en el controlador de comandos. ¿Dónde está la pregunta?
Magnus
2
Dos comentarios, no directamente relacionados con la pregunta. Primero, pasar las raíces de los agregados como argumentos a otro agregado parece incorrecto, esos deberían ser Ids, no hay nada útil que un agregado pueda hacer con otro agregado. Segundo, el Cliente y el Proveedor son ... difíciles, el libro de registro en ambos casos es el mundo real: no puede detener al proveedor en el mundo real enviando un comando CeaseOperations a su modelo de dominio.
VoiceOfUnreason

Respuestas:

1

Si la corrección transaccional requiere que un agregado conozca el estado actual de otro agregado, entonces su modelo está equivocado.

En la mayoría de los casos, no se requiere corrección transaccional . Las empresas tienden a tolerar la latencia y los datos obsoletos. Esto es especialmente cierto en el caso de inconsistencias que son fáciles de detectar y remediar.

Entonces, el comando será ejecutado por el agregado que cambia de estado. Para realizar la verificación no necesariamente correcta, necesita no necesariamente la última copia del estado del otro agregado.

Para los comandos en un agregado existente, el patrón habitual es pasar un Repositorio al agregado, y el agregado pasará su estado al repositorio, lo que proporciona una consulta que devuelve un estado / proyección inmutable del otro agregado

class Feedback {
    void downvote(Repository<Supplier.State> query) {
        Supplier.State supplier = query.getById(this->supplierId);
        boolean isOperating = state.isOperating();
        ....
    }
}

Pero los patrones de construcción son extraños: cuando está creando el objeto, la persona que llama ya conoce el estado interno, porque lo está proporcionando. El mismo patrón funciona, solo parece inútil

class Feedback {
    __construct(SupplierId supplierId, SupplierOperatingQuery query ...) {
        Supplier.State supplier = query.getById(this->supplierId);
        boolean isOperating = state.isOperating();
        ....
    }
}

Estamos siguiendo las reglas al mantener toda la lógica del dominio en los objetos del dominio, pero en realidad no estamos protegiendo el negocio invariante de ninguna manera útil al hacerlo (porque toda la misma información está disponible para el componente de la aplicación). Para el patrón de creación, sería igual de bueno escribir

class Feedback {
    __construct(Supplier.State supplier, ...) {
        boolean isOperating = state.isOperating();
        ....
    }
}
VoiceOfUnreason
fuente
1. ¿La SupplierOperatingQueryconsulta del modelo de lectura o "Consulta" en el nombre es engañosa? 2. No se requiere consistencia transaccional. No importa si el proveedor detiene las operaciones un segundo antes de que un cliente deje comentarios, pero ¿eso significa que no deberíamos verificarlo de todos modos? 3. En su ejemplo, ¿el suministro de un "servicio de consulta" en lugar del objeto mismo impone consistencia transaccional? ¿Si es así, cómo? 4. ¿Cómo el uso de tales servicios de consulta impacta las pruebas unitarias?
Magnus
1. Consulta, en el sentido de que llamarlo no cambia el estado de nada. 3. No hay coherencia transaccional con el servicio de consulta, no hay interacción entre este y el comando que se ejecuta simultáneamente y que está modificando el otro agregado. 4. En este caso, sería parte de la spi del modelo de dominio, así que solo proporcione una implementación de prueba. Hmm, eso es un poco extraño, sin embargo, DomainService podría no ser el mejor término para usar.
VoiceOfUnreason
2. Tenga en cuenta que debido a que los datos que está utilizando aquí están a través de un límite agregado, su cheque puede darle una respuesta incorrecta (por ejemplo: su cheque dice que no está bien, pero debería ser porque el otro agregado está cambiando). Por lo tanto, podría ser mejor mover esa verificación al modelo leído (siempre acepte el comando, pero cree un informe de excepción si el modelo es inconsistente). También puede organizar que el cliente solo envíe comandos que se supone que tienen éxito, es decir, el cliente no debe enviar comandos que espera fallar, en función de su comprensión del estado actual.
VoiceOfUnreason
1. Por lo general, está mal visto que el "lado de escritura" consulte el "lado de lectura" (por ejemplo, proyecciones de origen de eventos). "... en el sentido de que llamarlo no cambia el estado de nada", tampoco lo hace simplemente usando un descriptor de acceso inmutable, lo que yo diría es mucho más simple. 2. Estaría bien duplicar el cheque en el modelo de lectura, pero si lo mueve (léase: RETIRARLO del servidor), está creando problemas para usted mismo. En primer lugar, su regla empresarial debe duplicarse en cada cliente (navegador web y clientes móviles). En segundo lugar, es simple omitir esta verificación:
magnus
3. "... no hay interacción entre él y el comando en ejecución simultánea que está modificando el otro agregado" - tampoco carga el agregado del Proveedor en sí, ya que solo se está modificando el agregado de Comentarios. 4. ¿Entonces SupplierOperatingQuery es una interfaz que requiere una implementación concreta, lo que significa que debe crear una implementación simulada en su prueba unitaria simplemente para probar el valor verdadero / falso de una variable única que ya existe en el otro objeto? Huele a exageración. ¿Por qué no crear un CustomerOwnsOrderQuery y OrderIsPaidQuery también?
Magnus
-1

Sé que esta es una vieja pregunta, pero me gustaría señalar que el problema se deriva directamente de una premisa incorrecta. Es decir, las raíces agregadas que debemos asumir que existen son simplemente incorrectas.

Solo hay una raíz agregada en el sistema que ha descrito: Cliente. Tanto un Pedido como un Comentario, si bien pueden ser agregados por derecho propio, dependen de la existencia del Cliente, por lo que no son raíces agregadas. La lógica que proporciona en su constructor de comentarios parece indicar que un Pedido DEBE tener un ID de cliente y los Comentarios también DEBEN estar relacionados con un Cliente. Esto tiene sentido. ¿Cómo puede un pedido o comentario no estar relacionado con un cliente? Además, el Proveedor parece estar lógicamente relacionado con el Pedido (por lo que estaría dentro de este agregado).

Con lo anterior en mente, toda la información que desea ya está disponible en la raíz agregada del Cliente y queda claro que está aplicando sus reglas en el lugar equivocado. Los constructores son lugares terribles para hacer cumplir las reglas comerciales y deben evitarse a toda costa. Así es como debería verse (Nota: no voy a incluir constructores para Cliente y Pedido porque probablemente deberían usarse Fábricas. Además, no se muestran todos los métodos de interfaz).

/*******************************\
   Interfaces, explained below 
\*******************************/

interface ICustomer
{
    public function getId() : int;
}

interface IUser extends ICustomer
{
    public function getUsername() : string;

    public function getPassword() : string;

    public function changeUsername( string $new ) : void;

    public function resetPassword( string $new ) : void;

}

interface IReviewer extends ICustomer
{
    public function provideFeedback( IOrder $order, string $content ) : void;
}

interface IBuyer extends ICustomer
{
    public function placeOrder( IOrder $order ) : void;
}

interface IOrder
{
    public function getCustomerId() : int;

    public function addFeedback( string $content ) : void;
}


interface IFeedback
{
    public function addContent( string $content ) : void;

    public function isValidContent( string $content ) : void;
}



/*******************************\
   Implentation
\*******************************/



class Customer implements IReviewer, IBuyer
{
    protected $id;

    protected $orders = [];

    public function provideFeedback( IOrder $order, string $content ) : void
    {
        if( $order->getCustomerId() !== $this->getId() )
            throw new \InvalidArgumentException('Customers can only provide feedback on their own orders');

        $order->addFeedback( $content );
    }
}


class Order implements IOrder
{
    protected $supplier;

    protected $feedbacks = [];

    public function addFeedback( string $content ) : void
    {
        if( false === $this->supplier->isOperating() )
            throw new \Exception('Feedback can only be added to orders if the supplier is still operating.');

        // could be any IFeedback
        $feedback = new Feedback( $this );

        $feedback->addContent( $content );

        $this->feedbacks[] = $feedback;
    }
}


class Feedback implements IFeedback
{
    protected $order;

    protected $content;

    public function __construct( IOrder $order )
    {    
         // we don't carry our business rules in constructors
         $this->order = $order;
    }

    public function addContent( string $content ) : void
    {
        if( false === $this->isValidContent($content) )
            throw new \Exception("Content contains offensive language.");

        $this->content = $content;
    }
}

Bueno. Analicemos esto un poco. Lo primero que notará es cuánto más declarativo es este modelo. Todo es una acción, queda claro DÓNDE deben aplicarse las reglas de negocio. El diseño anterior no solo "hace" lo correcto, sino que "dice" lo correcto.

¿Qué llevaría a alguien a asumir que las reglas se están ejecutando en la siguiente línea?

// this is a BAD place for rules to execute
$feedback = new Feedback( $id, $customerId, $order, $supplier, $content);

En segundo lugar, puede ver que toda la lógica relacionada con la validación de las reglas de negocios se lleva a cabo de la manera más cercana posible a los modelos a los que pertenecen. En su ejemplo, el constructor (un único método) está realizando múltiples validaciones contra diferentes modelos. Eso rompe el diseño SÓLIDO. ¿Dónde agregaríamos un cheque para asegurarnos de que el contenido de Comentarios no contenga malas palabras? ¿Otro cheque en el constructor? ¿Qué sucede si diferentes tipos de comentarios necesitan diferentes comprobaciones de contenido? Feo.

Tercero, mirando las interfaces, puede ver que hay lugares naturales para extender / modificar las reglas a través de la composición. Por ejemplo, diferentes tipos de pedidos pueden tener diferentes reglas con respecto a cuándo se pueden proporcionar comentarios. El pedido también puede proporcionar diferentes tipos de comentarios, que a su vez pueden tener diferentes reglas para la validación.

También puede ver un montón de interfaces ICustomer *. Estos se utilizan para componer el agregado del Cliente que necesitamos aquí (probablemente no solo llamado Cliente). La razón de esto es simple. Es MUY probable que un Cliente sea una raíz agregada ENORME que se extiende por todo su dominio / DB. Mediante el uso de interfaces, podemos descomponer ese agregado (que probablemente sea demasiado grande para cargar) en múltiples raíces agregadas que solo proporcionan ciertas acciones (como ordenar o proporcionar comentarios). Puede ver que el agregado en mi implementación puede AMBOS hacer pedidos Y proporcionar comentarios, pero no se puede usar para restablecer una contraseña o cambiar un nombre de usuario.

Entonces, la respuesta a su pregunta es que los agregados deben validarse a sí mismos. Si no pueden, es probable que tenga un modelo deficiente.

tobogán lateral
fuente
1
Si bien los límites agregados son diferentes dependiendo de quién está diseñando el sistema, creo que "un agregado" derivado del orden es simplemente una tontería. Su ejemplo de que un Proveedor es parte de un pedido es un buen ejemplo: ¿puede un Proveedor no existir hasta que se haya creado un Pedido? ¿Qué hay de Proveedores duplicados:
magnus
@ user1420752 Creo que puede tenerlo al revés. El modelo anterior implica lo contrario. Que un Pedido no puede existir sin un Proveedor. Mi ejemplo es simplemente usar la información / reglas / relaciones que podría obtener del código proporcionado. Estoy de acuerdo en que, al igual que el Cliente, el pedido es un agregado grande y complejo por derecho propio (aunque no es una raíz). Uno que también puede requerir descomposición en un puñado de implementaciones concretas dependiendo del contexto. El punto que estoy ilustrando es que las entidades DEBEN validarse a sí mismas. Como puede ver, es más limpio de esa manera.
deslizamiento lateral del rey
@ user1420752 Me gustaría agregar que a menudo los métodos / constructores que requieren muchos argumentos son un signo de un modelo anémico en el que los datos se separan del comportamiento (y, por lo tanto, deben inyectarse en grandes mandriles a las piezas que actúan sobre los datos ) El constructor Feedback que proporcionó es un ejemplo de esto. Los modelos anémicos tienden a reducir la cohesión y a agregar semánticas de acoplamiento adicionales (como verificar las ID varias veces). La alta cohesión generalmente significa que cada método en una entidad utiliza todas sus variables de instancia. Esto naturalmente conduce a la descomposición de grandes agregados como Cliente o Pedido
diapositiva del lado del rey