Titel   Inhalt   Suchen   Index   API  Go To Java 2, Zweite Auflage, Handbuch der Java-Programmierung
 <<    <     >    >>  Kapitel 10 - OOP IV: Verschiedenes

10.3 Design-Patterns



Design-Patterns (oder Entwurfsmuster) sind eine der wichtigsten und interessantesten Entwicklungen der objektorientierten Programmierung der letzten Jahre. Basierend auf den Ideen des Architekten Christopher Alexander wurden sie durch das Buch "Design-Patterns - Elements of Reusable Object-Oriented Software" von Erich Gamma, Richard Helm, Ralph Johnson und John Vlissides 1995 einer breiten Öffentlichkeit bekannt.

Als Design-Patterns bezeichnet man (wohlüberlegte) Designvorschläge für den Entwurf objektorientierter Softwaresysteme. Ein Design-Pattern deckt dabei ein ganz bestimmtes Entwurfsproblem ab und beschreibt in rezeptartiger Weise das Zusammenwirken von Klassen, Objekten und Methoden. Meist sind daran mehrere Algorithmen und/oder Datenstrukturen beteiligt. Design-Patterns stellen wie Datenstrukturen oder Algorithmen vordefinierte Lösungen für konkrete Programmierprobleme dar, allerdings auf einer höheren Abstraktionsebene.

Einer der wichtigsten Verdienste standardisierter Design-Patterns ist es, Softwaredesigns Namen zu geben. Zwar ist es in der Praxis nicht immer möglich oder sinnvoll, ein bestimmtes Design-Pattern in allen Details zu übernehmen. Die konsistente Verwendung ihrer Namen und ihres prinzipiellen Aufbaus erweitern jedoch das Handwerkszeug und die Kommunikationsfähigkeit des OOP-Programmierers beträchtlich. Begriffe wie Factory, Iterator oder Singleton werden in OO-Projekten routinemäßig verwendet und sollten für jeden betroffenen Entwickler dieselbe Bedeutung haben.

Wir wollen nachfolgend einige der wichtigsten Design-Patterns vorstellen und ihre Implementierung in Java skizzieren. Die Ausführungen sollten allerdings nur als erster Einstieg in das Thema angesehen werden. Viele Patterns können hier aus Platzgründen gar nicht erwähnt werden, obwohl sie in der Praxis einen hohen Stellenwert haben (z.B. Adapter, Bridge, Mediator, Command etc.). Zudem ist die Bedeutung eines Patterns für den OOP-Anfänger oft gar nicht verständlich, sondern erschließt sich erst nach Monaten oder Jahren zusätzlicher Programmiererfahrung.

Die folgenden Abschnitte ersetzen also nicht die Lektüre weiterführender Literatur zu diesem Thema. Das oben erwähnte Werk von Gamma et al. ist nach wie vor einer der Klassiker schlechthin (die Autoren und ihr Buch werden meist als "GoF" bezeichnet, ein Akronym für "Gang of Four"). Daneben existieren auch spezifische Kataloge, in denen die Design-Patterns zu bestimmten Anwendungsgebieten oder auf der Basis einer ganz bestimmten Sprache, wie etwa C++ oder Java, beschrieben werden.

10.3.1 Singleton

Ein Singleton ist eine Klasse, von der nur ein einziges Objekt erzeugt werden darf. Es stellt eine globale Zugriffsmöglichkeit auf dieses Objekt zur Verfügung und instanziert es beim ersten Zugriff automatisch. Es gibt viele Beispiele für Singletons. So ist etwa der Spooler in einem Drucksystem ein Singleton oder der Fenstermanager unter Windows, der Firmenstamm in einem Abrechnungssystem oder die Übersetzungstabelle in einem Parser.

Wichtige Designmerkmale einer Singleton-Klasse sind:

Eine beispielhafte Implementierung könnte so aussehen:

001 public class Singleton
002 {
003   private static Singleton instance = null;
004 
005   public static Singleton getInstance()
006   {
007     if (instance == null) {
008       instance = new Singleton();
009     }
010     return instance;
011   }
012 
013   private Singleton()
014   {
015   }
016 }
Singleton.java
Listing 10.6: Implementierung eines Singletons

Singletons sind oft nützlich, um den Zugriff auf statische Variablen zu kapseln und ihre Instanzierung zu kontrollieren. Da in der vorgestellten Implementierung das Singleton immer an einer statischen Variable hängt, ist zu beachten, daß es während der Laufzeit des Programms nie an den Garbage Collector zurückgegeben und der zugeordnete Speicher freigegeben wird. Dies gilt natürlich auch für weitere Objekte, auf die von diesem Objekt verwiesen wird.

Manchmal begegnet man Klassen, die zwar nicht auf eine einzige, aber doch auf sehr wenige Instanzen beschränkt sind. Auch bei solchen "relativen Singletons", "Fewtons" oder "Oligotons" (Achtung, Wortschöpfungen des Autors) kann es sinnvoll sein, ihre Instanzierung wie zuvor beschrieben zu kontrollieren. Mitunter darf beispielsweise für eine Menge unterschiedlicher Kategorien jeweils nur eine Instanz pro Kategorie erzeugt werden (etwa ein Objekt der Klasse Uebersetzer je unterstützter Sprache). Dann müßten lediglich die getInstance-Methode parametrisiert und die erzeugten Instanzen anstelle einer einfachen Variable in einer statischen Hashtable gehalten werden (siehe Abschnitt 14.4).

 Tip 

10.3.2 Immutable

