November 2016

Band 31, Nummer 11

.NET Framework: Verdeckte verwerfbare Typen

Von Artak Mkrtchyan | November 2016

Verwerfbare Typen sind toll, da Sie Ihnen erlauben, Ressourcen deterministisch freizugeben. Es gibt jedoch Situationen, in denen Entwickler mit verwerfbaren Typen arbeiten, ohne es zu merken. Das Verwenden von Entwurfserzeugungsmustern ist ein Beispiel einer Situation, in der die Nutzung eines verwerfbaren Typs ggf. nicht offensichtlich ist, die aber dazu führen kann, dass ein Objekt nicht verworfen wird. In diesem Artikel werden Möglichkeiten zur Bewältigung des Problems gezeigt. Anfangen möchte ich mit der Prüfung einiger der Entwurfserzeugungsmuster.

Entwurfserzeugungsmuster

Ein großer Vorteil von Entwurfserzeugungsmustern ist, dass sie eine Abstraktion von den tatsächlichen Implementierungen darstellen und in der Schnittstellensprache „kommunizieren“. Es geht dabei um Objekterzeugungsmechanismen zum Erstellen von Objekten, die für die Lösung geeignet sind. Im Vergleich zur grundlegenden Objekterstellung verbessern Entwurfserzeugungsmuster mehrere Aspekte des Objekterstellungsprozesses. Es folgen die beiden bekannten Vorteile von Entwurfserzeugungsmustern:

  • Abstraktion: Dieses Muster abstrahiert den erstellten Objekttyp, sodass der Aufrufer nicht weiß, was das tatsächlich zurückgegebene Objekt ist, da er nur die Schnittstelle kennt.
  • Interne Aspekte bei der Erzeugung: Bei diesem Muster wird das Wissen um die spezifische Erzeugung einer Typinstanz gekapselt.

Als Nächstes gebe ich einen Kurzüberblick über zwei bekannte Entwurfserzeugungsmuster.

Das Entwurfsmuster „Factorymethode“ Dieses Entwurfsmuster zählt zu meinen bevorzugten und ich nutze es bei meiner täglichen Arbeit umfassend. Bei diesem Muster werden Factorymethoden zum Umgang mit dem Problem der Erstellung von Objekten verwendet, ohne die exakte Klasse des Objekts anzugeben, das erstellt wird. Anstatt einen Klassenkonstruktor direkt aufzurufen, rufen Sie zum Erstellen des Objekts eine Factorymethode auf. Die Factorymethode gibt eine Abstraktion (Schnittstelle oder Basisklasse) zurück, die untergeordnete Klassen implementieren. Abbildung 1 zeigt das UML-Diagramm (Unified Modeling Language) für dieses Muster.

In Abbildung 1 ist „ConcreteProduct“ ein bestimmter Typ der Abstraktion/Schnittstelle „IProduct“. Vergleichbar damit ist „ConcreteCreator“ eine spezifische Implementierung der „ICreator“-Schnittstelle.

Entwurfsmuster „Factorymethode“
Abbildung 1: Entwurfsmuster „Factorymethode“

Der Clientcode dieses Musters nutzt eine „ICreator“-Instanz und ruft seine „Create“-Methode auf, um eine neue Instanz von „IProduct“ abzurufen, ohne zu wissen, welches tatsächliche Produkt zurückgegeben wird.

Das Entwurfsmuster „Abstrakte Factory“ Ziel dieses Entwurfsmusters ist das Bereitstellen einer Schnittstelle zum Erstellen von Gruppen verwandter oder abhängiger Objekte, ohne konkrete Implementierungen anzugeben.

Dies schützt den Clientcode vor dem Aufwand der Objekterstellung, indem der Client das Factoryobjekt auffordern muss, ein Objekt des gewünschten abstrakten Typs zu erstellen und einen abstrakten Zeiger auf das Objekt an den Client zurückzugeben. Das bedeutet insbesondere, dass der Clientcode den konkreten Typ nicht kennt. Er beschäftigt sich ausschließlich mit einem abstrakten Typ.

Das Hinzufügen von Unterstützung für neue konkrete Typen erfolgt, indem neue Factorytypen erstellt werden und der Clientcode so geändert wird, dass bei Bedarf ein anderer Factorytyp verwendet wird. In den meisten Fällen muss dafür nur eine Codezeile geändert werden. Dadurch werden Änderungen offensichtlich erleichtert, da der Clientcode für die Unterstützung des neuen Factorytyps nicht geändert werden muss. Abbildung 2 zeigt das UML-Diagramm für das Entwurfsmuster „Abstrakte Factory“.

Entwurfsmuster „Abstrakte Factory“
Abbildung 2: Entwurfsmuster „Abstrakte Factory“

