Laden...

DependencyInjection: "statische" Caches

Erstellt von Palladin007 vor 5 Jahren Letzter Beitrag vor 5 Jahren 1.253 Views
Palladin007 Themenstarter:in
2.078 Beiträge seit 2012
vor 5 Jahren
DependencyInjection: "statische" Caches

Guten Abend,

ich sehe immer wieder, dass Klassen statische Objekte enthalten, wie z.B. ein Dictionary um Reflection-Ergebnisse zu cachen. Prinzipiell spricht da ja nichts gegen, aber streng genommen kann so ein Cache die Ergebnisse von UnitTests verfälschen, da viele UnitTests auf den selben Cache zugreifen.

Daher mein Gedanke:
Ein globaler, undefinierter Cache, der im DI-Container als Singleton gehalten wird und den sich jede Klasse holen kann.

Ich verwende in meinem Beispiel Microsoft.Extensions.DependencyInjection und ich hab mir das Konzept von Microsoft.Extensions.Logging abgeschaut, durch das ich Services mit jedem beliebigen generischen Parameter injekten kann.

Die Interfaces:

public interface ICache
{
    TValue Get<TValue>(Func<TValue> getNew, [CallerMemberName] string name = null);
    // Hier könnte eine "Set"-Methode gut passen, das hab ich für das Beispiel aber weg gelassen
}
public interface ICache<TOwner> : ICache
{
}
public interface ICacheProvider
{
    ICache GetCache<TOwner>();
}

Die Implementierung:

// Diese Klasse erlaubt mir, nur "ICache<TOwner>" als Konstruktor-Parameter zu nutzen, da sie sich über den CacheProvider den echten Cache holt
public class Cache<TOwner> : ICache<TOwner>
{
    private readonly ICache _cache;

    public Cache(ICacheProvider cacheProvider)
    {
        _cache = cacheProvider.GetCache<TOwner>();
    }

    public TValue Get<TValue>(Func<TValue> getNew, [CallerMemberName] string name = null)
    {
        return _cache.Get(getNew, name);
    }
}
public class DictionaryCache : ICache
{
    private readonly ConcurrentDictionary<string, object> _cache;

    public DictionaryCache()
    {
        _cache = new ConcurrentDictionary<string, object>();
    }

    public TValue Get<TValue>(Func<TValue> getNew, [CallerMemberName] string name = null)
    {
        return (TValue)_cache.GetOrAdd(name, _ => getNew());
    }
}
public class DictionaryCacheProvider : ICacheProvider
{
    private readonly ConcurrentDictionary<Type, ICache> _caches;

    public DictionaryCacheProvider()
    {
        _caches = new ConcurrentDictionary<Type, ICache>();
    }

    public ICache GetCache<TOwner>()
    {
        return _caches.GetOrAdd(typeof(TOwner), _ => new DictionaryCache());
    }
}

Nutzen kann man es so:

class Base
{
    private readonly ICache _cache;

    public string Value => _cache.Get(GetValue);

    public Base(ICache cache)
    {
        _cache = cache ?? throw new ArgumentNullException(nameof(cache));
    }

    private string GetValue()
    {
        var value = GetType().Name;
        Console.WriteLine("Get: " + value);
        return value;
    }
}
class A : Base
{
    public A(ICache<A> cache)
        : base(cache)
    {
    }
}
class B : Base
{
    public B(ICache<B> cache)
        : base(cache)
    {
    }
}
class C : Base
{
    public C(ICache<C> cache)
        : base(cache)
    {
    }
}

