Alkampfer's Place

Il blog di Gian Maria Ricci
posts - 659, comments - 871, trackbacks - 80

My Links

News

Gian Maria Ricci Mvp Logo CCSVI in Multiple Sclerosis

English Blog

Tag Cloud

Article Categories

Archives

Post Categories

Image Galleries

I miei siti

Siti utili

Considerazioni sul pattern repository

In questo articolo raccolgo tutta la serie di post sul repository pattern, in questo modo la consultazione è più chiara e leggibile.

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.

Nel precedente post ho iniziato l'implementazione di un repository generico ed ho mostrato come sia possibile effettuare test dei propri servizi tramite IoC e Mock object senza il database, simulando completamente gli oggetti con mock object. L'interfaccia del repository mostrato era però fortemente incompleta, la più grande mancanza è sicuramente un metodo con cui recuperare gli oggetti in base ad una condizione esposta dal chiamante. Quando si parla di repository e funzionalità di ricerca la prima cosa che viene in mente è il pattern Query Object. Implementare un pattern queryobject non è però banale, inoltre nhibernate ha un modello chiamato Criteria Query che è già completo e funzionale…per cui si rischia di riscrivere la ruota. Un Query Object è infatti un oggetto che al suo interno incapsula il concetto di una query, per realizzare una condizione via codice si configura un query object e lo si passa al repository.

Costruire un query object implica realizzare tutta una serie di funzioni e di criteri, ma la parte più noiosa ed error prone è senza dubbio la traduzione del proprio query object nel tipo di query object utilizzato dall'orm impiegato.

Il problema di questa scelta è che per effettuare la traduzione dal proprio query object a quello dell'orm è solitamente necessario parecchio codice, personalmente quindi preferisco una strada alternativa. Come prima cosa definisco un'interfaccia chiamata IQueryBuilder che permette di costruire una query.

public interface IQueryBuilder {

   IQueryBuilder Equal(String propertyName, object value);

   IQueryBuilder LessThan(String propertyName, object value);

   IQueryBuilder Greater(String propertyName, object value);

}

In questo caso ho messo solamente tre condizioni e inserendo più condizioni è implicito l'operatore and, nondimeno questa interfaccia è sufficiente per spiegare la tecnica utilizzata. Successivamente è necessario avere definito da qualche parte una lista di delegate per procedure e funzioni, questo può essere un esempio:

namespace RepositoryGeneric.FunctionDefinition {

   

   public delegate void Proc();

   public delegate void Proc<T>(T param1);

   public delegate void Proc<T1, T2>(T1 param1, T2 param2);

}

Anche in questo caso sono presenti come esempi solamente tre delegate. La funzione nell'interfaccia IRepository che permette di fare una ricerca con criteri è definita in questo modo.

IList<T> GetByQuery(Proc<IQueryBuilder> configurator);

Praticamente la GetByQuery() non accetta un query object come da manuale, ma invece accetta un delegate che ha il compito di configurare la query tramite un oggetto QueryBuilder. In questo caso quindi io non definisco un mio Query Object, ma definisco un interfaccia (IQueryBuilder) in grado di configurare un query object e fornisco la funzione di configurazione al metodo del repository. A questo punto debbo definire la classe concreta per la IQueryBuilder per il mio repository Nhibernate in questo modo.

public class QueryBuilder<T> : IQueryBuilder {
   
private ICriteria _criteria;
 
   
public QueryBuilder(ISession _session) {
      _criteria = _session.CreateCriteria(
typeof (T));
   }
 
   
internal IList<T> Execute() {
      
IList<T> result = _criteria.List<T>();
      
return result;
   }
 
   #region IQueryBuilder Members
 
   
public IQueryBuilder Equal(String propertyName, object value) {
      _criteria.Add(
Expression.Eq(propertyName, value));
      
return this;
   }
 
   
public IQueryBuilder LessThan(String propertyName, object value) {
      _criteria.Add(
Expression.Lt(propertyName, value));
      
return this;
   }
 
   
public IQueryBuilder Greater(String propertyName, object value) {
      _criteria.Add(
Expression.Gt(propertyName, value));
      
return this;
   }
 
   #endregion
}

Come si può vedere l'implementazione è veramente banale perché ogni funzione non fa altro che delegare la configurazione all'oggetto criteria di nhibernate. Il QueryBuilder necessita nel costruttore di un oggetto sessione utilizzato per creare un ICriteria e possiede un metodo internal che esegue il criterio ritornando i dati desiderati. L'ultimo passo è creare la funzione nel repository concreto.