Aus Sicht des Clients wird die Nutzung der abstrakten Factory mit dem folgenden Codeausschnitt veranschaulicht:

IAbstractFactory factory = new ConcreteFactory1();
IProductA product = factory.CreateProductA();

Der Client darf die tatsächliche Factoryimplementierung ändern, um den Typ des Produkts zu steuern, das im Hintergrund erstellt wird, was keinerlei Auswirkungen auf den Code hat.

Dieser Code ist bloß ein Beispiel. In einem ordnungsgemäß strukturierten Code würde die Instanziierung der Factory selbst wahrscheinlich abstrahiert werden, und zwar mit einem Muster einer Factorymethode als Beispiel.

Problemdefinition

An beiden Beispielen von Entwurfsmustern war eine Factory beteiligt. Eine Factory ist eine tatsächliche Methode/Prozedur, die als Antwort auf den Aufruf eines Clients einen konstruierten Typverweis mittels einer Abstraktion zurückgibt.

Aus technischer Sicht können Sie eine Factory zum Erstellen eines Objekts nutzen, sobald eine Abstraktion vorhanden ist (siehe Abbildung 3).

Beispiel einer einfachen Abstraktion und ihrer Verwendung
Abbildung 3: Beispiel einer einfachen Abstraktion und ihrer Verwendung

Die Factory übernimmt basierend auf den beteiligten Faktoren die Wahl zwischen verschiedenen verfügbaren Implementierungen.

Gemäß dem Prinzip der Umkehr von Abhängigkeiten gilt:

  • Module auf oberen Ebenen dürfen nicht von Modulen auf niedrigeren Ebenen abhängig sein. Beide müssen von Abstraktionen abhängen.
  • Abstraktionen dürfen nicht von Details abhängen. Details müssen von Abstraktionen abhängen.

Dies bedeutet in technischer Hinsicht, dass die Abhängigkeit auf allen Ebenen einer Abhängigkeitskette durch eine Abstraktion ersetzt werden muss. Darüber hinaus kann die Erstellung dieser Abstraktionen mithilfe von Factorys erfolgen (was in vielen Fällen sogar der Fall sein muss).

All dies betont, wie wichtig Factorys bei der alltäglichen Programmierung sind. Doch allerdings wird dabei ein Problem ausgeblendet: verwerfbare Typen. Ehe ich auf die Details eingehe, soll zunächst von der „IDisposable“-Schnittstelle und dem Entwurfsmuster „Verwerfen“ (engl. Dispose) die Rede sein.

Entwurfsmuster „Verwerfen“

Alle Programme rufen während ihrer Ausführung Ressourcen wie Arbeitsspeicher, Dateihandles und Datenbankverbindungen ab. Entwickler müssen bei der Nutzung solcher Ressourcen sorgfältig vorgehen, da die Ressourcen nach Abruf und Verwendung freigegeben werden müssen.

Die Common Language Runtime (CLR) unterstützt die automatische Arbeitsspeicherverwaltung mithilfe des Garbage Collectors (GC). Sie müssen den verwalteten Speicher nicht explizit bereinigen, da dies der GC automatisch übernimmt. Leider gibt es andere Arten von Ressourcen (die als „nicht verwaltete Ressourcen“ bezeichnet werden), die weiterhin explizit freigegeben werden müssen. Der GC ist nicht auf den Umgang mit diesen Arten von Ressourcen ausgelegt, weshalb der Entwickler für ihre Freigabe zuständig ist.

Die CLR hilft jedoch Entwicklern beim Umgang mit nicht verwalteten Ressourcen. Der Typ „System.Object“ definiert eine öffentliche virtuelle Methode namens „Finalize“, die vom GC aufgerufen wird, ehe der Arbeitsspeicher des Objekts freigegeben wird. Die „Finalize“-Methode wird üblicherweise als Finalizer bezeichnet. Sie können die Methode so überschreiben, dass zusätzliche vom Objekt verwendete nicht verwaltete Ressourcen bereinigt werden.

Dieser Mechanismus hat allerdings aufgrund bestimmter Aspekte der GC-Ausführung gewisse Nachteile.

Der Finalizer wird aufgerufen, wenn die GC erkennt, dass ein Objekt für die Bereinigung in Frage kommt. Dies erfolgt nach einem unbestimmten Zeitraum, sobald das Objekt nicht mehr benötigt wird.

