Het nut van abstracties

 
09 maart 2012

Ontwikkelaars zijn dol op abstracties. Hoe meer, hoe beter.

Een abstractie versimpelt de code, ten koste van controle. Het enige dat je kunt te weten is dát er iets gedaan of opgevraagd kan worden. Het mag niet uitmaken hoe het gebeurt.

Opvragen van de huidige gebruiker

Er zijn genoeg plekken in je applicatie te vinden waar het nodig is om te weten wie de huidige gebruiker is. De authenticatiegegevens staan misschien in een cookie. De gegevens in zo’n cookie zijn uiteraard versleuteld. Er zijn flink wat stappen te verzinnen om bij de huidige gebruiker te komen. Alleen in de implementatie wil ik me bezighouden met met AES-versleuteling, cookies, http requests. Nergens anders

In de rest van de applicatie verlies ik volledige controle over hoe het vaststellen van de huidige gebruiker achter de schermen gebeurt. In ruil daarvoor krijg ik wel een simpele abstractie voor terug. In dit geval is dat iets om na te streven.

public interface CurrentUserFacade
{
    User CurrentUser { get; }
}

public interface EncryptionService
{
    string Encrypt(string decrypted);
    string Decrypt(string encrypted);
}

public class AesEncryptionService : EncryptionService
{
    public string Encrypt(string decrypted)
    {
        return ...;
    }

    public string Decrypt(string encryped)
    {
        return ...;
    }
}

public class CookieBasedCurrentUserFacade : CurrentUserFacade
{
    private const string CookieName = "AuthCookie";

    public CookieBasedCurrentUserFacade(EncryptionService encryptionService,
                                        QueryFactory queryFactory)
    {
        EncryptionService = encryptionService;
        QueryFactory = queryFactory;
    }

    public void Initialize(IDictionary<string, string> cookies)
    {
        if (!cookies.ContainsKey(CookieName))
        {
            CurrentUser = new AnonymousUser();
            return;
        }

        var cookie = cookies[CookieName];
        var authenticationToken = EncryptionService.Decrypt(cookie);
        var user =
            QueryFactory.Create<UserByAuthenticationTokenQuery>()
                .AuthenticationToken(authenticationToken)
            .SingleResult();
        CurrentUser = user;
    }

    private EncryptionService EncryptionService { get; set; }
    private QueryFactory QueryFactory { get; set; }

    public User CurrentUser { get; private set; }
}

public class Global : HttpApplication
{
    protected void Application_BeginRequest(object sender, EventArgs e)
    {
        var currentUserFacade = IoC.Resolve<CookieBasedCurrentUserFacade>();
        currentUserFacade.Initialize(Request.Cookies.AsDictionary());
    }
}

public class CustomerController
{
    public CustomerController(CurrentUserFacade currentUserFacade,
                              ISessionFactory sessionFactory)
    {
        CurrentUserFacade = currentUserFacade;
        SessionFactory = sessionFactory;
    }

    public void AddCustomer(string name)
    {
        var session = SessionFactory.GetCurrentSession();
        using (var tx = session.BeginTransaction())
        {
            var user = CurrentUserFacade.CurrentUser;
            var customer = new Customer(name, user);

            session.Save(customer);
            tx.Commit();
        }
    }

    private CurrentUserFacade CurrentUserFacade { get; set; }
    private ISessionFactory SessionFactory { get; set; }
}

De versimpeling vind je overigens ook in je testen terug. Geen complexe setup meer. Omdat ik in mijn CustomerController niet mag weten hoe het geimplementeerd is, is een simpele mock dus ook goed.

Repositories als abstractie

Niet alle abstracties volgen dit principe. Veel projecten die NHibernate gebruiken, zetten om de ISession een abstractie in de vorm van een repository. In een repository verlies ik ook veel controle:

  • Moet deze query worden gecached?
  • Moet een bepaalde associatie niet worden ge-lazy-load? (Het bekende SELECT N+1 probleem)
  • Moet er een filter op een collectie komen? (Een one-to-many met potentieel duizenden entiteiten waarvan je er maar een paar wilt gebruiken)
  • Moet de sessie niet worden geflushed? (Data-intensieve processen waar niets wordt aangepast, bv. het genereren van een CSV-bestand)

De antwoorden op dit soort vragen staan in je repository implementatie. Je kunt je afvragen of dat de plek is waar je de beslissing kunt maken.

Als je applicatie 40 queries uitvoert per request i.p.v. 1 query, dan zal de klant je snel duidelijk maken dat die abstractie misschien beter niet gemaakt had kunnen worden.

Maar je krijgt er geen versimpeling voor terug:

customerRepository.FindById(1);

vs.

session.Get<Customer(1);

Het “ik kan dan makkelijk van ORM switchen”-argument geldt overigens niet, omdat je dat toch nooit zult doen en je zult merken dat NHibernate te zwaar is om als implementatie-detail weggezet te worden.

De wet van de Leaky Abstractions

“All non-trivial abstractions, to some degree, are leaky.”

Een abstractie is vaak bedoeld om implementatiedetails weg te werken. Soms is een implementatie zo zwaar, dat je er niet aan ontkomt:

var customer = customerRepository.FindById(1);
customer.ChangeAddress(...);
customer.MakePreferred();

if (customer.IsValid)
{
    customerRepository.Update(customer);
}

Als de customer in een ongeldig state zit, worden de wijzigingen dan opgeslagen in database? Als de implementatie ADO.NET is, dan niet. Als de implementatie NHibernate is, dan wel.

Abstracties kunnen het leven een stuk makkelijker maken, maar vergeet de wet van de Leaky Abstractions niet.


Werken met ?
Kijk dan bij onze mogelijkheden voor zowel starters als ervaren engineers.


Categorieën: Development