CacheContainer

Stellt das Grundgerüst für einfaches Caching als abstrakte Basisklasse bereit.
Mit der generischen CacheContainer-Klasse kann kurzerhand mit einem Delegate ein fertiger Cache verwendet werden.

Das Problem:

Sellt euch vor Ihr benötigt zur Laufzeit die Datei ‚ExampleImage.png‘ welche sich auf einem Netzlaufwerk befindet. Je nach Qualität der Verbindung geht der Zugriff mal schneller oder langsamer. Sagen wir mal im Schnitt 1 Sekunde. Wenn Ihr nun auf dieselbe Datei mehrmals, z.b. 10 mal, zugreift sind das schon 10 Sekunden die der User warten muss.

Beispiel:
In meinem Beispiel simuliere ich die 1 Sekunde Wartezeit indem der Thread einfach 1 Sekunde wartet. Außerdem wird für Analysezwecke noch die Dauer aufgezeichnet.

Hier bietet sich Caching an. Das bedeutet das die Datei einmalig geladen und direkt zwischengespeichert wird um bei den nächsten Zugriffen einen erheblichen Geschwindigkeitsvorteil zu haben.

Ist-Situation

Wenn einfaches Caching in Dot.Net gebraucht wird wird dies häufig mit einem Dictionary realisiert. Dafür wird einfach vor dem öffnen des Streams geprüft ob das Bild bereits im Cache enthalten ist (als Key). Sollte dies nicht der Fall sein wird das Bild nach dem Laden zu dem Cache hinzugefügt (wodurch der Key bei der nächsten Prüfung gefunden wird und ein Zugriff auf das Netzlaufwerk nicht mehr nötig ist).

Mit dieser einfachen Änderung wurde ganze 9 Sekunden eingespart.

Wenn der Cache ordentlich implementiert wurde dann erfolgt der Zugriff außschließlich über Zugriffsmethoden und nicht inline (da caching häufig im größeren Rahmen Verwendung findet als nur innerhalb einer Methode). Außerdem wird so die Integrität des Caches gewährleistet.

(in unterem Beispiel wurde die Caching- und Lade-Logik in die Methode 'GetImageFromCache' ausgelagert.)

In der Main-Methode wird nun einfach die eben erstellte Methode aufgerufen (was dort nebenbei die Lesbarkeit des Codes erhöht).

Das ist jetzt nur ein kompaktes Beispiel im kleinen Rahmen. Derartige Caches habe ich bereits sehr oft implementiert und immer separat (was Redunanz zur Folge hatte). Manche Caches mussten dann auch ThreadSafe sein und andere sollten sich selbst bereinigen wenn ein gecachtes Objekt ungültig oder veraltet ist. Außerdem besteht bei obigen Code immer die Gefahr das ein unwissender Kollege das originalle Dictionary direkt manipuliert und mir somit meine Integrität versaut.

Soll-Situation

Als Lösung habe ich das Caching-Verhalten, zusammen mit dem Dictionary, als Klasse gekapselt.
Bevor ich die Details dieser Klasse erkläre möchte ich vorerst das Ergebnis beim Verwenden dieser demonstrieren.

Das private Dictionary-Cache-Feld musste nun einem Property weichen welcher die neue Cache-Klasse bereitstellt. Das Property wird direkt nach betreten der Main-Methode initialisiert. Der Konstruktor erwartet dabei ein Delegate welches einen Key als Parameter und als Result ein Object vom Typ CacheCallbackResult erwartet (dazu später mehr). Kurz erklärt passiert in dem oben erstellten Delegate das gleiche wie in der Methode 'GetImageFromCache', aber mit dem Unterschied das die Prüfung des Methodenvertrags keinen Fehler wirft sondern stattdessen null zurück gibt.

Anschließend wird die Schleife 10mal aufgerufen welche die Methode ‚GetValue‘ von dem Cache-Objekt aufruft.

Von der Code-Menge gesehen hat sich wenig geändert, aber unter der Haube hat sich sehr viel getan.
Die Zugriffe gewährleisten nun immer das die Integrität erhalten bleibt, ThreadSafe und zusätzliche Methoden mit denen der Cache wieder geleert werden kann.

Die Basis-Klasse

Die Klasse wurde auf zwei Ebenen aufgeteilt. Die Parent-Klasse ist abstrakt und arbeitet ohne Delegate. Sie gewährleistet die Stabilität des Caches für alle erbenden Klassen.

