Programming Languages Hacks

Importanti regole per linguaggi di programmazione rilevanti come Java, C, C++, C#…

  • Subscribe

  • Lettori

    I miei lettori abituali

  • Twitter

DDD semplificato nel mondo reale

Posted by Ricibald on July 29th, 2014

Il DDD è un ottimo pattern ma se si implementa al 100% si rischia di ottenere troppa complessità rispetto a domini quasi sempre non così complessi da giustificarne una totale aderenza.

Basti riflettere infatti sul numero di astrazioni introdotte per fare una semplice insert:

  1. Application inizializza lo Unit of work e coordina i vari Domain Service
  2. Domain Service ottiene dal suo Repository il proxy del Aggregate che ha attivo il change tracking. Se necessario esegue logica specifica del caso d’uso che non si rimappa nel dominio
  3. Aggregate coordina il comportamento desiderato delegando le varie sottofunzioni alle varie Entity o Value Object in stile OO
  4. Entity o Value Object alterano il proprio stato, registrato dal change tracking verso lo Unit of work
  5. Application richiede il commit dello Unit of work
  6. Unit of work apre una micro-transazione che riversa su db lo stato alterato tracciato

Domain Service: perché??

Riporto di seguito una frase di Martin Fowler su cui mi trovo d’accordo:

Any logic that’s particular to a single transaction or use-case is normally placed in Transaction Scripts, which are commonly referred to as use-case controllers. Behavior that’s used in more than one use case goes on the domain objects which are called entities.
Although the controller entity approach is a common one, it’s not one that I’ve ever liked much. The use-case controllers, like any Transaction Script tend to encourage duplicate code. My view is that if you decide to use a Domain Model at all you really should go the whole hog and make it dominant

Il Domain Service è sostanzialmente uno use-case controller che incoraggia alla duplicazione di codice. Se veramente un’operazione è specifica di un caso d’uso bisogna comunque sforzarsi di farla entrare a pieno titolo del dominio. In altre parole: il Domain Service non deve esistere…

Repository e Unit of Work: necessari??

Il Repository pattern consente di ottenere dati da un data source layer mimando una collezione in memoria nascondendo la complessità del sottostante accesso ai dati facendo uso internamente di uno o più oggetti DAO e del pattern Query Object. Nel pattern è importante notare che è normalmente assente il concetto di Update: questo viene inferito automaticamente dallo Unit of work grazie al change tracking sulle entità restituite dal Repository.

Il pattern è efficace poiché tratta il db come aspetto secondario e permette in una prima fase di concentrarsi esclusivamente su GUI, business e TDD. Il pattern è però applicabile solo in presenza di framework ORM che gestiscano in modo efficace il change tracking e il relativo Data Mapper.

A volte il Repository pattern non è però applicabile poiché il data source layer sottostante non è un db, bensì un web service REST o SOAP o una lista Sharepoint. Implementare un meccanismo custom di change tracking diventa veramente complesso e rischioso e non è praticabile.

Bisogna dire addio quindi al DDD e tornare al classico Transaction Script? In realtà si possono implementare versioni semplificate del Repository pattern.

Repository con Unit of Work “a grana grossa”

Ciò che rende complesso il change tracking è la granularità fine: ogni singola modifica viene registrata per contribuire in modo puntuale alla query finale.

Si potrebbero ridimensionare le aspettative e semplicemente marcare l’intera Entity o Aggregate come “dirty” e quindi da aggiornare. Questo può essere implementato in modo esplicito o trasparente:

  • (esplicito) Entity esegue ove richiesto il metodo UnitOfWork.Current.SetDirty(this)
    • tramite PostSharp si può implementare in modo automatizzato su tutte le proprietà
    • tramite T4 si possono autogenerare le Entity rimappando da un modello dati (db, xml, …) arricchendo le Entity con i vari SetDirty
  • (trasparente) Repository crea un proxy delle Entity e decora ove richiesto i metodi con UnitOfWork.Current.SetDirty(this)
  • (trasparente) Entity implementa ove richiesto l’interfaccia INotifyPropertyChanged e Repository gestisce gli eventi PropertyChanged registrando la modifica UnitOfWork.Current.SetDirty(item)

Al momento del Commit dello Unit of work questo avrà la lista delle entità da aggiornare. Sarà compito del nostro Unit of work custom di rimappare gli stati “dirty” con i rispettivi metodi nei vari DAO.

