DDD incontra OOP: come implementare un repository orientato agli oggetti?


12

Un'implementazione tipica di un repository DDD non sembra molto OO, ad esempio un save()metodo:

package com.example.domain;

public class Product {  /* public attributes for brevity */
    public String name;
    public Double price;
}

public interface ProductRepo {
    void save(Product product);
} 

Parte dell'infrastruttura:

package com.example.infrastructure;
// imports...

public class JdbcProductRepo implements ProductRepo {
    private JdbcTemplate = ...

    public void save(Product product) {
        JdbcTemplate.update("INSERT INTO product (name, price) VALUES (?, ?)", 
            product.name, product.price);
    }
} 

Tale interfaccia prevede Productche sia un modello anemico, almeno con getter.

D'altra parte, OOP afferma che un Productoggetto dovrebbe sapere come salvarsi.

package com.example.domain;

public class Product {
    private String name;
    private Double price;

    void save() {
        // save the product
        // ???
    }
}

Il fatto è che quando Productsa come salvarsi, significa che il codice di infrastruttura non è separato dal codice di dominio.

Forse possiamo delegare il salvataggio a un altro oggetto:

package com.example.domain;

public class Product {
    private String name;
    private Double price;

    void save(Storage storage) {
        storage
            .with("name", this.name)
            .with("price", this.price)
            .save();
    }
}

public interface Storage {
    Storage with(String name, Object value);
    void save();
}

Parte dell'infrastruttura:

package com.example.infrastructure;
// imports...

public class JdbcProductRepo implements ProductRepo {        
    public void save(Product product) {
        product.save(new JdbcStorage());
    }
}

class JdbcStorage implements Storage {
    private final JdbcTemplate = ...
    private final Map<String, Object> attrs = new HashMap<>();

    private final String tableName;

    public JdbcStorage(String tableName) {
        this.tableName = tableName;
    }

    public Storage with(String name, Object value) {
        attrs.put(name, value);
    }
    public void save() {
        JdbcTemplate.update("INSERT INTO " + tableName + " (name, price) VALUES (?, ?)", 
            attrs.get("name"), attrs.get("price"));
    }
}

Qual è l'approccio migliore per raggiungere questo obiettivo? È possibile implementare un repository orientato agli oggetti?


6
OOP dice che un oggetto Prodotto dovrebbe sapere come salvarsi - Non sono sicuro che sia corretto davvero ... OOP in sé non lo impone davvero, è più un problema di progettazione / modello (che è dove DDD / qualunque-tu -use entra)
jleach

1
Ricorda che nel contesto di OOP, parla di oggetti. Solo oggetti, non persistenza dei dati. La tua dichiarazione indica che lo stato di un oggetto non deve essere gestito al di fuori di se stesso, con il quale sono d'accordo. Un repository è responsabile del caricamento / salvataggio da alcuni livelli di persistenza (che è al di fuori del regno di OOP). Le proprietà e i metodi della classe dovrebbero mantenere la propria integrità, sì, ma ciò non significa che un altro oggetto non possa essere responsabile della persistenza dello stato. Inoltre, getter e setter devono garantire l'integrità dei dati in entrata / in uscita dell'oggetto.
Giovedì

1
"questo non significa che un altro oggetto non possa essere responsabile della persistenza dello stato." - Non l'ho detto. L'importante è che un oggetto dovrebbe essere attivo . Significa che l'oggetto (e nessun altro) può delegare questa operazione a un altro oggetto, ma non viceversa: nessun oggetto dovrebbe semplicemente raccogliere informazioni da un oggetto passivo per elaborare la propria operazione egoistica (come farebbe un repository con i getter) . Ho cercato di implementare questo approccio nei frammenti sopra.
ttulka,

1
@jleach Hai ragione, la nostra comprensione di OOP è diversa, per me getter + setter non sono affatto OOP, altrimenti la mia domanda non aveva senso. Grazie lo stesso! :-)
ttulka,

1
Ecco un articolo sul mio punto: martinfowler.com/bliki/AnemicDomainModel.html Non sono in alcun modo contrario al modello anemico, ad esempio è una buona strategia per la programmazione funzionale. Solo non OOP.
ttulka,

Risposte:


7

Hai scritto