Wenn die GC den Finalizer aufrufen muss, muss sie die tatsächliche Speichersammlung auf die nächste Garbage Collection-Runde verschieben. Dadurch wird die Speichersammlung des Objekts noch weiter verschoben. Hier kommt die „System.IDisposable“-Schnittstelle ins Spiel. Das Microsoft .NET Framework bietet die „IDisposable“-Schnittstelle, die Sie implementieren müssen, um dem Entwickler eine Möglichkeit der manuellen Freigabe nicht verwalteter Ressourcen zu bieten. Typen, die diese Schnittstelle implementieren, werden als verwerfbare Typen bezeichnet. Die „IDisposable“-Schnittstelle definiert nur eine Methode ohne Parameter namens „Dispose“. „Dispose“ muss aufgerufen werden, um sämtliche nicht verwalteten Ressourcen freizugeben, auf die die Methode verweist, sobald das Objekt nicht benötigt wird.

Sie fragen sich vielleicht: „Warum soll ich 'Dispose' selbst aufrufen, wenn ich doch weiß, dass die GC das letztlich für mich übernimmt?“ Die Antwort würde einen eigenen Artikel erfordern, der sich auch mit Aspekten der Auswirkung der Ausführung der GC auf die Leistung beschäftigt. Dies zu erläutern, würde jedoch den Rahmen dieses Artikels sprengen, weshalb ich fortfahre.

Beim Bestimmen, ob ein Typ verwerfbar sein soll, müssen bestimmte Regeln befolgt werden. Als Faustregel gilt dabei Folgendes: Wenn ein Objekt eines bestimmten Typs auf eine nicht verwaltete Ressource oder andere verwerfbare Objekte verweist, sollte es auch verwerfbar sein.

Das „Dispose“-Muster definiert eine bestimmte Implementierung für die „IDisposable“-Schnittstelle. Es erfordert, dass zwei „Dispose“-Methoden implementiert werden: eine öffentliche ohne Parameter (die von der „IDisposable“-Schnittstelle definiert wird) und eine zweite, die geschützt und virtuell ist sowie eine einzelnen booleschen Parameter hat. Wenn dieser Typ versiegelt werden soll, muss die geschützte virtuelle Methode freilich durch eine private ersetzt werden.

Abbildung 4: Implementierung des Entwurfsmusters „Verwerfen“

public class DisposableType : IDisposable {
  ~DisposableType() {
    this.Dispose(false);
  }
  public void Dispose() {
    this.Dispose(true);
    GC.SuppressFinalize(this);
  }
  protected virtual void Dispose(bool disposing) {
    if (disposing) {
      // Dispose of all the managed resources here
    }
    // Dispose of all the unmanaged resources here
  }
}

Der boolesche Parameter gibt an, wie die „dispose“-Methode aufgerufen werden soll. Die öffentliche Methode ruft die geschützte mit dem Parameterwert „true“ auf. Vergleichbar müssen die Überladungen der „Dispose(bool)“-Methode in der Klassenhierarchie „base.Dispose(true)“ aufrufen.

Die Implementierung des Musters „Verwerfen“ erfordert auch eine Überladung der „Finalize“-Methode. Dies erfolgt zum Abdecken von Szenarien, bei denen ein Entwickler vergisst, die „Dispose“-Methode aufzurufen, sobald das Objekt nicht mehr benötigt wird. Da der Finalizer von der GC aufgerufen wird, wurden (bzw. werden) die referenzierten verwalteten Ressourcen ggf. bereinigt. Deshalb müssen Sie die Freigabe nicht verwalteter Ressourcen nur dann übernehmen, wenn die „Dispose(bool)“-Methode vom Finalizer aufgerufen wird.

Nach Rückkehr zum Hauptthema tritt das Problem auf, wenn Sie bei Verwenden von Entwurfserzeugungsmustern mit verwerfbaren Objekten zu tun haben.

Stellen Sie sich ein Szenario vor, bei dem einer der konkreten Typen, der die Abstraktion implementiert, auch die „IDisposable“-Schnittstelle implementiert. Lassen Sie uns hier „ConcreteImplementation2“ in meinem Beispiel in Abbildung 5 verwenden.

Abstraktion mit einer „IDisposable“-Implementierung
Abbildung 5: Abstraktion mit einer „IDisposable“-Implementierung

Beachten Sie, dass die „IAbstraction“-Schnittstelle selbst nicht von „IDisposable“ erbt.

Sehen wir uns nun den Clientcode an, in dem die Abstraktion verwendet wird. Da sich die „IAbstraction“-Schnittstelle nicht geändert hat, kümmert sich der Client nicht um etwaige Änderungen im Hintergrund. Logischerweise errät der Client nicht, dass er ein Objekt erhalten hat, für dessen Verwerfen er nun zuständig ist. Realität ist, dass eine „IDisposable“-Instanz hier nicht erwartet wird und dass diese Objekte in vielen Fällen nie vom Clientcode explizit verworfen werden.

Die Hoffnung ist, dass die tatsächliche Implementierung von „Concreate­Implementation2“ das Entwurfsmuster „Verwerfen“ implementiert, was nicht immer der Fall ist.