Als immutable (unveränderlich) bezeichnet man Objekte, die nach ihrer Instanzierung nicht mehr verändert werden können. Ihre Membervariablen werden im Konstruktor oder in Initialisierern gesetzt und danach ausschließlich im lesenden Zugriff verwendet. Unveränderliche Objekte gibt es an verschiedenen Stellen in der Java-Klassenbibliothek. Bekannte Beispiele sind die Klassen String (siehe Kapitel 11) oder die in Abschnitt 10.2 erläuterten Wrapper-Klassen. Unveränderliche Objekte können gefahrlos mehrfach referenziert werden und erfordern im Multithreading keinen Synchronisationsaufwand.

Wichtige Designmerkmale einer Immutable-Klasse sind:

Eine beispielhafte Implementierung könnte so aussehen:

001 public class Immutable
002 {
003   private int      value1;
004   private String[] value2;
005 
006   public Immutable(int value1, String[] value2)
007   {
008     this.value1 = value1;
009     this.value2 = (String[])value2.clone();
010   }
011 
012   public int getValue1()
013   {
014     return value1;
015   }
016 
017   public String getValue2(int index)
018   {
019     return value2[index];
020   }
021 }
Immutable.java
Listing 10.7: Implementierung eines Immutable

Durch Ableitung könnte ein unveränderliches Objekt wieder veränderlich werden. Zwar ist es der abgeleiteten Klasse nicht möglich, die privaten Membervariablen der Basisklasse zu verändern. Sie könnte aber ohne weiteres eigene Membervariablen einführen, die die Immutable-Kriterien verletzen. Nötigenfalls ist die Klasse als final zu deklarieren, um weitere Ableitungen zu verhindern.

 Warnung 

10.3.3 Interface

Ein Interface trennt die Beschreibung von Eigenschaften einer Klasse von ihrer Implementierung. Dabei ist es sowohl erlaubt, daß ein Interface von mehr als einer Klasse implementiert wird, als auch, daß eine Klasse mehrere Interfaces implementiert. In Java ist ein Interface ein fundamentales Sprachelement. Es wurde in Kapitel 9 ausführlich beschrieben und soll hier nur der Vollständigkeit halber aufgezählt werden. Für Details verweisen wir auf die dort gemachten Ausführungen.

10.3.4 Factory

Eine Factory ist ein Hilfsmittel zum Erzeugen von Objekten. Sie wird verwendet, wenn das Instanzieren eines Objekts mit dem new-Operator alleine nicht möglich oder sinnvoll ist - etwa weil das Objekt schwierig zu konstruieren ist oder aufwendig konfiguriert werden muß, bevor es verwendet werden kann. Manchmal müssen Objekte auch aus einer Datei, über eine Netzwerkverbindung oder aus einer Datenbank geladen werden, oder sie werden auf der Basis von Konfigurationsinformationen aus systemnahen Modulen generiert. Eine Factory wird auch dann eingesetzt, wenn die Menge der Klassen, aus denen Objekte erzeugbar sind, dynamisch ist und zur Laufzeit des Programms erweitert werden kann.

In diesen Fällen ist es sinnvoll, das Erzeugen neuer Objekte von einer Factory erledigen zu lassen. Wir wollen nachfolgend die drei wichtigsten Varianten einer Factory vorstellen.

Factory-Methode

Gibt es in einer Klasse, von der Instanzen erzeugt werden sollen, eine oder mehrere statische Methoden, die Objekte desselben Typs erzeugen und an den Aufrufer zurückgeben, so bezeichnet man diese als Factory-Methoden. Sie rufen implizit den new-Operator auf, um Objekte zu instanzieren, und führen alle Konfigurationen durch, die erforderlich sind, ein Objekt in der gewünschten Weise zu konstruieren.

Das Klassendiagramm für eine Factory-Methode sieht so aus:

Abbildung 10.1: Klassendiagramm einer Factory-Methode

Wir wollen beispielhaft die Implementierung einer Icon-Klasse skizzieren, die eine Factory-Methode loadFromFile enthält. Sie erwartet als Argument einen Dateinamen, dessen Erweiterung sie dazu verwendet, die Art des Ladevorgangs zu bestimmen. loadFromFile instanziert ein Icon-Objekt und füllt es auf der Basis des angegebenen Formats mit den Informationen aus der Datei:

001 public class Icon
002 {
003   private Icon()
004   {
005     //Verhindert das manuelle Instanzieren
006   }
007 
008   public static Icon loadFromFile(String name)
009   {
010     Icon ret = null;
011     if (name.endsWith(".gif")) {
012       //Code zum Erzeugen eines Icons aus einer gif-Datei...
013     } else if (name.endsWith(".jpg")) {
014       //Code zum Erzeugen eines Icons aus einer jpg-Datei...
015     } else if (name.endsWith(".png")) {
016       //Code zum Erzeugen eines Icons aus einer png-Datei...
017     }
018     return ret;
019   }
020 }
Icon.java
Listing 10.8: Eine Klasse mit einer Factory-Methode

Eine Klasse mit einer Factory-Methode hat große Ähnlichkeit mit der Implementierung des Singletons, die in Listing 10.6 vorgestellt wurde. Anders als beim Singleton kann allerdings nicht nur eine einzige Instanz erzeugt werden, sondern beliebig viele von ihnen. Auch merkt sich die Factory-Methode nicht die erzeugten Objekte. Die Singleton-Implementierung kann damit gewissermaßen als Spezialfall einer Klasse mit einer Factory-Methode angesehen werden.

 Hinweis 

