Modello di dominio Rich vs Anemic [chiuso]


93

Sto decidendo se devo utilizzare un modello di dominio ricco rispetto a un modello di dominio anemico e sto cercando buoni esempi dei due.

Ho creato applicazioni Web utilizzando un modello di dominio anemico, supportato da un servizio -> Repository -> sistema di livello di archiviazione , utilizzando FluentValidation per la convalida BL e inserendo tutto il mio BL nel livello di servizio.

Ho letto il libro DDD di Eric Evan e lui (insieme a Fowler e altri) sembra pensare che i modelli di dominio anemico siano un anti-pattern.

Quindi volevo solo avere un'idea di questo problema.

Inoltre, sto davvero cercando alcuni buoni esempi (di base) di un modello di dominio ricco e i vantaggi rispetto al modello di dominio anemico che fornisce.



14
DDD> ADM , ADM> DDD , DDD> ADM , ADM> DDD , ADM + DDD ... DDD / ADM o come non essere d'accordo sulla progettazione del software !
sp00m

Ecco un esempio di come evitare il modello di dominio anemico: medium.com/@wrong.about/…
Vadim Samokhin

11
È divertente che a questa domanda si potesse rispondere con un unico collegamento a un progetto del mondo reale finanziato da un'organizzazione reale. Dopo 5 anni, nessuna buona risposta, IMO. Parlare è economico. Mostrami il codice.
Mateusz Stefek

Risposte:


57

La differenza è che un modello anemico separa la logica dai dati. La logica è spesso collocato in classi denominate **Service, **Util, **Manager, **Helpere così via. Queste classi implementano la logica di interpretazione dei dati e quindi prendono il modello dei dati come argomento. Per esempio

public BigDecimal calculateTotal(Order order){
...
}

mentre l'approccio del dominio ricco inverte ciò inserendo la logica di interpretazione dei dati nel modello del dominio ricco. Quindi mette insieme logica e dati e un modello di dominio ricco sarebbe simile a questo:

order.getTotal();

Ciò ha un grande impatto sulla consistenza degli oggetti. Poiché la logica di interpretazione dei dati racchiude i dati (è possibile accedere ai dati solo tramite metodi oggetto), i metodi possono reagire ai cambiamenti di stato di altri dati -> Questo è ciò che chiamiamo comportamento.

In un modello anemico i modelli di dati non possono garantire che siano in uno stato legale mentre in un modello di dominio ricco possono. Un ricco modello di dominio applica i principi dell'OO come l'incapsulamento, l'occultamento delle informazioni e il raggruppamento di dati e logica e quindi un modello anemico è un anti pattern dal punto di vista dell'OO.

Per una visione più approfondita dai un'occhiata al mio blog https://www.link-intersystems.com/blog/2011/10/01/anemic-vs-rich-domain-models/


15
Supponiamo che il calcolo del prezzo totale di un ordine implichi: 1) Applicare uno sconto che dipende dall'appartenenza del cliente a uno dei possibili programmi fedeltà. 2) Applicare uno sconto per gli ordini che contengono un gruppo specifico di articoli insieme a seconda della campagna di marketing corrente condotta dal negozio. 3) Calcolo dell'imposta in cui l'importo dell'imposta dipende da ogni articolo specifico dell'ordine. Secondo te, dove starebbe tutta questa logica? Potrebbe fornire un semplice esempio di pseudo-codice. Grazie!
Nik

4
@Nik Nel modello avanzato, l'Ordine avrebbe un riferimento all'oggetto Cliente e l'oggetto Cliente avrebbe un riferimento al Programma fedeltà. Pertanto, l'Ordine avrebbe accesso a tutte le informazioni di cui ha bisogno senza bisogno di riferimenti espliciti a cose come servizi e archivi da cui prelevare tali informazioni. Tuttavia, sembra facile imbattersi in un caso in cui si verificano riferimenti ciclici. Vale a dire che l'ordine fa riferimento al cliente, il cliente ha un elenco di tutti gli ordini. Penso che questo possa essere in parte il motivo per cui le persone preferiscono Anemic ora.
schiacciare il

