(0) exportieren Drucken
Alle erweitern
Erweitern Minimieren

Vom Segen der Regelmäß;igkeit

Veröffentlicht: 08. Jan 2002 | Aktualisiert: 13. Jun 2004
Von Ralf Westphal

Regelmäßigkeiten erleichtern das Lernen. Diesem Grundsatz sind auch die Architekten des .NET Framework gefolgt. Der Einstieg in die .NET-Programmierung ist daher einfacher über eine Bewegung vom Allgemeinen zum Speziellen, von der Identifikation wiederkehrender Muster hin zu konkreten Implementationen. Die Realisierung von "aufzählbaren" und "vergleichbaren" Typen sind dafür gute Beispiele.

* * *

Auf dieser Seite

MSDN Kolumne Januar 2002 MSDN Kolumne Januar 2002
Alles neu mit dem .NET Framework Alles neu mit dem .NET Framework
Das Strategie-Entwurfsmuster Das Strategie-Entwurfsmuster
Das Fabrik-Entwurfsmuster Das Fabrik-Entwurfsmuster
Fazit Fazit
Quellen Quellen

MSDN Kolumne Januar 2002

Bild02

Gewachsene Strukturen sind ein Merkmal der Programmierschnittstellen in der "alten Windows-Welt". Zahl, Umfang, Aufbau und Technologie der vielen APIs zeugen davon. Am deutlichsten ist das zu spüren beim Jahre währenden und nicht abgeschlossenen Übergang von der Win32-Programmierung zur COM-Programmierung. Viele APIs sind immer noch nicht hinter einer COM-Schnittstelle gekapse< andere schon, aber sie können nicht von Scriptsprachen oder aus VB6 heraus bedient werden; und wieder andere haben inzwischen einen komfortablen, breit nutzbaren Ausdruck in COM gefunden.

Sie werden es am eigenen Leib schon gespürt haben, wie schwer es sein kann, sich mehrere APIs anzueignen. Die Zahl der Regelmäßigkeiten, der wiederkehrenden Muster ist relativ gering. Trotz aller internen Microsoft-Guidelines haben über die Jahre einfach zu viele Entwicklerteams Funktionalitäten hinzugefügt. Der Erfolg der Windows-Programmierplattform, d.h. ihre lange Lebensdauer, steht ihr am Ende damit sozusagen selbst im Wege: Vorhandenes muss beibehalten werden, um rückwärtskompatibel zu sein; Neues kann schrittweise neue Technologien und Philosophien nutzen. Von einem Design "aus einem Guss" lässt sich da schon lange nicht mehr reden. Die Windows-Programmierung ist über die Jahre nicht nur aufgrund einer wachsenden Zahl von APIs immer schwieriger geworden.

Alles neu mit dem .NET Framework

Der .NET Framework hat viele Aufgaben. Er soll Anwendungen z.B. robuster und sicherer machen. Er soll eine Programmierplattform für WebServices enthalten. Er soll eine grundsätzlich einfach zu portierende Infrastruktur sein. Aber er soll auch eine einheitliche, regelmäßige Standardbibliothek bieten: die Base Class Library (BCL).

Einheitlichkeit
Einheitlich ist die BCL, weil sie komplett in Managed Code vorliegt, also selbst eine .NET-Anwendung ist und somit von allen .NET-Sprachen gleichermaßen genutzt werden kann. C#, VB .NET, Eiffel#, Component Pascal und all die anderen .NET-Sprachen benötigen keine eigenen Bibliotheken mehr, um auf Betriebssystemdienste zuzugreifen oder Fenster zu öffnen. Sie finden alle nötigen Werkzeuge in der BCL. Wissen, das Sie sich über die BCL in einer Sprache angeignet haben, ist damit auf andere Sprachen übertragbar. Der folgende Vergleich zeigt das:

C#

VB .NET

using System.Data; 
using System.Data.SqlClient; 
_ 
SqlConnection conn =  
   new SqlConnection("…"); 
DataSet ds = new DataSet(); 
SqlDataAdapter adap =  
   new SqlDataAdapter("select…", conn); 
adap.Fill(ds);

Imports System.Data 
Imports System.Data.SqlClient 
... 
Dim conn as  
new SqlConnection("…") 
Dim ds as  
new DataSet() 
Dim adap as  
new SqlDataAdapter("select…", conn)adap.Fill(ds)

