L'isolamento del modello dominio / persistenza di solito è così imbarazzante?

9

Mi sto immergendo nei concetti di Domain-Driven Design (DDD) e ho trovato alcuni principi strani, soprattutto per quanto riguarda l'isolamento del modello di dominio e persistenza. Ecco la mia comprensione di base:

  1. Un servizio sul livello applicazione (che fornisce un set di funzionalità) richiede gli oggetti del dominio da un repository di cui ha bisogno per svolgere la sua funzione.
  2. L'implementazione concreta di questo repository recupera i dati dall'archiviazione implementata per
  3. Il servizio indica all'oggetto dominio, che incapsula la logica aziendale, di eseguire determinati compiti che ne modificano lo stato.
  4. Il servizio indica al repository di mantenere l'oggetto dominio modificato.
  5. Il repository deve mappare l'oggetto dominio alla corrispondente rappresentazione in archivio.

Ora,dateleipotesidicuisopra,ilseguentesembrastrano:

Annuncio2.:

Ilmodellodidominiosembracaricarel'interooggettodeldominio(compresituttiicampieiriferimenti),anchesenonsononecessariperlafunzionecheloharichiesto.Ilcaricamentocompletopotrebbenonesserenemmenopossibilesesifariferimentoadaltrioggettididominio,amenochenonsicarichianchequestioggettidominioetuttiglioggettiacuifannoriferimentoaturno,ecosìviaecosìvia.Vieneinmenteuncaricamentolento,ilchesignificatuttaviacheiniziaeseguirequerysuglioggettideltuodominiochedovrebberoesserelaresponsabilitàdelrepositoryinprimoluogo.

Datoquestoproblema,ilmodo"corretto" di caricare oggetti di dominio sembra avere una funzione di caricamento dedicata per ogni caso d'uso. Queste funzioni dedicate caricheranno quindi solo i dati richiesti dal caso d'uso per il quale sono stati progettati. Qui è dove entra in gioco l'imbarazzo: in primo luogo, dovrei mantenere una notevole quantità di funzioni di caricamento per ogni implementazione del repository, e gli oggetti di dominio finirebbero in stati incompleti che trasportano null nei loro campi. Quest'ultimo dovrebbe tecnicamente non essere un problema perché se un valore non è stato caricato, non dovrebbe essere richiesto dalla funzionalità che lo richiedeva comunque. Eppure è imbarazzante e potenzialmente pericoloso.

Annuncio 3.:

In che modo un oggetto di dominio verifica i vincoli di unicità alla costruzione se non ha alcuna nozione del repository? Ad esempio, se volessi creare un nuovo User con un numero di previdenza sociale univoco (che è dato), il conflitto più antico si verificherebbe chiedendo al repository di salvare l'oggetto, solo se è stato definito un vincolo di univocità sul database . Altrimenti, potrei cercare un User con la sicurezza sociale fornita e segnalare un errore nel caso esista, prima di crearne uno nuovo. Ma poi i controlli dei vincoli sarebbero presenti nel servizio e non nell'oggetto di dominio a cui appartengono. Mi sono appena reso conto che gli oggetti di dominio sono molto ben autorizzati a utilizzare repository (iniettati) per la convalida.

Annuncio 5.:

Percepisco la mappatura degli oggetti di dominio su un back-end di storage come un processo ad alta intensità di lavoro rispetto a quando gli oggetti di dominio modificano direttamente i dati di underlaying. È, ovviamente, un prerequisito essenziale per disaccoppiare l'implementazione dello storage concreto dal codice di dominio. Tuttavia, ha davvero un costo così alto?

A quanto pare hai la possibilità di usare gli strumenti ORM per fare la mappatura per te. Ciò richiederebbe spesso di progettare il modello di dominio in base alle restrizioni dell'ORM, tuttavia, o addirittura di introdurre una dipendenza dal dominio al livello infrastruttura (utilizzando, ad esempio, le annotazioni ORM negli oggetti dominio). Inoltre ho letto che gli ORM introducono un considerevole overhead computazionale.