D'altra parte, OOP afferma che un oggetto Prodotto dovrebbe sapere come salvarsi

e in un commento.

... dovrebbe essere responsabile di tutte le operazioni fatte con esso

Questo è un malinteso comune. Productè un oggetto dominio, quindi dovrebbe essere responsabile delle operazioni del dominio che coinvolgono un singolo oggetto prodotto, nientemeno, niente di più - quindi sicuramente non per tutte le operazioni. Di solito la persistenza non è vista come un'operazione di dominio. Al contrario, nelle applicazioni aziendali, non è raro cercare di ottenere l'ignoranza della persistenza nel modello di dominio (almeno in una certa misura) e mantenere la meccanica della persistenza in una classe di repository separata è una soluzione popolare per questo. "DDD" è una tecnica che mira a questo tipo di applicazioni.

Quindi quale potrebbe essere un'operazione di dominio ragionevole per un Product? Questo dipende effettivamente dal contesto del dominio del sistema applicativo. Se il sistema è piccolo e supporta solo le operazioni CRUD in modo esclusivo, allora effettivamente Productpuò rimanere piuttosto "anemico" come nel tuo esempio. Per questo tipo di applicazioni, può essere discutibile se vale la pena mettere le operazioni del database in una classe repository separata o usare DDD.

Tuttavia, non appena l'applicazione supporta operazioni commerciali reali, come l'acquisto o la vendita di prodotti, la loro conservazione in magazzino e la loro gestione o il calcolo delle imposte per essi, è abbastanza comune iniziare a scoprire operazioni che possono essere collocate in modo ragionevole in una Productclasse. Ad esempio, potrebbe esserci un'operazione CalcTotalPrice(int noOfItems)che calcola il prezzo di `n articoli di un determinato prodotto quando si tiene conto degli sconti sul volume.

Quindi, in breve, quando progetti classi, devi pensare al tuo contesto, in quale dei cinque mondi di Joel Spolsky sei, e se il sistema contiene abbastanza logica di dominio, il DDD sarà vantaggioso. Se la risposta è sì, è abbastanza improbabile che tu finisca con un modello anemico solo perché mantieni la meccanica di persistenza fuori dalle classi di dominio.


Il tuo punto mi sembra molto ragionevole. Pertanto, il prodotto diventa una struttura di dati anemica quando attraversa un confine di un contesto di strutture di dati anemiche (database) e il repository è un gateway. Ma ciò significa ancora che devo fornire accesso alla struttura interna dell'oggetto tramite getter e setter, che diventano quindi parte della sua API e potrebbero essere facilmente utilizzati in modo improprio da altri codici, che non hanno nulla a che fare con la persistenza. C'è una buona pratica come evitarlo? Grazie!
ttulka,

