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 Name
já 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 Product
ou 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.ProductCollection
classe 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.
Respostas:
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 .
fonte
Domain.ProductCollection
eu tinha em mente, considerando que eles são responsáveis por recuperar objetos da camada Domínio?Domain.ProductCollection
pergunte ao repositório, o mesmo que perguntando, se eleDomain.ProductCollection
conté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.IProductRepository
interfaceDoesNameExist
, é ó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.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?
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
.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.
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).
fonte