3
@crush L'approccio che descrivi funziona davvero bene. C'è un problema. È probabile che archiviamo le entità in un DB. Quindi, per calcolare il totale di un ordine, dobbiamo recuperare l'ordine, il cliente, il programma fedeltà, la campagna di marketing, la tabella delle tasse dal database. Considera inoltre che un cliente ha una raccolta di ordini, un programma fedeltà ha una raccolta di clienti e così via. Se ingenuamente recuperiamo tutti questi, finiremo per caricare l'intero DB nella RAM. Questo non è ovviamente fattibile, quindi ricorriamo al caricamento solo dei dati rilevanti dal DB ... 1/2
Nik

3
@Nik "Se recuperiamo tutti questi elementi in modo nativo, finiremo per caricare l'intero DB nella RAM." Questo è anche uno dei principali svantaggi del modello ricco nella mia mente. Il modello ricco è utile fino a quando il tuo dominio non diventa grande e complesso, quindi inizi a raggiungere i limiti dell'infrastruttura. È qui che gli ORM a caricamento lento possono venire in aiuto, però. Trovane uno buono e puoi conservare modelli ricchi senza caricare l'intero DB in memoria quando ti serviva solo 1/20 di esso. Detto questo, tendo a usare il modello anemico con CQRS io stesso dopo molti anni di andare avanti e indietro tra anemico e ricco.
schiacciare il

2
Un'altra cosa da considerare è dove risiede la logica del tuo dominio aziendale. Sempre più sviluppatori lo stanno spostando fuori dal database e verso le applicazioni a cui appartiene secondo me. Ma se sei bloccato in una situazione in cui la tua azienda richiede che la logica di business rimanga nel livello del database (stored procedure), allora quasi certamente non trarrai vantaggio dall'aggiunta anche di quella logica in un ricco modello di dominio. In effetti, potresti semplicemente prepararti a incorrere in conflitti in cui le stored procedure hanno regole diverse rispetto al livello di dominio della tua applicazione ...
schiacciare il

53

Bozhidar Bozhanov sembra argomentare a favore del modello anemico in questo post del blog.

Ecco il riassunto che presenta:

  • gli oggetti di dominio non dovrebbero essere gestiti a molla (IoC), non dovrebbero avere DAO o qualsiasi cosa relativa all'infrastruttura iniettata in loro

  • gli oggetti di dominio hanno gli oggetti di dominio da cui dipendono impostati da hibernate (o dal meccanismo di persistenza)

  • gli oggetti di dominio eseguono la logica di business, come l'idea principale di DDD è, ma questo non include query di database o CRUD - solo operazioni sullo stato interno dell'oggetto

  • raramente c'è bisogno di DTO - gli oggetti del dominio sono i DTO stessi nella maggior parte dei casi (il che salva un po 'di codice boilerplate)

  • i servizi eseguono operazioni CRUD, inviano e-mail, coordinano gli oggetti del dominio, generano report basati su più oggetti del dominio, eseguono query, ecc.

  • il livello di servizio (applicazione) non è così sottile, ma non include regole di business intrinseche agli oggetti del dominio

  • la generazione di codice dovrebbe essere evitata. Astrazione, modelli di progettazione e DI dovrebbero essere utilizzati per superare la necessità della generazione di codice e, in ultima analisi, per eliminare la duplicazione del codice.

AGGIORNARE

Recentemente ho letto questo articolo in cui l'autore sostiene di seguire una sorta di approccio ibrido - gli oggetti di dominio possono rispondere a varie domande basate esclusivamente sul loro stato (che nel caso di modelli totalmente anemici sarebbe probabilmente fatto nel livello di servizio)


11
Non posso estrarre da quell'articolo che Bozho sembra sostenere a favore del modello di dominio anemico. il livello di servizio (applicazione) non è così sottile, ma non include regole di business intrinseche agli oggetti del dominio . Quello che capisco è che gli oggetti di dominio dovrebbero contenere la logica di business che è loro intrinseca, ma non dovrebbero contenere nessun'altra logica di infrastruttura . Questo approccio non mi sembra affatto un modello di dominio anemico.
Utku