"Ma questo significa ancora che devo fornire l'accesso alla struttura interna dell'oggetto tramite getter e setter" - improbabile. Lo stato interno di un oggetto di dominio ignaro della persistenza è generalmente dato esclusivamente da un insieme di attributi relativi al dominio. Per questi attributi, devono esistere getter e setter (o un'inizializzazione del costruttore), altrimenti nessuna operazione di dominio "interessante" sarebbe possibile. In diversi framework, sono disponibili anche funzionalità di persistenza che consentono di mantenere gli attributi privati ​​per riflessione, quindi l'incapsulamento viene interrotto solo per questo meccanismo, non per "altro codice".
Doc Brown,

1
Concordo sul fatto che la persistenza di solito non fa parte delle operazioni di dominio, tuttavia dovrebbe essere parte delle operazioni di dominio "reali" all'interno dell'oggetto che ne ha bisogno. Ad esempio, Account.transfer(amount)dovrebbe persistere il trasferimento. Come lo fa è responsabilità dell'oggetto, non di qualche entità esterna. La visualizzazione dell'oggetto invece è di solito un'operazione di dominio! I requisiti di solito descrivono in modo molto dettagliato come dovrebbero apparire le cose. Fa parte del linguaggio tra i membri del progetto, affari o altro.
Robert Bräutigam,

@ RobertBräutigam: il classico Account.transferdi solito coinvolge due oggetti account e un'unità di oggetto di lavoro. L'operazione transazionale persistente potrebbe quindi far parte di quest'ultima (insieme alle chiamate ai repository correlati), quindi rimane fuori dal metodo di "trasferimento". In questo modo, Accountpuò rimanere ignaro della persistenza. Non sto dicendo che questo sia necessariamente migliore della tua presunta soluzione, ma la tua è anche solo uno dei numerosi approcci possibili.
Doc Brown,

1
@ RobertBräutigam Abbastanza sicuro che stai pensando troppo alla relazione tra l'oggetto e la tabella. Pensa all'oggetto come se avesse uno stato per se stesso, tutto in memoria. Dopo aver effettuato i trasferimenti negli oggetti del tuo account, ti rimarrebbero oggetti con un nuovo stato. Questo è ciò che vorresti persistere e fortunatamente gli oggetti account forniscono un modo per farti conoscere il loro stato. Ciò non significa che il loro stato debba essere uguale alle tabelle nel database, ovvero che l'importo trasferito potrebbe essere un oggetto denaro contenente l'importo grezzo e la valuta.
Steve Chamaillard,

5

Pratica teoria dei briscole.

L'esperienza ci insegna che Product.Save () porta a molti problemi. Per ovviare a questi problemi abbiamo inventato il modello di repository.

Sicuramente infrange la regola OOP di nascondere i dati del prodotto. Ma funziona bene.

È molto più difficile stabilire una serie di regole coerenti che coprano tutto ciò che è fare alcune buone regole generali che hanno eccezioni.


3

DDD incontra OOP

Aiuta a tenere presente che non è prevista una tensione tra queste due idee: oggetti valore, aggregati, repository sono una serie di schemi utilizzati è ciò che alcuni considerano fatto OOP fatto nel modo giusto.

D'altra parte, OOP afferma che un oggetto Prodotto dovrebbe sapere come salvarsi.

Non così. Gli oggetti incapsulano le proprie strutture di dati. La tua rappresentazione in memoria di un Prodotto è responsabile dell'esibizione dei comportamenti del prodotto (qualunque essi siano); ma l'archiviazione persistente è laggiù (dietro il repository) e ha il suo lavoro da fare.

È necessario un modo per copiare i dati tra la rappresentazione in memoria del database e il suo ricordo persistente. Al limite , le cose tendono a diventare piuttosto primitive.

Fondamentalmente, scrivere solo i database non sono particolarmente utili e i loro equivalenti in memoria non sono più utili dell'ordinamento "persistente". Non ha senso inserire informazioni in un Productoggetto se non le toglierai mai. Non utilizzerai necessariamente i "getter": non stai cercando di condividere la struttura dei dati del prodotto e certamente non dovresti condividere l'accesso mutevole alla rappresentazione interna del Prodotto.

Forse possiamo delegare il salvataggio a un altro oggetto:

Funziona sicuramente: l'archiviazione persistente diventa effettivamente un callback. Probabilmente renderei l'interfaccia più semplice:

interface ProductStorage {
    onProduct(String name, double price);
}

Ci sarà un accoppiamento tra la rappresentazione in memoria e il meccanismo di archiviazione, perché le informazioni devono andare da qui a lì (e viceversa). La modifica delle informazioni da condividere avrà un impatto su entrambe le estremità della conversazione. Quindi potremmo anche renderlo esplicito dove possiamo.

Questo approccio - il passaggio di dati tramite callback, ha svolto un ruolo importante nello sviluppo di simulazioni nel TDD .

Si noti che il passaggio delle informazioni alla richiamata ha tutte le stesse restrizioni della restituzione delle informazioni da una query: non si dovrebbe passare tra copie mutabili delle strutture dei dati.

Questo approccio è un po 'contrario a quello che Evans ha descritto nel Blue Book, in cui la restituzione dei dati tramite una query era il modo normale di procedere e gli oggetti del dominio sono stati progettati specificamente per evitare di mescolare "problemi di persistenza".

Capisco il DDD come una tecnica OOP e quindi voglio comprendere appieno questa apparente contraddizione.

Una cosa da tenere a mente: il libro blu è stato scritto quindici anni fa, quando Java 1.4 vagava per la terra. In particolare, il libro precede i generici Java : ora abbiamo molte più tecniche disponibili quando Evans stava sviluppando le sue idee.


2
Vale anche la pena ricordare che "salvare se stesso" richiederebbe sempre l'interazione con altri oggetti (o un oggetto file system o un database o un servizio Web remoto, alcuni di questi potrebbero inoltre richiedere la creazione di una sessione per il controllo dell'accesso). Quindi un tale oggetto non sarebbe autonomo e indipendente. OOP non può quindi richiederlo, poiché il suo intento è incapsulare l'oggetto e ridurre l'accoppiamento.
Christophe,