Abgesehen von vernachlässigbaren syntaktischen Unterschieden funktioniert der Umgang mit einer Datenbank mit ADO .NET in C# und VB .NET identisch. Es werden die selben Klassen der BCL benutzt. Das mag ganz natürlich und einleuchtend aussehen - war bisher aber keine Selbstverständlichkeit in der Programmierwelt. VC++ 6.0, VB 6.0, VBScript, Delphi und andere Programmiersprachen für die Windows-Plattform waren nicht nur in Syntax, Semantik und Datentypen unterschiedlich, sondern favorisierten auch jeweils andere Bibliothekfunktionen für denselben Zweck. Wenn Sie z.B. die Datenbankprogrammierung in Delphi mit der Borland Database Engine (BDE) erlernt hatten, dann fiel es Ihnen schwer nach VB 6.0 und ADO 2.x zu wechseln.

Diese Situation ändert nun die BCL grundlegend. Sie können eine Programmiersprache entsprechend ihrem Angebot an Syntax und Semantik wählen und kaufen sich damit nicht länger notwendig Lernaufwand für neue Bibliotheksfunktionen ein.

Regelmäßigkeit
Verinnerlichte BCL-Funktionalität verspricht eine viel längere Halbwertzeit für Sie zu haben, weil Sie sie sprachübergreifend einsetzen können. Wie schwierig aber ist es, sich BCL-Funktionalität anzueignen? Um es Ihnen so einfach wie möglich zu machen, ruht die BCL auf drei Säulen:

  • Bibliotheksfunktionen werden nur in einer API-Form angeboten: als .NET Assemblies mit CTS-basierten Typen

  • Die BCL ist eine komplett objektorientierte API-Sammlung.

  • Die Klassenhierarchien und Methoden sind "aus einem Guss" und weisen API-übergreifend viele Regelmäßigkeiten auf.

In Assemblies verpackte Typen sind der Weg, wie in .NET Funktionalität zu Bibliotheken zusammengefasst wird. Formal unterscheiden sich BCL-Dienste also nicht von Ihren eigenen Komponenten. Sie benutzen immer dasselbe Programmiermodell.

Diese Typen implementieren jedoch nicht nur eine flache Liste statischer Methoden, sondern bilden Ressourcen und Services auf Objektmodelle ab. Das bisherige Win32-Denken in Handles hat damit endgültig ausgedient: Registry-Einträge oder Fenster oder Pinsel sind jetzt "echte" Objekte. Sowohl beim Entwurf Ihrer Programme wie auch beim Umgang mit der BCL bewegen Sie sich in der wunderbaren Welt der OOP.

Die Segnungen der OOP lassen sich allerdings erst voll empfangen, wenn nicht für jedes Problem immer wieder eine neue Lösung erarbeitet werden muss. Ähnliche Probleme sollten vielmehr Ausdruck in ähnlichen Lösungen, d.h. Abbildungen auf Klassen- und Objektstrukturen, finden. Da die BCL eine ungeheure Bandbreite von Problembereichen abdeckt, haben es sich ihre Architekten angelegen sein lassen, die notwendig vielen Ähnlichkeiten in diesen Bereichen auch immer wieder ähnlich umzusetzen. Sie haben auf verschiedenen Ebenen auf regelmäßige Strukturen gesetzt: das betrifft z.B. die Benennung von Methoden oder Klassen, die Abbildung von Strukturen auf Objektmodelle, aber insbesondere auch den Einsatz von Entwurfsmustern (Design Patterns [1]).

Entwurfsmuster-Beispiele
Dass Mengen von Objekten immer wieder in speziellen Listenobjekten verwaltet werden, ist eine augenfällige Regelmäßigkeit der BCL. Diese Listenobjekte lassen sich vielfach wie Felder ansprechen - sind jedoch flexibler - und bedienen so den Wunsch nach Minimierung der Programmiermodelle: auf ein Objekt zeigt eine Objektvariable, auf viele ein Feld. Damit können Sie auf Dateien in einem Verzeichnis genauso komfortabel zugreifen wie auf Typen in einer Assembly.

