Existe uma maneira elegante de verificar restrições exclusivas nos atributos do objeto de domínio sem mover a lógica de negócios para a camada de serviço?


10

Estou adaptando o design orientado a domínio há cerca de 8 anos e, mesmo depois de todos esses anos, ainda há uma coisa que me incomoda. Isso está verificando um registro exclusivo no armazenamento de dados em um objeto de domínio.

Em setembro de 2013, Martin Fowler mencionou o princípio TellDon'tAsk , que, se possível, deve ser aplicado a todos os objetos de domínio, que devem retornar uma mensagem, como foi a operação (no design orientado a objetos, isso é feito principalmente por meio de exceções, quando a operação não teve êxito).

Meus projetos geralmente são divididos em várias partes, onde dois deles são Domínio (contendo regras de negócios e nada mais, o domínio ignora completamente a persistência) e Serviços. Serviços que conhecem a camada de repositório usada para dados CRUD.

Como a exclusividade de um atributo pertencente a um objeto é uma regra de domínio / negócio, ele deve demorar muito para o módulo de domínio, portanto, a regra está exatamente onde deveria estar.

Para poder verificar a exclusividade de um registro, você precisa consultar o conjunto de dados atual, geralmente um banco de dados, para descobrir se outro registro com digamos que Namejá existe.

Considerando que a camada de domínio é persistente e ignorante e não tem idéia de como recuperar os dados, mas apenas como executar operações neles, ela não pode realmente tocar os próprios repositórios.

O design que eu tenho adaptado fica assim:

class ProductRepository
{
    // throws Repository.RecordNotFoundException
    public Product GetBySKU(string sku);
}

class ProductCrudService
{
    private ProductRepository pr;

    public ProductCrudService(ProductRepository repository)
    {
        pr = repository;
    }

    public void SaveProduct(Domain.Product product)
    {
        try {
            pr.GetBySKU(product.SKU);

            throw Service.ProductWithSKUAlreadyExistsException("msg");
        } catch (Repository.RecordNotFoundException e) {
            // suppress/log exception
        }

        pr.MarkFresh(product);
        pr.ProcessChanges();
    }
}

Isso faz com que os serviços definam as regras de domínio em vez da própria camada de domínio e você tenha as regras espalhadas por várias seções do seu código.

Mencionei o princípio TellDon'tAsk, porque, como você pode ver claramente, o serviço oferece uma ação (ele salva Productou lança uma exceção), mas dentro do método você está operando objetos usando a abordagem procedural.

A solução óbvia é criar uma Domain.ProductCollectionclasse com um Add(Domain.Product)método lançando o ProductWithSKUAlreadyExistsException, mas está com um desempenho muito baixo, porque você precisaria obter todos os produtos do armazenamento de dados para descobrir no código se um produto já tem o mesmo SKU como o produto que você está tentando adicionar.

Como vocês resolvem esse problema específico? Isso não é realmente um problema em si, tive a camada de serviço que representa certas regras de domínio há anos. A camada de serviço geralmente também serve operações de domínio mais complexas. Estou simplesmente me perguntando se você encontrou uma solução melhor e mais centralizada durante sua carreira.


Por que puxar uma lista inteira de nomes quando você pode apenas pedir um e basear a lógica em se foi ou não encontrado? Você está dizendo à camada de persistência o que fazer e não como fazê-lo, desde que haja um acordo com a interface.
JeffO 17/05

1
Ao usar o termo Serviço, devemos nos esforçar para obter mais clareza, como a diferença entre serviços de domínio na camada de domínio e serviços de aplicativo na camada de aplicativo. Veja gorodinski.com/blog/2012/04/14/…
Erik Eidt

Excelente artigo, @ErikEidt, obrigado. Acho que meu design não está errado, então, se posso confiar no Sr. Gorodinski, ele está dizendo o mesmo: uma solução melhor é fazer com que um serviço de aplicativo recupere as informações exigidas por uma entidade, configure efetivamente o ambiente de execução e forneça para a entidade e tem a mesma objeção contra a injeção de repositório no modelo de domínio diretamente como eu, principalmente através da quebra do SRP.
21716 Andy