Nel caso di database NoSQL, per i quali non esistono praticamente concetti simili a ORM, come tenere traccia delle proprietà modificate nei modelli di dominio su save() ?

Modifica : Inoltre, affinché un repository possa accedere allo stato dell'oggetto dominio (cioè il valore di ogni campo), l'oggetto dominio deve rivelare il suo stato interno che interrompe l'incapsulamento.

In generale:

  • Dove andrebbe la logica transazionale? Questa è certamente persistenza specifico. Alcune infrastrutture di storage potrebbero non supportare nemmeno transazioni (come i depositi fittizi in memoria).
  • Per le operazioni di massa che modificano più oggetti, dovrei caricare, modificare e archiviare ogni oggetto singolarmente per passare attraverso la logica di validazione incapsulata dell'oggetto? Questo è contrario all'esecuzione di una singola query direttamente sul database.

Gradirei qualche chiarimento su questo argomento. Le mie supposizioni sono corrette? In caso contrario, qual è il modo corretto di affrontare questi problemi?

    
posta Double M 14.10.2018 - 17:19
fonte

2 risposte

4

La tua comprensione di base è corretta e l'architettura che disegna è buona e funziona bene.

Leggendo tra le righe sembra che tu stia venendo da uno stile di programmazione più attivo e incentrato su database? Per ottenere un'implementazione funzionante, direi che è necessario

1: Gli oggetti del dominio non devono includere l'intero grafico dell'oggetto. Ad esempio potrei avere:

public class Customer
{
    public string AddressId {get;set;}
    public string Name {get;set;}
}

public class Address
{
    public string Id {get;set;}
    public string HouseNumber {get;set;
}

Indirizzo e Cliente devono essere solo parte dello stesso aggregato se si dispone di una logica come "il nome del cliente può iniziare solo con la stessa lettera del nome della casa". Hai ragione ad evitare il caricamento pigro e le versioni "Lite" degli oggetti.

2: I vincoli di unicità generalmente sono la competenza del repository e non dell'oggetto dominio. Non iniettare repository in Oggetti Dominio, si tratta di tornare al record attivo, semplicemente errore quando il servizio tenta di salvare.

La regola aziendale non è "Non possono esistere due istanze di Utente con lo stesso numero di SocialSecurity nello stesso momento"

È che non possono esistere nello stesso repository.

3: Non è difficile scrivere repository piuttosto che singoli metodi di aggiornamento delle proprietà. In effetti, scoprirai che hai praticamente lo stesso codice in ogni caso. È proprio in che classe lo metti.

Gli ORM in questi giorni sono facili e non hanno vincoli extra sul tuo codice. Detto questo, personalmente preferisco semplicemente sfornare l'SQL. Non è così difficile, non hai mai riscontrato problemi con le funzionalità di ORM e puoi ottimizzare dove richiesto.

Non è davvero necessario tenere traccia delle proprietà modificate al momento del salvataggio. Mantieni i tuoi oggetti Dominio piccoli e semplicemente sovrascrivi la vecchia versione.

Domande generali

  1. La logica delle transazioni va nel repository. Ma non dovresti avere molto se nessuno di questi. Certo ne hai bisogno se hai delle tabelle figlio in cui stai mettendo gli oggetti figlio dell'aggregato, ma questo sarà interamente incapsulato all'interno del metodo del repository SaveMyObject.

  2. Aggiornamenti collettivi. Sì, dovresti modificare singolarmente ciascun oggetto, quindi aggiungere semplicemente un metodo SaveMyObjects (Elenco oggetti) al tuo repository, per effettuare l'aggiornamento collettivo.