Zwei generische Klassenparameter werden vorausgesetzt (Key und Value).
Das private Feld ‚_sync‘ soll die Klasse ThreadSafe machen und ‚_cacheDict‘ ist das Kernelement der ganzen Klasse und sollte aus dem Beispiel der Ist-Situation noch bekannt sein.

Die Methode ‚GetValue‘ versucht einen Wert anhand des übergebenen Keys zu finden indem sie die Methode ‚TryGetValue‘ aufruft. Sollte der Key nicht erlaubt sein fliegt eine ‚KeyNotFoundException‘-Exception.

‚TryGetValue‘ gibt ebenfalls einen Wert anhand des Keys zurück (über ‚out‘ Parameter) wirft aber keine Exception sondern gibt stattdessen false zurück.

Um einen Wert zu finden wird zuerst im Cache nachgesehen ob er bereits existiert. Wenn nicht wird die abstrakte Method ‚TryGetNonCachedValue‘ aufgerufen.

Um einer erbenden Klasse mehr Kontrolle über den Cache zu geben ohne das dieser gefährdet wird stellen wir weitere Methoden zu Manipulation bereit.

Die überladene ‚ClearCache‘-Methode macht das was Sie sagt. Entweder werden alle Einträge aus dem Cache entfernt oder nur für bestimmte Keys.

‚CacheContainsKey‘ oder ‚CacheContainsValue‘ geben je true zurück bei passender Übereinstimmung.

Zu erwähnen ist das alle vier Methoden das selbe Objekt für lock verwenden welches auch die Methode ‚TryGetValue‘ verwendet. Das führt dazu das jeder asynchrone Zugriff über alle Methoden hinweg synchronisiert wird und damit die Integrität des Caches auch bei asynchroner Verwendung sichergestellt ist.

Außerdem sind die vier Methoden als protected markiert um einer möglichen Child-Klasse Kontrolle über das Objekt zu geben ohne dabei nach außen Funktionalität zu öffnen (ein praktisches Anwendungsbeispiel wird später erläutert).

Cache-Klasse mithilfe von Delegate

Die Child-Klasse überschreibt die abstrakte Methode vom Parent (TryGetNonCachedValue). Dort wird lediglich ein Delegate aufgerufen welches im Konstruktor der Klasse zwingend erforderlich ist.

Das Delegate muss als Parameter den Key (T1) entgegennehmen und den Value gekapselt in der Klasse CacheCallbackResult zurückgeben (dazu später mehr). Is das Result NULL oder aber dessen Property ‚IsKeyValid‘ false ist zu erwarten das der Key im Parameter nicht gültig ist und es kann false zurückgegeben werden.

Da die Child-Klasse nach außen recht geschloßen ist empfehle ich die vier Methoden vom Parent welche protected sind nach außen freizugeben. Dies ist enorm hilfreich für Debugging-Zwecke.

Übrigens wird in der Child-Klasse komplett auf das ‚lock‘-Schlüßelwort verzichten da der Parent sich bereits darum kümmert.

CacheCallbackResult

Welcher Delegatetyp eignet sich am besten wenn man eine Methoden-Signatur benötigt welche einen Parameter und zwei Rückgabewerte verlangt? Bei einer normalen Methode würde man wohl einfach einen out Parameter verwenden was folgender Delegate-Signatur entspricht.

delegate bool TryGetNonCachedValueDelegate(T1 key, out T2 value);

Leider unterstützen Inline-Delegates keine 'out'-Parameter. Es währe sehr schade wenn der CacheContainer keine Inline-Schreibweise unterstützden würde. Daher bin ich zum Entschluss gekommen das man beide Results einfach in folgender Wrapper-Klasse zurückgibt und auf das 'out' verzichtet.

Der erste Parameter 'keyIsValid' enthält die Information ob der Key eine gültige Struktur aufweist (ungültige Keys geben natürlich keinen Wert zurück und werden nicht im Cache gehalten).
Als zweiten, generischen, Parameter 'value' wird der Value erwartet welcher zu dem Key gefunden wurde.

Der komplette Code

Den kompletten Code könnt Ihr unten (mit Kommentaren) finden. Ich habe dort noch zwei Ergänzung in der Klasse ‚CacheContainerBase‘ gemacht welche ebenfalls hilfreich sind.

1. Ein zweiter Cache welcher sich alle Invaliden Keys merkt.
2. Ein Property ‚CanCacheInvalidKeys‘ welches bei false obiges Verhalten unterdrückt.

CacheContainerBase

CacheContainer

 

Themen:

- Themen: C#, Modul