¿En dónde debería validar el estado de "otros" agregados?

8

Escenario:

Un cliente hace un pedido, luego, después de recibir el producto, proporciona información sobre el proceso de pedido.

Supongamos las siguientes raíces agregadas:

  • Cliente
  • Orden
  • Comentarios

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 se ha pagado el pedido.

    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 la empresa quiere una nueva regla:

  1. Un cliente solo puede proporcionar comentarios si el Supplier del pedido los bienes siguen funcionando.

    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;
        }
    }
    

He colocado la implementación de las dos primeras reglas dentro de Feedback agregado en sí mismo. Me siento cómodo haciendo esto, especialmente dado que Feedback agregado hace referencia a todos los otros agregados por identidad. P.ej., Las propiedades del componente Feedback indican que conoce el existencia de los otros agregados, por lo que me siento cómodo al saberlo el estado de solo lectura de estos agregados también.

Sin embargo, según sus propiedades, el agregado Feedback no tiene conocimiento de la existencia de Supplier aggregate, por lo que debe 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 apropiado CommandHandler . Sin embargo, esto se siente como si estuviera moviendo la lógica del dominio. lejos del "centro" de mi arquitectura basada en cebolla.

    
pregunta magnus 20.05.2016 - 07:33

2 respuestas

1

Si la corrección transaccional requiere que un agregado sepa sobre el estado actual de otro agregado, entonces su modelo es incorrecto.

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

Por lo tanto, el comando será ejecutado por el agregado que cambia de estado. Para realizar la verificación no necesariamente correcta, no es 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, que proporciona una consulta que devuelve un estado / proyección inmutables 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 creas el objeto, el llamante ya conoce el estado interno, porque lo está proporcionando. El mismo patrón funciona, simplemente 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 manteniendo toda la lógica del dominio en los objetos del dominio, pero en realidad no estamos protegiendo el negocio 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();
        ....
    }
}
    
respondido por el VoiceOfUnreason 20.05.2016 - 08:47
-1

Sé que esta es una pregunta antigua, 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 en sí mismas. La lógica que proporciona en su constructor de comentarios parece indicar que una orden DEBE tener un Id. De cliente y la Respuesta también debe estar relacionada 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 relacionado lógicamente con el Pedido (por lo que estaría dentro de este agregado).

Teniendo en cuenta lo anterior, 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. Este es el aspecto que debe tener (Nota: no voy a incluir constructores para Cliente y Pedido porque probablemente deberían usarse Fábricas. Tampoco 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;
    }
}

Está bien. Vamos a romper 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 se deben aplicar las reglas comerciales. El diseño anterior no solo "hace" lo correcto, sino que "dice" lo correcto.

¿Qué podría llevar a alguien a asumir que las reglas se ejecutan 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 comerciales se lleva a cabo lo más cerca posible de los modelos a los que pertenecen. En su ejemplo, el constructor (un solo método) está realizando varias validaciones contra diferentes modelos. Eso rompe el diseño SOLID. ¿Dónde agregaríamos un cheque para asegurarnos de que el contenido de Comentarios no contenga palabras malas? ¿Otra comprobación en el constructor? ¿Qué pasa si diferentes tipos de comentarios necesitan verificaciones de contenido diferentes? 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 órdenes 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 se llame Cliente). La razón de esto es simple. Es MUY probable que un Cliente sea una raíz agregada ENORME que se extienda por todo su dominio / DB. Al usar 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 hacer AMBOS pedidos y proporcionar comentarios, pero no se puede usar para restablecer una contraseña o cambiar un nombre de usuario.

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

    
respondido por el king-side-slide 09.02.2018 - 22:13