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


12

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

  1. Un servizio a livello di applicazione (che fornisce un set di funzionalità) richiede oggetti di dominio da un repository necessario per svolgere la sua funzione.
  2. L'implementazione concreta di questo repository recupera i dati dall'archivio per il quale è stata implementata
  3. Il servizio dice all'oggetto dominio, che incapsula la logica aziendale, di eseguire determinate attività che ne modificano lo stato.
  4. Il servizio dice al repository di persistere l'oggetto dominio modificato.
  5. Il repository deve mappare l'oggetto di dominio sulla rappresentazione corrispondente nella memoria.

Illustrazione di flusso

Ora, dati i presupposti di cui sopra, i seguenti sembrano imbarazzanti:

Annuncio 2 .:

Il modello di dominio sembra caricare l'intero oggetto di dominio (inclusi tutti i campi e i riferimenti), anche se non sono necessari per la funzione che lo ha richiesto. Il caricamento completo potrebbe non essere nemmeno possibile se si fa riferimento ad altri oggetti di dominio, a meno che non vengano caricati anche quegli oggetti di dominio e tutti gli oggetti a cui fanno riferimento a turno, e così via e così via. Viene in mente il caricamento lento, il che significa tuttavia che si inizia a interrogare gli oggetti del dominio che dovrebbero essere in primo luogo responsabilità del repository.

Dato questo problema, il modo "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 cui sono stati progettati. Ecco 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 del dominio finirebbero in stati incompleti che si svolgono nullnei loro campi. Quest'ultimo non dovrebbe tecnicamente essere un problema perché se un valore non è stato caricato, non dovrebbe essere richiesto dalla funzionalità che lo ha richiesto comunque. Tuttavia è imbarazzante e un potenziale pericolo.

Annuncio 3 .:

In che modo un oggetto dominio verificherebbe i vincoli di unicità sulla costruzione se non ha alcuna nozione di repository? Ad esempio, se volessi crearne uno nuovo Usercon un numero univoco di previdenza sociale (che viene fornito), il primo conflitto si verificherebbe chiedendo al repository di salvare l'oggetto, solo se nel database è stato definito un vincolo di unicità. Altrimenti, potrei cercare un Usercon la previdenza sociale fornita e segnalare un errore nel caso esista, prima di crearne uno nuovo. Ma i controlli dei vincoli vivrebbero nel servizio e non nell'oggetto dominio a cui appartengono. Mi sono appena reso conto che agli oggetti del dominio è consentito utilizzare i repository (iniettati) per la convalida.

Annuncio 5 .:

Percepisco il mapping degli oggetti di dominio su un back-end di archiviazione come un processo ad alta intensità di lavoro rispetto al fatto che gli oggetti di dominio modificano direttamente i dati di base. Naturalmente, è un prerequisito essenziale per disaccoppiare l'implementazione di archiviazione concreta dal codice di dominio. Tuttavia, ha davvero un costo così elevato?

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

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

Modifica : Inoltre, affinché un repository acceda allo stato dell'oggetto dominio (ovvero il valore di ciascun campo), l'oggetto dominio deve rivelare il suo stato interno che interrompe l'incapsulamento.

In generale:

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

Gradirei alcuni chiarimenti su questo argomento. I miei presupposti sono corretti? In caso contrario, qual è il modo corretto di affrontare questi problemi?


1
Buoni punti e domande, mi interessano anche quelli. Una nota a margine: se stai modellando correttamente l'aggregato, il che significa che in un dato momento dell'esistenza, l'istanza aggregata deve essere in stato valido - questo è il punto principale dell'aggregato (e non usare un aggregato come contenitore di composizione). Ciò significa anche che, al fine di ripristinare la forma aggregata dei dati DB, il repository stesso di solito dovrebbe utilizzare un costruttore specifico e un insieme di operazioni di mutazione, e non vedo come qualsiasi ORM potrebbe sapere magicamente come eseguire tali operazioni .
Dusan,