Uma citação da referência "diga, não pergunte": "Mas, pessoalmente, eu não uso o dizer não pergunte".
Radarbob 17/05

Respostas:


7

Considerando que a camada de domínio é persistente e ignorante e não tem idéia de como recuperar os dados, mas apenas como executar operações neles, ela não pode realmente tocar os próprios repositórios.

Eu discordaria desta parte. Especialmente a última frase.

Embora seja verdade que o domínio deva ser persistente ignorante, ele sabe que existe "Coleção de entidades de domínio". E que existem regras de domínio que dizem respeito a essa coleção como um todo. Exclusividade sendo um deles. E como a implementação da lógica real depende muito do modo de persistência específico, deve haver algum tipo de abstração no domínio que especifique a necessidade dessa lógica.

Portanto, é tão simples quanto criar uma interface que possa consultar se o nome já existe, que é implementado em seu armazenamento de dados e chamado por quem precisa saber se o nome é exclusivo.

E eu gostaria de enfatizar que os repositórios são serviços DOMAIN. São abstrações em torno da persistência. É a implementação do repositório que deve ser separada do domínio. Não há absolutamente nada de errado com a entidade de domínio chamar um serviço de domínio. Não há nada errado com uma entidade poder usar o repositório para recuperar outra entidade ou recuperar algumas informações específicas, que não podem ser prontamente mantidas na memória. Esta é uma razão pela qual o Repositório é um conceito-chave no livro de Evans .


Obrigado pela contribuição. O repositório poderia realmente representar o que Domain.ProductCollectioneu tinha em mente, considerando que eles são responsáveis ​​por recuperar objetos da camada Domínio?
187 Andy

@DavidPacker Eu realmente não entendo o que você quer dizer. Porque não deve haver necessidade de manter todos os itens na memória. A implementação do método "DoesNameExist" deve ser (provavelmente) a consulta SQL no lado do armazenamento de dados.
Euphoric

O que significa é que, em vez de armazenar os dados em uma coleção na memória, quando eu quero conhecer todo o Produto do qual não preciso obtê-los, Domain.ProductCollectionpergunte ao repositório, o mesmo que perguntando, se ele Domain.ProductCollectioncontém um Produto com o passou no SKU, desta vez novamente pedindo ao repositório (este é realmente o exemplo), que, em vez de iterar nos produtos pré-carregados, consulta o banco de dados subjacente. Não pretendo armazenar todo o Produto na memória, a menos que seja necessário, seria um absurdo completo.
21716 Andy

O que leva a outra pergunta, então o repositório deveria saber se um atributo deve ser único? Eu sempre implementei repositórios como componentes bastante estúpidos, salvando o que você os passa para salvar e tentando recuperar dados com base nas condições passadas e colocar a decisão em serviços.
217 Andy

@ DavidPacker Bem, o "conhecimento de domínio" está na interface. A interface diz "o domínio precisa dessa funcionalidade" e o armazenamento de dados fornece essa funcionalidade. Se você possui IProductRepositoryinterface DoesNameExist, é óbvio o que o método deve fazer. Mas garantir que o Produto não possa ter o mesmo nome que o produto existente deve estar no domínio em algum lugar.
Euphoric

4

Você precisa ler Greg Young sobre a validação de conjunto .

Resposta curta: antes de ir muito longe do ninho de ratos, você precisa entender o valor do requisito da perspectiva do negócio. Quão caro é realmente detectar e atenuar a duplicação, em vez de evitá-la?

O problema com os requisitos de "exclusividade" é que, muitas vezes, há uma razão subjacente mais profunda pela qual as pessoas os desejam - Yves Reynhout

Resposta mais longa: já vi um menu de possibilidades, mas todas elas têm vantagens e desvantagens.

Você pode procurar duplicatas antes de enviar o comando para o domínio. Isso pode ser feito no cliente ou no serviço (seu exemplo mostra a técnica). Se você não estiver satisfeito com o vazamento da lógica da camada de domínio, poderá obter o mesmo tipo de resultado com a DomainService.