Dass sowohl Datenbankverdindungen wie Streams mit einer Methode gleichen Namens (Close()) geschlossen werden, ist ebenfalls eine dankenswerte sowie augenfällige Regelmäßigkeit. Nicht alle APIs haben in der Vergangenheit Gleiches wirklich gleich benannt. (Unglücklich ist in dieser Hinsicht allerdings, dass die BCL zwei Eigenschaftennamen für die Zahl der Einträge in Listen/Feldern kennt: Length und Count.)

Dass aber auch Klassenhierarchien bzw. das Zusammenspiel von Klassen, Objekten und Methoden in der BCL oft einem Muster folgen, ist weniger augenfällig - aber ebenfalls eine große Erleichterung für das Lernen. Zwei Entwurfsmuster für zwei Szenarien sollen hierfür als Beispiel dienen.

  • Szenario 1 dreht sich um den Vergleich von Einträgen in Listen, z.B. beim Sortieren, wenn diese Einträge komplexe Typen sind.

  • Szenario 2 betrifft das Durchlaufen von Listen. Sowohl C# wie Visual Basic .NET bieten dafür eine spezielle Iterationsanweisung: foreach bzw. For Each.

So unterschiedlich sich diese Szenarien anhören, so haben sie doch eine wesentliche Gemeinsamkeit: Es geht um vorgegebene Operationen (Iteration, Vergleich) auf unbekannten Typen. Es ist daher zu erwarten, dass beide Probleme in der BCL auch ähnlich gelöst werden.

Das Strategie-Entwurfsmuster

Beide Szenarien definieren eine Operation auf einem Objekt. Im einen soll nacheinander auf alle im Listenobjekt enthaltenen Elemente zugegriffen, im anderen sollen zwei Objekte verglichen werden.

Die Bekannte in diesen Gleichungen ist jeweils die Operation. Die Unbekannte der Objekttyp mit dem sie arbeiten soll. Operation bedeutet dabei allerdings nicht notwendig, dass es sich nur um einen Befehl, um eine IL-Anweisung oder einen Methodenaufruf handelt. Allgemein ausgedrückt erwartet die Operation vielmehr, dass die Objekte, mit denen sie umgeht - bzw. deren Typen -, einen Satz von ganz genau definierten Methoden unterstützt.

Für einfache Typen wie ganze Zahlen und z.B. die einfachen Vergleichsoperationen ist diese Voraussetzung implizit durch das Common Type System (CTS) gegeben.

Aber was ist mit Ihren eigenen Typen? Wie vergleichen Sie z.B. zwei Objekte der folgenden Klasse?

public class Point { 
 public int x, y; 
 public Point(int x, int y)  
 { 
  this.x=x; this.y=y; 
 } 
}

Der Versuch

Point a, b; 
... 
if (a>b) ...

schlägt fehl. Auf der Klasse Point ist kein Vergleichsoperator im Hinblick auf den Inhalt einer Instanz definiert. Wenn schon das aber nicht funktioniert, wie wollen Sie dann eine Liste von Objekten sortieren?

Dim pl as new ArrayList() 
pl.Add(new Point(4,4)) 
pl.Add(new Point(2,7)) 
pl.Add(new Point(2,3)) 
pl.Sort()

Die Sortieroperation erfordert, dass jedes Element in der Liste eine Vergleichsoperation implementiert. Da der .NET Framework nicht vorhersehen kann, wasfür Typen Sie implementieren, kann er ihnen aber keine solche allgemeine, immer funktionierende Vergleichsoperation automatisch mitgeben. Sie müssen sie selbst realisieren - und zwar in einer Weise, dass die Sortierroutine sie auch erkennen und nutzen kann.

Oder wie ist es mit dem Durchlaufen von Elementen in einer Liste?

foreach(Point p in pl)…

funktioniert genauso wie

int[] il = new int[10]; 
foreach(int i in il)…

oder

DirectoryInfo di = new DirectoryInfo("c:\\"); 
foreach(FileInfo fi in di.GetFiles())…

Wie kann foreach Instanzen so unterschiedlicher Typen wie ArrayList, Ganzzahlfeld und FileInfo-Feld durchlaufen? foreach stellt wie Sort() eine Forderung an die verschiedenen Typen: Sie müssen eine Handvoll von Grundfunktionen realisieren, die in Summe die foreach-Operation ermöglichen.