public IList<T> GetByQuery(Proc<IQueryBuilder> configurator) {
   
QueryBuilder<T> qb = new QueryBuilder<T>(_session);
   configurator(qb);
   
return qb.Execute();
}

Non si può fare a meno di apprezzare la compattezza della funzione, si crea un QueryBuilder basato sulla sessione corrente, poi si invoca la funzione di creazione query fornita dal chiamante ed infine si invoca il metodo Execute dell'oggetto QueryBuilder stesso per eseguire la query. Con questa tecnica non devo creare il mio Query Object, non devo creare le funzionalità di traduzione dal mio query object all'oggetto ICriteria di nhibernate ed ho scritto veramente poco codice. L'ultimo spezzone di codice mostra come viene vista questa funzionalità dall'utilizzatore, realizzando una versione migliore della funzione del servizio che imposta uno sconto ai clienti di una certa città.

public IList<String> BetterSetDiscount(String cityname, Single amount) {
   
IList<Customer> AllCustomers = _customerRepo.GetByQuery(
      
delegate(IQueryBuilder qb) {
         qb.Equal(
"AddressInfo.City", cityname);
      });
   
List<String> reslist = new List<string>();
   
foreach (Customer c in AllCustomers) {         
         c.SetDiscount(amount);
         reslist.Add(c.Id);
   }
   
return reslist;
}

Grazie ai delegate anonimi la sintassi è coincisa, il chiamante invoca la GetByQuery del repository e gli passa un delegate che configurerà il QueryObject per specificare il criterio.

Vediamo ora come effettuare un test di questa nuova funzionalità del servizio, questa volta il test è veramente succoso perché stiamo spremendo le funzionalità di RhinoMock al limite.

   85 [Test]
   86 public void TestBetterAddDiscount() {
   87    IRepository<Customer> mockrepo = mockery.CreateMock<IRepository<Customer>>();
   88    IQueryBuilder querybuilder = mockery.CreateMock<IQueryBuilder>();
   89 
   90    IList<Customer> Ret = new List<Customer>();
   91    Ret.Add(mockery.PartialMock<Customer>());
   92    Ret.Add(mockery.PartialMock<Customer>());
   93 
   94    using (mockery.Record()) {
   95       Expect.Call(Ret[0].SetDiscount(20)).Return((Single)20.0);
   96       Expect.Call(Ret[1].SetDiscount(20)).Return((Single)20.0);
   97       Expect.Call(querybuilder.Equal("AddressInfo.City""ROME"))
   98          .Return(querybuilder);
   99       Expect.Call(mockrepo.GetByQuery(null))
  100          .Constraints(new PredicateConstraint<Proc<IQueryBuilder>>(
  101          delegate  (Proc<IQueryBuilder> builder) {
  102             builder(querybuilder);
  103             return true;
  104          }))
  105          .Return(Ret);
  106 
  107    }
  108    Ret[0].AddressInfo = new AddressInfo();
  109    Ret[0].Id = "ALFIO";
  110    Ret[0].AddressInfo.City = "ROME";
  111    Ret[1].AddressInfo = new AddressInfo();
  112    Ret[1].Id = "BERTO";
  113    Ret[1].AddressInfo.City = "ROME";
  114 
  115    mockery.ReplayAll();
  116 
  117    using (mockery.Playback()) {
  118       CustomerService service = new CustomerService(mockrepo);
  119       IList<String> result = service.BetterSetDiscount("ROME", 20);
  120       Assert.AreEqual(2, result.Count);
  121       Assert.IsTrue(result.Contains("ALFIO"));
  122       Assert.IsTrue(result.Contains("BERTO"));
  123    }
  124 }

Come prima cosa si deve notare che non ho solamente creato un mock per il repository, ma anche per l'interfaccia IQueryBuilder. La parte veramente interessante del test è compresa nelle righe 94-105 in cui imposto le Expectations. Nella riga 97 chiedo a Rhino.Mock di verificare che venga chiamato il metodo Equal dell'oggetto IQueryBuilder e nella riga 99 inizia la definizione dell'expectation per il repository, che è decisamente complessa. Per prima cosa richiedo che venga chiamato il metodo GetByQuery ma passo null come argomento, perché il criterio di match dell'argomento viene specificato nella riga successiva con l'istruzione Constraints. Un constraints è un oggetto della libreria Rhino.Mock che permette di impostare una condizione di verifica particolare sui parametri della funzione su cui si sta facendo l'expectation. Nel nostro caso la funzione GetByQuery accetta un parametro per questo io ho un solo constraints. Il parametro che viene passato è il famoso delegate alla funzione di configurazione del QueryBuilder, per questa ragione io utilizzo un PredicateConstraint che permette di inviare a Rhino.Mock un predicato per verificare la correttezza del parametro. Il predicato è di tipo Predicate<Proc<IQueryBuilder>> perché deve essere in grado di verificare un delegate. Il predicato che io passo è a sua volta un delegate anonimo con cui semplicemente invoco il delegate originale che viene passato al mock e ritorno true. La vera expectation viene dal fatto che io passo il mock dell'oggetto IQueryBuilder al delegate passato dal servizio al repository, in questo modo il mock può verificare che il metodo Equal venga chiamato con i giusti parametri.

