DDD: la regola secondo cui le entità non possono accedere direttamente ai repository


185

In Domain Driven Design, sembra che ci sia un sacco di accordo che le entità non dovrebbe accedere ai repository direttamente.

Questo proviene dal libro di Eric Evans Domain Driven Design o da altre parti?

Dove ci sono alcune buone spiegazioni per il ragionamento alla base?

modifica: per chiarire: non sto parlando della classica pratica OO di separare l'accesso ai dati in un livello separato dalla logica aziendale - sto parlando della disposizione specifica in base alla quale in DDD, le entità non dovrebbero parlare con i dati livello di accesso (ovvero non devono contenere riferimenti a oggetti Repository)

aggiornamento: ho dato la grazia a BacceSR perché la sua risposta mi è sembrata più vicina, ma sono ancora abbastanza al buio per questo. Se si tratta di un principio così importante, ci dovrebbero essere alcuni buoni articoli a riguardo online da qualche parte, sicuramente?

aggiornamento: marzo 2013, i giudizi sulla domanda implicano che c'è molto interesse in questo, e anche se ci sono state molte risposte, penso ancora che ci sia spazio per altro se le persone hanno idee al riguardo.


Dai un'occhiata alla mia domanda stackoverflow.com/q/8269784/235715 , mostra una situazione in cui è difficile acquisire la logica, senza che Entity abbia accesso al repository. Anche se penso che le entità non dovrebbero avere accesso ai repository, e c'è una soluzione alla mia situazione in cui il codice può essere riscritto senza riferimento al repository, ma al momento non riesco a pensare a nessuno.
Alex Burtsev,

Non so da dove venga. I miei pensieri: penso che questo malinteso provenga da persone che non capiscono di cosa si tratta DDD. Questo approccio non è per l'implementazione del software ma per la sua progettazione (dominio ... progettazione). Ai tempi avevamo architetti e implementatori, ma ora ci sono solo sviluppatori di software. DDD è pensato per gli architetti. E quando un architetto sta progettando un software, ha bisogno di alcuni strumenti o schemi per rappresentare una memoria o un database per gli sviluppatori che implementeranno il progetto preparato. Ma il design stesso (dal punto di vista aziendale) non ha né necessita di un repository.
Berhalak,

Risposte:


47

C'è un po 'di confusione qui. I repository accedono alle radici aggregate. Le radici aggregate sono entità. La ragione di ciò è la separazione delle preoccupazioni e una buona stratificazione. Questo non ha senso su piccoli progetti, ma se fai parte di una grande squadra, vuoi dire: "Accedi a un prodotto attraverso il repository di prodotti. Il prodotto è una radice aggregata per una raccolta di entità, incluso l'oggetto ProductCatalog. Se si desidera aggiornare il ProductCatalog, è necessario passare al ProductRepository. "

In questo modo hai una separazione molto, molto chiara sulla logica aziendale e su dove le cose vengono aggiornate. Non hai un bambino che è fuori da solo e scrive l'intero programma che fa tutte queste cose complicate nel catalogo prodotti e quando si tratta di integrarlo nel progetto a monte, sei seduto lì a guardarlo e realizzarlo tutto deve essere abbandonato. Significa anche quando le persone si uniscono al team, aggiungono nuove funzionalità, sanno dove andare e come strutturare il programma.

Ma aspetta! Il repository si riferisce anche al livello di persistenza, come nel modello del repository. In un mondo migliore un Repository di Eric Evans e il Repository Pattern avrebbero nomi separati, perché tendono a sovrapporsi un po '. Per ottenere il modello di repository hai contrasto con altri modi in cui si accede ai dati, con un bus di servizio o un sistema modello di eventi. Di solito quando si arriva a questo livello, la definizione del repository di Eric Evans va di lato e si inizia a parlare di un contesto limitato. Ogni contesto limitato è essenzialmente la propria applicazione. Potresti avere un sofisticato sistema di approvazione per inserire le cose nel catalogo prodotti. Nel tuo progetto originale il prodotto era l'elemento centrale, ma in questo contesto limitato è il catalogo dei prodotti. Potresti comunque accedere alle informazioni sul prodotto e aggiornare il prodotto tramite un bus di servizio,

Torna alla tua domanda originale. Se si accede a un repository da un'entità, significa che l'entità non è in realtà un'entità aziendale ma probabilmente qualcosa che dovrebbe esistere in un livello di servizio. Questo perché le entità sono oggetti di business e dovrebbero preoccuparsi di essere il più possibile simili a un DSL (linguaggio specifico del dominio). Hai solo informazioni commerciali in questo livello. Se stai risolvendo un problema di prestazioni, saprai di cercare altrove poiché dovrebbero essere disponibili solo le informazioni aziendali. Se improvvisamente, hai problemi di applicazione qui, stai rendendo molto difficile estendere e mantenere un'applicazione, che è davvero il cuore di DDD: creare software gestibile.