Wege zur Definition von Grundfunktionen
In der Welt der OOP bzw. Komponentenorientierten Programmierung gibt es zwei Wege, um Typen einen Satz von Grundfunktionen für Operationen definieren zu lassen: Vererbung und Interface-Implementation (Abbildung 1).

  • Vererbung: foreach könnte z.B. fordern, dass die Klasse eines Objektes, das als Liste durchlaufen werden soll, von einer bestimmten Basisklasse (AbstractClass in Abbildung 1) abgeleitet sein muss. Die Basisklasse würde virtuelle Methoden als Grundfunktionen für die Operation definieren, die die Listenklasse dann ausfleischen müsste.

  • Interface-Implementation: Alternativ könnte foreach fordern, dass die Klasse eines Listenobjektes ein Interface (IList in Abbildung 1) mit den Grundfunktionen implementiert.

Bild01

Abbildung 1: Zwei Wege zur Implementation der Grundfunktionalität einer Operation.

Beide Wege sind Beispiele für das Strategie-Entwurfsmuster [1]: Ein nicht näher spezifizierter "Algorithmus" wird hinter einer festgelegten Liste von Methoden versteckt. Der Nutzer des Algorithmus (hier: foreach) macht keine Annahmen über ihn, außer der über seine Schnittstelle, d.h. welche Methoden mit welchen Signaturen ihn darstellen.

Implementation in der BCL
Die Architekten der BCL haben sich aus zwei Gründen dafür entschieden, das Strategie-Entwurfsmuster für die beiden Szenarien als Interfaces zu realisieren:

  1. Das CTS unterstützt keine Mehrfachvererbung. Mit dem Zwang zur Ableitung z.B. einer Listenklasse von einer abstrakten Basisklasse würde man Sie daher in eine bestimmte Klassenhierarchie zwingen. Beim Design der Klassen Ihrer Anwendungen müssten Sie schon sehr früh entscheiden, ob eine Klasse z.B. "aufzählbar", d.h. für foreach durchlaufbar sein soll.

  2. Vererbung beschreibt eine "ist ein"-Beziehung zwischen zwei Klassen. Wenn Sie eine Klasse Auto von der Basisklasse Fahrzeug ableiten, dann entspricht die Klassenhierarchie dieser Beziehung: ein Auto ist ein Fahrzeug. Gleiches gälte, wenn Sie die Klasse ListClass1 von AbstractList ableiteten (Abbildung 1).

Was aber, wenn Objekte einer Klasse Abteilung "aufzählbar" im Hinblick auf in ihnen enthaltene Objekte der Klasse Mitarbeiter sein sollen? Eine Ableitung von einer Basisklasse für Listen würde eine falsche Beziehung suggeriert: eine Abteilung ist einfach keine Liste. Vielmehr soll eine Abteilung nur "so aussehen" wie eine Liste. Das aber wird passenderweise über die Implementation eines Interface ausgedrückt.

IComparable
Im ersten Szenario ging es darum, Objekte beliebiger Typen vergleichbar zu machen. Für primitive Typen des CTS sind die Vergleichsoperatoren bereits definiert. Wie soll ArrayList.Sort() jedoch Instanzen Ihrer eigenen Klassen (s.o. z.B. Point) vergleichen können, um sie in die richtige Reihenfolge zu bringen? Indem Ihre Klassen das Interface System.IComparable implementieren. Es besteht nur aus einer Methode:

Interface IComparable 
Function CompareTo(ByVal obj As Object) As Integer 
End Interface


Die obige Klasse Point würde damit wie folgt "vergleichbar" gemacht:

class Point : IComparable 
{ 
 public int x, y; 
 … 
 public int CompareTo(object obj) 
 { 
  if(obj is Point) 
  { 
   int compResu< 
   // Vergleich von this mit obj 
   … 
   return compResu< 
  } 
  else 
   throw new InvalidCastException("Cannot compare Point to " + obj.GetType().Name + "!"); 
 } 
}

Die Methode CompareTo() liefert als Ergebnis einen Wert zurück, der das "Größenverhältnis" zweier verglichener Objekte angibt:

  • <0: Das Objekt auf dem CompareTo() aufgerufen wird, "ist kleiner" als das mit ihm zu vergleichende.

  • =0: Beide Objekte "sind gleich groß".

  • >0: Das Objekt "ist größer" als das mit ihm zu vergleichende.

