Qualche tempo fa con un amico si stava discutendo di `Repository Pattern` e di tutto quello che gli gira intorno, tutta la disquisizione ruotava intorno a quale fosse il ruolo di un `repository` in un mondo orientato a CQRS. Siamo dopo un po’ di scambi di opinioni giunti alle seguenti questa conclusioni.
Un repository deve
- consentire di caricare un aggregato data la sua chiave primaria;
- consentire di aggiungere una nuova istanza di un aggregato;
- persistere le modifiche apportate ad un aggregato;
- rappresentare una Unit Of Work;
Un repository non deve
- consentire di eseguire query di nessun tipo;
- forzare la dichiarazione di intenti: Non è responsabilità di chi usa un aggregato sapere se deve persistere o meno delle modifiche, se il repository è una UoW la responsabilità è sua;
Se pensiamo di esprimere i requisiti di cui sopra con del codice C# lo possiamo fare usando la seguente interfaccia ad esempio:
namespace Sample.Repository.Pattern
{
public interface IRepository<T> where T : SomeConstraint
{
void Add(T entity);
T GetById(int Id);
void CommitChanges();
}
}
Che sebbene sembri soddisfare tutti i nostri requisiti non soddisfa il punto ‘4’ delle cose che un repository deve fare: Unit of Work.
Perché?
Provate a pensare come usereste l’interfaccia di cui sopra:
IRepository<Person> peopleRepo = …
var aPerson = peopleRepo.GetById( 123 );
aPerson.ChangeName( “new person name” );
peopleRepo.CommitChanges();
Se il repository è una UoW non dobbiamo dichiarare che vogliamo aggiornare l’istanza di Person che abbiamo appena modificato, lo sa già quindi ci basta a e avanza dichiarare che vogliamo salvare, CommitChanges().
Ma, osservate il seguente scenario:
IRepository<Person> peopleRepo = …
IRepository<Company> companiesRepo = …
var aPerson = peopleRepo.GetById( 123 );
aPerson.ChangeName( “new person name” );
var aCompany = companiesRepo.GetById( 456 );
aCompany.RegisterVATNumber( “vat number” );
peopleRepo.CommitChanges();
companiesRepo.CommitChanges();
Non è più una singola Unit Of Work e l`unico che abbiamo per garantire l’atomicità delle operazioni di cui sopra è racchiudere il codice in un blocco `using` con un `TransactionScope`.
Sebbene non sia un gran problema, e non è neanche sbagliato, la cosa che ci sta forzando ad andare in quella direzione non è l’architettura ma meramente la scelta di design che abbiamo fatto per l’API del nostro repository.
Se lo disegnassimo così?
namespace Sample.Repository.Pattern
{
public interface IRepository
{
void Add<T>(T entity) where T : SomeConstraint;
T GetById<T>(int Id) where T : SomeConstraint;
void CommitChanges();
}
}
La modifica è in apparenza banale, ci siamo limitati a spostare i generics dalla dichiarazione dell’interfaccia ai singoli membri della stessa, con la differenza fondamentale che adesso possiamo scrivere questo:
IRepository repo = …
var aPerson = repo.GetById<Person>( 123 );
aPerson.ChangeName( “new person name” );
var aCompany = repo.GetById<Company>( 456 );
aCompany.RegisterVATNumber( “vat number” );
repo.CommitChanges();
Che oltre a farci risparmiare un apio di righe di codice, ma chi se ne frega, ci permette di soddisfare elegantemente anche il punto “4”.
Volete vederlo in azione?
.m