Onde você deve validar o estado de "outros" agregados?

8

Cenário:

Um cliente faz um pedido e, depois de receber o produto, fornece feedback sobre o processo do pedido.

Suponha as seguintes raízes agregadas:

  • Cliente
  • Ordem
  • Comentários

Aqui estão as regras de negócios:

  1. Um cliente pode apenas fornecer feedback sobre seu próprio pedido, não sobre o de outra pessoa.
  2. Um cliente pode fornecer feedback apenas se o pedido tiver sido pago.

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

Agora, suponha que a empresa queira uma nova regra:

  1. Um cliente pode fornecer feedback apenas se as Suppliermercadorias do pedido ainda estiverem em operação.

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

Coloquei a implementação das duas primeiras regras no Feedback próprio agregado. Sinto-me à vontade para fazer isso, especialmente porque o Feedbackagregado faz referência a todos os outros agregados por identidade. Por exemplo, as propriedades do Feedbackcomponente indicam que ele conhece a existência dos outros agregados, então me sinto confortável em saber também o estado somente leitura desses agregados.

No entanto, com base em suas propriedades, o Feedbackagregado não tem conhecimento da existência do Supplieragregado; portanto, ele deve ter conhecimento do estado somente leitura desse agregado?

A solução alternativa para a implementação da regra 3 é mover essa lógica para o apropriado CommandHandler. No entanto, parece que está afastando a lógica do domínio do "centro" da minha arquitetura baseada em cebola.

Visão geral da minha arquitetura de cebola

magnus
fonte
Interfaces de repositório fazem parte do domínio. Portanto, uma lógica de construção (que por si só é considerada um serviço no livro DDD) pode chamar o repositório de um pedido para perguntar se o fornecedor do pedido ainda está operando.
Euphoric
Primeiro, Suppliero estado operacional de um agregado não seria consultado por meio de um Orderrepositório; Suppliere Ordersão dois agregados separados. Em segundo lugar, havia uma pergunta na lista de discussão DDD / CQRS sobre a passagem de raízes e repositórios agregados para outros métodos raiz agregados (incluindo o construtor). Havia uma variedade de opiniões, mas Greg Young mencionou que transmitir raízes agregadas como parâmetros é comum, enquanto outra pessoa disse que os repositórios estão mais intimamente relacionados à infraestrutura do que ao domínio. Por exemplo, repositórios "abstraem coleções de memória" e não têm lógica.
magnus
O fornecedor não está relacionado ao pedido? O que acontece quando o Fornecedor não relacionado ao Pedido é repassado? Bem, "o fornecedor está operando" não é uma lógica. É uma consulta simples. Além disso, há razões para isso ser comum: sem ele, seu código se torna muito mais complexo e requer a transmissão de informações onde erros podem ocorrer. Além disso, "interface do repositório" não é infraestrutura. A implementação do repositório é.
Euphoric
Você está certo. Assim como um Customersó pode fornecer feedback sobre um de seus próprios pedidos ( $order->customerId() == $customer->customerId()), também precisamos comparar o ID do fornecedor ( $order->supplierId() == $supplier->supplierId()). A primeira regra protege contra o usuário que fornece valores incorretos. A segunda regra protege contra o programador que fornece valores incorretos. No entanto, a verificação sobre se o fornecedor está operando deve ser na Feedbackentidade ou no manipulador de comandos. Onde está a questão?
magnus
2
Dois comentários, não diretamente relacionados à pergunta. Primeiro, passar raízes agregadas como argumentos para outro agregado parece errado - esses devem ser IDs - não há nada útil que um agregado possa fazer com outro agregado. Segundo, Cliente e Fornecedor são ... difíceis, o livro de registro em ambos os casos é o mundo real: você não pode parar o fornecedor no mundo real enviando um comando CeaseOperations para o seu modelo de domínio.
VoiceOfUnreason

Respostas:

1

Se a correção transacional exigir que um agregado saiba sobre o estado atual de outro agregado, seu modelo está errado.

Na maioria dos casos, a correção transacional não é necessária . As empresas tendem a ter tolerância em relação à latência e aos dados obsoletos. Isso se aplica especialmente às inconsistências fáceis de detectar e remediar.

Portanto, o comando será executado pelo agregado que muda de estado. Para executar a verificação não necessariamente correta, é necessária uma cópia não necessariamente da última versão do estado do outro agregado.

