Layerless: Repositories

Na wat abstract gezweef is het nu tijd om de borst nat te maken en in het codebad te duiken. Laten we beginnen met het elimineren van een noodzakelijk kwaad: Repositories. Niet dat deze zo slecht zijn, maar als je constructors er als volgt beginnen uitzien:

public UserController(IUserRepository userRepository,
                      IRoleRepository roleRepository, 
                      IAccessRepository accessRepository, 
                      IExternalLinkRepository externalLinkRepository)

Dan noem ik dat een code smell. Gelukkig hebben we Dependency Injection om ons het leven gemakkelijk te maken, maar elegante code kan je dit bezwaarlijk noemen. Het unit testen ervan is al een hele ramp:

//Arrange
var userRepository = MockRepository.GenerateMock<IUserRepository>();
var roleRepository = MockRepository.GenerateMock<IRoleRepository>();
var accessRepository = MockRepository.GenerateMock<IAccessRepository>();
var externalLinkRepository = MockRepository.GenerateMock>IExternalLinkRepository>();
            
var userController =  UserController(userController,
                                     roleRepository, 
                                     accessRepository, 
                                     externalLinkRepository);

Dat allemaal om een methode te testen die mogelijks maar de helft van die dependencies nodig heeft.

“Ok meneertje beterweet”, hoor ik je al denken, “wat stel je dan voor?” Simpel: we koppelen het opbouwen van de query los van de uitvoering ervan. Aanschouw onze QueryExecutor:

public interface IQueryExecutor
{        
    T One<T>(Guid id);
    void Save>T>(T entity);
    void Delete>T<(T entity);

    IQueryable<T> Query<T>();
    T Query<T>(Query<T> query);
}

’One’, ‘Save’ en ‘Delete’ spreken voor zich. Het wordt pas interessant wanneer we bij de twee Query functions komen. De eerste geeft je IQueryable waarmee een linq query kan opgebouwd worden:

var result = from u in Db.Query<User>()
             where u.BirthDate < DateTime.Now.Date.AddYears(-10)
             select u;

Je kan dus gemakkelijk de queries gaan declareren in de applicatiecode die het resultaat nodig heeft. Dit maakt de boel leesbaarder, maar roept wel herinneringen op van de tijd toen de sql-statements kwistig in de codebehind werden rondgestrooid. Niets houdt ons echter tegen om onze queries nog steeds te gaan groeperen in Repositories. Enkel het uitvoeren van die queries koppelen we los. Dankzij Extension Methods kunnen we dit op een bijzonder elegante manier doen:

public static class UserRepositroy
{
    public static IQueryable<User> GetUsersOlderThan(this IQueryable<User> db, int years)
    {
        return from u in db
               where u.BirthDate < DateTime.Now.Date.AddYears(-years)
               select u;
    }
}

In gebruik ziet dit er als volgt uit:

var users = Db.Query>User>().GetUsersOlderThan(10);

Unit testen zijn een makkie, want we kunnen de QueryExecutor gemakkelijk wegmocken en een in-memory lijstje opbouwen met de inhoud van de database.

Er zijn natuurlijk momenten dat je linq-provider tekort schiet. Om die op te vangen hebben we de tweede query methode. Deze onvangt een Query object dat een implementatie hiervan is:

public abstract class Query<T>
{
    public abstract< IList<T> Execute(ISession session);
}

De query van daarnet zou er bijvoorbeeld als volgt kunnen uitzien:

public class UsersOlderThan : Query<User>
{
    private DateTime _birthDate;
    public UsersOlderThan(int age)
    {
        _birthDate = DateTime.Now.Date.AddYears(-age);
    }

    public override IList<User> Execute(ISession session)
    {
        return  session.CreateCriteria<User>().Add(Expression.Lt("BirthDate", _birthDate)).List&lt;User&gt;();
    }
}

Dit valt opnieuw perfect te unit testen alhoewel je nu wel naar de database zal moeten gaan.

Door het uitvoeren van queries los te trekken, is de Repository teruggebracht tot zijn oorspronkelijke functie: het verzamelen van queries. Dit is eigenlijk niets meer dan het toepassen van het Single responsibility principle. Zo vermijden we dat ons systeem overbelast wordt met eenmalig geïmplementeerde interfaces en nodeloos lange constructors. De boel wordt er simpeler door zonder compromissen te sluiten.

De proof-of-concept code van dit stuk kan je vinden in een Github repository. Commentaar is altijd welkom.