8
Anche questo: gli oggetti di dominio eseguono la logica di business, come l'idea centrale di DDD è, ma questo non include query di database o CRUD - solo operazioni sullo stato interno dell'oggetto . Queste affermazioni non sembrano affatto favorire il modello di dominio anemico. Affermano solo che la logica dell'infrastruttura non deve essere accoppiata agli oggetti del dominio. Almeno questo è quello che ho capito.
Utku

@Utku A mio modo di vedere sembra abbastanza chiaro che Bozho sostiene una sorta di ibrido tra i due modelli, un ibrido che direi più vicino al modello anemico che al modello ricco.
geoand

41

Il mio punto di vista è questo:

Modello di dominio anemico = tabelle di database mappate su oggetti (solo valori di campo, nessun comportamento reale)

Rich domain model = una raccolta di oggetti che espongono il comportamento

Se vuoi creare una semplice applicazione CRUD, forse è sufficiente un modello anemico con un classico framework MVC. Ma se vuoi implementare un qualche tipo di logica, il modello anemico significa che non farai programmazione orientata agli oggetti.

* Notare che il comportamento degli oggetti non ha nulla a che fare con la persistenza. Un livello diverso (mappatori di dati, archivi, ecc.) È responsabile della persistenza degli oggetti di dominio.


5
Scusa per la mia ignoranza, ma come fa un ricco modello di dominio a seguire SOLID principe se metti tutta la logica relativa all'entità nella classe. Ciò viola il principio SOLIDO, la "S" esattamente, che sta per responsabilità singola, che dice che una classe dovrebbe fare solo una cosa e farla bene.
redigaffi

5
@redigaffi Dipende da come definisci "una cosa". Si consideri una classe con due proprietà e due metodi: x, y, sume difference. Sono quattro cose. Oppure potresti sostenere che si tratta di addizione e sottrazione (due cose). Oppure potresti sostenere che è matematica (una cosa). Esistono molti post sul blog su come trovare un equilibrio nell'applicazione di SRP. Eccone uno: hackernoon.com/…
Rainbolt

2
In DDD, la responsabilità singola significa che una classe / modello può gestire il proprio stato senza causare effetti collaterali al resto del sistema nel suo insieme. Qualsiasi altra definizione si traduce solo in noiosi dibattiti filosofici nella mia esperienza.
ZombieTfk

12

Prima di tutto, ho copiato incollato la risposta da questo articolo http://msdn.microsoft.com/en-gb/magazine/dn385704.aspx

La Figura 1 mostra un Anemic Domain Model, che è fondamentalmente uno schema con getter e setter.

Figure 1 Typical Anemic Domain Model Classes Look Like Database Tables

public class Customer : Person
{
  public Customer()
  {
    Orders = new List<Order>();
  }
  public ICollection<Order> Orders { get; set; }
  public string SalesPersonId { get; set; }
  public ShippingAddress ShippingAddress { get; set; }
}
public abstract class Person
{
  public int Id { get; set; }
  public string Title { get; set; }
  public string FirstName { get; set; }
  public string LastName { get; set; }
  public string CompanyName { get; set; }
  public string EmailAddress { get; set; }
  public string Phone { get; set; }
}

In questo modello più ricco, piuttosto che esporre semplicemente le proprietà su cui leggere e scrivere, la superficie pubblica del Cliente è costituita da metodi espliciti.

Figure 2 A Customer Type That’s a Rich Domain Model, Not Simply Properties