Para comandos em um agregado existente, o padrão usual é passar um Repositório para o agregado, e o agregado passará seu estado para o repositório, o que fornece uma consulta que retorna um estado imutável / projeção do outro agregado

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

Mas os padrões de construção são estranhos - quando você cria o objeto, o chamador já conhece o estado interno, porque está fornecendo. O mesmo padrão funciona, apenas parece inútil

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

Estamos seguindo as regras, mantendo toda a lógica do domínio nos objetos de domínio, mas não estamos protegendo os negócios invariantes de nenhuma maneira útil (porque todas as mesmas informações estão disponíveis para o componente do aplicativo). Para o padrão de criação, seria tão bom escrever

class Feedback {
    __construct(Supplier.State supplier, ...) {
        boolean isOperating = state.isOperating();
        ....
    }
}
VoiceOfUnreason
fonte
1. A SupplierOperatingQueryconsulta é enganosa no modelo de leitura ou "Consulta" no nome? 2. A consistência transacional não é necessária. Não importa se o fornecedor interrompe as operações um segundo antes de um cliente deixar o feedback, mas isso significa que não devemos verificar isso de qualquer maneira? 3. No seu exemplo, o fornecimento de um "serviço de consulta" em vez do próprio objeto impõe consistência transacional? Se sim, como? 4. Como o uso desses serviços de consulta afeta o teste de unidade?
Magnus
1. Consulta, no sentido de que chamá-lo não altera o estado de nada. 3. Não há consistência transacional com o serviço de consulta, não há interação entre ele e o comando em execução simultânea que está modificando o outro agregado. 4. Nesse caso, ele faria parte do spi do modelo de domínio, portanto, forneça uma implementação de teste. Hmm, isso é um pouco estranho - o DomainService pode não ser o melhor termo para usar.
VoiceOfUnreason
2. Lembre-se de que, como os dados que você está usando aqui estão além de um limite agregado, seu cheque pode dar a resposta errada (por exemplo: seu cheque diz que não está bom, mas deve ser porque o outro agregado está mudando). Portanto, pode ser melhor mover essa verificação para o modelo de leitura (sempre aceite o comando, mas crie um relatório de exceção se o modelo for inconsistente). Você também pode providenciar para que o cliente envie apenas comandos que deveriam ter sucesso - ou seja, o cliente não deve enviar comandos que espera falhar, com base no entendimento do estado atual.
VoiceOfUnreason
1. Geralmente, é desaprovado o "lado de gravação" consultar o "lado de leitura" (por exemplo, projeções originadas por eventos). "... no sentido de que chamá-lo não muda o estado de nada" - nem o simples uso de um acessador imutável, o que eu argumentaria ser muito mais simples. 2. Seria bom duplicar a verificação no modelo de leitura, mas se você a mover (leia: REMOVER do servidor), você estará criando problemas para si mesmo. Em primeiro lugar, sua regra de negócios deve ser duplicada em cada cliente (navegador da Web e clientes móveis). Em segundo lugar, simples, de ignorar esta verificação:
magnus
3. "... não há interação entre ele e o comando de execução simultânea que está modificando o outro agregado" - nem o carregamento do agregado do Fornecedor em si, pois apenas o agregado de Feedback está sendo modificado. 4. Portanto, SupplierOperatingQuery é uma interface que requer uma implementação concreta, o que significa que você deve criar uma implementação simulada em seu teste de unidade simplesmente para testar o valor verdadeiro / falso de uma única variável que já existe no outro objeto? Cheira a exagero. Por que não criar um CustomerOwnsOrderQuery e OrderIsPaidQuery também ??
magnus
-1

Sei que essa é uma pergunta antiga, mas gostaria de ressaltar que o problema decorre diretamente de uma premissa incorreta. Ou seja, as raízes agregadas que devemos assumir que existem são simplesmente incorretas.

Há apenas uma raiz agregada no sistema que você descreveu: Cliente. Tanto um Pedido quanto um Feedback, embora possam ser agregados por si mesmos, dependem da existência do Cliente, portanto não são eles mesmos raízes agregadas. A lógica que você fornece no seu construtor de feedback parece indicar que um Pedido DEVE ter um ID do cliente e o Feedback também deve estar relacionado a um Cliente. Isso faz sentido. Como um Pedido ou Feedback não pode estar relacionado a um Cliente? Além disso, o Fornecedor parece estar logicamente relacionado ao Pedido (estaria dentro desse agregado).