Hier ein Beispiel für den Vergleich:

Point a = new Point(2,3), b = new Point(4,4); 
If (a.CompareTo(b)<0) Console.WriteLine("a<b");

Ähnlich geht ArrayList.Sort() vor. Für jedes zu sortierende Listenelement prüft die Routine, ob es IComparable implementiert (oder einem primitiven Typ angehört, für den die normalen Vergleichsoperatoren gelten). Wenn ein Listenelement IComparable implementiert, dann kann es mit anderen darüber verglichen werden, um seine Position in der herzustellenden sortierten Reihenfolge festzustellen. (Für den Vergleich von Objekten, die IComparable implementieren, gibt es spezielle Vergleichsklassen (z.B. System.Collections.Comparer, System.Collections.CaseInsensitiveComparer), die Sie benutzen können. Sie wiederum implementieren ebenfalls ein Interface - IComparer -, um in einer Schnittstelle austauschbar zu sein.)

IEnumerator
Im zweiten Szenario ging es darum, ein Objekt als Liste zu durchlaufen. Die Befehle foreach und For Each erwarten dafür die Implementation des Interface System.Collections.IEnumerator:

Interface IEnumerator 
    ReadOnly Property Current() As Object 
    Function MoveNext() As Boolean 
    Sub Reset() 
End Interface

Es definiert mehrere Methoden, die alle zusammen erst die foreach-Funktionalität ermöglichen:

  • Current(): Gibt das aktuelle Element der Liste während einer Iteration zurück.

  • MoveNext(): Bewegt Current() auf das nächste Element in der Liste. Muss vor dem ersten Zugriff auf Current() einmal aufgerufen werden, um das IEnumerator-Objekt auf den Listenanfang zu stellen.

  • Reset(): Setzt das IEnumerator-Objekt zurück, so dass der nächste Aufruf von MoveNext() Current() auf das erste Listenelement bewegt.

Wie IComparable ist IEnumerator ein Strategie-Entwurfsmuster; gleichzeitig hat es aber auch, weil es so häufig vorkommt (allein in der BCL implementieren mehrere Dutzend Klassen IEnumerator), einen eigenen Namen: Iterator [1].

Die Realisierung des oben angesprochenen Abteilungsbeispiels könnte mit IEnumerator grundsätzlich so aussehen:

class Mitarbeiter 
{ 
 public string name; 
 internal Mitarbeiter next; 
 public Mitarbeiter(string name) {...} 
} 
class Abteilung :System.Collections.IEnumerator 
{ 
 public string bez; 
 private Mitarbeiter _mitarbeiterlistenkopf; 
 public Abteilung(string bez) {...} 
 public Mitarbeiter MitarbeiterHinzufügen(Mitarbeiter mit) 
 { 
  mit.next = _mitarbeiterlistenkopf; 
  _mitarbeiterlistenkopf = mit; 
  return mit; 
 } 
 // IEnumerator 
 private Mitarbeiter _current; 
 public object Current 
 { 
  get 
  { 
   return _current; 
  } 
 } 
 public bool MoveNext() 
 { 
  if (_current == null)  
   _current = _mitarbeiterlistenkopf; 
  else 
   _current = _current.next; 
   return _current != null; 
 } 
 public void Reset() 
 { 
  _current = null; 
 } 
}

Eine Abteilung enthält eine verkettete Liste von Mitarbeitern. Diese Liste füllen Sie mit der Methode MitarbeiterHinzufügen():

Abteilung a = new Abteilung("Verkauf"); 
a.MitarbeiterHinzufügen(new Mitarbeiter("Peter")); 
a.MitarbeiterHinzufügen(new Mitarbeiter("Paul")); 
a.MitarbeiterHinzufügen(new Mitarbeiter("Mary"));

Die Implementation von IEnumerator könnte dann ein Abteilung-Objekt schon "aufzählbar" machen:

foreach(Mitarbeiter m in a) 
 Console.WriteLine(m.name);

foreach erwartet ein Objekt auf dem IEnumerator implementiert ist und a ist im vorstehenden Beispiel ein solches Objekt. Prinzipiell funktioniert die "Aufzählbarkeit" im .NET Framework so.