Factory-Klasse

Eine Erweiterung des Konzepts der Factory-Methode ist die Factory-Klasse. Hier ist nicht eine einzelne Methode innerhalb der eigenen Klasse für das Instanzieren neuer Objekte zuständig, sondern es gibt eine eigenständige Klasse für diesen Vorgang. Das kann beispielsweise sinnvoll sein, wenn der Herstellungsvorgang zu aufwendig ist, um innerhalb der zu instanzierenden Klasse vorgenommen zu werden. Eine Factory-Klasse könnte auch sinnvoll sein, wenn es später erforderlich werden könnte, die Factory selbst austauschbar zu machen. Ein dritter Grund kann sein, daß es gar keine Klasse gibt, in der eine Factory-Methode untergebracht werden könnte. Das ist insbesondere dann der Fall, wenn unterschiedliche Objekte hergestellt werden sollen, die lediglich ein gemeinsames Interface implementieren.

Das Klassendiagramm für eine Factory-Klasse sieht so aus:

Abbildung 10.2: Klassendiagramm einer Factory-Klasse

Als Beispiel wollen wir noch einmal das Interface DoubleMethod aus Listing 9.13 aufgreifen. Wir wollen dazu eine Factory-Klasse DoubleMethodFactory entwickeln, die verschiedene Methoden zur Konstruktion von Objekten zur Verfügung stellt, die das Interface DoubleMethod implementieren:

001 public class DoubleMethodFactory
002 {
003   public DoubleMethodFactory()
004   {
005     //Hier wird die Factory selbst erzeugt und konfiguriert
006   }
007 
008   public DoubleMethod createFromClassFile(String name)
009   {
010     //Lädt die Klassendatei mit dem angegebenen Namen,
011     //prüft, ob sie DoubleMethod implementiert, und
012     //instanziert sie gegebenenfalls...
013     return null;
014   }
015 
016   public DoubleMethod createFromStatic(String clazz, 
017                                        String method)
018   {
019     //Erzeugt ein Wrapper-Objekt, das das Interface
020     //DoubleMethod implementiert und beim Aufruf von
021     //compute die angegebene Methode der vorgegebenen
022     //Klasse aufruft...
023     return null;
024   }
025 
026   public DoubleMethod createFromPolynom(String expr)
027   {
028     //Erzeugt aus dem angegebenen Polynom-Ausdruck ein
029     //DoubleMethod-Objekt, in dem ein äquivalentes
030     //Polynom implementiert wird...
031     return null;
032   }
033 }
DoubleMethodFactory.java
Listing 10.9: Eine Factory-Klasse

Die Anwendung einer Factory-Klasse ist hier sinnvoll, weil der Code zum Erzeugen der Objekte sehr aufwendig ist und weil Objekte geliefert werden sollen, die zwar ein gemeinsames Interface implementieren, aber aus sehr unterschiedlichen Vererbungshierarchien stammen können.

Aus Gründen der Übersichtlichkeit wurde das Erzeugen des Rückgabewerts im Beispielprogramm lediglich angedeutet. Anstelle von return null; würde in der vollständigen Implementierung natürlich der Code zum Erzeugen der jeweiligen DoubleMethod-Objekte stehen.

 Hinweis 

Abstrakte Factory

Eine Abstracte Factory ist eine recht aufwendige Erweiterung der Factory-Klasse, bei der zwei zusätzliche Gedanken im Vordergrund stehen:

Eine abstrakte Factory wird auch als Toolkit bezeichnet. Ein Beispiel dafür findet sich in grafischen Ausgabesystemen bei der Erzeugung von Dialogelementen (Widgets) für unterschiedliche Fenstermanager. Eine konkrete Factory muß in der Lage sein, unterschiedliche Dialogelemente so zu erzeugen, daß sie in Aussehen und Bedienung konsistent sind. Auch die Schnittstelle für Programme sollte über Fenstergrenzen hinweg konstant sein. Konkrete Factories könnte es etwa für Windows, X-Window oder die Macintosh-Oberfläche geben.

Eine abstrakte Factory kann durch folgende Bestandteile beschrieben werden:

Das Klassendiagramm für eine abstrakte Factory sieht so aus:

Abbildung 10.3: Klassendiagramm einer abstrakten Factory

Das folgende Listing skizziert ihre Implementierung:

001 /* Listing1010.java */
002 
003 //------------------------------------------------------------------
004 //Abstrakte Produkte
005 //------------------------------------------------------------------
006 abstract class Product1
007 {
008 }
009 
010 abstract class Product2
011 {
012 }
013 
014 //------------------------------------------------------------------
015 //Abstrakte Factory
016 //------------------------------------------------------------------
017 abstract class ProductFactory
018 {
019   public abstract Product1 createProduct1();
020 
021   public abstract Product2 createProduct2();
022 
023   public static ProductFactory getFactory(String variant)
024   {
025     ProductFactory ret = null;
026     if (variant.equals("A")) {
027       ret = new ConcreteFactoryVariantA();
028     } else if (variant.equals("B")) {
029       ret = new ConcreteFactoryVariantB();
030     }
031     return ret;
032   }
033 
034   public static ProductFactory getDefaultFactory()
035   {
036     return getFactory("A");
037   }
038 }
039 
040 //------------------------------------------------------------------
041 //Konkrete Produkte für Implementierungsvariante A
042 //------------------------------------------------------------------
043 class Product1VariantA
044 extends Product1
045 {
046 }
047 
048 class Product2VariantA
049 extends Product2
050 {
051 }
052 
053 //------------------------------------------------------------------
054 //Konkrete Factory für Implementierungsvariante A
055 //------------------------------------------------------------------
056 class ConcreteFactoryVariantA
057 extends ProductFactory
058 {
059   public Product1 createProduct1()
060   {
061     return new Product1VariantA();
062   }
063 
064   public Product2 createProduct2()
065   {
066     return new Product2VariantA();
067   }
068 }
069 
070 //------------------------------------------------------------------
071 //Konkrete Produkte für Implementierungsvariante B
072 //------------------------------------------------------------------
073 class Product1VariantB
074 extends Product1
075 {
076 }
077 
078 class Product2VariantB
079 extends Product2
080 {
081 }
082 
083 //------------------------------------------------------------------
084 //Konkrete Factory für Implementierungsvariante B
085 //------------------------------------------------------------------
086 class ConcreteFactoryVariantB
087 extends ProductFactory
088 {
089   public Product1 createProduct1()
090   {
091     return new Product1VariantB();
092   }
093 
094   public Product2 createProduct2()
095   {
096     return new Product2VariantB();
097   }
098 }
099 
100 //------------------------------------------------------------------
101 //Beispielanwendung
102 //------------------------------------------------------------------
103 public class Listing1010
104 {
105   public static void main(String[] args)
106   {
107     ProductFactory fact = ProductFactory.getDefaultFactory();
108     Product1 prod1 = fact.createProduct1();
109     Product2 prod2 = fact.createProduct2();
110   }
111 }
Listing1010.java
Listing 10.10: Eine abstrakte Factory

Bemerkenswert an diesem Pattern ist, wie geschickt es die komplexen Details seiner Implementierung versteckt. Der Aufrufer kennt lediglich die Produkte, die abstrakte Factory und besitzt eine Möglichkeit, eine konkrete Factory zu beschaffen. Er braucht weder zu wissen, welche konkreten Factories oder Produkte es gibt, noch müssen ihn Details ihrer Implementierung interessieren. Diese Sichtweise verändert sich auch nicht, wenn eine neue Implementierungsvariante hinzugefügt wird. Das würde sich lediglich in einem neuen Wert im variant-Parameter der Methode getFactory der ProductFactory äußern.

Ein wenig mehr Aufwand muß allerdings getrieben werden, wenn ein neues Produkt hinzukommt. Dann müssen nicht nur neue abstrakte und konkrete Produktklassen definiert werden, sondern auch die Factories müssen um eine Methode erweitert werden.

Mit Absicht wurde bei der Benennung der abstrakten Klassen nicht die Vor- oder Nachsilbe "Abstract" verwendet. Da die Clients nur mit den Schnittstellen der abstrakten Klassen arbeiten und die Namen der konkreten Klassen normalerweise nie zu sehen bekommen, ist es vollkommen unnötig, sie bei jeder Deklaration daran zu erinnern, daß sie eigentlich nur mit Abstraktionen arbeiten.

 Tip 

Auch in Java gibt es Klassen, die nach dem Prinzip der abstrakten Factory implementiert sind. Ein Beispiel ist die Klasse Toolkit des Pakets java.awt. Sie dient dazu, Fenster, Dialogelemente und andere plattformabhängige Objekte für die grafische Oberfläche eines bestimmten Betriebssystems zu erzeugen. In Abschnitt 24.2.2 finden sich ein paar Beispiele für die Anwendung dieser Klasse.

 Hinweis 

10.3.5 Iterator

Ein Iterator ist ein Objekt, das es ermöglicht, die Elemente eines Collection-Objekts nacheinander zu durchlaufen. Als Collection-Objekt bezeichnet man ein Objekt, das eine Sammlung (meist gleichartiger) Elemente eines anderen Typs enthält. In Java gibt es eine Vielzahl von vordefinierten Collections, sie werden in Kapitel 14 und Kapitel 15 ausführlich erläutert.

Obwohl die Objekte in den Collections unterschiedlich strukturiert und auf sehr unterschiedliche Art und Weise gespeichert sein können, ist es bei den meisten von ihnen früher oder später erforderlich, auf alle darin enthaltenen Elemente zuzugreifen. Dazu stellt die Collection einen oder mehrere Iteratoren zur Verfügung, die das Durchlaufen der Elemente ermöglichen, ohne daß die innere Struktur der Collection dem Aufrufer bekannt sein muß.

Ein Iterator enthält folgende Bestandteile:

Das Klassendiagramm für einen Iterator sieht so aus:

Abbildung 10.4: Klassendiagramm eines Iterators

Das folgende Listing zeigt die Implementierung eines Iterators, mit dem die Elemente der Klasse StringArray (die ein einfaches Array von Strings kapselt) durchlaufen werden können:

001 /* Listing1011.java */
002 
003 interface StringIterator
004 {
005   public boolean hasNext();
006   public String next();
007 }
008 
009 class StringArray
010 {
011   String[] data;
012 
013   public StringArray(String[] data)
014   {
015     this.data = data;
016   }
017 
018   public StringIterator getElements()
019   {
020     return new StringIterator()
021     {
022       int index = 0;
023       public boolean hasNext()
024       {
025         return index < data.length;
026       }
027       public String next()
028       {
029         return data[index++];
030       }
031     };
032   }
033 }
034 
035 public class Listing1011
036 {
037   static final String[] SAYHI = {"Hi", "Iterator", "Buddy"};
038 
039   public static void main(String[] args)
040   {
041     //Collection erzeugen
042     StringArray strar = new StringArray(SAYHI);
043     //Iterator beschaffen und Elemente durchlaufen
044     StringIterator it = strar.getElements();
045     while (it.hasNext()) {
046       System.out.println(it.next());
047     }
048   }
049 }
Listing1011.java
Listing 10.11: Implementierung eines Iterators

