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

Gestire un repository Generico [1]

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.

Alk.

Print | posted on domenica 5 agosto 2007 13:05 | Filed Under [ DDD ]

Feedback

Gravatar

# re: Gestire un repository Generico [1]

In generale HQL mi lega a nhibernate e quindi perdo la generalità che ho nel repository. In generale le Criteria Query funzionano meglio quando devi creare un criterio programmaticamente perchè ti evita di dovere concatenare delle stringhe.

Con il pattern Repository Generico LINQ lo vedo bene, appena ho tempo ci faccio una considerazione.

Alk.
09/08/2007 17:41 | Gian MAria
Comments have been closed on this topic.

Powered by:
Powered By Subtext Powered By ASP.NET