Wäre sie jedoch exakt in dieser Weise implementiert, gäbe es ein Problem, wenn z.B. zwei Threads gleichzeitig versuchten, das selbe Listenobjekt zu durchlaufen. Jedes Listenobjekt kann mit der Variablen _current nur für eine foreach-Schleife zur Zeit Buch über das aktuelle Listenelement führen. Zwei gleichzeitige Iterationen würden sich also gegenseitig beeinflussen. Das darf nicht sein.

Das Fabrik-Entwurfsmuster

Als Abhilfe gegen konkurrierende Iterationen haben die Architekten der BCL ein zweites Entwurfsmuster herangezogen: die Fabrik [1]. Eine "Fabrik" ist eine Methode, die ein Objekt erzeugt. Vor dem Nutzer der Methode wird damit verborgen, wie genau das Resultat generiert, initialisiert und verwaltet wird, da das u.U. sehr aufwändig sein kann und sehr kontrolliert stattfinden muss.

foreach erwartet also nicht, auf dem Listenobjekt selbst IEnumerator zu finden. Stattdessen muss es das Interface System.Collections.IEnumerable implementieren:

Interface IEnumerable 
    Function GetEnumerator() As IEnumerator 
End Interface

Erst der Aufruf von GetEnumerator() liefert dann ein Objekt, welches IEnumerator implementiert, welches für foreach "aufzählbar" ist.
Das Abteilungsbeispiel muss damit z.B. wie folgt abgeändert werden:

class Abteilung : System.Collections.IEnumerable 
{ 
 class Enumerator : System.Collections.IEnumerator 
 { 
  private Abteilung _abt; 
  private Mitarbeiter _current; 
  internal Enumerator(Abteilung abt) 
  { 
   _abt = abt; 
  } 
  // IEnumerator 
  public object Current 
  { 
   get 
   { 
    return _current; 
   } 
  } 
  public bool MoveNext() 
  { 
   if (_current == null)  
    _current = _abt._mitarbeiterlistenkopf; 
   else 
    _current = _current.next; 
   return _current != null; 
  } 
  public void Reset() 
  { 
   _current = null; 
  } 
 } 
 public string bez; 
 internal Mitarbeiter _mitarbeiterlistenkopf; 
 ...  
 // IEnumerable 
 public IEnumerator GetEnumerator() 
 { 
  return new Enumerator(this); 
 } 
}

Die interne Klasse Enumerator von Abteilung implementiert jetzt IEnumerator, nicht mehr Abteilung selbst. Stattdessen implementiert Abteilung IEnumerable und erzeugt bei jedem Aufruf von GetEnumerator ein neues Enumerator-Objekt, welches jeweils einen eigenen Zeiger in die Liste der Mitarbeiter "seines" Abteilung-Objektes führt.

Die interne Vorgehensweise von foreach sähe damit in etwa wie folgt aus (bezogen auf die obige Nutzung im Beispiel):

IEnumerator e = ((IEnumerable)a).GetEnumerator(); 
while(e.MoveNext()) 
{ 
 Mitarbeiter m = (Mitarbeiter)e.Current; 
 Console.WriteLine(m.name); 
}

Fazit

Auch wenn oder gerade weil die Interfaces IComparable und IEnumerator sehr unterschiedlichen Zwecken dienen, zeigen sie deutlichen, welchen Gewinn die vorgeplante durchgängige Anwendung von Entwurfsmustern in Klassenstrukturen bringen kann.

Strategie- und Fabrik-Entwurfsmuster prägen dem Klassengeflecht der BCL eine etwas gröbere, regelmäßigere Struktur auf. Während Sie sich die Funktionalität der .NET-Standardbibliothek aneignen, halten Sie also am besten Ausschau nach diesen und anderen wiederkehrenden Mustern. So wie Kenntnisse der Grammatik einer Sprache deren Erwerb erleichtern, so machen es Ihnen Entwurfsmuster leichter, umfangreiche Klassenstrukturen zu erlernen.

Quellen

[1] Erich Gamma et al., Entwurfsmuster: Elemente wiederverwendbarer objektorientierter Software, Bonn: Addison-Wesley, 1996


Anzeigen:
© 2014 Microsoft