In sostanza accade questo, il servizio crea il suo delegate anonimo che configura la query e lo passa all'oggetto mock del repository, questo oggetto mock vede che io ho impostato un PredicateConstraint e quindi lo invoca passando il delegate anonimo fornito dal servizio, nel mio predicato io invoco questo delegate passando il mock dell'IQueryBuilder ed il gioco è fatto.

Con questo test io sono in grado di fare

  • Simulare il database fornendo i valori di ritorno
  • Inserire delle Expectation negli oggetti tornati per verificare che effettivamente il servizio li utilizzi nel modo corretto
  • Verificare i valori di ritorno del servizio
  • Verificare che il servizio configuri in maniera corretta la query.

L'ultimo punto è il più importante, la definizione delle query infatti è un processo Looely typed, infatti io specifico un criterio Equal impostando il nome della proprietà e la condizione, ma se invece di scrivere correttamente AddressInfo.City scrivessi AdressInfo.City l'errore si rivela solo a runtime quando nhibernate mi lancia un eccezione perché non riesce a trovare la proprietà nel mapping. Con questo test invece io sono in grado anche di impostare un controllo su come viene costruita la query e verificare anche errori tipografici direttamente con oggetti mock.

Il codice dell'esempio è disponibile qui.

Nei precedenti post si è parlato di repository generici, ma non è sicuramente questo l'unico modo per implementare il repository pattern, come anche ayende fa notare. Una soluzione alternativa è creare un'interfaccia di repository per ogni radice di aggregato, o per ogni oggetto se non si usa la segmentazione del dominio in aggregati. In sostanza invece di avere un'unica interfaccia per il repository avrò ad esempio un'interfaccia del tipo ICustomerRepository, IOrderRepository etc etc. Per minimizzare il codice è possibile avere una interfaccia base IBaseRepository generica che gestisce le operazioni che sono indipendenti dal tipo di oggetto da gestire (Es. Save, Delete, GetByKey, GetAll) lasciando alle interfacce specifiche i metodi legati all'oggetto, solitamente quelli di query.

In questo modo invece di configurare un query object o un IQueryBuilder per passarlo ad un repository generico avrò dei metodi del tipo ICustomerRepository.GetCustomerLivingIn(String city). Questa soluzione ha i suoi pro:

  • Interfaccia "parlante" più esplicita, l'utilizzatore vede subito dall'intellisense come si può recuperare una lista di oggetti customer
  • Test più facili da realizzare

Il secondo punto è particolarmente importante, è infatti molto più semplice scrivere un test quando debbo solamente creare un mock di ICustomerRepository e poi fare ad esempio un expectation sulla chiamata di GetCustomerLivingIn() invece di utilizzare i predicateExpectation sui query object come fatto nel precedente post. Avere un interfaccia "parlante" è comunque un bel vantaggio, soprattutto perché specificare le proprietà nelle query per le proprietà che hanno un path è un procedimento error prone, è facile sbagliarsi ed invece di scrivere AddressInfo.City scrivere invece Addressinfo.City e vedersi generare un bell'errore a runtime. Naturalmente ogni medaglia ha il suo rovescio:

  • Interfacce che tendono ad esplodere
  • Più codice da scrivere

