Een refactoring met gebruik van drie C# features

Door Daan Stolp In Microsoft

Soms zijn het maar kleine wijzigingen die je code net dat extra beetje cleaner kunnen maken. Een tijdje terug was ik bezig met het implementeren van caching. Hierbij schreef ik regelmatig code die controleert of data in de cache zit. Zo ja, dan wordt deze data teruggegeven. Zo nee, dan wordt de data opgehaald en in de cache geplaatst.

De code ziet er steeds ongeveer zo uit:

public SomeObject GetSomeObject()
 {
 string cacheKey = "SomeCacheKey";
 SomeObject thingToCache;
 if (!cache.TryGet(cacheKey, out thingToCache))
 {
 // do stuff to fetch the data
 // ..
 thingToCache = ...;
 cache.Set(cacheKey, thingToCache);
 }
 return thingToCache;
 }

In dit voorbeeld verwijst cache naar een eenvoudige caching module, die gebruik maakt
van Microsoft’s MemoryCache class. De module implementeert de volgende interface:

bool TryGet<T>(string cacheKey, out T value);
 void Set(string cacheKey, object value);

De TryGet methode probeert een waarde uit de cache te halen via de opgegeven cache
key. Als dit lukt, dan geeft de methode true terug en wijst de gevonden waarde toe aan
de out parameter. Als het niet lukt, dan komt er false terug en blijft de out parameter
leeg. Dit is een relatief eenvoudige manier om het Cache-Aside pattern te
implementeren. Set spreekt voor zich: hiermee schrijf je een object weg in de cache
onder de opgegeven cache key.

Na verloop van tijd merkte ik dat ik dit patroon telkens opnieuw aan het schrijven was. Iedere keer
als gegevens gecacht moesten worden, kwam dit patroon weer terug. Het zijn maar een paar regels,
maar toch zat deze duplicatie me niet lekker. Het volgt namelijk niet het DRY principe: Don’t Repeat
Yourself.

Als je dezelfde code telkens herhaalt, is er een voor de hand liggende refactoring die je kunt
toepassen om de herhaling te voorkomen: Extract Method, oftewel het verplaatsen van de code
naar een aparte methode. Het lastige in dit caching patroon is echter dat er drie zaken variabel zijn.
Voor elk van deze drie zaken moet een generieke oplossing worden verzonnen. Het gaat om:

  1. Een variabele waarde, de cache key
  2. Een variabel type, namelijk het type van het object dat je wilt cachen
  3. Een variabele serie statements, namelijk de logica die je moet uitvoeren om de data op te halen

Gelukkig biedt C# voor elk van deze drie variabele zaken een bijpassende constructie om dit
generiek te kunnen maken:

  1. Waardes kun je heel eenvoudig toekennen aan een variabele die je doorgeeft tussen methodes.
  2. Voor variable types zijn er de generics. Dit zie je ook al in de TryGet methode van de ICache
    interface.
  3. Een serie statements groepeer je in methode, die je met behulp van de Func<T> delegate kunt
    doorgegeven tussen methodes.

Door deze drie constructies te combineren, is het mogelijk om een methode te schrijven die het
bovenstaande patroon op een generieke manier implementeert. Dit is het eindresultaat:

01. public interface ICache
 02. {
 03. T Get<T>(string cacheKey, Func<T> retrieveData);
 04. }
 05.
 06. public class Cache : ICache
 07. {
 08. private static readonly DateTimeOffset CACHE_DURATION =
 DateTimeOffset.UtcNow.AddDays(1);
 09. private MemoryCache memoryCache;
 10.
 11. public Cache()
 12. {
 13. memoryCache = MemoryCache.Default;
 14. }
 15.
 16. public T Get<T>(string cacheKey, Func<T> retrieveData)
 17. {
 18. T value;
 19. if (!TryGet(cacheKey, out value))
 20. {
 21. value = retrieveData();
 22. Set(cacheKey, value);
 21. }
 23. return value;
 24. }
 25.
 26. private void Set(string cacheKey, object value)
 27. {
 28. memoryCache.Add(cacheKey, value, CACHE_DURATION);
 29. }
 30.
 31. private bool TryGet<T>(string cacheKey, out T value)
 32. {
 33. if (memoryCache.Contains(cacheKey))
 34. {
 35. value = (T)memoryCache.Get(cacheKey);
 36. return true;
 37. }
 38. value = default(T);
 39. return false;
 40. }
 41. }

Op regel 16 zie je de cacheKey terugkomen. Deze waarde wordt als argument meegegeven aan de
methode.

Het tweede argument van de methode is een delegate. Op deze manier hoeft de methode niet te
weten hoe hij aan de data moet komen die gecacht moet worden. Je kunt dit overlaten aan de code
die deze methode aanroept, die dit als een Func<T> meegeeft.

Regel 18 definieert een lege ‘placeholder’ voor de gecachte data. Deze wordt in regel 19 gevuld
door de TryCache methode als de data in de cache zit, of in regel 38 door een standaard waarde als
de data niet in de cache zit, met behulp van C#’s default keyword.

Regels 21 en 22 worden alleen uitgevoerd als de waarde niet in de cache zit. Hier wordt de
meegegeven Func<T> aangeroepen om de data op te halen, en het resultaat ervan wordt
opgeslagen in de cache in regel 22.

Verder zie je het gebruik van generics terug. Door zowel het return type als het type van de Func<T> generiek te maken, is deze methode te gebruiken voor ieder mogelijk type dat je maar wilt cachen.

En met deze constructie is het patroon netjes geïsoleerd in de nieuwe Get methode. De aanroep
ziet er als volgt uit:

return cache.Get("MyCacheKey", () => {
 SomeObject data = ...; // Write logic to fetch the data when it is not in
 the cache.
 return data;
 });

Hiermee is een patroon van 4 regels dat zich telkens herhaalt, teruggebracht tot één enkele
methode-aanroep.

Meer informatie

daan-stolp

Daan Stolp

.NET Developer

+31 6 52 01 51 53 Stuur Daan een e-mail

Reacties

Er zijn nog geen reacties op dit bericht.

Plaats een reactie

Dit veld is verplicht.

Vul een geldig e-mailadres in.

Dit veld is verplicht.