Il pattern repository è uno dei più importanti nel domain driven design e merita per questo una trattazione particolareggiata. In questo primo articolo verrà mostrato come gestire un repository generico la cui interfaccia è:
public
interface
IRepository<T> : IDisposable {
T GetByKey(object key);
IList<T> GetAll();
void Delete(T entity);
void Save(T entity);
}
Un repository permette di gestire uno storage system per un dominio presentando un'interfaccia che dialoga direttamente con oggetti e collezioni di oggetti. Una prima possibilità di implementazione è creare un interfaccia generica, capace quindi di gestire qualsiasi tipologia di oggetto del dominio. L'interfaccia mostrata nello snippet precedente è indubbiamente incompleta, manca ad esempio un modo per recuperare gli oggetti in base a condizioni (solitamente espresso con un Query Model), ma è sufficiente per questo primo esempio e verrà espanso negli esempi seguenti. Naturalmente lavorando in DDD la persistenza è gestita da ORM come nhibernate, che internamente implementa il pattern di repository. Chi conosce nhibernate sa infatti che le funzionalità esposte dall'interfaccia precedente sono accessibili tramite l'oggetto sessione, ecco infatti che implementare un repository concreto con nhibernate diventa un gioco da ragazzi.
public
class
Repository<T> : IRepository<T> {
private
ISession _session;
public Repository() {
_session = SessionHelper.GetSession();
_session.BeginTransaction();
}
#region IRepository<T> Members
public T GetByKey(object key) {
return _session.Get<T>(key);
}
public
IList<T> GetAll() {
return _session.CreateCriteria(typeof (T)).List<T>();
}
public
void Delete(T entity) {
_session.Delete(entity);
}
public
void Save(T entity) {
_session.SaveOrUpdate(entity);
}
#endregion
#region IDisposable Members
private
void Dispose(Boolean disposing) {
if (disposing) {
_session.Transaction.Commit();
_session.Dispose();
}
}
public
void Dispose() {
Dispose(true);
}
#endregion
}
Ci si chiede allora se sia veramente necessario creare l'interfaccia IRepository<T> o se non sia possibile utilizzare direttamente l'oggetto sessione di nhibernate. I vantaggi di questa tecnica sono essenzialmente due: in primo luogo il software non è legato a Nhibernate e quindi è possibile sostituire l'orm utilizzato, ma cosa più importante questo layer ulteriore di indirezione facilita i test come vedremo tra poco. Lo svantaggio principale è che purtroppo un'interfaccia generica di un repository non permette di accedere alle funzionalità specifiche di un ORM e quindi si rischia di perdere qualche caratteristica di NHIbernate.
Vediamo ora come usare un repository. Nel DDD le funzionalità del dominio vengono solitamente eposte all'esterno tramite servizi, nell'esempio accluso al post potete infatti vedere un CustomerService che contiene una sola funzione il cui scopo è chiamare il metodo Customer.SetDiscount() per ogni cliente che abita in una determinata città. Secondo i principi di Inversione di Controllo l'oggetto CustomerService deve avere tutte le sue dipendenze iniettate dall'esterno per questa ragione dichiara il seguente costruttore
public CustomerService(IRepository<Customer> customerRepo) {
_customerRepo = customerRepo;
}
Come si può vedere per funzionare correttamente il servizio dichiara di necessitare di un repository per l'oggetto Customer. L'implementazione del metodo SetDiscount() è veramente banale.
public
IList<String> SetDiscount(String cityname, Single amount) {
IList<Customer> AllCustomers = _customerRepo.GetAll();
List<String> reslist = new
List<string>();
foreach(Customer c in AllCustomers) {
if (string.Compare(c.AddressInfo.City, cityname, true) == 0) {
c.SetDiscount(amount);
reslist.Add(c.Id);
}
}
return reslist;
}
Naturalmente il metodo è assai inefficiente perché il nostro repository è ancora incompleto. Il servizio infatti può solamente recuperare tutti i Customer dal repository ed iterare tra di essi per individuare quelli che risiedono nella città specificata come parametro. Per ogni Customer che soddisfa le caratteristiche richieste si chiama il suo metodo SetDiscount ed al termine è necessario ritornare una lista degli Id di tutti i clienti a cui è stato cambiato lo sconto. L'utilizzo del Repository generico ci permette di scrivere un test con i mock object.
1 [Test]
2
public
void TestAddDiscount() {
3
IRepository<Customer> mockrepo = mockery.CreateMock<IRepository<Customer>>();
4
IList<Customer> Ret = new
List<Customer>();
5 Ret.Add(mockery.PartialMock<Customer>());
6 Ret.Add(mockery.PartialMock<Customer>());
7 Ret.Add(mockery.PartialMock<Customer>());
8
9
using (mockery.Record()) {
10
Expect.Call(Ret[0].SetDiscount(20)).Return((Single)20.0);
11
Expect.Call(Ret[2].SetDiscount(20)).Return((Single) 20.0);
12
Expect.Call(mockrepo.GetAll()).Return(Ret);
13 }
14 Ret[0].AddressInfo = new
AddressInfo();
15 Ret[0].Id = "ALFIO";
16 Ret[0].AddressInfo.City = "ROME";
17 Ret[1].AddressInfo = new
AddressInfo();
18 Ret[1].Id = "GINO";
19 Ret[1].AddressInfo.City = "MILAN";
20 Ret[2].AddressInfo = new
AddressInfo();
21 Ret[2].Id = "BERTO";
22 Ret[2].AddressInfo.City = "ROME";
23
24 mockery.ReplayAll();
25
26
using (mockery.Playback()) {
27
CustomerService service = new
CustomerService(mockrepo);
28
IList<String> result = service.SetDiscount("ROME", 20);
29
Assert.AreEqual(2, result.Count);
30
Assert.IsTrue(result.Contains("ALFIO"));
31
Assert.IsTrue(result.Contains("BERTO"));
32 }
33 }
L'oggetto MockRepository viene creato e verificato nei metodi SetUp e TearDown. Nella riga 3 viene creato un Mock per il repository e poi si parte creando la lista di Customer che verrà ritornata al chiamante. Nelle righe 5, 6 e 7 vengono aggiunti alla lista tre oggetti Mock per l'oggetto Customer, l'istruzione PartialMock di RhinoMock permette di creare un mock di un oggetto e fare l'override dei suoi soli metodi virtuali se necessario. Nelle righe 9-13 il MockRepository viene messo in modalità "record", questo significa che stiamo dichiarando una serie di Expectation riguardo gli oggetti creati. Nel caso specifico vogliamo essere sicuri che venga chiamato il metodo SetDiscount() con il parametro 20 per il primo e terzo custode e che venga ritornato il valore 20.0, nella linea 12 viene invece dichiarato che verrà chiamato il metodo GetAll() del mock di IRepository<Customer> e viene impostata la collection da ritornare. Nelle linee 14-22 vengono invece configurati gli oggetti Customer, in particolare vengono settati gli id fittizi e le città di appartenenza, il primo e terzo abitano in "ROME". Essendo al di fuori del blocco using di mockery.Record(), le nostre chiamate ai vari metodi degli oggetti custode non generano expectations, ma servono solamente a configurare gli oggetti. Nel blocco finale mockery.Playback() si crea un nuovo oggetto CustomerSErvice iniettando come dipendenza il nostro repository mock, si chiama il metodo SetDiscount e si verifica che il risultato contenga gli id dei reali clienti che abitano a "ROME".
Questo test è in grado quindi di
- Simulare la presenza dei dati di un database, in questo modo potete testare le logiche di qualsiasi servizio o codice senza dovere avere un database
- Simulare i dati di ritorno ed impostare delle condizioni su come gli oggetti ritornati verranno usati
- Verificare che con i dati simulati il servizio ritorni i risultati corretti
Ora non resta che da capire come utilizzare il servizio nel codice di produzione, naturalmente la risposta è utilizzare un motore di Inversione di Controllo (trovate un web cast su dotnetmarche) per configurare il servizio. Nell'esempio è stato utilizzato Castle.Windsor. Il primo passo è creare la configurazione desiderata nel file di configurazione del progetto.
<CastleWindsor>
<components>
<component
id="RepositoryGeneric"
service="RepositoryGeneric.IRepository`1, RepositoryGeneric"
type="NhibernateRepositories.Generic.Repository`1, NhibernateRepositories"
lifestyle="Transient">
</component>
<component
id="CustomerService"
service="Domain.Services.CustomerService, Domain"
type="Domain.Services.CustomerService, Domain"
lifestyle="Transient">
</component>
</components>
</CastleWindsor>
La configurazione è veramente banale, come primo componente si specifica il repository da utilizzare, la sintassi con l'apice inverso è tipica del .NET ed indica una classe generica con un parametro di tipo. Il primo componente dichiara quindi che l'interfaccia IRepository<T> è soddisfatta dall'oggetto NhibernateRepositories.Generic.Repository<T>. Il secondo componente invece dichiara che l'oggetto CustomerService è soddisfatto dalla classe CustomerService. L'auto wiring di castle mi permette di non dovere specificare nulla di altro al contenitore. A questo punto aggiungo un semplice test che utilizza il repository di produzione (quello che funziona con nhibernate) puntando al mio database locale di northwind.
[Test]
public
void TestIoCContainer() {
CustomerService service = IoC.Resolve<CustomerService>();
IList<String> result = service.SetDiscount("sao paulo", 20);
Assert.AreEqual(4, result.Count);
Assert.IsTrue(result.Contains("COMMI"));
Assert.IsTrue(result.Contains("FAMIA"));
Assert.IsTrue(result.Contains("QUEEN"));
Assert.IsTrue(result.Contains("TRADH"));
}
In questo caso non si può non apprezzare la compattezza di Castle.Windsor che tramite il metodo Resolve<CustomerService> ci restituisce un istanza del servizio desiderato opportunamente configurata con iniettato il repository di produzione in maniera completamente automatica. Il test in questo caso viene basato sul contenuto attuale della mia tabella Customer.
Con questo piccolo esempio si è mostrato un utilizzo base del pattern repository, nei post successivi vedremo come espanderlo per renderlo più flessibile e funzionale.
Il codice dell'esempio è disponibile qui.
Alk.
Seconda parte
Terza parte
Quarta parte