Der Iterator wurde in StringIterator als Interface realisiert, um in unterschiedlicher Weise implementiert werden zu können. Die Methode getElements erzeugt beispielsweise eine anonyme Klasse, die das Iterator-Interface implementiert und an den Aufrufer zurückgibt. Dazu wird in diesem Fall lediglich eine Hilfsvariable benötigt, die als Zeiger auf das nächste zu liefernde Element zeigt. Im Hauptprogramm wird nach dem Erzeugen der Collection der Iterator beschafft und mit seiner Hilfe die Elemente durch fortgesetzten Aufruf von hasNext und next sukzessive durchlaufen.

Die Implementierung eines Iterators erfolgt häufig mit Hilfe lokaler oder anonymer Klassen. Das hat den Vorteil, daß alle benötigten Hilfsvariablen je Aufruf angelegt werden. Würde die Klasse StringArray dagegen selbst das StringIterator-Interface implementieren (und die Hilfsvariable index als Membervariable halten), so könnte sie jeweils nur einen einzigen aktiven Iterator zur Verfügung stellen.

 Hinweis 

Iteratoren können auch gut mit Hilfe von for-Schleifen verwendet werden. Das folgende Programmfragment ist äquivalent zum vorigen Beispiel:

for (StringIterator it = strar.getElements(); it.hasNext(); ) {
  System.out.println(it.next());
}
 Tip 

10.3.6 Delegate

In objektorientierten Programmiersprachen gibt es zwei grundverschiedene Möglichkeiten, Programmcode wiederzuverwenden. Die erste von ihnen ist die Vererbung, bei der eine abgeleitete Klasse alle Eigenschaften ihrer Basisklasse erbt und deren nicht-privaten Methoden aufrufen kann. Die zweite Möglichkeit wird als Delegation bezeichnet. Hierbei verwendet eine Klasse die Dienste von Objekten, aus denen sie nicht abgeleitet ist. Diese Objekte werden oft als Membervariablen gehalten.

Das wäre an sich noch nichts Besonderes, denn Programme verwenden fast immer Code, der in anderen Programmteilen liegt, und delegieren damit einen Teil ihrer Aufgaben. Ein Designpattern wird daraus, wenn Aufgaben weitergegeben werden müssen, die eigentlich in der eigenen Klasse erledigt werden sollten. Wenn also der Leser des Programmes später erwarten würde, den Code in der eigenen Klasse vorzufinden. In diesem Fall ist es sinnvoll, die Übertragung der Aufgaben explizit zu machen und das Delegate-Designpattern anzuwenden.

Anwendungen für das Delegate-Pattern finden sich meist, wenn identische Funktionalitäten in Klassen untergebracht werden sollen, die nicht in einer gemeinsamen Vererbungslinie stehen. Ein Beispiel bilden die Klassen JFrame und JInternalFrame aus dem Swing-Toolkit (sie werden in Kapitel 36 ausführlich besprochen). Beide Klassen stellen Hauptfenster für die Grafikausgabe dar. Eines von ihnen ist ein eigenständiges Top-Level-Window, das andere wird meist zusammen mit anderen Fenstern in ein Desktop eingebettet. Soll eine Anwendung wahlweise in einem JFrame oder einem JInternalFrame laufen, müssen alle Funktionalitäten in beiden Klassen zur Verfügung gestellt werden. Unglücklicherweise sind beide nicht Bestandteil einer gemeinsamen Vererbungslinie. Hier empfiehlt es sich, die Gemeinsamkeiten in einer neuen Klasse zusammenzufassen und beiden Fensterklassen durch Delegation zur Verfügung zu stellen.

 Hinweis 

Das Delegate-Pattern besitzt folgende Bestandteile:

Das Klassendiagramm für ein Delegate sieht so aus:

Abbildung 10.5: Klassendiagramm eines Delegates

Eine Implementierungsskizze könnte so aussehen:

001 /* Listing1012.java */
002 
003 class Delegate
004 {
005   private Delegator delegator;
006 
007   public Delegate(Delegator delegator)
008   {
009     this.delegator = delegator;
010   }
011 
012   public void service1()
013   {
014   }
015 
016   public void service2()
017   {
018   }
019 }
020 
021 interface Delegator
022 {
023   public void commonDelegatorServiceA();
024   public void commonDelegatorServiceB();
025 }
026 
027 class Client1
028 implements Delegator
029 {
030   private Delegate delegate;
031 
032   public Client1()
033   {
034     delegate = new Delegate(this);
035   }
036 
037   public void service1()
038   {
039     //implementiert einen Service und benutzt
040     //dazu eigene Methoden und die des 
041     //Delegate-Objekts
042   }
043 
044   public void commonDelegatorServiceA()
045   {
046   }
047 
048   public void commonDelegatorServiceB()
049   {
050   }
051 }
052 
053 class Client2
054 implements Delegator
055 {
056   private Delegate delegate;
057 
058   public Client2()
059   {
060     delegate = new Delegate(this);
061   }
062 
063   public void commonDelegatorServiceA()
064   {
065   }
066 
067   public void commonDelegatorServiceB()
068   {
069   }
070 }
071 
072 public class Listing1012
073 {
074   public static void main(String[] args)
075   {
076     Client1 client = new Client1();
077     client.service1();
078   }
079 }
Listing1012.java
Listing 10.12: Das Delegate-/Delegator-Pattern