Com o exposto acima, todas as informações que você deseja já estão disponíveis na raiz agregada do Cliente e fica claro que você está aplicando suas regras no lugar errado. Os construtores são lugares terríveis para impor regras de negócios e devem ser evitados a todo custo. É assim que deve ser (Observação: não incluirei construtores para Cliente e Pedido, porque provavelmente as Fábricas devem ser usadas. Também não mostramos todos os métodos de interface).

/*******************************\
   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;
    }
}

OK. Vamos dividir isso um pouco. A primeira coisa que você notará é o quão mais declarativo esse modelo é. Tudo é uma ação, fica claro ONDE as regras de negócios devem ser aplicadas. O design acima não apenas "faz" a coisa certa, mas "diz" a coisa certa.

O que levaria alguém a assumir que as regras estão sendo executadas na linha a seguir?

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

Segundo, você pode ver que toda a lógica referente à validação de regras de negócios é realizada o mais próximo possível dos modelos aos quais elas pertencem. No seu exemplo, o construtor (um único método) está executando várias validações em diferentes modelos. Isso quebra o design do SOLID. Onde adicionaríamos uma verificação para garantir que o conteúdo do Feedback não contenha palavrões? Outra verificação no construtor? E se diferentes tipos de Feedback precisarem de verificações de conteúdo diferentes? Feio.

Terceiro, olhando as interfaces, você pode ver que existem lugares naturais para estender / modificar as regras através da composição. Por exemplo, diferentes tipos de pedidos podem ter regras diferentes sobre quando o feedback pode ser fornecido. O pedido também pode fornecer diferentes tipos de feedback, que por sua vez podem ter regras diferentes para validação.

Você também pode ver várias interfaces do ICustomer *. Eles são usados ​​para compor o agregado do Cliente que precisamos aqui (provavelmente não apenas chamado de Cliente). A razão para isso é simples. É MUITO provável que um cliente seja uma raiz agregada ENORME que se espalha por todo o seu domínio / banco de dados. Usando interfaces, podemos decompor esse agregado (que provavelmente é muito grande para carregar) em várias raízes agregadas que fornecem apenas determinadas ações (como pedir ou fornecer feedback). Você pode ver que o agregado em minha implementação pode fazer pedidos e fornecer feedback, mas não pode ser usado para redefinir uma senha ou alterar um nome de usuário.

Portanto, a resposta para sua pergunta é que os agregados devem se validar. Se eles não puderem, provavelmente você tem um modelo deficiente.

deslize do lado do rei
fonte
1
Embora os limites agregados sejam diferentes dependendo de quem está projetando o sistema, acho que “um agregado” decorrente da ordem é simplesmente tolo. Seu exemplo de um fornecedor que faz parte de um pedido é um bom exemplo - um fornecedor não pode existir até depois que um pedido é criado? Que tal fornecedores duplicados:
magnus
@ user1420752 Acho que você pode tê-lo ao contrário. O modelo acima implica o contrário. Que um Pedido não pode existir sem um Fornecedor. Meu exemplo é simplesmente usar as informações / regras / relacionamentos que eu poderia obter do código fornecido. Concordo que, assim como o Cliente, o Order provavelmente é um agregado grande e complexo por si só (embora não seja raiz). Um que também pode exigir a decomposição em algumas implementações concretas, dependendo do contexto. O ponto que estou ilustrando é que as entidades DEVEM se validar. Como você pode ver, é mais limpo assim.
king-side-slide
@ user1420752 Gostaria de acrescentar que frequentemente os métodos / construtores que exigem muitos argumentos são um sinal de um modelo anêmico no qual os dados são separados do comportamento (e, portanto, precisam ser injetados em pedaços grandes nas peças que atuam nos dados ) O construtor Feedback fornecido é um exemplo disso. Modelos anêmicos tendem a reduzir a coesão e adicionar semântica de acoplamento extra (como verificar IDs várias vezes). Coesão alta geralmente significa que todo método em uma entidade utiliza todas as suas variáveis ​​de instância. Isto naturalmente leva à decomposição de grandes agregados como cliente ou Ordem
king-side-slide