Modelos de domínio rico - como, exatamente, o comportamento se encaixa?

84

No debate dos modelos de domínio Rich vs. Anêmico, a Internet está cheia de conselhos filosóficos, mas com poucos exemplos de autoridade. O objetivo desta pergunta é encontrar diretrizes definitivas e exemplos concretos de modelos adequados de Design Orientado a Domínios. (Idealmente em c #.)

Para um exemplo do mundo real, essa implementação do DDD parece estar errada:

Os modelos de domínio WorkItem abaixo são nada além de pacotes de propriedades, usados ​​pelo Entity Framework para um banco de dados com código primeiro. Para Fowler, é anêmico .

A camada WorkItemService é aparentemente uma percepção errada comum dos Serviços de Domínio; Ele contém toda a lógica de comportamento / negócios do WorkItem. Por Yemelyanov e outros, é processual . (pág. 6)

Então, se o abaixo está errado, como posso fazer isso certo?
O comportamento, ou seja, AddStatusUpdate ou Checkout , deve pertencer à classe WorkItem correta?
Quais dependências o modelo WorkItem deve ter?

insira a descrição da imagem aqui

public class WorkItemService : IWorkItemService {
    private IUnitOfWorkFactory _unitOfWorkFactory;

    //using Unity for dependency injection
    public WorkItemService(IUnitOfWorkFactory unitOfWorkFactory) {
        _unitOfWorkFactory = unitOfWorkFactory;
    }

    public void AddStatusUpdate(int workItemId, int statusId) {

        using (var unitOfWork = _unitOfWorkFactory.GetUnitOfWork<IWorkItemUnitOfWork>()) {
            var workItemRepo = unitOfWork.WorkItemRepository;
            var workItemStatusRepo = unitOfWork.WorkItemStatusRepository;

            var workItem = workItemRepo.Read(wi => wi.Id == workItemId).FirstOrDefault();
            if (workItem == null)
                throw new ArgumentException(string.Format(@"The provided WorkItem Id '{0}' is not recognized", workItemId), "workItemId");

            var status = workItemStatusRepo.Read(s => s.Id == statusId).FirstOrDefault();
            if (status == null)
                throw new ArgumentException(string.Format(@"The provided Status Id '{0}' is not recognized", statusId), "statusId");

            workItem.StatusHistory.Add(status);

            workItemRepo.Update(workItem);
            unitOfWork.Save();
        }
    }
}

(Este exemplo foi simplificado para ser mais legível. O código definitivamente ainda é complicado, porque é uma tentativa confusa, mas o comportamento do domínio era: atualizar status adicionando o novo status ao histórico do arquivo. Finalmente, concordo com as outras respostas, este pode ser tratado pelo CRUD.)

Atualizar

O @AlexeyZimarev deu a melhor resposta, um vídeo perfeito sobre o assunto em c # por Jimmy Bogard, mas aparentemente foi movido para um comentário abaixo porque não forneceu informações suficientes além do link. Eu tenho um rascunho das minhas anotações resumindo o vídeo na minha resposta abaixo. Por favor, sinta-se livre para comentar a resposta com quaisquer correções. O vídeo dura uma hora, mas vale a pena assistir.

Atualização - 2 anos depois

Eu acho que é um sinal da maturidade nascente do DDD que, mesmo depois de estudá-lo por 2 anos, ainda não posso prometer que sei o "caminho certo" de fazê-lo. Linguagem onipresente, raízes agregadas e sua abordagem ao design orientado por comportamento são as valiosas contribuições da DDD para a indústria. A ignorância da persistência e a fonte de eventos causam confusão, e acho que uma filosofia assim impede a adoção mais ampla. Mas se eu tivesse que repetir esse código novamente, com o que aprendi, acho que seria algo assim:

insira a descrição da imagem aqui

Ainda agradeço qualquer resposta a esta postagem (muito ativa) que forneça qualquer código de práticas recomendadas para um modelo de domínio válido.

RJB
fonte
6
Todas as teorias filosóficas caem no chão quando você as conta "I don't want to duplicate all my entities into DTOs simply because I don't need it and it violates DRY, and I also don't want my client application to take a dependency on EntityFramework.dll". "Entidades" no Entity Framework jargão não são os mesmos como "Entidades" como em "Modelo de Domínio"
Federico Berasategui
Estou bem em duplicar minhas entidades de domínio em um DTO, usando uma ferramenta automatizada como Automapper, se é isso que é preciso. Só não tenho certeza de como isso deve parecer no final do dia.
RJB 06/10
16
Eu recomendo que você assista à sessão do NDC 2012 de Jimmy Bogard "Criando modelos de domínio maligno" no Vimeo . Ele explica o que um domínio rico deve ser e como implementá-lo na vida real, tendo comportamento em suas entidades. Exemplos são muito práticos e todos em C #.
Alexey Zimarev 07/10
Obrigado, estou no meio do vídeo e isso é perfeito até agora. Eu sabia que se isso estava errado, tinha de haver um "direito" responder lá fora em algum lugar ....
RJB
2
Também exijo amor por Java: /
uylmz 12/02/16

Respostas:

59

A resposta mais útil foi dada por Alexey Zimarev e obteve pelo menos 7 votos positivos antes que um moderador o movesse para um comentário abaixo da minha pergunta original ....

Sua resposta:

Eu recomendo que você assista à sessão do NDC 2012 de Jimmy Bogard "Criando modelos de domínio maligno" no Vimeo. Ele explica o que um domínio rico deve ser e como implementá-lo na vida real, tendo comportamento em suas entidades. Exemplos são muito práticos e todos em C #.

http://vimeo.com/43598193

Fiz algumas anotações para resumir o vídeo, tanto para o benefício da minha equipe quanto para fornecer detalhes um pouco mais imediatos neste post. (O vídeo dura uma hora, mas vale a pena cada minuto, se você tiver tempo. Jimmy Bogard merece muito crédito por sua explicação.)

  • "Para a maioria das aplicações ... não sabemos que elas serão complexas quando começarmos. Elas simplesmente se tornam assim."
    • A complexidade cresce naturalmente à medida que códigos e requisitos são adicionados. Os aplicativos podem começar muito simples, como CRUD, mas o comportamento / regras podem se tornar polêmicos.
    • "O bom é que não precisamos começar de forma complexa. Podemos começar com o modelo de domínio anêmico, que é apenas propriedade, e com apenas técnicas de refatoração padrão, podemos avançar em direção a um verdadeiro modelo de domínio".
  • Modelos de domínio = objetos de negócios. Comportamento do domínio = regras de negócios.
  • O comportamento geralmente está oculto em um aplicativo - pode estar em PageLoad, Button1_Click ou em classes auxiliares como 'FooManager' ou 'FooService'.
  • As regras de negócios separadas dos objetos de domínio "exigem que lembremos" dessas regras.
    • No meu exemplo pessoal acima, uma regra de negócios é WorkItem.StatusHistory.Add (). Não estamos apenas mudando o status, estamos arquivando-o para auditoria.
  • Comportamentos de domínio "eliminam erros em um aplicativo com muito mais facilidade do que apenas escrever vários testes". Os testes exigem que você saiba escrever esses testes. Os comportamentos do domínio oferecem os caminhos corretos para testar .
  • Os serviços de domínio são "classes auxiliares para coordenar atividades entre diferentes entidades de modelo de domínio".
    • Serviços de domínio! = Comportamento do domínio. As entidades têm comportamento, os serviços de domínio são apenas intermediários entre as entidades.
  • Objetos de domínio não devem possuir a infraestrutura de que precisam (por exemplo, IOfferCalculatorService). O serviço de infraestrutura deve ser passado para o modelo de domínio que o utiliza.
  • Os modelos de domínio devem oferecer a você o que eles podem fazer e só devem poder fazer essas coisas.
  • As propriedades dos modelos de domínio devem ser protegidas com setters privados, para que apenas o modelo possa definir suas próprias propriedades, através de seus próprios comportamentos . Caso contrário, é "promíscuo".
  • Objetos de modelo de domínio anêmico, que são apenas pacotes de propriedades para um ORM, são apenas "uma camada fina - uma versão fortemente tipada no banco de dados".
    • "Por mais fácil que seja colocar uma linha do banco de dados em um objeto, é isso que temos".
    • A maioria dos modelos de objetos persistentes é exatamente isso. O que diferencia um modelo de domínio anêmico versus um aplicativo que realmente não tem comportamento é se um objeto possui regras de negócios, mas essas regras não são encontradas em um modelo de domínio. "
  • "Para muitos aplicativos, não há necessidade real de construir qualquer tipo de camada lógica de aplicativos de negócios, é apenas algo que pode falar com o banco de dados e talvez uma maneira fácil de representar os dados que estão lá".
    • Portanto, em outras palavras, se tudo o que você está fazendo é CRUD sem objetos de negócios ou regras de comportamento especiais, você não precisa de DDD.

Por favor, sinta-se livre para comentar com outros pontos que você acha que devem ser incluídos, ou se você acha que alguma dessas notas está errada. Tentou citar diretamente ou parafrasear o máximo possível.

RJB
fonte
Ótimo vídeo, especialmente para ver como a refatoração funciona em uma ferramenta. Muito é sobre o encapsulamento adequado de objetos de domínio (para garantir que eles sejam consistentes). Ele faz um ótimo trabalho dizendo as regras de negócios sobre ofertas, membros etc. Ele menciona a palavra invariante algumas vezes (que é modelagem de domínio baseada em contrato). Desejo que o código .net comunique melhor o que é uma regra comercial formal, já que eles são alterados e você precisa mantê-los.
Fuhrmanator
6

Sua pergunta não pode ser respondida, porque seu exemplo está errado. Especificamente, porque não há comportamento. Pelo menos não na área do seu domínio. O exemplo do AddStatusUpdatemétodo não é uma lógica de domínio, mas uma lógica que usa esse domínio. Esse tipo de lógica faz sentido estar dentro de algum tipo de serviço, que lida com solicitações externas.

Por exemplo, se havia um requisito de que um item de trabalho específico possa ter apenas status específico ou que possa ter apenas status N, isso é lógica de domínio e deve fazer parte de um WorkItemou de StatusHistoryum método.

O motivo da sua confusão é porque você está tentando aplicar uma diretriz ao código que não precisa dela. Os modelos de domínio são relevantes apenas se você tiver muita lógica de domínio complexa. Por exemplo. lógica que funciona nas próprias entidades e decorre dos requisitos. Se o código trata da manipulação de entidades a partir de dados externos, isso não é, muito provavelmente, uma lógica de domínio. Mas, no momento em que você obtém muitos ifs com base em quais dados e entidades você está trabalhando, isso é lógica de domínio.

Um dos problemas da modelagem de domínio verdadeiro é o gerenciamento de requisitos complexos. E, como tal, seu verdadeiro poder e benefícios não podem ser exibidos em código simples. Você precisa de dezenas de entidades com muitos requisitos ao seu redor para realmente ver os benefícios. Novamente, seu exemplo é muito simples para o modelo de domínio brilhar de verdade.

Finalmente, uma coisa do AT que eu mencionaria é que seria realmente difícil persistir um modelo de domínio verdadeiro com design de OOP real usando o Entity Framework. Embora os ORMs tenham sido projetados com o mapeamento da verdadeira estrutura de POO para as relacionais, ainda existem muitos problemas, e o modelo relacional geralmente vaza para o modelo de POO. Mesmo com o nHibernate, que considero muito mais poderoso que o EF, isso pode ser um problema.

Eufórico
fonte
Bons pontos. Onde o método AddStatusUpdate pertenceria então, em Dados ou em outro projeto na Infraestrutura? O que é um exemplo de qualquer comportamento que possa pertencer teoricamente ao WorkItem? Qualquer código ou modelo psuedo seria muito apreciado. Meu exemplo foi realmente simplificado para ser mais legível. Existem outras entidades e, por exemplo, o AddStatusUpdate tem algum comportamento extra - ele realmente leva um nome de categoria de status e, se essa categoria não existir, a categoria é criada.
RJB 07/10
@RJB Como eu disse, AddStatusUpdate é o código que está usando o domínio. Então, algum tipo de serviço da web ou aplicativo que usa as classes de domínio. E, como eu disse, você não pode esperar nenhum tipo de maquete ou pseudocódigo, porque seria necessário criar um projeto inteiro com complexidade grande o suficiente para mostrar a vantagem real do modelo de domínio OOP.
Euphoric
5

Sua suposição de que encapsular sua lógica de negócios associada ao WorkItem em um "serviço fat" é um antipadrão inerente que eu argumentaria que não é necessariamente.

Independentemente de suas opiniões sobre o modelo de domínio anêmico, os padrões e práticas padrão típicos de um aplicativo Line of Business .NET incentivam uma abordagem em camadas transacional composta por vários componentes. Eles incentivam a separação da lógica de negócios do modelo de domínio especificamente para facilitar a comunicação de um modelo de domínio comum entre outros componentes .NET, bem como componentes em diferentes pilhas de tecnologia ou em camadas físicas.

Um exemplo disso seria um serviço Web SOAP baseado em .NET que se comunica com um aplicativo cliente Silverlight que possui uma DLL contendo tipos de dados simples. Esse projeto de entidade do domínio pode ser incorporado a um assembly .NET ou Silverlight, onde os componentes Silverlight interessados ​​que possuem essa DLL não serão expostos a comportamentos de objetos que podem depender de componentes disponíveis apenas para o serviço.

Independentemente da sua posição sobre esse debate, esse é o padrão adotado e aceito apresentado pela Microsoft e, na minha opinião profissional, não é uma abordagem errada, mas um modelo de objeto que define seu próprio comportamento também não é necessariamente um antipadrão. Se você avançar com esse design, é melhor perceber e entender algumas das limitações e pontos problemáticos que você pode enfrentar se precisar se integrar a outros componentes que precisam ver o seu modelo de domínio. Nesse caso específico, talvez você queira que um tradutor converta seu modelo de domínio de estilo orientado a objetos em objetos de dados simples que não exponham certos métodos de comportamento.

maple_shaft
fonte
1
1) Como você pode separar a lógica de negócios do modelo de domínio? É o domínio em que essa lógica de negócios vive; as entidades nesse domínio estão executando o comportamento associado a essa lógica de negócios. O mundo real não tem serviços, nem existe na cabeça de especialistas em domínio. 2) Qualquer componente que deseje integrar-se a você precisa criar seu próprio modelo de domínio, pois suas necessidades serão diferentes e terão uma visão diferente do seu modelo de domínio. É uma falácia de longa data que você pode criar um modelo de domínio que pode ser compartilhado.
precisa
1
@StefanBilliet Esses são bons pontos sobre a falácia de um modelo de domínio universal, mas isso é possível em componentes mais simples e na interação de componentes, como fiz antes. Minha opinião é que a lógica de tradução entre modelos de domínio pode gerar muitos códigos tediosos e clichê e, se puder ser evitada com segurança, pode ser uma boa opção de design.
maple_shaft
1
Para ser franco, acho que a única boa opção de design é um modelo que um especialista em negócios possa raciocinar. Você está criando um modelo de domínio, para uma empresa usar para resolver certos problemas dentro desse domínio. Dividir o comportamento de entidades de domínio em serviços torna mais difícil para todos os envolvidos, porque você constantemente precisa mapear o que os especialistas em domínio dizem para um código de serviço que quase não tem nenhuma semelhança com a conversa atual. Na minha experiência, você perde muito mais tempo com isso do que digitar clichê. Isso não quer dizer que não haja maneiras de contornar o código do local da caldeira.
precisa
@StefanBilliet Em um mundo perfeito, eu concordo com você em que um especialista em negócios tem tempo para se sentar com os desenvolvedores. A realidade da indústria de software é que o especialista em negócios não tem tempo ou interesse em se envolver nesse nível ou pior, mas espera-se que os desenvolvedores descubram isso apenas com orientações vagas.
maple_shaft
É verdade, mas essa não é uma razão para aceitar essa realidade. Continuar nessa busca é desperdiçar o tempo (e possivelmente a reputação) dos desenvolvedores e o dinheiro do cliente. O processo que descrevi é um relacionamento que precisa ser construído ao longo do tempo; é preciso muito esforço, mas produz resultados muito melhores. Há uma razão para que "Ubiquitous Language" seja frequentemente considerado o aspecto mais importante do DDD.
Stefan Billiet 27/11
5