Grazie per un'ottima risposta Innanzitutto, ho progettato l' Storageinterfaccia come hai fatto tu, poi ho considerato l'accoppiamento elevato e l'ho cambiata. Ma hai ragione, c'è comunque un inevitabile accoppiamento, quindi perché non renderlo più esplicito.
ttulka,

1
"Questo approccio è un po 'contrario a quello che Evans ha descritto nel Libro blu" - quindi dopo tutto c'è un po' di tensione :-) Questo era in realtà il punto della mia domanda, capisco DDD come una tecnica OOP e quindi voglio comprendere appieno questa apparente contraddizione.
ttulka,

1
Nella mia esperienza, ognuna di queste cose (OOP in generale, DDD, TDD, acronimo di pick-your-acronym) sembrano tutte belle e belle in sé e per sé, ma ogni volta che si tratta dell'implementazione del "mondo reale", c'è sempre qualche compromesso o tutt'altro che idealismo che deve essere perché funzioni.
giovedì

Non sono d'accordo con l'idea che la persistenza (e la presentazione) siano in qualche modo "speciali". Non sono. Dovrebbero far parte della modellistica per estendere la domanda di requisiti. Non è necessario che vi sia un limite artificiale (basato sui dati) all'interno dell'applicazione, a meno che non vi siano requisiti effettivi contrari.
Robert Bräutigam,

1

Ottime osservazioni, sono completamente d'accordo con te su di esse. Ecco un mio discorso (correzione: solo diapositive) su questo argomento: Design guidato dal dominio orientato agli oggetti .

Risposta breve: no. Non ci dovrebbe essere un oggetto nella tua applicazione che sia puramente tecnico e non abbia rilevanza per il dominio. È come implementare il framework di registrazione in un'applicazione di contabilità.

Il tuo Storageesempio di interfaccia è eccellente, supponendo che Storagesia considerato un framework esterno, anche se lo scrivi.

Inoltre, save()in un oggetto dovrebbe essere consentito solo se fa parte del dominio (la "lingua"). Ad esempio, non dovrei essere obbligato a "salvare" esplicitamente una Accountvolta che ho chiamato transfer(amount). Dovrei giustamente aspettarmi che la funzione aziendale transfer()persista il mio trasferimento.

Tutto sommato, penso che le idee di DDD siano buone. Usando un linguaggio onnipresente, esercitando il dominio con la conversazione, contesti limitati, ecc. Tuttavia, per essere compatibili con l'orientamento agli oggetti, i blocchi costitutivi richiedono una seria revisione. Vedi il mazzo collegato per i dettagli.


Il tuo discorso è da qualche parte a guardare? (Vedo che ci sono solo diapositive sotto il link). Grazie!
ttulka,

Ho solo una registrazione tedesca del discorso, qui: javadevguy.wordpress.com/2018/11/26/…
Robert Bräutigam

Ottima chiacchierata! (Fortunatamente parlo tedesco). Penso che valga la pena di leggere tutto il tuo blog ... Grazie per il tuo lavoro!
ttulka,

Cursore molto penetrante Robert. L'ho trovato molto illustrativo, ma ho avuto la sensazione che alla fine, molte delle soluzioni indirizzate a non rompere l'incapsulamento e il LoD siano basate sul dare molte responsabilità all'oggetto dominio: stampa, serializzazione, formattazione dell'interfaccia utente, ecc. che aumenta l'accoppiamento tra dominio e tecnica (dettagli di implementazione)? Ad esempio, AccountNumber associato all'API Wicket di Apache. O conto con qualunque oggetto Json è? Pensi che valga la pena avere un accoppiamento?
Laiv