Die Klasse Delegate implementiert die Methoden service1 und service2. Zusätzlich hält sie einen Verweis auf ein Delegator-Objekt, über das sie die Callback-Methoden commonDelegatorServiceA und commonDelegatorServiceB der delegierenden Klasse erreichen kann. Die beiden Klassen Client1 und Client2 verwenden das Delegate, um Services zur Verfügung zu stellen (am Beispiel der Methode service1 angedeutet).

10.3.7 Composite

In der Programmierpraxis werden häufig Datenstrukturen benötigt, bei denen die einzelnen Objekte zu Baumstrukturen zusammengesetzt werden können.

Es gibt viele Beispiele für derartige Strukturen:

Für diese häufig anzutreffende Abstraktion gibt es ein Design-Pattern, das als Composite bezeichnet wird. Es ermöglicht derartige Kompositionen und erlaubt eine einheitliche Handhabung von individuellen und zusammengesetzten Objekten. Ein Composite enthält folgende Bestandteile:

Somit sind beide Bedingungen erfüllt. Der Container ermöglicht die Komposition der Objekte zu Baumstrukturen, und die Basisklasse stellt die einheitliche Schnittstelle für elementare Objekte und Container zur Verfügung. Das Klassendiagramm für ein Composite sieht so aus:

Abbildung 10.6: Klassendiagramm eines Composite

Das folgende Listing skizziert dieses Design-Pattern am Beispiel einer einfachen Menüstruktur:

001 /* Listing1013.java */
002 
003 class MenuEntry1
004 {
005   protected String name;
006 
007   public MenuEntry1(String name)
008   {
009     this.name = name;
010   }
011 
012   public String toString()
013   {
014     return name;
015   }
016 }
017 
018 class IconizedMenuEntry1
019 extends MenuEntry1
020 {
021   private String iconName;
022 
023   public IconizedMenuEntry1(String name, String iconName)
024   {
025     super(name);
026     this.iconName = iconName;
027   }
028 }
029 
030 class CheckableMenuEntry1
031 extends MenuEntry1
032 {
033   private boolean checked;
034 
035   public CheckableMenuEntry1(String name, boolean checked)
036   {
037     super(name);
038     this.checked = checked;
039   }
040 }
041 
042 class Menu1
043 extends MenuEntry1
044 {
045   MenuEntry1[] entries;
046   int          entryCnt;
047 
048   public Menu1(String name, int maxElements)
049   {
050     super(name);
051     this.entries = new MenuEntry1[maxElements];
052     entryCnt = 0;
053   }
054 
055   public void add(MenuEntry1 entry)
056   {
057     entries[entryCnt++] = entry;
058   }
059 
060   public String toString()
061   {
062     String ret = "(";
063     for (int i = 0; i < entryCnt; ++i) {
064       ret += (i != 0 ? "," : "") + entries[i].toString();
065     }
066     return ret + ")";
067   }
068 }
069 
070 public class Listing1013
071 {
072   public static void main(String[] args)
073   {
074     Menu1 filemenu = new Menu1("Datei", 5);
075     filemenu.add(new MenuEntry1("Neu"));
076     filemenu.add(new MenuEntry1("Laden"));
077     filemenu.add(new MenuEntry1("Speichern"));
078 
079     Menu1 confmenu = new Menu1("Konfiguration", 3);
080     confmenu.add(new MenuEntry1("Farben"));
081     confmenu.add(new MenuEntry1("Fenster"));
082     confmenu.add(new MenuEntry1("Pfade"));
083     filemenu.add(confmenu);
084 
085     filemenu.add(new MenuEntry1("Beenden"));
086 
087     System.out.println(filemenu.toString());
088   }
089 }
Listing1013.java
Listing 10.13: Das Composite-Pattern

Die Komponentenklasse hat den Namen MenuEntry1. Sie repräsentiert Menüeinträge und ist Vaterklasse der spezialisierteren Menüeinträge IconizedMenuEntry1 und CheckableMenuEntry1. Zudem ist sie Vaterklasse des Containers Menu1, der Menüeinträge aufnehmen kann.

Bestandteil der gemeinsamen Schnittstelle ist die Methode toString. In der Basisklasse und den elementaren Menüeinträgen liefert sie lediglich den Namen des Objekts. In der Containerklasse wird sie überlagert und liefert eine geklammerte Liste aller darin enthaltenen Menüeinträge. Dabei arbeitet sie unabhängig davon, ob es sich bei dem jeweiligen Eintrag um einen elementaren oder einen zusammengesetzten Eintrag handelt, denn es wird lediglich die immer verfügbare Methode toString aufgerufen.

Das Testprogramm erzeugt ein "Datei"-Menü mit einigen Elementareinträgen und einem Untermenü "Konfiguration" und gibt es auf Standardausgabe aus:

(Neu,Laden,Speichern,(Farben,Fenster,Pfade),Beenden)

10.3.8 Visitor

Das vorige Pattern hat gezeigt, wie man komplexe Datenstrukturen mit einer inhärenten Teile-Ganzes-Beziehung aufbaut. Solche Strukturen müssen oft auf unterschiedliche Arten durchlaufen und verarbeitet werden. Ein Menü muß beispielsweise auf dem Bildschirm angezeigt werden, aber es kann auch die Gliederung für einen Teil eines Benutzerhandbuchs zur Verfügung stellen. Verzeichnisse in einem Dateisystem müssen nach einem bestimmten Namen durchsucht werden, die kumulierte Größe ihrer Verzeichnisse und Unterverzeichnisse soll ermittelt werden, oder es sollen alle Dateien eines bestimmten Typs gelöscht werden können.

