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.
Alk.