public class Customer : Contact
{
  public Customer(string firstName, string lastName, string email)
  {
    FullName = new FullName(firstName, lastName);
    EmailAddress = email;
    Status = CustomerStatus.Silver;
  }
  internal Customer()
  {
  }
  public void UseBillingAddressForShippingAddress()
  {
    ShippingAddress = new Address(
      BillingAddress.Street1, BillingAddress.Street2,
      BillingAddress.City, BillingAddress.Region,
      BillingAddress.Country, BillingAddress.PostalCode);
  }
  public void CreateNewShippingAddress(string street1, string street2,
   string city, string region, string country, string postalCode)
  {
    ShippingAddress = new Address(
      street1,street2,
      city,region,
      country,postalCode)
  }
  public void CreateBillingInformation(string street1,string street2,
   string city,string region,string country, string postalCode,
   string creditcardNumber, string bankName)
  {
    BillingAddress = new Address      (street1,street2, city,region,country,postalCode );
    CreditCard = new CustomerCreditCard (bankName, creditcardNumber );
  }
  public void SetCustomerContactDetails
   (string email, string phone, string companyName)
  {
    EmailAddress = email;
    Phone = phone;
    CompanyName = companyName;
  }
  public string SalesPersonId { get; private set; }
  public CustomerStatus Status { get; private set; }
  public Address ShippingAddress { get; private set; }
  public Address BillingAddress { get; private set; }
  public CustomerCreditCard CreditCard { get; private set; }
}

2
C'è un problema con i metodi che creano un oggetto e assegnano una proprietà con l'oggetto appena creato. Rendono il codice meno estensibile e flessibile. 1) Cosa succede se il consumatore di codice desidera creare non Address, ma ExtendedAddressereditato da Address, con diverse proprietà aggiuntive? 2) O modificare CustomerCreditCardi parametri del costruttore da prendere BankIDinvece di BankName?
Lightman

Cosa sta creando un indirizzo richiede servizi aggiuntivi rispetto a ciò che compone l'oggetto? Ti resta l'iniezione del metodo per ottenere quei servizi. E se si tratta di molti servizi?
schiacciare il

8

Uno dei vantaggi delle ricche classi di dominio è che puoi chiamare il loro comportamento (metodi) ogni volta che hai il riferimento all'oggetto in qualsiasi livello. Inoltre, tendi a scrivere metodi piccoli e distribuiti che collaborano insieme. Nelle classi di dominio anemiche, si tende a scrivere metodi procedurali pesanti (nel livello di servizio) che di solito sono guidati dal caso d'uso. Di solito sono meno gestibili rispetto alle classi di domini ricchi.

Un esempio di classi di dominio con comportamenti:

class Order {

     String number

     List<OrderItem> items

     ItemList bonus

     Delivery delivery