Se si vuole dare la possibilità di recuperare un oggetto imponendo criteri su qualsiasi proprietà non è pensabile di fare interfacce parlanti, tipo GetByCity, GetByCityAndTelephoneNumber, GetBy…. In questo modo ogni interfaccia avrà decine e decine di funzioni, rendendo il tutto assai difficile da mantenere ed utilizzare. Talvolta si crea invece una funzione "catch all" che permette di specificare qualsiasi parametro (Es. CustomerRepository.GetBy(String cyty, String telephoneNumber, …), ma questa tecnica non è soddisfacente. In primo luogo è necessario definire una convenzione con il quale la funzione capisce quali sono i parametri da utilizzare, operazione che può risultare quantomeno complessa. Se si adotta la convenzione che ogni parametro null non deve essere considerato, si costringe l'interfaccia ad utilizzare i nullable object per i value types e si toglie la possibilità di cercare per valori nulli. Con questa convenzione è infatti impossibile cercare un customer che ha null come numero di telefono, ricerca che potrebbe sicuramente avere significato nel dominio.
Un altro svantaggio è che per ogni tipologia di repository è necessario scrivere l'interfaccia e la classe concreta, di fatto aumentando il quantitativo di codice che viene scritto. Una soluzione alternativa è lasciare la funzione GetByQuery() vista nel precedente post nell'interfaccia base del repository e inserire nell'interfaccia specifica solamente quelle funzioni che vengono utilizzate maggiormente dai servizi, in questo modo le interfacce specifiche non "esplodono" mantenendo nel contempo sia la possibilità di effettuare ricerche su ogni proprietà dell'oggetto sia chiamare funzioni strongly typed per effettuare le ricerche più comuni.

Una possibilità alternativa è quella di lasciare il repository completamente generico, ma dotare ogni oggetto di funzioni statiche in grado di creare un Query Object o un Proc<IQueryBuilder> in grado di configurare una ricerca. In questo modo si scriverebbe codice del genere: repository.GetByQuery(Customer.ByCity("ROME")). In questo caso non faccio esplodere l'interfaccia del repository ma sono in grado di definire direttamente nell'entità di dominio le query più utilizzate. Grazie alla keyword partial posso includere tutte queste funzioni statiche in un altro file, e soprattutto essendo statiche non vanno ad influenzare l'oggetto entity. Se si vuole lasciare le entity completamente pulite senza definire nessuna funzionalità infrastrutturale si possono costruire degli oggetti chiamati ad esempio CustomerQueries e definire al loro interno tutte le query che si vogliono creare.

L'interfaccia del repository generico ha un punto che non è soddisfacente: la specifica delle query. Se ad esempio si volesse cercare un customer che abita a "Sao Paulo" e il cui nome contenga una lettera "a" si può invocare il repository in questo modo.

IList<Customer> AllCustomers = repo.GetByQuery(
   
delegate(IQueryBuilder qb) {
      qb.Equal(
"AddressInfo.City""Sao paulo")
         .Like(
"CompanyName""%a%");
   });

In primo luogo la specifica del nome delle proprietà con delle stringhe è error prone come detto precedentemente, ma in generale il problema maggiore è che partizionando il proprio dominio in aggregati non è architetturalmente corretto che il chiamante possa specificare al repository criteri di ricerca che possono essere basati su oggetti aggregati, come ad esempio l'oggetto Address. Come ultimo punto talvolta capita che alcune regole di business siano particolarmente complesse, ad esempio potrei definire un "gold customer" come un cliente che abbia fatto almeno 30 ordini nell'ultimo anno con un importo totale superiore ai 10.000€, in questo caso è consigliabile che l'oggetto root (in questo caso il cliente) definisca direttamente un criterio (Ad esempio GoldCustomer) che può essere utilizzato all'esterno senza sapere effettivamente come è costruito il criterio al suo interno. Una soluzione possibile è creare un oggetto query in questo modo.

    8 public class ChainQuery {
    9 
   10    private List<Proc<IQueryBuilder>> _chainOfQueries = new List<Proc<IQueryBuilder>>();
   11 
   12    public ChainQuery Add(Proc<IQueryBuilder> query) {
   13       _chainOfQueries.Add(query);
   14       return this;
   15    }
   16 
   17    /// <summary>
   18    /// Build the chain of query
   19    /// </summary>
   20    /// <returns></returns>
   21    private Proc<IQueryBuilder> BuildChainQuery() {
   22       return delegate(IQueryBuilder qb) {
   23          foreach (Proc<IQueryBuilder> query in _chainOfQueries) {
   24             query(qb);
   25          }
   26       };
   27    }
   28 
   29    public static implicit operator Proc<IQueryBuilder>(ChainQuery cq) {
   30       return cq.BuildChainQuery();
   31    }
   32 }

Questo oggetto tiene al suo interno una lista di Proc<IQueryBuilder>, ovvero il nostro Query Object ed è in grado di creare un queryobject componendo in AND tutti i criteri inseriti al suo interno. Un operator Implicito di conversione (linea 29) permette inoltre di convertire automaticamente da questo tipo di oggetto ad un Proc<IQueryBuilder>. Armati di questa classe base procediamo a definire una classe statica nel nostro dominio che contiene un riferimento ad una serie di oggetti query, uno per ogni radice di aggregato.

public static class Query {
 
   
public static CustomerQueries Customer {
      
get {return new CustomerQueries();}
   }
}

In questo caso ho solamente l'aggregato Customer, l'oggetto CustomerQueries contiene al suo interno la definizione di tutti i criteri più utilizzati per questo aggregato.

public class CustomerQueries : ChainQuery {
 
   
public CustomerQueries City(String cityName) {
      Add(
delegate(IQueryBuilder qb) {
         qb.Equal(
"AddressInfo.City", cityName);
      });
      
return this;
   }
 
   
public CustomerQueries Name(String name) {
      Add(
delegate(IQueryBuilder qb) {
         qb.Like(
"CompanyName""%" + name + "%");
      });
      
return this;
   }
}

Come si può vedere ogni definizione di criterio torna un riferimento all'oggetto query stesso, in questo modo posso comporre le query in maniera decisamente facile. Ereditando dalla classe ChainQuery abbiamo supporto alla composizione di criteri e la possibilità di usufruire dell'operatore di conversione implicito che ci permette di invocare il repository in questo modo.

IList<Customer> AllCustomers = _repo.GetByQuery(
   
Query.Customer.City("Sao Paulo").Name("a"));

In questo caso il codice è sicuramente più leggibile, non è necessario specificare le proprietà con stringhe e cosa più importante si passa per un oggetto che racchiude tutti i criteri più comuni da utilizzare con l'aggregato desiderato. Il funzionamento è semplice, l'oggetto statico Query crea nella proprietà Customer un nuovo CustomerQueries, a questo punto io posso chiamare tutti i suoi criteri specificando i valori, ogni chiamata torna un riferimento allo stesso oggetto, in questo modo io posso concatenare le chiamate (fowler fluent interface) e fermarmi quando necessario dato che un oggetto CustomerQueries può essere convertito automaticamente in un Proc<IQueryBuilder> e quindi passato al metodo GetByQuery() del repository.

Nei precedenti post (1 2 3 4) ho parlato un po' di repository e naturalmente il cuore di un repository è la metodologia utilizzata per fare le query. Nei precedenti post ho mostrato una tecnica poco intrusiva per specificare i criteri di una query senza dovere costruire un Query Model vero e proprio, ma tramite delegate anonimi. Per chi ha già visto un po' di .NET 3.5 la domanda che ci si pone è "ha senso parlare di querymodel quando il futuro ci riserva LINQ?".

Premesso che non sono assolutamente un esperto di LINQ, sicuramente la domanda ha una risposta sola, probabilmente non avrà più senso di parlare di query model quando il linguaggio fornisce già nativamente una sintassi (LINQ appunto) completamente integrata e con supporto del compilatore. Di LINQ esistono molte versioni, in pratica LINQ è un linguaggio integrato di query che permette di fare query su diversi oggetti. Nativamente LINQ permette di fare query direttamente su collezioni di oggetti tipo List<T>, ma esistono altre implementazioni come il LINQ to SQL che è un ORM vero e proprio, qui potete trovare alcune informazioni. Ma vediamo ora se è possibile dotare il nostro codice di supporto a LINQ con poco sforzo.

Nei post precedenti abbiamo implementato la nostra interfaccia IRepository<T> tramite NHibernate e come si può verificare da questo link, c'è qualcuno nella community che si è dedicato a scrivere il codice per fare LINQ to NHibernate, in pratica una serie di classi che permettono di effettuare query LINQ su una sessione NHIbernate. Nel caso della nostra interfaccia IRepository<T> si potrebbe scrivere un provider del tipo LINQ to repository, ma in generale questa cosa appare assai superflua, una possibilità potrebbe essere la seguente, aggiungere una funzione che permetta di effettuare query LINQ su di un repository.

In pratica la funzione aggiunta restituisce un interfaccia IOrderedQueryable che permette di effettuare query LINQ. Questa soluzione è comoda perché il progetto LINQ to NHIbernate supporta proprio questa modalità. Il nostro repository concreto non deve fare altro che implementare questa funzione.

Come si può vedere il codice è veramente banale perché tutto il vero lavoro viene fatto dalla libreria LINQ to NHibernate. Con questo semplice codice possiamo fare query LINQ sul nostro repository.

Chiaramente questo è solo un primo esempio di come LINQ potrebbe interagire con un nostro IRepository<T>, ma con pochissime righe di codice è già possibile avere un piccolo esempio funzionante grazie a LINQ to Nhibernate.

Alk.

 

Print | posted on mercoledì 5 settembre 2007 13:03 |

Comments have been closed on this topic.

Powered by:
Powered By Subtext Powered By ASP.NET