All diese Operationen erfordern einen flexiblen Mechanismus zum Durchlaufen und Verarbeiten der Datenstruktur. Natürlich könnte man die einzelnen Bestandteile jeder Operation in den Komponenten- und Containerklassen unterbringen, aber dadurch würden diese schnell unübersichtlich, und für jede neu hinzugefügte Operation müßten alle Klassen geändert werden.

Das Visitor-Pattern zeigt einen eleganteren Weg, Datenstrukturen mit Verarbeitungsalgorithmen zu versehen. Es besteht aus folgenden Teilen:

Das Klassendiagramm für einen Visitor sieht so aus:

Abbildung 10.7: Klassendiagramm eines Visitors

Das folgende Listing erweitert das Composite des vorigen Abschnitts um einen Visitor-Mechanismus:

001 /* Listing1014.java */
002 
003 interface MenuVisitor
004 {
005   abstract void visitMenuEntry(MenuEntry2 entry);
006   abstract void visitMenuStarted(Menu2 menu);
007   abstract void visitMenuEnded(Menu2 menu);
008 }
009 
010 class MenuEntry2
011 {
012   protected String name;
013 
014   public MenuEntry2(String name)
015   {
016     this.name = name;
017   }
018 
019   public String toString()
020   {
021     return name;
022   }
023 
024   public void accept(MenuVisitor visitor)
025   {
026     visitor.visitMenuEntry(this);
027   }
028 }
029 
030 class Menu2
031 extends MenuEntry2
032 {
033   MenuEntry2[] entries;
034   int         entryCnt;
035 
036   public Menu2(String name, int maxElements)
037   {
038     super(name);
039     this.entries = new MenuEntry2[maxElements];
040     entryCnt = 0;
041   }
042 
043   public void add(MenuEntry2 entry)
044   {
045     entries[entryCnt++] = entry;
046   }
047 
048   public String toString()
049   {
050     String ret = "(";
051     for (int i = 0; i < entryCnt; ++i) {
052       ret += (i != 0 ? "," : "") + entries[i].toString();
053     }
054     return ret + ")";
055   }
056 
057   public void accept(MenuVisitor visitor)
058   {
059     visitor.visitMenuStarted(this);
060     for (int i = 0; i < entryCnt; ++i) {
061       entries[i].accept(visitor);
062     }
063     visitor.visitMenuEnded(this);
064   }
065 }
066 
067 class MenuPrintVisitor
068 implements MenuVisitor
069 {
070   String indent = "";
071 
072   public void visitMenuEntry(MenuEntry2 entry)
073   {
074     System.out.println(indent + entry.name);
075   }
076 
077   public void visitMenuStarted(Menu2 menu)
078   {
079     System.out.println(indent + menu.name);
080     indent += " ";
081   }
082 
083   public void visitMenuEnded(Menu2 menu)
084   {
085     indent = indent.substring(1);
086   }
087 }
088 
089 public class Listing1014
090 {
091   public static void main(String[] args)
092   {
093     Menu2 filemenu = new Menu2("Datei", 5);
094     filemenu.add(new MenuEntry2("Neu"));
095     filemenu.add(new MenuEntry2("Laden"));
096     filemenu.add(new MenuEntry2("Speichern"));
097     Menu2 confmenu = new Menu2("Konfiguration", 3);
098     confmenu.add(new MenuEntry2("Farben"));
099     confmenu.add(new MenuEntry2("Fenster"));
100     confmenu.add(new MenuEntry2("Pfade"));
101     filemenu.add(confmenu);
102     filemenu.add(new MenuEntry2("Beenden"));
103 
104     filemenu.accept(new MenuPrintVisitor());
105   }
106 }
Listing1014.java
Listing 10.14: Das Visitor-Pattern

Das Interface MenuVisitor stellt den abstrakten Visitor für Menüeinträge dar. Die Methode visitMenuEntry wird bei jedem Durchlauf eines MenuEntry2-Objekts aufgerufen; die Methoden visitMenuStarted und visitMenuEnded zu Beginn und Ende des Besuchs eines Menu2-Objekts. In der Basisklasse MenuItem ruft accept die Methode visitMenuEntry auf. Für die beiden abgeleiteten Elementklassen IconizedMenuEntry und CheckableMenuEntry gibt es keine Spezialisierungen; auch für diese Objekte wird visitMenuEntry aufgerufen. Lediglich der Container Menu2 verfeinert den Aufruf und unterteilt ihn in drei Schritte. Zunächst wird visitMenuStarted aufgerufen, um anzuzeigen, daß ein Menüdurchlauf beginnt. Dann werden die accept-Methoden aller Elemente aufgerufen, und schließlich wird durch Aufruf von visitMenuEnded das Ende des Menüdurchlaufs angezeigt.

Der konkrete Visitor MenuPrintVisitor hat die Aufgabe, ein Menü mit allen Elementen zeilenweise und entsprechend der Schachtelung seiner Untermenüs eingerückt auszugeben. Die letzte Zeile des Beispielprogramms zeigt, wie er verwendet wird. Die Ausgabe des Programms ist:

Datei
 Neu
 Laden
 Speichern
 Konfiguration
  Farben
  Fenster
  Pfade
 Beenden