@Laiv La grammatica della tua domanda suggerisce che c'è qualcosa di sbagliato nell'usare la tecnologia per implementare le funzioni aziendali? Mettiamola in questo modo: non è l'accoppiamento tra dominio e tecnologia il problema, è l'accoppiamento tra diversi livelli di astrazione. Ad esempio AccountNumber dovrebbe sapere che può essere rappresentato come a TextField. Se altri (come una "Vista") lo sapessero, questo è un accoppiamento che non dovrebbe esistere, perché quel componente dovrebbe sapere in cosa AccountNumberconsiste, cioè gli interni.
Robert Bräutigam,

1

Forse possiamo delegare il salvataggio a un altro oggetto

Evitare di diffondere inutilmente la conoscenza dei campi. Più cose sanno di un singolo campo, più diventa difficile aggiungere o rimuovere un campo:

public class Product {
    private String name;
    private Double price;

    void save(Storage storage) {
        storage.save( toString() );
    }
}

Qui il prodotto non ha idea se si sta salvando in un file di registro o in un database o entrambi. Qui il metodo di salvataggio non ha idea se hai 4 o 40 campi. È liberamente accoppiato. È una buona cosa.

Ovviamente questo è solo un esempio di come puoi raggiungere questo obiettivo. Se non ti piace costruire e analizzare una stringa da utilizzare come DTO, puoi anche utilizzare una raccolta. LinkedHashMapè uno dei miei preferiti in quanto conserva l'ordine ed è toString () sembra buono in un file di registro.

Comunque lo faccia, per favore non diffondere la conoscenza dei campi in giro. Questa è una forma di accoppiamento che le persone spesso ignorano fino a tardi. Voglio che poche cose sappiano staticamente quanti campi il mio oggetto ha il più possibile. In questo modo l'aggiunta di un campo non comporta molte modifiche in molti punti.


Questo è in effetti il ​​codice che ho pubblicato nella mia domanda, giusto? Ho usato a Map, tu proponi a Stringo a List. Ma, come ha menzionato @VoiceOfUnreason nella sua risposta, l'accoppiamento è ancora lì, ma non esplicito. Non è ancora necessario conoscere la struttura dei dati del prodotto per salvarlo sia in un database che in un file di registro, almeno quando viene riletto come oggetto.
ttulka,

Ho cambiato il metodo di salvataggio, ma altrimenti sì, è più o meno lo stesso. La differenza è che l'accoppiamento non è più statico, consentendo l'aggiunta di nuovi campi senza forzare una modifica del codice al sistema di archiviazione. Ciò rende il sistema di archiviazione riutilizzabile su molti prodotti diversi. Ti costringe solo a fare cose che sembrano un po 'innaturali come trasformare un doppio in una stringa e tornare in una doppia. Ma questo può essere risolto anche se è davvero un problema.
candied_orange


Ma come ho detto, vedo l'accoppiamento ancora lì (analizzando), solo perché non essere statico (esplicito) porta lo svantaggio di non poter essere verificato da un compilatore e quindi più soggetto a errori. La Storageè una parte del dominio (così come l'interfaccia repository è) e rende tale API persistenza. Quando viene modificato, è meglio informare i client in fase di compilazione, perché devono comunque reagire per non essere interrotti in fase di esecuzione.
ttulka,

È un'idea sbagliata. Il compilatore non può controllare un file di registro o un DB. Tutto ciò che sta controllando è se un file di codice è coerente con un altro file di codice che non è garantito che sia coerente con il file di registro o il DB.
candied_orange,

0

C'è un'alternativa ai modelli già menzionati. Il modello Memento è ottimo per incapsulare lo stato interno di un oggetto dominio. L'oggetto memento rappresenta un'istantanea dello stato pubblico dell'oggetto dominio. L'oggetto dominio sa come creare questo stato pubblico dal suo stato interno e viceversa. Un repository funziona quindi solo con la rappresentazione pubblica dello stato. Con ciò, l'implementazione interna è disaccoppiata da qualsiasi specifica di persistenza e deve solo mantenere l'appalto pubblico. Inoltre, l'oggetto del tuo dominio non deve esporre alcun getter che effettivamente lo renderebbe un po 'anemico.

Per ulteriori informazioni su questo argomento, raccomando il grande libro: "Patterns, Principles and and Practices of Domain-Driven Design" di Scott Millett e Nick Tune

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.