Risposta al commento 1 : giusto, buona domanda. Quindi non tutta la convalida avviene nel livello di dominio. Sharp ha un attributo "DomainSignature" che fa quello che vuoi. È consapevole della persistenza, ma essendo un attributo mantiene pulito il livello del dominio. Assicura di non avere un'entità duplicata con, nel tuo esempio lo stesso nome.

Ma parliamo di regole di convalida più complicate. Diciamo che sei Amazon.com. Hai mai ordinato qualcosa con una carta di credito scaduta? Ho, dove non ho aggiornato la carta e comprato qualcosa. Accetta l'ordine e l'interfaccia utente mi informa che tutto è peachy. Circa 15 minuti dopo riceverò un'e-mail che dice che c'è un problema con il mio ordine, la mia carta di credito non è valida. Quello che sta succedendo qui è che, idealmente, c'è qualche convalida regex nel livello del dominio. È un numero di carta di credito corretto? Se sì, persisti l'ordine. Tuttavia, esiste un'ulteriore convalida a livello di attività dell'applicazione, in cui viene richiesto un servizio esterno per verificare se è possibile effettuare il pagamento sulla carta di credito. In caso contrario, non spedire nulla, sospendere l'ordine e attendere il cliente.

Non abbiate paura di creare oggetti di convalida a livello di servizio in grado di accedere ai repository. Tienilo fuori dal livello del dominio.


15
Grazie. Ma dovrei sforzarmi di ottenere quanta più logica di business possibile nelle entità (e nelle loro fabbriche e specifiche associate e così via), giusto? Ma se nessuno di loro è autorizzato a recuperare dati tramite i repository, come dovrei scrivere una logica aziendale (ragionevolmente complicata)? Ad esempio: all'utente Chatroom non è consentito cambiare il proprio nome con un nome già utilizzato da qualcun altro. Vorrei che quella regola fosse incorporata dall'entità ChatUser, ma non è molto facile da fare se non puoi colpire il repository da lì. Quindi cosa dovrei fare?
codeulike,

La mia risposta è stata più ampia di quella consentita dalla casella di commento, vedere la modifica.
kertosi,

6
L'entità dovrebbe sapere come proteggersi dai pericoli. Ciò include assicurarsi che non possa entrare in uno stato non valido. Quello che stai descrivendo con l'utente della chat room è una logica aziendale che risiede IN AGGIUNTA alla logica che l'entità deve mantenere valida. La logica aziendale come quella che desideri davvero appartiene a un servizio di Chatroom, non all'entità ChatUser.
Alec,

9
Grazie Alec. Questo è un modo chiaro di esprimerlo. Ma a me sembra che la regola d'oro incentrata sul dominio di Evans di "tutte le logiche aziendali dovrebbero andare nel livello del dominio" sia in conflitto con la regola delle "entità non dovrebbero accedere ai repository". Posso conviverci se capisco perché, ma non riesco a trovare una buona spiegazione online del perché le entità non debbano accedere ai repository. Evans non sembra menzionarlo esplicitamente. Da dove proviene? Se riesci a pubblicare una risposta indicando della buona letteratura, potresti essere in grado di procurarti una taglia 50pt
:)

4
"il suo non ha senso sul piccolo" Questo è un grosso errore che i team fanno ... è un piccolo progetto, in quanto tale posso farlo e quello ... smettere di pensare in quel modo. Molti dei piccoli progetti con cui lavoriamo finiscono per diventare grandi, a causa delle esigenze aziendali. Se fai qualcosa di avvizzito piccolo o grande, fallo bene.
MeTitus

35

All'inizio ero convinto a consentire ad alcune delle mie entità di accedere ai repository (ad es. Caricamento lento senza un ORM). Più tardi sono giunto alla conclusione che non avrei dovuto e che avrei potuto trovare modi alternativi:

  1. Dovremmo conoscere le nostre intenzioni in una richiesta e ciò che desideriamo dal dominio, quindi possiamo effettuare chiamate al repository prima di costruire o invocare il comportamento aggregato. Ciò consente inoltre di evitare il problema di uno stato in memoria incoerente e la necessità di un caricamento lento (vedere questo articolo ). L'odore è che non puoi più creare un'istanza in memoria della tua entità senza preoccuparti dell'accesso ai dati.
  2. CQS (Command Query Separation) può aiutare a ridurre la necessità di voler chiamare il repository per cose nelle nostre entità.
  3. Possiamo usare una specifica per incapsulare e comunicare le esigenze della logica di dominio e passarla al repository (un servizio può orchestrare queste cose per noi). Le specifiche possono provenire dall'entità incaricata di mantenere tale invariante. Il repository interpreterà parti della specifica nella propria implementazione della query e applicherà le regole della specifica sui risultati della query. Questo mira a mantenere la logica del dominio nel livello del dominio. Serve anche meglio la lingua Ubiquitous e la comunicazione. Immagina di dire "specifica ordine scaduto" invece di dire "ordine filtro da tbl_order dove Place_at è meno di 30 minuti prima del sysdate" (vedi questa risposta ).
  4. Rende più difficile il ragionamento sul comportamento delle entità poiché viene violato il principio della responsabilità singola. Se devi risolvere problemi di archiviazione / persistenza, sai dove andare e dove non andare.
  5. Evita il pericolo di dare a un'entità l'accesso bidirezionale allo stato globale (tramite il repository e i servizi di dominio). Inoltre, non si desidera infrangere il limite della transazione.