class Product {
    void register(SKU sku, DuplicationService skuLookup) {
        if (skuLookup.isKnownSku(sku) {
            throw ProductWithSKUAlreadyExistsException(...)
        }
        ...
    }
}

Obviamente, dessa maneira, a implementação do DeduplicationService precisará saber algo sobre como procurar os skus existentes. Portanto, embora ele remova parte do trabalho para o domínio, você ainda enfrenta os mesmos problemas básicos (necessitando de uma resposta para a validação do conjunto, problemas com as condições da corrida).

Você pode fazer a validação na própria camada de persistência. Os bancos de dados relacionais são realmente bons na validação de conjuntos. Coloque uma restrição de exclusividade na coluna de sku do seu produto e você estará pronto. O aplicativo salva o produto no repositório e você recebe uma violação de restrição se houver um problema. Portanto, o código do aplicativo parece bom e sua condição de corrida é eliminada, mas você tem regras de "domínio" vazando.

Você pode criar um agregado separado em seu domínio que represente o conjunto de skus conhecidos. Eu posso pensar em duas variações aqui.

Um é algo como um ProductCatalog; Os produtos existem em outro lugar, mas o relacionamento entre produtos e skus é mantido por um catálogo que garante a exclusividade do sku. Não que isso implique que os produtos não tenham skus; Os skus são atribuídos por um ProductCatalog (se você precisar que os skus sejam únicos, você conseguirá isso tendo apenas um único agregado ProductCatalog). Revise a linguagem onipresente com seus especialistas em domínio - se isso existir, essa pode ser a abordagem correta.

Uma alternativa é algo mais como um serviço de reserva de sku. O mecanismo básico é o mesmo: um agregado conhece todos os skus, portanto, pode impedir a introdução de duplicatas. Mas o mecanismo é um pouco diferente: você adquire uma concessão de um sku antes de atribuí-lo a um produto; Ao criar o produto, você o concede ao sku. Ainda existe uma condição de corrida em jogo (agregados diferentes, portanto transações distintas), mas o sabor é diferente. A desvantagem real aqui é que você está projetando no modelo de domínio um serviço de leasing sem realmente ter uma justificativa no idioma do domínio.

Você pode puxar todas as entidades de produtos em um único agregado - ou seja, o catálogo de produtos descrito acima. Você obtém absolutamente a exclusividade do skus ao fazer isso, mas o custo é uma contenção adicional, modificar qualquer produto realmente significa modificar o catálogo inteiro.

Não gosto da necessidade de extrair todos os SKUs de produtos do banco de dados para fazer a operação na memória.

Talvez você não precise. Se você testar seu sku com um filtro Bloom , poderá descobrir muitos skus exclusivos sem carregar o aparelho.

Se o seu caso de uso permitir que você seja arbitrário sobre quais skus você rejeita, poderá eliminar todos os falsos positivos (não é grande coisa se você permitir que os clientes testem os skus que eles propõem antes de enviar o comando). Isso permitiria evitar o carregamento do aparelho na memória.

(Se você deseja aceitar mais, pode considerar preguiçosamente carregar os skus no caso de uma correspondência no filtro de bloom; você ainda corre o risco de carregar todos os skus na memória às vezes, mas não deve ser o caso comum se você permitir o código do cliente para verificar se há erros no comando antes de enviar).


Não gosto da necessidade de extrair todos os SKUs de produtos do banco de dados para fazer a operação na memória. É a solução óbvia que sugeri na pergunta inicial e questionei seu desempenho. Além disso, a IMO, contando com a restrição em seu banco de dados, para ser responsável pela exclusividade, é ruim. Se você alternasse para um novo mecanismo de banco de dados e de alguma forma a restrição exclusiva se perdesse durante uma transformação, você quebrou o código, porque as informações do banco de dados que estavam lá antes não estão mais lá. De qualquer forma, obrigado pelo link, leitura interessante.
187 Andy

Talvez um filtro Bloom (veja editar).
VoiceOfUnreason
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.