    Si desidera che l'oggetto dominio o il servizio dominio contengano la logica. Non il database. Ciò significa che non puoi semplicemente "aggiornare il nome del set cliente = x dove y", perché per quanto ne sai l'oggetto Cliente, o CustomerUpdateService fa altre 20 strane cose quando cambi il nome.

risposta data 14.10.2018 - 19:24
fonte
1

Risposta breve: la tua comprensione è corretta e le domande che hai posto indicano problemi validi per i quali le soluzioni non sono dirette né accettate universalmente.

Punto 2.: (caricamento di grafici a oggetti completi)

Non sono il primo a sottolineare che gli ORM non sono sempre una buona soluzione. Il problema principale è che gli ORM non sanno nulla del caso d'uso effettivo, quindi non hanno idea di cosa caricare o come ottimizzare. Questo è un problema.

Come hai detto, la soluzione ovvia è di avere metodi di persistenza per ogni caso d'uso. Ma se usi ancora un ORM per questo, l'ORM ti costringerà a impacchettare tutto in oggetti dati. Che oltre a non essere realmente orientato agli oggetti, non è ancora il miglior design per alcuni casi d'uso.

Che cosa succede se voglio aggiornare in blocco alcuni record? Perché avrei bisogno di una rappresentazione di oggetti per tutti i record? Ecc.

Quindi la soluzione a questo è semplicemente non usare un ORM per casi d'uso per i quali non è una buona scelta. Implementa un caso d'uso "naturalmente" così com'è, che a volte non richiede un'ulteriore "astrazione" dei dati stessi (dati-oggetti) né un'astrazione sulle "tabelle" (repository).

Avere gli oggetti dati riempiti a metà o sostituire i riferimenti agli oggetti con gli "id" sono soluzioni ottimali, non buoni, come hai sottolineato.

Punto 3.: (controllo dei vincoli)

Se la persistenza non è astratta, ogni caso d'uso può ovviamente controllare qualsiasi vincolo che vuole facilmente. Il requisito che gli oggetti non conoscano il "repository" è completamente artificiale e non un problema della tecnologia.

Punto 5.. (ORM)

It is, of course, an essential prerequisite to decouple the concrete storage implementation from the domain code. However, does it indeed come at such a high cost?

No, non è così. Ci sono molti altri modi per avere persistenza. Il problema è che l'ORM è visto come "la" soluzione da usare, sempre (almeno per i database relazionali). Cercare di suggerire di non usarlo per alcuni casi d'uso in un progetto è futile e a seconda dell'Orm stesso a volte anche impossibile, dal momento che le cache e l'esecuzione tardiva sono talvolta utilizzate da questi strumenti.

Domanda generale 1.: (transazioni)

Non penso che esista un'unica soluzione. Se il tuo progetto è orientato agli oggetti, ci sarà un metodo "top" per ogni caso d'uso. La transazione dovrebbe essere lì.

Qualsiasi altra restrizione è completamente artificiale.

Domanda generale 2.: (operazioni collettive)

Con un ORM, sei (per la maggior parte degli ORM che conosco) costretto a passare attraverso singoli oggetti. Questo non è assolutamente necessario e probabilmente non sarebbe il tuo progetto se la tua mano non fosse vincolata dall'ORM.

Il requisito per separare la "logica" da SQL proviene dagli ORM. Loro hanno per dirlo, perché non possono supportarlo. Non è intrinsecamente "cattivo".

Riepilogo

Credo che il mio punto sia che gli ORM non sono sempre lo strumento migliore per un progetto, e anche se lo fosse, è altamente improbabile che sia il migliore per tutti i casi d'uso in un progetto.

Allo stesso modo, l'astrazione dataware-repository di DDD non è sempre la migliore. Andrei anche così lontano da dire, sono raramente il design ottimale.

Questo ci lascia senza una soluzione valida per tutti, quindi dovremmo pensare individualmente alle soluzioni per ogni caso d'uso, che non è una buona notizia e ovviamente rende più difficile il nostro lavoro:)

    
risposta data 16.10.2018 - 05:23
fonte

Leggi altre domande sui tag