Vernon Vaughn nel libro rosso Implementing Domain-Driven Design fa riferimento a questo problema in due punti di cui sono a conoscenza (nota: questo libro è pienamente approvato da Evans come puoi leggere nella prefazione). Nel capitolo 7 sui servizi, utilizza un servizio di dominio e una specifica per ovviare alla necessità che un aggregato utilizzi un repository e un altro aggregato per determinare se un utente è autenticato. Ha citato come dicendo:

Come regola generale, dovremmo cercare di evitare l'uso dei repository (12) dall'interno degli aggregati, se possibile.

Vernon, Vaughn (06-02-2013). Implementazione del design guidato dal dominio (Kindle Location 6089). Pearson Education. Edizione Kindle.

E nel capitolo 10 sugli aggregati, nella sezione intitolata "Navigazione dei modelli" , afferma (subito dopo raccomanda l'uso di ID univoci globali per fare riferimento ad altre radici aggregate):

Il riferimento per identità non impedisce completamente la navigazione attraverso il modello. Alcuni useranno un repository (12) dall'interno di un aggregato per la ricerca. Questa tecnica si chiama Disconnected Domain Model ed è in realtà una forma di caricamento lento. Esiste tuttavia un diverso approccio raccomandato: utilizzare un repository o un servizio di dominio (7) per cercare oggetti dipendenti prima di richiamare il comportamento aggregato. Un servizio applicazione client può controllarlo, quindi spedire all'aggregato:

Egli mostra un esempio di questo nel codice:

public class ProductBacklogItemService ... { 

   ... 
   @Transactional 
   public void assignTeamMemberToTask( 
        String aTenantId, 
        String aBacklogItemId, 
        String aTaskId, 
        String aTeamMemberId) { 

        BacklogItem backlogItem = backlogItemRepository.backlogItemOfId( 
                                        new TenantId( aTenantId), 
                                        new BacklogItemId( aBacklogItemId)); 

        Team ofTeam = teamRepository.teamOfId( 
                                  backlogItem.tenantId(), 
                                  backlogItem.teamId());

        backlogItem.assignTeamMemberToTask( 
                  new TeamMemberId( aTeamMemberId), 
                  ofTeam,
                  new TaskId( aTaskId));
   } 
   ...
}     

Continua poi citando anche un'altra soluzione su come un servizio di dominio può essere utilizzato in un metodo di comando Aggregate insieme al doppio dispacciamento . (Non posso raccomandare abbastanza quanto sia utile leggere il suo libro. Dopo che ti sei stancato di frugare in internet senza fine, batti i soldi meritati e leggi il libro.)

Poi ho avuto qualche discussione con il sempre gentile Marco Pivetta @Ocramius che mi ha mostrato un po 'di codice per estrarre una specifica dal dominio e usarlo:

1) Non è raccomandato:

$user->mountFriends(); // <-- has a repository call inside that loads friends? 

2) In un servizio di dominio, questo è buono:

public function mountYourFriends(MountFriendsCommand $mount) { /* see http://store.steampowered.com/app/296470/ */ 
    $user = $this->users->get($mount->userId()); 
    $friends = $this->users->findBySpecification($user->getFriendsSpecification()); 
    array_map([$user, 'mount'], $friends); 
}

1
Domanda: ci viene sempre insegnato a non creare un oggetto in uno stato non valido o incoerente. Quando carichi utenti dal repository e poi chiami getFriends()prima di fare qualsiasi altra cosa, sarà vuoto o pigro caricato. Se vuoto, questo oggetto si trova e in uno stato non valido. Qualche idea su questo?
Jimbo,

Il repository chiama il dominio per rinnovare un'istanza. Non ottieni un'istanza di Utente senza passare attraverso il Dominio. Il problema risolto da questa risposta è il contrario. Dove il dominio fa riferimento al repository, e questo dovrebbe essere evitato.
prograhammer,

28

È un'ottima domanda. Non vedo l'ora di discuterne. Ma penso che sia menzionato in diversi libri DDD e Jimmy Nilssons ed Eric Evans. Immagino sia anche visibile attraverso esempi su come usare il modello di reposistory.