class Program
{
    static void Main(string[] args)
    {
        var serviceProvider = new ServiceCollection()
            .AddSingleton<ICacheProvider, DictionaryCacheProvider>()
            .AddSingleton(typeof(ICache<>), typeof(Cache<>))
            .BuildServiceProvider();

        var a = ActivatorUtilities.CreateInstance<A>(serviceProvider);
        var b = ActivatorUtilities.CreateInstance<B>(serviceProvider);
        var c = ActivatorUtilities.CreateInstance<C>(serviceProvider);

        Console.WriteLine(a.Value);
        Task.Run(() => Console.WriteLine(a.Value)).Wait();

        Console.WriteLine(b.Value);
        Task.Run(() => Console.WriteLine(b.Value)).Wait();

        Console.WriteLine(c.Value);
        Task.Run(() => Console.WriteLine(c.Value)).Wait();

        Console.ReadKey();
    }
}

Vorteile, die ich sehe:

Ich kann jedem UnitTest sozusagen sein eigenes "static" bereitstellen.
Ich kann bei Bedarf diese Caches zurück setzen und dadurch - warum auch immer - jede Klasse dazu zwingen, die Caches neu zu füllen.

Man könnte auch noch weiter gehen und einen Thread-spezifischen Cache ermöglichen.

Was haltet Ihr von dem Gedanken?

Beste Grüße

16.807 Beiträge seit 2008
vor 5 Jahren

Das Grundkonstrukt ist nichts neues; verwende ich ebenfalls für gewisse Dinge - sofern sich der Aufwand lohnt.
Als fiktives Beispiel: generischen Repositories sehe ich es zB. nicht als lohnenswert an; da würde ich nicht IRepository<Person> sondern IPersonRepository weiterhin verwenden.

Im Falle von (statischem) Caching sehe ich es zwiespalten; vor allem bei Reflection.
Wie willst Du bei getrennten Instanzen sicherstellen, dass die gleiche Reflection nicht in verschiedenen Code-Bereichen doppelt im Cache liegt?
Sehe ich in Deinem Konstrukt in dieser Form nicht (ohne weiteres) lösbar.

Ich habe nochmal geschaut, wie ich Caching selbst implementiere; und ich habe hier nichts statisch.
Entweder ich habe Felder und die Klasse selbst wird als Singleton deklariert - oder ich kippe das Caching von oben rein.
Damit wären so oder so meine Unit Tests nicht negativ von betroffen.

Ich denke, dass sich die Anwendungsszenarien dafür in Grenzen halten.

Palladin007 Themenstarter:in
2.078 Beiträge seit 2012
vor 5 Jahren

Der Gedanke betrifft nur das, was normalerweise statisch wäre, nur darum geht es mir.
Für alles, was instanzbezogen funktionieren soll, ist statisch sowieso selten eine gute Idee.

In vielen Fällen, wo ich Reflection verwende, bereite ich die Reflection-Daten vorher statisch vor, sprich: Ich suche z.B. die MethodInfo, die ich später ausführen will.
Wenn ich so drüber nachdenke, ist Reflection der einzige Fall, der mit einfällt 😄

Ich sehe keinen großen Aufwand oder Nachteil darin, das so zu verwenden, ich kann nur nicht ganz abschätzen, ob das langfristig sinnvoll ist.

16.807 Beiträge seit 2008
vor 5 Jahren

Evtl. ist das ganze hier auch eine Lösung für ein spezifisches Problem, das es evtl gar nicht gibt? 😉

Ich meine; es gibt kein Grund für ein statisches Cachen. Problemlos kann man das entsprechend injizieren, um dies zu steuern...
Vermutlich machen das die meisten unbewusst aufgrund von KISS (oder aus Faulheit) 😃

Aber ja, an für sich ist der Pattern interessant - für gewisse Dinge.

Palladin007 Themenstarter:in
2.078 Beiträge seit 2012
vor 5 Jahren

Ich bin gerade Mal die größten Beispiele, die mir so eingefallen sind, durchgegangen. Dabei ist mir aufgefallen, dass deine Aussage...

[...] ich habe Felder und die Klasse selbst wird als Singleton deklariert [...]

... in beinahe jedem Fall 100% zutrifft 😄

Ich behalte das Mal im Hinterkopf, vielleicht stoße ich doch noch auf eine Situation, wo das sinnvoll ist.
Und ich danke dir für deine Hilfe 😃