Es ist nun offensichtlich, dass der einfachste Mechanismus zum Umgang mit dem Fall, bei dem die zurückgegebene „IAbstraction“-Instanz auch die „IDisposable“-Schnittstelle implementiert, ist das Hinzufügen einer expliziten Überprüfung im Clientcode (siehe den folgenden Codeausschnitt):

IAbstraction abstraction = factory.Create();
try {
  // Operations with abstraction go here
}
finally {
  if (abstraction is IDisposable)
    (abstraction as IDisposable).Dispose();
}

Dies wird jedoch schon bald sehr mühselig.

Leider kann ein „using“-Block nicht mit „IAbstraction“ verwendet werden, da dadurch „IDisposable“ nicht explizit erweitert wird. Deshalb greife ich zu einer Hilfsklasse, die die Logik im „finally“-Block umschließt und Ihnen auch das Verwenden des „using“-Blocks ermöglicht. Abbildung 6 zeigt den vollständigen Code der Klasse und auch eine beispielhafte Verwendung.

Abbildung 6: „PotentialDisposable“-Typ und seine Verwendung

public sealed class PotentialDisposable<T> : IDisposable where T : class {
  private readonly T instance;
  public T Instance { get { return this.instance; } }
  public PotentialDisposable(T instance) {
    if (instance == null) {
      throw new ArgumentNullException("instance");
    }
    this.instance = instance;
  }
  public void Dispose() {
    IDisposable disposableInstance = this.Instance as IDisposable;
    if (disposableInstance != null) {
      disposableInstance.Dispose();
    }
  }
}
The client code:
IAbstraction abstraction = factory.Create();
using (PotentialDisposable<IAbstraction> wrappedInstance =
  new PotentialDisposable<IAbstraction>(abstraction)) {
    // Operations with abstraction wrapedInstance.Instance go here
}

Wie Sie im Teil mit dem „Clientcode“ von Abbildung 6 erkennen, wird durch Verwenden der „PotentialDisposable<T>“-Klasse der Clientcode auf nur ein paar Codezeilen mit einem „using“-Block reduziert.

Sie können einwenden, dass nur die „IAbstraction“-Schnittstelle so aktualisiert werden müsste, dass sie „IDisposable“ wäre. Dies mag in einigen Situationen die bevorzugte Lösung sein, in anderen hingegen nicht.

In einer Situation, in der Sie die „IAbstraction“-Schnittstelle besitzen und es für „IAbstraction“ sinnvoll ist, „IDisposable“ zu erweitern, sollten Sie dies tun. Ein wirklich gutes Beispiel hierfür ist die abstrakte Klasse „System.IO.Stream“. Die Klasse implementiert tatsächlich die „IDisposable“-Schnittstelle, ohne dass für sie eigentliche Logik definiert ist. Der Grund ist, dass die Autoren der Klasse wussten, dass die meisten untergeordneten Klassen über einen bestimmten Typ von verwerfbaren Elementen verfügen.

Eine andere Situation: Sie besitzen die „IAbstraction“-Schnittstelle, aber es ist nicht sinnvoll, für diese „IDisposable“ zu erweitern, da die meisten ihrer Implementierungen nicht verwerfbar sind. Nehmen Sie als Beispiel eine „ICustomCollection“-Schnittstelle. Sie haben mehrere speicherinterne Implementierungen und müssen plötzlich eine datenbankgestützte Implementierung hinzufügen, bei der es sich um die einzige verwerfbare Implementierung handelt.

Die letzte Situation wäre, wenn Sie die „IAbstraction“-Schnittstelle nicht besitzen, sodass Sie keine Kontrolle darüber haben. Sehen Sie sich ein Beispiel von „ICollection“ an, das von einer Datenbank gestützt wird.

Zusammenfassung

Unabhängig davon, ob Sie die Abstraktion über eine Factorymethode erhalten oder nicht, sollten Sie verwerfbare Objekte im Hinterkopf behalten, wenn Sie Ihren Clientcode schreiben. Das Verwenden dieser einfachen Hilfsklasse ist eine Möglichkeit zum Sicherstellen, dass Ihr Code so effizient wie möglich ist, wenn Sie mit verwerfbaren Objekten arbeiten.


Artak Mkrtchyan ist ein erfahrener Softwareentwickler aus Redmond, Washington, USA. Programmieren und Angeln sind seine Leidenschaften.

Unser Dank gilt dem folgenden technischen Experten bei Microsoft für die Durchsicht dieses Artikels: Paul Brambilla
Paul Brambilla ist ein leitender Softwareentwickler bei Microsoft mit Schwerpunkt auf Clouddiensten und grundlegender Infrastruktur.