MA parliamo. Penso che un pensiero molto valido sia perché un'entità dovrebbe sapere come persistere un'altra entità? Importante con DDD è che ogni entità ha la responsabilità di gestire la propria "sfera di conoscenza" e non dovrebbe sapere nulla su come leggere o scrivere altre entità. Sicuramente puoi probabilmente aggiungere un'interfaccia di repository all'entità A per leggere le entità B. Ma il rischio è che tu esponga le conoscenze su come persistere B. L'entità A eseguirà anche la convalida su B prima di perseguire B in db?

Come puoi vedere, l'entità A può essere più coinvolta nel ciclo di vita dell'entità B e ciò può aggiungere più complessità al modello.

Immagino (senza alcun esempio) che i test unitari saranno più complessi.

Ma sono sicuro che ci saranno sempre scenari in cui sei tentato di utilizzare i repository tramite entità. Devi esaminare ogni scenario per dare un giudizio valido. Pro e contro. Ma la soluzione di entità repository secondo me inizia con molti contro. Deve essere uno scenario molto speciale con i professionisti che bilanciano i contro ...


1
Buon punto. Il modello di dominio della vecchia scuola avrebbe probabilmente l'Entità B responsabile della validazione di se stessa prima che si permetta di perseverare, immagino. Sei sicuro che Evans menzioni le entità che non usano i repository? Sono a metà del libro e non lo ha ancora menzionato ...
codeulike,