2
Ciò che è ancora più deludente è che quelle domande come la tua vengono poste abbastanza spesso in giro, ma, per quanto ne sappia, ci sono esempi ZERO dell'implementazione degli aggregati e dei repository che sono dal libro
Dusan,

Risposte:


5

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

Leggendo tra le righe sembra che tu provenga da uno stile di programmazione di record attivo più incentrato sul database? Per arrivare a un'implementazione funzionante direi che è necessario

1: gli oggetti di dominio non devono includere l'intero grafico degli oggetti. 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;
}

L'indirizzo e il cliente devono far parte dello stesso aggregato solo se si dispone di una logica come "il nome del cliente può iniziare solo con la stessa lettera del nome host". Hai ragione a evitare il caricamento lento e le versioni "Lite" degli oggetti.

2: I vincoli di unicità sono generalmente di competenza del repository e non dell'oggetto di dominio. Non iniettare repository in Domain Objects, è un ritorno al record attivo, semplicemente errore quando il servizio tenta di salvare.

La regola aziendale non è "Non possono esistere due istanze utente con lo stesso SocialSecurityNumber contemporaneamente"

È 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 entrambi i modi. È proprio in quale classe lo metti.

Gli ORM in questi giorni sono facili e non hanno vincoli aggiuntivi sul tuo codice. Detto questo, personalmente preferisco semplicemente far girare a mano l'SQL. Non è così difficile, non si verificano mai problemi con le funzionalità ORM e si può ottimizzare dove richiesto.

Non è davvero necessario tenere traccia di quali proprietà sono state modificate al momento del salvataggio. Mantieni piccoli i tuoi oggetti dominio e sovrascrivi semplicemente la versione precedente.

Domande generali

  1. La logica di transazione va nel repository. Ma non dovresti avere molto se non c'è ne. Sicuramente ne avrai bisogno se hai tabelle figlio in cui stai inserendo gli oggetti figlio dell'aggregato, ma che saranno interamente incapsulati nel metodo di repository SaveMyObject.

  2. Aggiornamenti collettivi. Sì, dovresti modificare individualmente ogni oggetto, quindi aggiungi un metodo SaveMyObjects (Elenco oggetti) al tuo repository, per eseguire l'aggiornamento in blocco.

    Si desidera che l'oggetto dominio o il servizio dominio contenga la logica. Non il database. Ciò significa che non puoi semplicemente fare "aggiorna nome set cliente = x dove y", perché per quanto conosci l'oggetto Cliente o CustomerUpdateService fa 20 cose dispari quando cambi il nome.


Bella risposta. Hai assolutamente ragione, sono abituato a uno stile di registrazione attivo di codifica, motivo per cui il modello di repository sembra strano a prima vista. Tuttavia, gli oggetti di dominio "lean" ( AddressIdanziché Address) non contraddicono i principi di OO?
Doppio M

no, hai ancora un oggetto Address, non è figlio del cliente
Ewan,

mappatura degli oggetti pubblicitari senza tracciamento delle modifiche softwareengineering.stackexchange.com/questions/380274/…
Double M

2

Risposta breve: la vostra comprensione è corretta e le domande che ponete 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 reale, quindi non hanno idea di cosa caricare o come ottimizzare. Questo è un problema.

Come hai detto, la soluzione ovvia è 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.

Cosa succede se voglio solo aggiornare in blocco alcuni record? Perché dovrei aver bisogno di una rappresentazione dell'oggetto per tutti i record? Eccetera.

Quindi la soluzione a ciò non è semplicemente quella di utilizzare un ORM per casi d'uso per i quali non è adatto. Implementare un caso d'uso "naturalmente" così com'è, che a volte non richiede un'ulteriore "astrazione" dei dati stessi (oggetti-dati) né un'astrazione sulle "tabelle" (repository).

