Mittendrin.

Zurück

Flurfunk der eXperts.

Hier erfahren Sie mehr über eXperts, Technologien und das wahre Leben in der SDX.

Lazy Proxies mit Reflection.Emit

25.04.201208:35 Uhr , Sebastian Weber

Man kommt immer wieder in die Situation, eine bestimmte Funktionalität in einer Serviceklasse zu kapseln (z.B. den Zugriff auf eine Datenbank oder das Dateisystem). Eine solche Serviceklasse on-demand zu instanziieren kann Ressourcen sparen. Doch das Wissen um den Aufwand, den eine Objekterzeugung benötigt, sollte nicht bei allen Konsumenten des Diensterbringers aufschlagen. Eine für den Verwender vollkommen transparente Möglichkeit, diese Information auf die Konfiguration der Anwendung zu beschränken, bietet die dynamische Erzeugung von Klassen mittels Reflection.Emit.

Angenommen ich habe einen Service, beschrieben durch das Interface IMyContract und implementiert in der Klasse MyService. Die Instanziierung von MyService ist, aus welchen Gründen auch immer, sehr zeitaufwendig und/oder ressourcenfressend. Außerdem wird dieser Service zwar an mehreren Stellen benötigt, aber nicht unbedingt in jedem Fall. Z.B. könnte ein MessageHandler in Abhängigkeit vom Inhalt einer Nachricht diesen oder jenen Service aufrufen. Ich möchte also keine fertige Instanz des Service in alle potentiellen Konsumenten injizieren, weil der Aufwand zur Erzeugung des Service häufig nicht gerechtfertigt ist.

Eine Lösung wäre, eine Factory über den Konstruktor zu injizieren. Dies kann im einfachsten Fall ein Delegate vom Typ Func<IMyContract> sein. Diesen kann ich nutzen, um mir im Fall der Fälle eine Instanz des Service zu besorgen. Verwendet man jetzt noch Lazy<T>, dann kann man das Ganze sogar recht hübsch machen.

   1:  public class MyConsumer
   2:  {
   3:    private readonly Lazy<IMyContract> lazy;
   4:    public MyConsumer(Func<IMyContract> serviceFactory)
   5:    {
   6:      this.lazy = new Lazy<IMyContract>(serviceFactory);
   7:    } 
   8:    public IMyContract Service
   9:    {
  10:      get { return this.lazy.Value; }
  11:    } 
  12:    public void DoSomething()
  13:    {
  14:      this.Service.DoSomething();
  15:    }
  16:  }

Das hat aber einen entscheidenden Nachteil: Ich injiziere eine Factory, weil ich weiß, dass meine Implementierung von IMyContract teuer in der Erzeugung ist. Eine andere Implementierung hat diesen Pferdefuß vielleicht gar nicht. Das ist eine sogenannte leaky abstraction. Das Wissen über eine Implementierung steuert, wie diese Implementierung verwendet wird. Dadurch kann ich sie nicht mehr beliebig austauschen.

Eine saubere Lösung würde MyConsumer einfach eine Implementierung von IMyContract übergeben. Wir haben keine leaky abstraction mehr. Pattern eingehalten, Soll erfüllt.

Die Erzeugung von MyService ist aber nun mal teuer. Also schieben wir diese akademische Diskussion beiseite und wählen einen “pragmatischen” Ansatz.

Allerdings entstehen Pattern und Best Practices nicht ohne Grund. Darum lohnt es sich, hier ein wenig mehr Hirnschmalz zu investieren.

Wenn mir mein DI-Container einen Proxy generieren würde, der im Kern eine Kapselung von Lazy<T> ist, und der MyService wirklich nur dann instanziiert, wenn er tatsächlich gebraucht wird, hätte ich das Performance-Problem gelöst, die leaky abstraction vermieden und das Wissen um die Kosten von MyService wäre auf die Konfiguration des Containers beschränkt (also genau da, wo man solche Entscheidungen auch treffen sollte).

Mit den Interception-Mechanismen von Unity oder Castle DynamicProxy kann man sich etwas derartiges zurechtbiegen. Aber schön ist die Lösung nicht und schneller wird die Anwendung dadurch auch nicht. Wrapper für jeden Service in einer Anwendung von Hand zu kodieren oder mit Hilfe von T4-Templates zu generieren erfordert immer noch manuelle Eingriffe durch den Entwickler.

Doch das muss nicht sein! Durch den Einsatz von Reflection.Emit kann man den IL-Code für die Wrapper generieren. Damit kombiniert man die Performance der handgefertigten Wrapper mit dem Komfort der dynamisch erzeugten Proxies.

Einen guten Einstieg in die IL-Programmierung bieten zwei Artikel auf The Code Project.

Den Wrapper kann man einmal von Hand kodieren und sich dann mittels Reflector den zugehörigen IL Code anschauen. Danach geht es daran herauszufinden, wie sich etwas gleichwertiges generieren lässt. Das ist nicht ganz einfach und läuft im Endeffekt auf jede Menge trial-and-error hinaus. Aber das Ergebnis lohnt den Aufwand.

Der generierte Proxy sieht in etwa folgendermaßen aus:

   1:  public class MyContract_LazyInstantiationProxy : IMyContract
   2:  {
   3:    private readonly Lazy<IMyContract> lazy;
   4:    public MyConsumer(Func<IMyContract> serviceFactory)
   5:    {
   6:      this.lazy = new Lazy<IMyContract>(serviceFactory);
   7:    } 
   8:    public IMyContract Service
   9:    {
  10:      get { return this.lazy.Value; }
  11:    } 
  12:    public void DoSomething()
  13:    {
  14:      this.Service.DoSomething();
  15:    }
  16:  }

Er implementiert IMyContract. Kapselt die Erzeugung des tatsächlichen Diensterbringers in einem Lazy<IMyContract> und delegiert einfach die eingehenden Aufrufe. Durch eine Extension-Methode für IUnityContainer gestaltet sich die Registrierung am Container denkbar einfach:

   1:  IUnityContainer container = new UnityContainer();
   2:  container.RegisterLazyProxy(
   3:    x =>
   4:      {
   5:        x.Contract = typeof(IFooService);
   6:        x.ServiceImplementation = typeof(FooService);
   7:      });

Der Proxy muss keine Performanceverluste durch die Interception-Infrastruktur hinnehmen. Er ist genauso schnell wie selbstgeschriebener Code. Die Implementierung von IMyContract kann weiterhin beliebig ausgetauscht werden, es ist lediglich eine kleine Änderung in der Konfiguration des Containers notwendig.

Bei Interesse kann der Quellcode zum Beitrag auf CodePlex heruntergeladen werden. Der Generator liegt unter TecX.Unity.Proxies. Die zugehörigen Tests, die seine Verwendung demonstrieren, sind unter TecX.Unity.Proxies.Test zu finden.

0 Kommentare
Dein Kommentar wartet auf Freischaltung.

Artikel kommentieren

Zurück

Tag Cloud


Kontakt aufnehmen


Anrufen

Gerne beantworten wir Ihre Fragen in einem persönlichen Gespräch!


Kontakt aufnehmen

Schreiben Sie uns eine E-Mail mit Ihren Fragen und Kommentaren!