In Abschnitt 21.4.1 zeigen wir eine weitere Anwendung des Visitor-Patterns. Dort wird eine generische Lösung für den rekursiven Durchlauf von geschachtelten Verzeichnisstrukturen vorgestellt.

 Hinweis 

10.3.9 Observer

Bei der objektorientierten Programmierung werden Programme in viele kleine Bestandteile zerlegt, die für sich genommen autonom arbeiten. Mit zunehmender Anzahl von Bausteinen steigt allerdings der Kommunikationsbedarf zwischen diesen Objekten, und der Aufwand, sie konsistent zu halten, wächst an.

Ein Observer ist ein Design-Pattern, das eine Beziehung zwischen einem Subject und seinen Beobachtern aufbaut. Als Subject wird dabei ein Objekt bezeichnet, dessen Zustandsänderung für andere Objekte interessant ist. Als Beobachter werden die Objekte bezeichnet, die von Zustandsänderungen des Subjekts abhängig sind; deren Zustand also dem Zustand des Subjekts konsistent folgen muß.

Das Observer-Pattern wird sehr häufig bei der Programmierung grafischer Oberflächen angewendet. Ist beispielsweise die Grafikausgabe mehrerer Fenster von einer bestimmten Datenstruktur abhängig, so müssen die Fenster ihre Ausgabe verändern, wenn die Datenstruktur sich ändert. Auch Dialogelemente wie Buttons, Auswahlfelder oder Listen müssen das Programm benachrichtigen, wenn der Anwender eine Veränderung an ihnen vorgenommen hat. In diesen Fällen kann das Observer-Pattern angewendet werden. Es besteht aus folgenden Teilen:

Das Klassendiagramm für einen Observer sieht so aus:

Abbildung 10.8: Klassendiagramm eines Observers

Das folgende Listing zeigt eine beispielhafte Implementierung:

001 /* Listing1015.java */
002 
003 interface Observer
004 {
005   public void update(Subject subject);
006 }
007 
008 class Subject
009 {
010   Observer[] observers   = new Observer[5];
011   int        observerCnt = 0;
012 
013   public void attach(Observer observer)
014   {
015     observers[observerCnt++] = observer;
016   }
017 
018   public void detach(Observer observer)
019   {
020     for (int i = 0; i < observerCnt; ++i) {
021       if (observers[i] == observer) {
022         --observerCnt;
023         for (;i < observerCnt; ++i) {
024           observers[i] = observers[i + 1];
025         }
026         break;
027       }
028     }
029   }
030 
031   public void fireUpdate()
032   {
033     for (int i = 0; i < observerCnt; ++i) {
034       observers[i].update(this);
035     }
036   }
037 }
038 
039 class Counter
040 {
041   int cnt = 0;
042   Subject subject = new Subject();
043 
044   public void attach(Observer observer)
045   {
046     subject.attach(observer);
047   }
048 
049   public void detach(Observer observer)
050   {
051     subject.detach(observer);
052   }
053 
054   public void inc()
055   {
056     if (++cnt % 3 == 0) {
057       subject.fireUpdate();
058     }
059   }
060 }
061 
062 public class Listing1015
063 {
064   public static void main(String[] args)
065   {
066     Counter counter = new Counter();
067     counter.attach(
068       new Observer()
069       {
070         public void update(Subject subject)
071         {
072           System.out.print("divisible by 3: ");
073         }
074       }
075     );
076     while (counter.cnt < 10) {
077       counter.inc();
078       System.out.println(counter.cnt);
079     }
080   }
081 }
Listing1015.java
Listing 10.15: Das Observer-Pattern

Als konkretes Subjekt wird hier die Klasse Counter verwendet. Sie erhöht bei jedem Aufruf von inc den eingebauten Zähler um eins und informiert alle registrierten Beobachter, falls der neue Zählerstand durch drei teilbar ist. Im Hauptprogramm instanzieren wir ein Counter-Objekt und registrieren eine lokale anonyme Klasse als Listener, die bei jeder Benachrichtigung eine Meldung ausgibt. Während des anschließenden Zählerlaufs von 1 bis 10 wird sie dreimal aufgerufen:

1
2
divisible by 3: 3
4
5
divisible by 3: 6
7
8
divisible by 3: 9
10

Das Observer-Pattern ist in Java sehr verbreitet, denn die Kommunikation zwischen graphischen Dialogelementen und ihrer Anwendung basiert vollständig auf dieser Idee. Allerdings wurde es etwas erweitert, die Beobachter werden als Listener bezeichnet, und es gibt von ihnen eine Vielzahl unterschiedlicher Typen mit unterschiedlichen Aufgaben. Da es zudem üblich ist, daß ein Listener sich bei mehr als einem Subjekt registriert, wird ein Aufruf von update statt des einfachen Arguments jeweils ein Listener-spezifisches Ereignisobjekt übergeben. Darin werden neben dem Subjekt weitere spezifische Informationen untergebracht. Zudem haben die Methoden gegenüber der ursprünglichen Definition eine andere Namensstruktur, und es kann sein, daß ein Listener nicht nur eine, sonderen mehrere unterschiedliche Update-Methoden zur Verfügung stellen muß, um auf unterschiedliche Ereignistypen zu reagieren. Das Listener-Konzept von Java wird auch als Delegation Based Event Handling bezeichnet und in Kapitel 28 ausführlich erläutert.

 Hinweis 


 Titel   Inhalt   Suchen   Index   API  Go To Java 2, Zweite Auflage, Addison Wesley, Version 2.0
 <<    <     >    >>  © 2000 Guido Krüger, http://www.gkrueger.com