     void addItem(Item item) { // add bonus if necessary }

     ItemList needToDeliver() { // items + bonus }

     void deliver() {
         delivery = new Delivery()
         delivery.items = needToDeliver()
     }

}

Il metodo needToDeliver()restituirà l'elenco degli articoli che devono essere consegnati incluso il bonus. Può essere chiamato all'interno della classe, da un'altra classe correlata o da un altro livello. Ad esempio, se si passa Orderalla visualizzazione, è possibile utilizzare needToDeliver()di selezionato Orderper visualizzare l'elenco di elementi che devono essere confermati dall'utente prima che faccia clic sul pulsante Salva per persistere Order.

Risposta al commento

Ecco come utilizzo la classe di dominio dal controller:

def save = {
   Order order = new Order()
   order.addItem(new Item())
   order.addItem(new Item())
   repository.create(order)
}

La creazione di Ordere la sua LineItemè in un'unica transazione. Se uno dei LineItemnon può essere creato, non Orderverrà creato nessuno .

Tendo ad avere metodi che rappresentano una singola transazione, come ad esempio:

def deliver = {
   Order order = repository.findOrderByNumber('ORDER-1')
   order.deliver()       
   // save order if necessary
}

Qualsiasi cosa all'interno deliver()verrà eseguita come una singola transazione. Se ho bisogno di eseguire molti metodi non correlati in una singola transazione, creerei una classe di servizio.

Per evitare un'eccezione di caricamento lento, utilizzo il grafico di entità denominato JPA 2.1. Ad esempio, nella schermata del controller per la consegna, posso creare un metodo per caricare l' deliveryattributo e ignorare bonus, come repository.findOrderByNumberFetchDelivery(). Nella schermata bonus, chiamo un altro metodo che carica l' bonusattributo e ignoro delivery, come repository.findOrderByNumberFetchBonus(). Ciò richiede dicipline poiché non riesco ancora a chiamare deliver()nella schermata bonus.


1
E l'ambito della transazione?
kboom

5
I comportamenti del modello di dominio non dovrebbero contenere logica di persistenza (inclusa la transazione). Dovrebbero essere testabili (in unit test) senza connessione al database. L'ambito della transazione è responsabilità del livello di servizio o del livello di persistenza.
jocki

1
Che ne dici del caricamento lento, quindi?
kboom

Quando crei istanze di classi di dominio in unit test, non sono nello stato gestito perché sono oggetti semplici. Tutti i comportamenti possono essere testati correttamente.
jocki

E cosa succede quando ti aspetti l'oggetto dominio dal livello di servizio? Non è gestito allora?
kboom

8

Quando scrivevo app desktop monolitiche, creavo modelli ricchi di dominio, mi divertivo a crearli.

Ora scrivo minuscoli microservizi HTTP, c'è il minor numero di codice possibile, inclusi DTO anemici.

Penso che DDD e questo argomento anemico risalgano all'era monolitica delle app desktop o server. Ricordo quell'epoca e sarei d'accordo che i modelli anemici sono strani. Ho costruito una grande app monolitica di trading FX e non c'era un modello, davvero, era orribile.

Con i microservizi, i piccoli servizi con il loro ricco comportamento, sono probabilmente i modelli componibili e gli aggregati all'interno di un dominio. Quindi le implementazioni dei microservizi potrebbero non richiedere ulteriore DDD. L'applicazione del microservizio può essere il dominio.

Un microservizio di ordini può avere pochissime funzioni, espresse come risorse RESTful o tramite SOAP o altro. Il codice del microservizio degli ordini può essere estremamente semplice.

Un servizio singolo (micro) più grande e monolitico, in particolare uno che lo mantiene modello in RAM, può trarre vantaggio dal DDD.


Hai esempi di codice per microservizi HTTP che rappresentano il tuo attuale stato dell'arte? Non chiedendoti di scrivere nulla, condividi i link se hai qualcosa a cui potresti puntare. Grazie.
Casey Plummer

3

Penso che la radice del problema sia nella falsa dicotomia. Come è possibile estrarre questi 2 modelli: ricco e "anemico" e confrontarli tra loro? Penso che sia possibile solo se hai idee sbagliate su cosa sia una classe . Non ne sono sicuro, ma penso di averlo trovato in uno dei video di Bozhidar Bozhanov su Youtube. Una classe non è un metodo dati + su questi dati. È una comprensione totalmente non valida che porta alla divisione delle classi in due categorie: solo dati, quindi modello anemico e dati + metodi - modello così ricco (per essere più corretti esiste una terza categoria: solo metodi pari).

La verità è che la classe è un concetto in un modello ontologico, una parola, una definizione, un termine, un'idea, è un DENOTAT . E questa comprensione elimina la falsa dicotomia: non puoi avere SOLO modello anemico o SOLO modello ricco, perché significa che il tuo modello non è adeguato, non è rilevante per la realtà: alcuni concetti hanno solo dati, alcuni hanno solo metodi, altri di loro sono misti. Perché proviamo a descrivere, in questo caso, alcune categorie, insiemi di oggetti, relazioni, concetti con classi e, come sappiamo, alcuni concetti sono solo processi (metodi), alcuni di essi sono solo set di attributi (dati), alcuni di sono relazioni con attributi (misti).

Penso che un'applicazione adeguata dovrebbe includere tutti i tipi di classi ed evitare di auto-limitarsi fanaticamente a un solo modello. Non importa come la logica rappresenti: con codice o con oggetti dati interpretabili (come Free Monads ), comunque: dovremmo avere classi (concetti, denotati) che rappresentano processi, logica, relazioni, attributi, caratteristiche, dati, ecc. E non cercare di evitarne alcuni o ridurli tutti all'unico tipo.

Quindi, possiamo estrarre la logica in un'altra classe e lasciare i dati in quella originale, ma non ha senso perché alcuni concetti possono includere attributi e relazioni / processi / metodi e una loro separazione duplicherà il concetto sotto 2 nomi che possono essere ridotto a modelli: "OBJECT-Attributes" e "OBJECT-Logic". Va bene nei linguaggi procedurali e funzionali a causa della loro limitazione, ma è un eccessivo autocontrollo per un linguaggio che consente di descrivere tutti i tipi di concetti.


1

I modelli di dominio anemico sono importanti per ORM e per un facile trasferimento su reti (la linfa vitale di tutte le applicazioni commerciali) ma OO è molto importante per l'incapsulamento e la semplificazione delle parti "transazionali / gestionali" del codice.

L'importante, quindi, è riuscire a identificarsi e convertirsi da un mondo all'altro.

Assegna ai modelli Anemic un nome come AnemicUser, o UserDAO ecc. In modo che gli sviluppatori sappiano che esiste una classe migliore da usare, quindi hanno un costruttore appropriato per la classe anemica none

User(AnemicUser au)

e il metodo dell'adattatore per creare la classe anemica per il trasporto / persistenza

User::ToAnemicUser() 

Cerca di utilizzare l'utente anemico nessuno ovunque al di fuori del trasporto / persistenza


-1

Ecco un esempio che potrebbe aiutare:

Anemico

class Box
{
    public int Height { get; set; }
    public int Width { get; set; }
}

Non anemico

class Box
{
    public int Height { get; private set; }
    public int Width { get; private set; }

    public Box(int height, int width)
    {
        if (height <= 0) {
            throw new ArgumentOutOfRangeException(nameof(height));
        }
        if (width <= 0) {
            throw new ArgumentOutOfRangeException(nameof(width));
        }
        Height = height;
        Width = width;
    }

    public int area()
    {
       return Height * Width;
    }
}

Sembra che possa essere convertito in un ValueObject contro un'entità.
codice5

Basta una copia incolla da Wikipedia senza alcuna spiegazione
wst

chi ha scritto prima? @wst
Alireza Rahmani Khalili

@AlirezaRahmaniKhalili secondo la storia di Wikipedia, sono stati i primi ... A meno che, non ho capito la tua domanda.
wst

-1

L'approccio classico al DDD non prevede di evitare a tutti i costi Anemic vs Rich Models. Tuttavia, MDA può ancora applicare tutti i concetti DDD (contesti limitati, mappe di contesto, oggetti valore, ecc.) Ma utilizza i modelli Anemic vs Rich in tutti i casi. Ci sono molti casi in cui l'utilizzo di Servizi di dominio per orchestrare casi d'uso di domini complessi in un insieme di aggregazioni di dominio rappresenta un approccio molto migliore rispetto ai semplici aggregati richiamati dal livello dell'applicazione. L'unica differenza rispetto al classico approccio DDD è dove risiedono tutte le convalide e le regole aziendali? C'è un nuovo costrutto noto come validatori di modelli. I validatori assicurano l'integrità del modello di input completo prima che avvenga qualsiasi caso d'uso o flusso di lavoro del dominio. Le entità radice e figlio aggregate sono anemiche ma ciascuna può avere i propri validatori di modello invocati secondo necessità, dal suo validatore di root. I validatori aderiscono ancora a SRP, sono di facile manutenzione e sono testabili in unità.

La ragione di questo cambiamento è che ora ci stiamo muovendo più verso un'API prima rispetto a un primo approccio UX ai microservizi. REST ha svolto un ruolo molto importante in questo. L'approccio API tradizionale (a causa di SOAP) è stato inizialmente fissato su un'API basata su comandi rispetto ai verbi HTTP (POST, PUT, PATCH, GET e DELETE). Un'API basata su comandi si adatta bene all'approccio orientato agli oggetti Rich Model ed è ancora molto valida. Tuttavia, semplici API basate su CRUD, sebbene possano adattarsi a un Rich Model, sono molto più adatte con semplici modelli anemici, validatori e servizi di dominio per orchestrare il resto.

Adoro DDD in tutto ciò che ha da offrire, ma arriva un momento in cui è necessario allungarlo un po 'per adattarsi a un approccio all'architettura in costante cambiamento e migliore.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.