Sei que essa pergunta é bastante antiga, então essa resposta é para a posteridade. Quero responder com um exemplo concreto em vez de um baseado na teoria.

Encapsule a "alteração do status do item de trabalho" na WorkItemclasse da seguinte maneira:

public SomeStatusUpdateType Status { get; private set; }

public void ChangeStatus(SomeStatusUpdateType status)
{
    // Maybe we designed this badly at first ;-)
    Status = status;       
}

Agora sua WorkItemturma é responsável por se manter em um estado legal. A implementação é bem fraca, no entanto. O proprietário do produto deseja um histórico de todas as atualizações de status feitas no WorkItem.

Nós mudamos para algo assim:

private ICollection<SomeStatusUpdateType> StatusUpdates { get; private set; }
public SomeStatusUpdateType Status => StatusUpdates.OrderByDescending(s => s.CreatedOn).FirstOrDefault();

public void ChangeStatus(SomeStatusUpdateType status)
{
    // Better...
    StatusUpdates.Add(status);       
}

A implementação mudou drasticamente, mas o responsável pela chamada do ChangeStatusmétodo desconhece os detalhes subjacentes da implementação e não tem motivos para alterar a si próprio.

Este é um exemplo de uma entidade de modelo de domínio avançado, IMHO.

Don
fonte