Avere oggetti dati riempiti per metà o sostituire i riferimenti a oggetti con "id" sono soluzioni alternative, non buoni progetti, come hai sottolineato.

Punto 3 .: (controllo dei vincoli)

Se la persistenza non viene sottratta, ogni caso d'uso può ovviamente controllare qualsiasi vincolo desideri facilmente. Il requisito secondo cui gli oggetti non conoscono il "repository" è completamente artificiale e non costituisce un problema di tecnologia.

Punto 5 .: (ORM)

Naturalmente, è un prerequisito essenziale per disaccoppiare l'implementazione di archiviazione concreta dal codice di dominio. Tuttavia, ha davvero un costo così elevato?

No, non lo fa. Esistono 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 è inutile e, a seconda dell'ORM stesso, a volte è persino impossibile, poiché a volte questi strumenti utilizzano cache e esecuzione tardiva.

Domanda generale 1 .: (transazioni)

Non credo che esista un'unica soluzione. Se il tuo design è 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 in blocco)

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

Il requisito per separare la "logica" da SQL proviene dagli ORM. Essi hanno a dire che, perché non riescono a sostenerla. Non è intrinsecamente "cattivo".

Sommario

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

Allo stesso modo, anche l'astrazione di oggetti dati repository di DDD non è sempre la migliore. Vorrei anche spingermi così lontano da dire, raramente sono il design ottimale.

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


Punti molto interessanti lì dentro, grazie per aver confermato le mie ipotesi. Hai detto che ci sono molti altri modi per avere persistenza. Puoi consigliare un modello di progettazione performante da utilizzare con database di grafi (no ORM), che fornirebbe comunque PI?
Doppio M

1
Veramente mi chiederei se hai bisogno di isolamento (e che tipo) in primo luogo. L'isolamento per tecnologia (ad es. Database, interfaccia utente, ecc.) Porta quasi automaticamente la "imbarazzo" che si sta cercando di evitare, a vantaggio della sostituzione in qualche modo più semplice della tecnologia del database. Il costo è tuttavia un cambiamento più difficile della logica aziendale poiché si diffonde attraverso i livelli. Oppure è possibile suddividere le funzioni aziendali, il che renderebbe più difficile la modifica dei database, ma la modifica della logica sarà più semplice. Quale vuoi davvero?
Robert Bräutigam,

1
È possibile ottenere le prestazioni migliori solo modellando il dominio (ovvero funzioni aziendali) e non astrarre il database (indipendentemente dal fatto che sia relazionale o grafico). Poiché il database non viene estratto dal caso d'uso, il caso d'uso può implementare le query / gli aggiornamenti più ottimali che desidera e non è necessario passare attraverso un modello di oggetto scomodo per ottenere ciò che desidera.
Robert Bräutigam,

Bene, l'obiettivo principale è mantenere le preoccupazioni di persistenza lontano dalla logica aziendale, al fine di avere un codice pulito che sia facile da capire, espandere e testare. Essere in grado di scambiare tecnologie DB è solo un vantaggio. Posso vedere che c'è ovviamente attrito tra efficienza e ignoranza, che sembra essere più forte con i DB grafici a causa delle potenti query che puoi (ma non ti è permesso) usare.
Doppio M

1
Come sviluppatore di Java Enterprise, posso dirti che negli ultimi due decenni abbiamo cercato di separare la persistenza dalla logica. Non funziona Innanzitutto, la separazione non è mai stata realmente raggiunta. Ancora oggi, ci sono tutti i tipi di cose relative al database in presunti oggetti "business", il principale è l'ID del database (e molte annotazioni del database). In secondo luogo, come hai detto, a volte la logica aziendale viene eseguita nel database in entrambi i modi. In terzo luogo, questa è la ragione per cui disponiamo di database specifici, per essere in grado di scaricare la logica meglio eseguita dove si trovano i dati.
Robert Bräutigam,
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.