Modelli di dominio ricco: come si adatta esattamente il comportamento?

80

Nel dibattito sui modelli di dominio Rich vs. Anemic, Internet è pieno di consigli filosofici ma di esempi autorevoli. L'obiettivo di questa domanda è quello di trovare linee guida definitive ed esempi concreti di opportuni modelli di progettazione basata sul dominio. (Idealmente in C #.)

Per un esempio del mondo reale, questa implementazione di DDD sembra essere sbagliata:

I modelli di dominio WorkItem di seguito non sono altro che sacchetti di proprietà, utilizzati da Entity Framework per un database code-first. Per Fowler, è anemico .

Il livello WorkItemService è apparentemente un errore comune dei Domain Services; contiene tutto il comportamento / business logic per WorkItem. Per Yemelyanov e altri, è procedurale . (pagina 6)

Quindi se il sotto è sbagliato, come posso farlo bene?
Il comportamento, ad esempio AddStatusUpdate o Checkout , dovrebbe appartenere alla classe WorkItem corretta?
Quali dipendenze dovrebbe avere il modello WorkItem?

publicclassWorkItemService:IWorkItemService{privateIUnitOfWorkFactory_unitOfWorkFactory;//usingUnityfordependencyinjectionpublicWorkItemService(IUnitOfWorkFactoryunitOfWorkFactory){_unitOfWorkFactory=unitOfWorkFactory;}publicvoidAddStatusUpdate(intworkItemId,intstatusId){using(varunitOfWork=_unitOfWorkFactory.GetUnitOfWork<IWorkItemUnitOfWork>()){varworkItemRepo=unitOfWork.WorkItemRepository;varworkItemStatusRepo=unitOfWork.WorkItemStatusRepository;varworkItem=workItemRepo.Read(wi=>wi.Id==workItemId).FirstOrDefault();if(workItem==null)thrownewArgumentException(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();
        }
    }
}

(Questo esempio è stato semplificato per essere più leggibile.Il codice è sicuramente ancora goffo, perché è un tentativo confuso, ma il comportamento del dominio è stato: stato di aggiornamento aggiungendo il nuovo stato alla cronologia degli archivi.Infine sono d'accordo con l'altro risposte, questo potrebbe essere gestito da CRUD.)

Aggiornamento

@AlexeyZimarev ha fornito la risposta migliore, un video perfetto sull'argomento in C # di Jimmy Bogard, ma apparentemente è stato spostato in un commento di seguito perché non forniva informazioni sufficienti al di là del collegamento. Ho una bozza preliminare dei miei appunti che riassumono il video nella mia risposta qui sotto. Non esitate a commentare la risposta con eventuali correzioni. Il video dura un'ora, ma vale la pena guardare.

Aggiornamento - 2 anni dopo

Penso che sia un segno della nascente maturità della DDD che, anche dopo averlo studiato per 2 anni, non posso ancora promettere di conoscere la "maniera giusta" per farlo. Il linguaggio onnipresente, le radici aggregate e il suo approccio al design comportamentale sono i preziosi contributi di DDD al settore. L'ignoranza di persistenza e l'individuazione di eventi causano confusione e penso che la filosofia del genere la trattiene da una più ampia adozione. Ma se dovessi fare di nuovo questo codice, con quello che ho imparato, penso che sarebbe simile a questo:

Ricevo ancora eventuali risposte a questo post (molto attivo) che fornisce un codice di best practice per un modello di dominio valido.

    
posta RJB 06.10.2013 - 20:49
fonte

4 risposte

52

La risposta più utile è stata data da Alexey Zimarev e ha ottenuto almeno 7 upvotes prima che un moderatore la trasferisse in un commento sotto la mia domanda originale ....

La sua risposta:

I would recommend you to watch Jimmy Bogard's NDC 2012 session "Crafting Wicked Domain Models" on Vimeo. He explains what rich domain should be and how to implement them in real life by having behaviour in your entities. Examples are very practical and all in C#.

http://vimeo.com/43598193