Bene, ho letto il libro diversi anni fa (bene 3 ...) e la mia memoria non mi riesce. Non riesco a ricordare se lo abbia definito esattamente MA comunque credo che l'abbia illustrato attraverso esempi. Puoi anche trovare un'interpretazione della comunità del suo esempio Cargo (dal suo libro) su dddsamplenet.codeplex.com . Scarica il progetto del codice (guarda il progetto Vanilla - è l'esempio del libro). Scoprirai che i repository vengono utilizzati solo nel livello Applicazione per accedere alle entità del dominio.
Magnus Backeus,

1
Scaricando l'esempio DDD SmartCA dal libro p2p.wrox.com/… vedrai un altro approccio (anche se si tratta di un client Windows RIA) in cui i repository vengono utilizzati nei servizi (niente di strano qui) ma i servizi vengono utilizzati all'interno delle entità. Questo è qualcosa che non farei MA sono un tipo di app webb. Dato lo scenario per l'app SmartCA in cui devi essere in grado di lavorare offline, forse il design di ddd avrà un aspetto diverso.
Magnus Backeus,

L'esempio di SmartCA sembra interessante, in quale capitolo si trova? (i download del codice sono organizzati per capitolo)
codeulike,

1
@codeulike Attualmente sto progettando e implementando un framework usando concetti ddd. A volte per eseguire la convalida è necessario accedere al database e interrogarlo (esempio: query per controllo indice univoco su più colonne) .Per quanto riguarda questo e il fatto che le query devono essere scritte nel livello repository Viene fuori che le entità del dominio devono avere riferimenti a le loro interfacce di repository nel livello del modello di dominio al fine di inserire completamente la convalida nel livello del modello di dominio. Quindi è finalmente ok per le entità di dominio avere accesso ai repository?
Karamafrooz,

13

Perché separare l'accesso ai dati?

Dal libro, penso che le prime due pagine del capitolo Model Driven Design forniscano una giustificazione per il motivo per cui si desidera estrarre i dettagli tecnici di implementazione dall'implementazione del modello di dominio.

  • Vuoi mantenere una stretta connessione tra il modello di dominio e il codice
  • Separare le preoccupazioni tecniche aiuta a dimostrare che il modello è pratico per l'implementazione
  • Volete che il linguaggio onnipresente permea fino alla progettazione del sistema

Questo sembra essere tutto allo scopo di evitare un "modello di analisi" separato che viene divorziato dall'attuazione effettiva del sistema.

Da quanto ho capito del libro, si dice che questo "modello di analisi" può finire per essere progettato senza considerare l'implementazione del software. Una volta che gli sviluppatori cercano di implementare il modello compreso dal lato aziendale, formano le proprie astrazioni per necessità, causando un muro nella comunicazione e nella comprensione.

Nella direzione opposta, anche gli sviluppatori che introducono troppe preoccupazioni tecniche nel modello di dominio possono causare questa divisione.

Quindi potresti considerare che praticare la separazione di preoccupazioni come la persistenza può aiutare a salvaguardare da questi progetti e modelli di analisi divergenti. Se sembra necessario introdurre elementi come la persistenza nel modello, allora è una bandiera rossa. Forse il modello non è pratico per l'implementazione.

citando:

"Il singolo modello riduce le possibilità di errore, perché il design è ora una crescita diretta del modello attentamente considerato. Il design, e persino il codice stesso, ha la comunicatività di un modello."

Il modo in cui lo sto interpretando, se hai finito con più righe di codice che trattano cose come l'accesso al database, perdi quella comunicatività.

Se la necessità di accedere a un database riguarda cose come il controllo dell'unicità, dai un'occhiata a:

Udi Dahan: i più grandi errori che i team commettono quando applicano DDD

http://gojko.net/2010/06/11/udi-dahan-the-biggest-mistakes-teams-make-when-applying-ddd/

in "Tutte le regole non sono create uguali"

e

Impiegando il modello del modello di dominio

http://msdn.microsoft.com/en-us/magazine/ee236415.aspx#id0400119

in "Scenari per non utilizzare il modello di dominio", che tocca lo stesso argomento.

Come separare l'accesso ai dati

Caricamento dei dati tramite un'interfaccia

Il "livello di accesso ai dati" è stato estratto attraverso un'interfaccia, che si chiama per recuperare i dati richiesti:

var orderLines = OrderRepository.GetOrderLines(orderId);

foreach (var line in orderLines)
{
     total += line.Price;
}

Pro: L'interfaccia separa il codice idraulico di "accesso ai dati", permettendoti di scrivere ancora dei test. L'accesso ai dati può essere gestito caso per caso consentendo prestazioni migliori rispetto a una strategia generica.

Contro: il codice chiamante deve assumere ciò che è stato caricato e cosa no.

Supponiamo che GetOrderLines restituisca oggetti OrderLine con una proprietà ProductInfo null per motivi di prestazioni. Lo sviluppatore deve avere una profonda conoscenza del codice dietro l'interfaccia.

Ho provato questo metodo su sistemi reali. Si finisce per cambiare l'ambito di ciò che viene caricato continuamente nel tentativo di risolvere i problemi di prestazioni. Finisci per sbirciare dietro l'interfaccia per guardare il codice di accesso ai dati per vedere cosa è e non viene caricato.

Ora, la separazione delle preoccupazioni dovrebbe consentire allo sviluppatore di concentrarsi su un aspetto del codice alla volta, per quanto possibile. La tecnica di interfaccia rimuove il COME vengono caricati questi dati, ma NON COME SONO caricati MOLTI dati, QUANDO vengono caricati e DOVE vengono caricati.

Conclusione: separazione abbastanza bassa!

Caricamento pigro

I dati vengono caricati su richiesta. Le chiamate per caricare i dati sono nascoste nel grafico dell'oggetto stesso, dove l'accesso a una proprietà può causare l'esecuzione di una query sql prima di restituire il risultato.

foreach (var line in order.OrderLines)
{
    total += line.Price;
}

Pro: "QUANDO, DOVE, e COME" dell'accesso ai dati è nascosto allo sviluppatore e si concentra sulla logica del dominio. Nell'aggregato non esiste alcun codice che si occupa del caricamento dei dati. La quantità di dati caricati può essere la quantità esatta richiesta dal codice.

Contro: quando si è colpiti da un problema di prestazioni, è difficile risolvere una soluzione generica "taglia unica". Il caricamento lento può causare prestazioni complessivamente peggiori e l'implementazione del caricamento lento può essere complicata.

Interfaccia ruolo / Recupero desideroso

Ogni caso d'uso viene reso esplicito tramite un'interfaccia ruolo implementata dalla classe aggregata, che consente di gestire le strategie di caricamento dei dati per caso d'uso.

La strategia di recupero può essere simile al seguente:

public class BillOrderFetchingStrategy : ILoadDataFor<IBillOrder, Order>
{
    Order Load(string aggregateId)
    {
        var order = new Order();

        order.Data = GetOrderLinesWithPrice(aggregateId);
    
        return order;
    }

}
   

Quindi il tuo aggregato può apparire come:

public class Order : IBillOrder
{
    void BillOrder(BillOrderCommand command)
    {
        foreach (var line in this.Data.OrderLines)
        {
            total += line.Price;
        }

        etc...
    }
}

BillOrderFetchingStrategy viene utilizzato per creare l'aggregato, quindi l'aggregato fa il suo lavoro.

Pro: consente il codice personalizzato per caso d'uso, consentendo prestazioni ottimali. È in linea con il principio di segregazione dell'interfaccia . Nessun requisito di codice complesso. I test unitari aggregati non devono imitare la strategia di caricamento. La strategia di caricamento generica può essere utilizzata per la maggior parte dei casi (ad esempio una strategia "carica tutto") e, se necessario, possono essere implementate strategie di caricamento speciali.

Contro: lo sviluppatore deve ancora modificare / rivedere la strategia di recupero dopo aver modificato il codice di dominio.

Con l'approccio della strategia di recupero potresti ancora ritrovarti a modificare il codice di recupero personalizzato per un cambiamento nelle regole aziendali. Non è una separazione perfetta delle preoccupazioni, ma finirà per essere più mantenibile ed è migliore della prima opzione. La strategia di recupero incapsula i dati HOW, WHEN e WHERE caricati. Ha una migliore separazione delle preoccupazioni, senza perdere la flessibilità come l'unica soluzione adatta a tutti i metodi di caricamento lento.


Grazie, controllerò i link. Ma nella tua risposta stai confondendo la "separazione delle preoccupazioni" con "nessun accesso ad essa"? Certamente la maggior parte delle persone concorderebbe sul fatto che il livello di persistenza dovrebbe essere tenuto separato dal livello in cui si trovano le Entità. Ma questo è diverso dal dire 'le entità non dovrebbero essere in grado di vedere nemmeno il livello di persistenza, anche attraverso un agnostico di implementazione molto generale interfaccia'.
codeulike,

Caricando i dati attraverso un'interfaccia o no, ti preoccupi ancora del caricamento dei dati durante l'implementazione delle regole aziendali. Concordo sul fatto che molte persone chiamano ancora questa separazione delle preoccupazioni, forse il principio della singola responsabilità sarebbe stato un termine migliore da usare.
ttg

1
Non sei sicuro di come analizzare il tuo ultimo commento, ma penso che stai suggerendo che i dati non debbano essere caricati durante l'elaborazione delle regole aziendali? Vedo che renderebbe le regole più "pure". Ma molti tipi di regole aziendali dovranno fare riferimento ad altri dati - stai suggerendo che dovrebbero essere caricati in anticipo da un oggetto separato?
codeulike

@codeulike: ho aggiornato la mia risposta. Puoi comunque caricare i dati durante le regole aziendali se ritieni di doverlo assolutamente fare, ma ciò non richiede l'aggiunta di righe di codice di accesso ai dati nel tuo modello di dominio (ad es. Caricamento lento). Nei modelli di dominio che ho progettato, i dati vengono generalmente caricati in anticipo come hai detto. Ho scoperto che l'esecuzione delle regole aziendali di solito non richiede una quantità eccessiva di dati.
ttg


12

Che domanda eccellente. Sono sullo stesso percorso di scoperta e la maggior parte delle risposte su Internet sembrano portare tanti problemi quanti ne portano soluzioni.

Quindi (a rischio di scrivere qualcosa con cui non sono d'accordo tra un anno) ecco le mie scoperte finora.

Prima di tutto, ci piace un modello di dominio avanzato , che ci offre alta rilevabilità (di ciò che possiamo fare con un aggregato) e leggibilità (chiamate di metodo espressive).

// Entity
public class Invoice
{
    ...
    public void SetStatus(StatusCode statusCode, DateTime dateTime) { ... }
    public void CreateCreditNote(decimal amount) { ... }
    ...
}

Vogliamo raggiungere questo obiettivo senza iniettare alcun servizio nel costruttore di un'entità, perché:

  • L'introduzione di un nuovo comportamento (che utilizza un nuovo servizio) potrebbe portare a una modifica del costruttore, il che significa che la modifica influisce su ogni linea che crea un'istanza !
  • Questi servizi non fanno parte del modello , ma l'iniezione del costruttore suggerirebbe che lo fossero.
  • Spesso un servizio (anche la sua interfaccia) è un dettaglio di implementazione piuttosto che parte del dominio. Il modello di dominio avrebbe una dipendenza rivolta verso l' esterno .
  • Può confondere il motivo per cui l'entità non può esistere senza queste dipendenze. (Un servizio di note di credito, dite? Non ho intenzione di fare nulla con le note di credito ...)
  • Lo renderebbe un'istanza difficile, quindi difficile da testare .
  • Il problema si diffonde facilmente, perché altre entità che contengono questo otterrebbero le stesse dipendenze, che su di esse potrebbero apparire come dipendenze molto innaturali .

Come possiamo fare questo? La mia conclusione finora è che le dipendenze del metodo e il doppio dispacciamento forniscono una soluzione decente.

public class Invoice
{
    ...

    // Simple method injection
    public void SetStatus(IInvoiceLogger logger, StatusCode statusCode, DateTime dateTime)
    { ... }

    // Double dispatch
    public void CreateCreditNote(ICreditNoteService creditNoteService, decimal amount)
    {
        creditNoteService.CreateCreditNote(this, amount);
    }

    ...
}

CreateCreditNote()ora richiede un servizio responsabile della creazione delle note di credito. Esso utilizza doppia spedizione , completamente scaricando il lavoro al servizio responsabile, pur mantenendo la reperibilità dalla Invoiceentità.

SetStatus()ora ha una semplice dipendenza da un logger, che ovviamente eseguirà parte del lavoro .

Per quest'ultimo, per semplificare le cose sul codice client, potremmo invece accedere a un IInvoiceService. Dopotutto, la registrazione delle fatture sembra piuttosto intrinseca a una fattura. Tale singolo IInvoiceServiceaiuta a evitare la necessità di ogni sorta di mini-servizi per varie operazioni. Il rovescio della medaglia è che diventa oscurare ciò che esattamente questo servizio sarà fare . Potrebbe anche iniziare a sembrare un doppio dispaccio, mentre la maggior parte del lavoro è davvero ancora fatta da SetStatus()sola.

Potremmo ancora nominare il parametro 'logger', nella speranza di rivelare il nostro intento. Sembra un po 'debole, però.

Invece, opterei per chiedere un IInvoiceLogger(come già facciamo nell'esempio di codice) e IInvoiceServiceimplementare tale interfaccia. Il codice client può semplicemente usare il suo unico IInvoiceServiceper tutti i Invoicemetodi che richiedono un "mini-servizio" molto particolare, intrinseco alla fattura, mentre le firme dei metodi rendono ancora chiaramente chiaro ciò che stanno chiedendo.

Noto che non ho indirizzato esplicitamente i repository . Bene, il logger è o usa un repository, ma lasciatemi anche fornire un esempio più esplicito. Possiamo usare lo stesso approccio, se il repository è necessario solo in uno o due metodi.

public class Invoice
{
    public IEnumerable<CreditNote> GetCreditNotes(ICreditNoteRepository repository)
    { ... }
}

In realtà, questo fornisce un'alternativa ai carichi pigri sempre fastidiosi .

Aggiornamento: ho lasciato il testo qui sotto per scopi storici, ma suggerisco di evitare carichi pigri al 100%.

Per i veri, di proprietà a base di carichi pigri, io faccio attualmente uso di iniezione del costruttore, ma in un modo persistence-ignoranti.

public class Invoice
{
    // Lazy could use an interface (for contravariance if nothing else), but I digress
    public Lazy<IEnumerable<CreditNote>> CreditNotes { get; }

    // Give me something that will provide my credit notes
    public Invoice(Func<Invoice, IEnumerable<CreditNote>> lazyCreditNotes)
    {
        this.CreditNotes = new Lazy<IEnumerable<CreditNotes>>() => lazyCreditNotes(this));
    }
}

Da un lato, un repository che carica un Invoicedal database può avere libero accesso a una funzione che caricherà le corrispondenti note di credito e inserirà quella funzione nel file Invoice.

D'altra parte, il codice che crea un nuovo effettivo Invoicepasserà semplicemente una funzione che restituisce un elenco vuoto:

new Invoice(inv => new List<CreditNote>() as IEnumerable<CreditNote>)

(Un'usanza ILazy<out T>potrebbe liberarci del brutto cast IEnumerable, ma ciò complicherebbe la discussione.)

// Or just an empty IEnumerable
new Invoice(inv => IEnumerable.Empty<CreditNote>())

Sarei felice di ascoltare le tue opinioni, preferenze e miglioramenti!


3

Per me questa sembra essere una buona pratica generale relativa a OOD piuttosto che essere specifica per DDD.

I motivi che mi vengono in mente sono:

  • Separazione delle preoccupazioni (le entità dovrebbero essere separate dal modo in cui sono persistenti. In quanto potrebbero esserci più strategie in cui la stessa entità verrebbe mantenuta a seconda dello scenario di utilizzo)
  • Logicamente, le entità potrebbero essere viste in un livello inferiore al livello in cui operano i repository. I componenti di livello inferiore non dovrebbero avere conoscenze sui componenti di livello superiore. Pertanto le voci non dovrebbero avere conoscenza dei repository.

2

semplicemente Vernon Vaughn offre una soluzione:

Utilizzare un repository o un servizio di dominio per cercare oggetti dipendenti prima di richiamare il comportamento aggregato. Un servizio di applicazione client può controllare questo.


Ma non da un'entità.
fabbro,

Dalla fonte IDDD di Vernon Vaughn: il calendario della classe pubblica estende EventSourcedRootEntity {... calendario pubblicoEntry scheduleCalendarEntry (CalendarIdentityService aCalendarIdentityService,
Teimuraz

controlla il suo documento @Teimuraz
Alireza Rahmani Khalili il

1

Ho imparato a programmare la programmazione orientata agli oggetti prima che appaia tutto questo buzz di livello separato e i miei primi oggetti / classi DID mappano direttamente al database.

Alla fine, ho aggiunto un livello intermedio perché dovevo migrare su un altro server di database. Ho visto / sentito più volte lo stesso scenario.

Penso che separare l'accesso ai dati (alias "Repository") dalla tua logica aziendale, sia una di quelle cose, che sono state reinventate più volte, mentre il libro Domain Driven Design lo rende molto "rumoroso".

Attualmente uso 3 livelli (GUI, Logica, Accesso ai dati), come fanno molti sviluppatori, perché è una buona tecnica.

Separare i dati, in un Repositorylivello (aka Data Accesslivello), può essere visto come una buona tecnica di programmazione, non solo una regola, da seguire.

Come molte metodologie, potresti voler avviare, NON implementato, ed eventualmente aggiornare il tuo programma, una volta comprese.

Citazione: L'Iliade non è stata completamente inventata da Homer, Carmina Burana non è stata completamente inventata da Carl Orff, e in entrambi i casi, la persona che ha messo gli altri al lavoro, tutti insieme, ha ottenuto il merito ;-)


1
Grazie, ma non sto chiedendo di separare l'accesso ai dati dalla logica aziendale - è una cosa molto chiara su cui vi è un accordo molto ampio. Mi sto chiedendo perché nelle architetture DDD come S # arp, alle Entità non sia permesso nemmeno di "parlare" con il livello di accesso ai dati. È un accordo interessante di cui non sono stato in grado di trovare molte discussioni.
codeulike,

0

Questo proviene dal libro di Eric Evans Domain Driven Design o da altre parti?

È roba vecchia. Il libro di Eric ha appena fatto un altro ronzio.

Dove ci sono alcune buone spiegazioni per il ragionamento alla base?

La ragione è semplice: la mente umana si indebolisce quando affronta contesti multipli vagamente correlati. Conducono all'ambiguità (America nel Sud / Nord America significa Sud / Nord America), l'ambiguità porta a una mappatura costante delle informazioni ogni volta che la mente "le tocca" e ciò si riassume in una cattiva produttività ed errori.

La logica aziendale dovrebbe essere riflessa il più chiaramente possibile. Le chiavi esterne, la normalizzazione, la mappatura relazionale degli oggetti provengono da domini completamente diversi: quelle cose sono tecniche, legate al computer.

In analogia: se stai imparando a scrivere a mano, non dovresti essere gravato dalla comprensione di dove è stata fatta la penna, perché l'inchiostro si attacca alla carta, quando la carta è stata inventata e quali altre famose invenzioni cinesi.

modifica: per chiarire: non sto parlando della classica pratica OO di separare l'accesso ai dati in un livello separato dalla logica aziendale - sto parlando della disposizione specifica in base alla quale in DDD, le entità non dovrebbero parlare con i dati livello di accesso (ovvero non devono contenere riferimenti a oggetti Repository)

La ragione è sempre la stessa che ho menzionato sopra. Qui è solo un passo avanti. Perché le entità dovrebbero essere parzialmente ignoranti dalla persistenza se possono essere (almeno vicine a) totalmente? Meno preoccupazioni non correlate al dominio del nostro modello - più respiro che la nostra mente ottiene quando deve reinterpretarlo.


Destra. Quindi, come può un'entità ignorante totalmente perseverante implementare la logica di business se non è nemmeno autorizzata a parlare con il livello di persistenza? Cosa fa quando deve guardare i valori in altre entità arbitrarie?
codeulike,

Se la tua entità deve esaminare i valori in altre entità arbitrarie, probabilmente hai alcuni problemi di progettazione. Forse considera di rompere le classi in modo che siano più coerenti.
cdaq,

0

Per citare Carolina Lilientahl, "I pattern dovrebbero prevenire i cicli" https://www.youtube.com/watch?v=eJjadzMRQAk , dove si riferisce alle dipendenze cicliche tra le classi. Nel caso di repository all'interno di aggregati, c'è la tentazione di creare dipendenze cicliche per accordo con la navigazione degli oggetti come unica ragione. Il modello sopra menzionato da prograhammer, che è stato raccomandato da Vernon Vaughn, in cui altri aggregati sono referenziati da id anziché da istanze di root, (esiste un nome per questo modello?) Suggerisce un'alternativa che potrebbe guidare in altre soluzioni.

Esempio di dipendenza ciclica tra classi (confessione):

(Time0): due classi, Sample e Well, si riferiscono l'una all'altra (dipendenza ciclica). Well fa riferimento a Sample e Sample fa riferimento a Well, per comodità (a volte eseguendo il loop dei campioni, a volte eseguendo il loop di tutti i pozzetti in una piastra). Non riuscivo a immaginare casi in cui il campione non si riferisse al pozzo in cui è posizionato.

(Time1): un anno dopo, vengono implementati molti casi d'uso .... e ora ci sono casi in cui il campione non deve fare riferimento al pozzo in cui è inserito. Ci sono piastre temporanee all'interno di una fase di lavoro. Qui un pozzo si riferisce a un campione, che a sua volta si riferisce a un pozzo su un'altra piastra. Per questo motivo, a volte si verificano comportamenti strani quando qualcuno cerca di implementare nuove funzionalità. Ci vuole tempo per penetrare.

Sono stato anche aiutato da questo articolo di cui sopra sugli aspetti negativi del caricamento lento.


-1

Nel mondo ideale, DDD propone che le Entità non debbano fare riferimento ai livelli di dati. ma non viviamo nel mondo ideale. I domini potrebbero dover fare riferimento ad altri oggetti di dominio per la logica aziendale con la quale potrebbero non avere una dipendenza. È logico che le entità facciano riferimento al livello del repository a scopo di sola lettura per recuperare i valori.


No, ciò introduce un inutile accoppiamento alle entità, viola SRP e Separation of Preoccups e rende difficile deserializzare l'entità dalla persistenza (poiché il processo di deserializzazione deve ora iniettare anche servizi / depositi che l'entità frequenta).
fabbro,
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.