Repository senza Unit of Work: update espliciti

L’approccio precedente è una soluzione buona ma richiede persone esperte che sappiano rimappare lo Unit of work con i vari DAO. Un approccio più facilmente comprensibile è quello di annullare del tutto lo Unit of work e aggiungere nel Repository un metodo Update che opera sull’intero aggregato.

Non avendo nessun meccanismo di notifica dei cambiamenti, Application dovrà ricordarsi di invocare Update per riversare le informazioni contenute nelle Entity nel data source corrispondente altrimenti si otterrebbe un’inconsistenza.

Inoltre non avendo uno Unit of work dovremo farci carico della consistenza del blocco di aggiornamenti:

  • se si tratta di un singolo aggiornamento di un Aggregate in un Repository (caso comune) è sufficiente che la transazione sia definita dentro il Repository stesso
  • se si tratta di aggiornamenti di molteplici Aggregate di diversi Repository è necessario aprire una TransactionScope in Application prima di eseguire i vari Update
  • se si tratta di aggiornamenti di molteplici Aggregate con data source eterogenei è invece necessario prevedere un sistema di compensazione implementando il Command Pattern. Si potrebbe anche valutare l’uso del Distributed Transaction Coordinator

Lo svantaggio di questo approccio è che si delega al programmatore garantire che i metodi di update verso il repository siano 1) invocati correttamente 2) implementati correttamente. Per ridurre tale svantaggio si potrebbe trattare un solo singolo update per l’intero aggregato richiamato dall’Aggregate stesso (applicabile finquando salvare l’intero aggregato si mantiene in limiti di complessità accettabili e finquando abbiamo tutte le informazioni su cosa è cambiato).

Quale usare?

Questa tabella non scientifica cerca di riassumere i pro/contro dell’approccio

Repository standard Repository grana grossa Repository con update
Applicabilità Solo db, buon ORM Code gen (PostSharp, T4) Poco refactoring (no XP)
Skill Alto Medio Basso
Investimento Alto Medio Basso
Manutenibilità Alta Media Bassa

Mettiamo insieme i pezzi…

Vediamo come si rimappa il flusso esposto all’inizio con la soluzione “Repository con update” facendo a meno del Domain Service e immaginando il caso in cui nel caso d’uso si debba aggiornare molteplici Aggregate:

  1. Application ottiene dai Repository i vari Aggregate richiesti
  2. Aggregate coordina il comportamento desiderato delegando le varie sottofunzioni alle varie Entity o Value Object in stile OO. Se necessario esegue logica specifica del caso d’uso
  3. Entity o Value Object alterano il proprio stato
  4. Application apre un TransactionScope e richiede a seconda del caso d’uso l’update o add/remove dai Repository
  5. Repository esegue la corrispondente query
  6. Application richiede il commit del TransactionScope e lo chiude

Se invece ci limitiamo a considerare il semplice caso di aggiornamento di un singolo Aggregate il flusso si riduce come segue:

  1. Application ottiene dai Repository i vari Aggregate richiesti
  2. Aggregate coordina il comportamento desiderato delegando le varie sottofunzioni alle varie Entity o Value Object in stile OO. Se necessario esegue logica specifica del caso d’uso
  3. Entity o Value Object alterano il proprio stato
  4. Application richiede a seconda del caso d’uso l’update o add/remove dai Repository
  5. Repository esegue la corrispondente query nella transazione del DAO sottostante
    • Se coinvolge più aggregati si potrebbe sempre creare un unico metodo nel repository Update(AggregateA a, AggregateB b) che aggiorna entrambi gli aggregati senza dover introdurre lo Unit of work o il TransactionScope

Tramite questa strategia si riesce a ottenere il DDD anche in condizioni non ideali (data source eterogenei, ORM non possibili, team non skillato) senza appesantire eccessivamente la gestione, pur pagando in prospettiva in costi di manutenibilità. Questo implica che un modello di programmazione agile come l’XP non è proponibile poiché non è possibile essere aggressivi con il refactoring.

Appendice: esempio di codice

Si riporta un esempio di codice della soluzione “Repository con update” senza unit of work, con un metodo nel Repository che coinvolge più aggregati e che fa uso del Dependency Injection senza interfacce come illustrato nel precedente articolo:

Leave a Reply

You must be logged in to post a comment.