Ho preso alcune note per riepilogare il video a beneficio di entrambi i miei team e per fornire dettagli più immediati in questo post. (Il video dura un'ora, ma vale davvero ogni minuto se hai tempo. Jimmy Bogard merita molto credito per la sua spiegazione.)

  • "Per la maggior parte delle applicazioni ... non sappiamo che saranno complesse quando iniziamo, ma diventano così".
    • La complessità cresce spontaneamente con l'aggiunta di codice e requisiti. Le applicazioni possono iniziare in modo molto semplice, come CRUD, ma il comportamento / le regole possono essere elaborate.
    • "La cosa bella è che non dobbiamo iniziare complessi: possiamo iniziare con il modello di dominio anemico, cioè con i sacchetti di proprietà, e con le sole tecniche di refactoring standard possiamo muoverci verso un vero modello di dominio."
  • Modelli di dominio = oggetti business. Comportamento del dominio = regole aziendali.
  • Il comportamento è spesso nascosto in un'applicazione - può essere in PageLoad, Button1_Click o spesso in classi helper come "FooManager" o "FooService".
  • Le regole aziendali separate dagli oggetti del dominio "ci impongono di ricordare" tali regole.
    • Nel mio esempio personale sopra, una regola aziendale è WorkItem.StatusHistory.Add (). Non stiamo solo cambiando lo stato, lo stiamo archiviando per il controllo.
  • I comportamenti del dominio "eliminano i bug in un'applicazione molto più facilmente rispetto alla semplice scrittura di una serie di test." I test richiedono che tu sappia scrivere questi test. I comportamenti del dominio ti offrono i percorsi giusti per testare .
  • I servizi di dominio sono "classi helper per coordinare le attività tra diverse entità del modello di dominio".
    • Servizi di dominio! = comportamento del dominio. Le entità hanno un comportamento, i servizi di dominio sono solo intermediari tra le entità.
  • Gli oggetti di dominio non dovrebbero avere il possesso dell'infrastruttura di cui hanno bisogno (cioè IOfferCalculatorService). Il servizio di infrastruttura deve essere passato al modello di dominio che lo utilizza.
  • I modelli di dominio dovrebbero offrire di dirti cosa possono fare e dovrebbero essere in grado di fare solo quelle cose.
  • Le proprietà dei modelli di dominio devono essere protette con setter privati, in modo che solo il modello possa impostare le sue proprietà, attraverso i propri comportamenti . Altrimenti è "promiscua".
  • Gli oggetti del modello di dominio anemico, che sono solo sacchetti di proprietà per un ORM, sono solo "un rivestimento sottile - una versione strongmente tipizzata sul database".
    • "Per quanto sia facile ottenere una riga di database in un oggetto, ecco cosa abbiamo."
    • "La maggior parte dei modelli di oggetti persistenti sono proprio questo. Ciò che differenzia un modello di dominio anemico rispetto a un'applicazione che in realtà non ha un comportamento, è se un oggetto ha regole aziendali, ma tali regole non si trovano in un modello di dominio. '
  • "Per molte applicazioni, non è necessario creare alcun tipo di vero e proprio livello logico applicativo aziendale, è solo qualcosa che può comunicare con il database e forse un modo semplice per rappresentare i dati che ci sono dentro."
    • Quindi, in altre parole, se tutto ciò che fai è CRUD senza particolari oggetti business o regole comportamentali, non hai bisogno di DDD.

Non esitate a commentare con altri punti che ritenete debbano essere inclusi, o se pensate che una di queste note non sia nel giusto. Ho cercato di citare direttamente o parafrasare il più possibile.

    
risposta data 13.12.2013 - 04:22
fonte
6

La tua domanda non può essere risolta, perché il tuo esempio è sbagliato. In particolare, perché non c'è alcun comportamento. Almeno non nell'area del tuo dominio. L'esempio del metodo AddStatusUpdate non è una logica di dominio, ma una logica che utilizza quel dominio. Questo tipo di logica ha senso essere all'interno di un tipo di servizio, che gestisce le richieste esterne.

Ad esempio, se esistesse il requisito che uno specifico elemento di lavoro possa avere solo stati specifici, o che possa avere solo stati N, allora questa è logica di dominio e dovrebbe essere parte di WorkItem o StatusHistory come metodo.

Il motivo della tua confusione è perché stai cercando di applicare una linea guida al codice che non ne ha bisogno. I modelli di dominio sono rilevanti solo se disponi di molte logiche di dominio complesse. Per esempio. logica che funziona sulle entità stesse e deriva dai requisiti. Se il codice riguarda la manipolazione di entità da dati esterni, allora non è, molto probabilmente, una logica di dominio. Ma nel momento in cui ottieni un sacco di if s in base a quali dati ed entità stai lavorando, allora questa è la logica del dominio.

Uno dei problemi della vera modellazione del dominio è che si tratta di gestire requisiti complessi. E come tale il suo vero potere e benefici non possono essere mostrati su un semplice codice. Hai bisogno di decine di entità con tonnellate di requisiti intorno a loro per vedere veramente i benefici. Anche in questo caso, il tuo esempio è troppo semplice per far risplendere veramente il modello di dominio.

Infine, una cosa OT che vorrei menzionare è che un vero modello di dominio con un vero design OOP sarebbe davvero difficile persistere usando Entity Framework. Mentre gli ORM sono stati progettati con la mappatura della vera struttura OOP a quelli relazionali, ci sono ancora molti problemi e il modello relazionale spesso colerà nel modello OOP. Anche con nibernate, che considero molto più potente di EF, questo può essere un problema.

    
risposta data 07.10.2013 - 08:13
fonte
5

La tua ipotesi che incapsulare la tua logica di business associata a WorkItem in un "fat service" è un anti-pattern intrinseco che, a mio avviso, non è necessariamente.

Indipendentemente dalle tue considerazioni sul modello di dominio anemico, i modelli e le pratiche standard tipici di un'applicazione Line of Business .NET incoraggiano un approccio a livelli transazionali composto da varie componenti. Incoraggiano la separazione della logica aziendale dal modello di dominio in modo specifico per facilitare la comunicazione di un modello di dominio comune su altri componenti .NET e componenti su stack di tecnologia diversi o su livelli fisici.

Un esempio potrebbe essere un servizio Web SOAP basato su .NET che comunica con un'applicazione client Silverlight che ha una DLL contenente tipi di dati semplici. Questo progetto di entità dominio potrebbe essere incorporato in un assembly .NET o in un assembly Silverlight, in cui i componenti di Silverlight interessati che dispongono di questa DLL non saranno esposti a comportamenti oggetto che potrebbero dipendere da componenti disponibili solo per il servizio.

Indipendentemente dalla tua posizione in merito a questo dibattito, questo è il modello adottato e accettato da Microsoft e secondo il mio parere professionale non è un approccio sbagliato, ma un modello a oggetti che definisce il proprio comportamento non è necessariamente un anti-modello o. Se prosegui con questo progetto, è meglio comprendere e comprendere alcune delle limitazioni e dei punti deboli che potresti incontrare se devi integrarti con altri componenti che devono vedere il tuo modello di dominio. In questo caso specifico, forse dovresti avere un traduttore che converta il tuo modello di dominio di stile orientato agli oggetti in oggetti dati semplici che non espongono determinati metodi di comportamento.

    
risposta data 07.10.2013 - 02:27
fonte
4

Mi rendo conto che questa domanda è piuttosto vecchia quindi questa risposta è per i posteri. Voglio rispondere con un esempio concreto invece di uno basato sulla teoria.

Incapsula la "modifica dello stato degli articoli di lavoro" nella classe WorkItem in questo modo:

public SomeStatusUpdateType Status { get; private set; }

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

Ora la tua classe WorkItem è responsabile di mantenere se stessa in uno stato legale. L'implementazione è piuttosto debole, comunque. Il proprietario del prodotto desidera una cronologia di tutti gli aggiornamenti di stato apportati a WorkItem .

Lo cambiamo in qualcosa del genere:

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

L'implementazione è cambiata drasticamente ma il chiamante del metodo ChangeStatus non è a conoscenza dei dettagli di implementazione sottostanti e non ha motivo di cambiare se stesso.

Questo è un esempio di entità di un modello di dominio ricco, IMHO.

    
risposta data 07.05.2018 - 18:45
fonte