In objektorientierten Programmiersprachen werden Design Patterns verwendet, um ein ganz bestimmtes Software-Entwurfsproblem zu lösen. Das Design Pattern (Entwurfsmuster) dient dabei als eine Art Rezept, mit dessen Hilfe die gegebene Programmieraufgabe gelöst wird.
In Java wird mittels Design Pattern das Zusammenspiel von Klassen, Interfaces, Objekten und Methoden mit dem Ziel beschrieben, vordefinierte Lösungen für konkrete Programmierprobleme anzubieten.
Die Entwurfsmuster sind wohlüberlegte Designvorschläge, die Software-Entwickler für den Entwurf ihren eigenen Anwendungen nutzen können.
In Java werden die folgenden Entwurfsmuster häufig verwendet:
- Das Singleton (Das Einzelstück) Design Pattern
- Das Immutable (Das Unveränderliche) Design Pattern
- Interfaces (Schnittstellen) in Java
- Das Iterator (Zugriffszeiger) Design Pattern
- Das Delegate (übertragen) Design Pattern
Die Entstehungsgeschichte von Design Patterns
Mehr Infos auf Amazon*
Der breiten Öffentlichkeit wurden Design Patterns im Jahr 1995 vorgestellt. Die vier Autoren Erich Gamma, Richard Helm, Ralph Johnson und John Vlissides gelten als Wegbereiter dieser revolutionären Software-Entwurfsidee. Sie haben in ihrem Buch Design Patterns. Elements of Reusable Object-Oriented Software* oft benötigte Lösungsmuster für wichtige Programmierprobleme allgemein beschrieben.
Mit ihrem Werk über Software-Entwurfsmuster haben sie eine der bedeutsamsten und hilfreichsten Entwicklungen der objektorientierten Programmierung der späten 90er losgetreten und es überhaupt erst ermöglicht standardisierte Bezeichnungen für bestimmte Softwaredesigns zu finden.
Seit dem Erscheinen ihrer Buchs werden Begriffe wie Singleton, Factory oder Iterator routinemäßig von Programmieren bei der Beschreibung von objektorientierten Softwareprojekten verwendet und förderten die Kommunikationsfähigkeit der Entwickler untereinander erheblich.
In diesem Beitrag möchten wir die wichtigsten Java Design Patterns kurz vorstellen. Dabei werden wir auf die grundlegenden Eigenschaften des jeweiligen Entwurfsmusters eingehen und mögliche Einsatzgebiete vorstellen. Da Design Patterns ein sehr komplexes Thema sind, können wir mit diesem Beitrag nicht auf alle Details eingehen, daher sind unsere Ausführungen als Einstieg in dieses spannende Gebiet zu sehen.
Interessierte, die mehr über Java Entwurfsmuster erfahren möchten, können mit dem Buch Design Patterns. Elements of Reusable Object-Oriented Software* tiefer in die Materie eintauchen.
1 – Das Singleton (Das Einzelstück) Design Pattern
Beginnen wir mit einem der leichtesten Entwurfsmuster, dem Singleton Design Pattern. Das Singleton begegnet uns an vielen Stellen im Alltag, so sind bspw. der Windowmanager von Betriebssystemen und der Spooler in Drucksystemen nach dem Singleton Entwurfsmuster implementiert.
In Java ist ein Singleton eine Klasse, die nur ein einziges Mal istanziert werden darf. Von dieser Klasse existiert während der gesamten Programmlaufzeit nur ein einziges Objekt, das Singleton. Auf das Objekt kann über eine öffentliche get-Methode global zugegriffen werden. Beim ersten Zugriff (erstmaliges Aufrufen der get-Methode) wird das Objekt automatisch instanziert.
Besonders nützlich sind Singletons, wenn der Zugriff auf statische Variablen gekapselt werden soll. Über das Singleton kann zusätzlich die Instanzierung der statischen Variablen kontrolliert werden. Es ist zu beachten, dass das Singleton-Objekt während der gesamten Programmlaufzeit existiert und nicht an den Garbage Collector zurückgegeben wird. Auch die Objekte, auf die das Singleton referenziert, existieren über die gesamte Laufzeit.
Schauen wir uns eine mögliche Implementierung des Singleton Entwurfsmusters an:
Entwurfsmuster: Das Singleton Design Pattern in Java
public class Singleton { private static Singelton instance = null; private Singleton() {} public static Singleton getInstance() { if (instance == null) { instance = new Singleton(); } return instance; } }
Die Singleton-Klasse besitzt die private statische Membervariable singletonObject, in der die einzige Instanz des Singletons gespeichert wird. Weiterhin besitzt sie den privaten Konstruktor Singleton(), der nur aus der Singleton-Klasse selbst aufgerufen werden kann. Dadurch wird verhindert, dass andere Klassen zusätzliche Singleton-Instanzen erzeugen können. Diese Restriktion hat aber ihren Preis. Singleton-Klassen können aufgrund des privaten Konstruktors nicht abgeleitet werden.
Mit der öffentlichen statischen Methode getInstance kann die Singleton-Instanz zurückgeliefert und falls diese noch nicht vorhanden ist, erzeugt werden. Die Methode getInstance ist mit dem Java Modifier public deklariert und somit auch von außerhalb sicht- und zugreifbar. In unserem Beitrag: Modifier in Java könnt ihr mehr über die Auswirkungen von Modifiern auf die Eigenschaften von Variablen, Methoden und Klassen erfahren.
Seit Java 5 können Singletons auch mit Hilfe von Enumerations erstellt werden. Nach Joshua Bloch ist dies die beste Art ein Singleton zu implementieren.
Entwurfsmuster: Das Singleton Design Pattern als Enum
public enum Singleton { INSTANCE; public void doSomething() { //TODO } }
Das Singleton wird dann folgendermaßen verwendet: Singleton.INSTANCE.doSomething()
.
Neben diesen beiden oben vorgestellten Entwurfsmöglichkeiten, sind noch weitere komplexere Baupläne für Singleton-Klassen denkbar. Auf der folgenden Wikipedia-Seite ist eine sehr umfangreiche Liste von Singleton-Implementierungen zusammengestellt worden, die für Interessierte sehr zu empfehlen ist.
2 – Das Immutable (Das Unveränderliche) Design Pattern
In Java werden Objekte als unveränderlich (immutable) bezeichnet, wenn diese nach ihrer Instanzierung nicht mehr verändert werden können. Die Membervariablen sind nicht öffentlich zugänglich und werden im Konstruktor oder statischen Initialisierern gesetzt. Auf sie kann ausschließlich lesend zugegriffen werden.
Bevor man nun eine informationshaltende Klasse erstellt, sollte man festlegen, ob diese Klasse änderbar oder unveränderlich werden soll. Eine unveränderliche Klasse besitzt einige Vorteile, wie z.B.:
- Schutz vor Manipulation: Ein unveränderbares Objekt kann an jede Klasse gesendet werden, ohne dass diese das Objekt ändern kann. Somit muss auch keine tiefe Kopie (Deep Copy) erstellt oder dieses Objekt im Cache aktualisiert werden.
- Verbessert die Performance: Da keine tiefen Kopien angelegt werden müssen, wird der Speicherbedarf minimiert und der Garbage Collector hat weniger zu tun.
- Nebenläufigkeit ohne Synchronisation möglich: Es können mehrere Threads erstellt werden, diese können gleichzeitig auf das unveränderbare (immutable) Objekt zugreifen. Da es nicht verändert werden kann, muss nicht synchronisiert werden.
Sehr bekannte Beispiele für unveränderliche Klassen in der Java-Klassenbibliothek sind die Wrapper-Klassen oder auch die Klasse String.
Um ein wirklich unveränderliches Objekt zu erhalten, muss die Klasse selbst, ihre Membervariablen und Methoden als final deklariert werden. Dies hat jedoch den Nachteil, dass die Klasse nicht abgeleitet werden kann. Würde man die Klasse nicht als final deklarieren, könnten Tochterklassen von ihr abgeleitet werden, die zwar nicht die privaten Membervariablen der Basisklasse verändern können, aber selbst veränderbare Elemente hinzufügen könnten. Dies widerspricht der Idee des Immutable Design Patter und daher sollte die Klasse als final deklariert werden.
Schauen wir uns nun eine sehr einfache Implementierung des Immutable Entwurfsmusters in Java an:
Entwurfsmuster: Das Immutable Design Pattern in Java
public final class Immutable { // Die Deklaration als final ist aus 2 Gründen sinnvoll: // 1 - Zufälliges Ändern ist nicht möglich // 2 - Compilier-Fehler wenn Initialisierung vergessen wurde private final byte monat; private final byte[] werte; public Immutable(byte monat, byte[] werte) { // da ein byte-Wert übergeben wird, muss nicht geklont werden this.monat = monat; // veränderbare Argumente müssen geklont werden, bevor sie gespeichert werden können this.werte = (byte[]) werte.clone(); } public final byte getMonat() { return monat; // byte-Wert muss nicht geklont werden } public final byte getWert(int index) { // nur der nicht veränderbare byte-Wert wird zurückgegeben, nicht das Array-Objekt return werte[index]; } }
Wie im oberen Programmbeispiel zu sehen ist, sind alle Membervariablen privat und werden ausschließlich im Konstruktor gesetzt (schreibend zugegriffen). Werden veränderliche Objekte an den Konstruktor übergeben, dann müssen diese, wie in Zeile 15, kopiert (geklont) und erst danach der Membervariable zugewiesen werden.
Weiterhin darf nicht auf eine Membervariable lesend zugegriffen werden, wenn diese ein veränderliches Objekt ist. Daher wird in Zeile 25 auch nicht auf das Array selbst, sondern auf den angforderten byte-Wert lesend zugegriffen. Dieser ist unveränderlich.
Eine sehr ausführliche Beschreibung des Immutable Design Patterns wird von Mikael Grev auf http://www.javalobby.org/articles/immutable/ gegeben und ist sehr zu empfehlen.
3 – Interfaces (Schnittstellen) in Java
In Java kann eine abgeleitete Klasse nur genau eine Vaterklasse haben. Das Erben von mehr als einer Superklasse (Mehrfachvererbung) ist nicht möglich. Ganz auf Mehrfachvererbung verzichten wollten die Java-Entwickler aber nicht und schafften einen Mittelweg: das Interface.
Interfaces können als Ersatzkonstrukt für Mehrfachvererbung gesehen werden. Eine Klasse kann mehrere Interfaces implementieren, d.h. ihr können mehrere Schnittstellen zur Verfügung gestellt werden. Jede dieser Schnittstellen (Interfaces) muss aber von der Klasse vollständig implementiert werden.
Ein Interface ist eine Schnittstelle, über die einer Klasse bestimmte Funktionen zur Verfügung gestellt werden. Um die Funktionen nutzen zu können, müssen sie aber erst von der Klasse implementiert werden. Das Interface gibt nur den Rahmen (die Methodendeklarationen) vor. Ein Interface trennt somit die Beschreibung von Eigenschaften einer Klasse von ihrer Implementierung.
Interfaces können als eine besondere Form einer Klasse angesehen werden. Sie enthalten ausschließlich Konstanten und abstrakte Methoden. Die abstrakten Methoden müssen von der Klasse implementiert werden, der das Interface zugewiesen wird.
Auf unserer Übersichtsseite zu diesem fundamentalen Sprachelement von Java haben wir die wichtigsten Eigenschaften von Interfaces zusammengefasst. Weitere Details über dieses spannende Thema können dort ausführlich nachgelesen werden.
4 – Das Iterator (Zugriffszeiger) Design Pattern
Mit einem Iterator werden Elemente einer Sammlung (Collection) der Reihe nach durchlaufen. In der Sammlung sind meist Objekte des gleichen Datentyps. Es können aber auch gemischte Objekte in einer Sammlung gelagert werden.
In der Programmiersprache Java sind bereits einige Collections implementiert:
- List
- Set
- Map
- Vector
- Hashtable
- Stack
- BitSet
Mit Hilfe eines Iterator-Objekts ist es möglich jede dieser Sammlungen der Reihe nach zu durchlaufen. Wie die Collection dabei implementiert ist, spielt für den Iterator keine Rolle. Die Struktur der Sammlung bleibt somit für den Aufrufer verdeckt. Er erhält nur das jeweils nächste Element.
Aufbau eines Iterators in Java
Der Aufbau eines Iterator-Objekts ist sehr einfach und besteht aus mindestens den folgenden beiden Methoden: hasNext() und next().
Mit der Methode hasNext() wird überprüft, ob die Sammlung noch ein weiteres Element besitzt, das bisher noch nicht durchlaufen wurde. Die Methode next() liefert das nächste Element an den Aufrufer zurück und bedient sich dafür der Methode hasNext().
In der folgenden Abbildung ist das Zusammenspiel von einem Iterator und den Elementen einer Collection dargestellt:
Beispielanwendung: Implementieren und Nutzen eines Iterators in Java
Der folgende Quellcode definiert eine Iterator-Testklasse, in der ein Iterator die Elemente einer Sammlung (Collection) durchläuft:
Beispielanwendung: Erstellen eines Iterators in Java
/* * Die IteratorTest-Klasse definiert eine Collection und einen Iterator */ class ShortSammlung { short[] data; public ShortSammlung (short[] inputData) { data = inputData; } public Iterator getElements() { Iterator myIterator = new Iterator(); return myIterator; } // Innere lokale Klasse stellt Iterator zur Verfügung class Iterator { int index; public Iterator() { index = 0; } public boolean hasNext() { return index < data.length; } public short next() { // Post-Inkrement: aktuelles Element liefern, dann Index um 1 erhöhen return data[index++]; } } } public class IteratorTest { static short[] testData = {1,2,4,8,16,32,64,128,256}; public static void main(String[] args) { // Erzeugen der Sammlung (das Collection-Objekt) ShortSammlung sammlung = new ShortSammlung(testData); // Erzeugen von Iteratoren ShortSammlung.Iterator it1 = sammlung.getElements(); ShortSammlung.Iterator it2 = sammlung.getElements(); // Ausgeben der ersten Elemente der Sammlung System.out.println("\n" + it1.next()); System.out.println(it1.next()); System.out.println(it1.next() + "\n"); // Alle Elemente der Sammlung von Beginn an durchlaufen while (it2.hasNext()) { System.out.println(it2.next()); } } }
Die Klasse IteratorTest enthält die main-Methode und ist somit das Hauptprogramm unserer Testanwendung. Als Erstes wird eine Sammlung (Collection-Objekt) erzeugt. Danach werden zwei unabhängige Iteratoren beschafft. Dazu wird die Methode getElements() der Klasse ShortSammlung verwendet. Mit Hilfe der Iteratoren werden die Elemente der Sammlung durchlaufen.
Es ist zu beachten, dass die zurückgegebenen Iteratoren vom Datentyp ShortSammlung.Iterator
sind, da die Klasse Iterator eine innere Klasse der Klasse ShortSammlung ist. Jeder Iterator verfügt über eine eigene Hilfsvariable index, die auf das jeweils nächste Element der Sammlung zeigt. Daher ist es möglich mehrere aktive Iteratoren zu verwenden, die alle auf die gleiche Sammlung zugreifen.
Iterator werden oft mit Hilfe einer lokalen oder anonymen Klasse implementiert. Dies hat den Vorteil, das alle benötigten Hilfsvariablen (hier die Variable index) je Aufruf angelegt werden. Somit sind pro Sammlung beliebig viele aktive Iteratoren möglich und die Iteratoren sind für genau diese Sammlung optimiert.
Die Ausgabe der Iterator-Beispielanwendung ist in der unteren Abbildung dargestellt:
Der erste Iterator liefert die ersten drei Elemente zurück. Der zweite Iterator beginnt wieder beim ersten Element und durchläuft die gesamte Sammlung.
5 – Das Delegate (übertragen) Design Pattern
Möchte man Programmcode anderer Klassen nutzen, ohne die eigene Klasse von ihnen abzuleiten, ist das Delegate Design Pattern eine gute Wahl. Dabei werden bestimmte Aufgaben an fremde Klassen übertragen (delegiert).
Die eigene Klasse löst in diesem Fall die an sie gestellten Aufgaben nicht selbst, sondern übergibt die Aufgaben an die Hilfsklassen und überlässt ihnen das Lösen. Die eigene Klasse (Delegate-Klasse) kümmert sich nur um das Zurverfügungstellen der für die Lösung benötigten Informationen. Das eigentliche Lösen der Aufgabe findet in den Hilfsklassen statt.
Das Delegate Design Pattern wird auch als Proxy oder Adapter bezeichnet. Der Nutzer eines Dienstes (Klient) kennt nur die Schnittstelle zu dem Vertreter der Diensterbringung und das Protokoll. Er weiß nur wie er die Schnittstelle benutzen muss. Der Vertreter regelt die Diensterbringung, oft auf dynamische Weise, und wählt dazu einen konkreten Dienstleister aus.
Ein Beispiel für das Delegate Design Pattern ist eine grafische Anwendung, die sowohl in einem JFrame als auch in einem JInternalFrame laufen soll. Da beide Fensterklassen sich nicht die gleiche Vererbungshierarchie teilen, muss mittels Delegation auf ihre Dienste zugegriffen werden.
Dazu müssen alle relevanten Funktionen beider Klassen in einer gemeinsamen Delegate-Klasse zur Verfügung gestellt werden. Die Delegate-Klasse sammelt die benötigten Daten und stellt diese den Fensterklassen per Delegation zur Verfügung. Soll etwas Bestimmtes ausgegeben werden, so regelt die Delegate-Klasse wie dies von der jeweiligen Fensterklasse realisiert werden soll.
Konstruktion des Delegate Design Patterns:
- Erstellen einer Delegate-Klasse. Diese enthält alle zu delegierenden Funktionen, auf die öffentlich zugegriffen werden kann.
- Klassen, die auf die Delegate-Klasse zugreifen, erhalten eine Delegate-Membervariable. Über die Delegate-Membervariable wird die entsprechende Methode der Delegate-Klasse aufgerufen. In der Delegate-Klasse wird dann entschieden wie die Anfrage weiterbearbeitet wird.
Beispielanwendung: Implementieren des Delegate Design Patterns in Java
Der folgende Quellcode definiert eine Delegate-Testklasse, in der ein Delegator Printaufgaben an zwei Delegates überträgt. Jedes Delegate implementiert dabei das Interface Printable. Dadurch wird sichergestellt, dass alle Delegate die notwendige Methode print() implementiert haben.
Entwurfsmuster: Das Delegate Design Pattern in Java
/* * Die DelegateTest-Klasse veranschaulicht das Delegate Design Pattern */ interface Printable { // alle Methoden in Interfaces sind implizit public und abstract // man könnte auch schreiben: "public abstract void print();" void print(); } class BWPrinter implements Printable // Delegate { public void print() { System.out.println("\nBWPrinter prints!"); } } class ColorPrinter implements Printable // Delegate { public void print() { System.out.println("\nColorPrinter prints!"); } } class Printer // Delegator { Printable actualPrinter = new BWPrinter(); public void print() { // ruft die print-Methode von BWPrinter oder ColorPrinter auf // dieser Aufruf erfolgt dynamisch, je nachdem welches Objekt // in der Referenzvariable actualPrinter gerade gespeichert ist actualPrinter.print(); // delegation } public void switchTo(Printable otherPrinter) { this.actualPrinter = otherPrinter; } } public class DelegateTest { public static void main(String[] args) { Printer myPrinter = new Printer(); myPrinter.print(); myPrinter.switchTo(new ColorPrinter()); myPrinter.print(); } }
Durch das Verwenden des Printable-Interface, kann die Delegation flexibler und typsicherer erfolgen. Der Delegator (Printer-Klasse) besitzt eine flexible Referenz (actualPrinter) vom Datentyp des Interfaces auf die möglichen Delegate (BWPrinter und ColorPrinter). Dadurch wird das Umschalten zwischen den Delegaten abstrakt und ist um weitere Delegate erweiterbar. Neue Delegate müssen dazu nur das Printable-Interface implementieren.
Die Klasse Printer kann mit der Methode switchTo festlegen, an welche Klasse, BWPrinter oder ColorPrinter, Aufgaben delegiert werden. Für die Außenwelt scheint es, als würde die Klasse Printer den Druckvorgang ausführen.
In Wirklichkeit wird diese Aufgabe aber an das aktuell ausgewählte Delegat übertragen. Den tatsächlichen Druckvorgang führen somit die beiden Klassen BWPrinter und ColorPrinter aus. Delegation ist somit die einfache Weitergabe einer Aufgabe an jemand anderen.
Die Ausgabe der Delegate-Beispielanwendung ist in der unteren Abbildung dargestellt:
… to be continued
Comments 2
Ich weiß nicht ob das hier noch aktiv ist, aber ich versuche einmal mein Glück… ,-) Ich hätte eine Frage zum Delegate-Pattern.
Durch die hilfreiche Beschreibung oben konnte ich das Pattern umsetzen. Ich hänge jedoch an einem gewissen Punkt fest. Ich beschreibe das einmal anhand des oben genutzten Beispiels, damit es für alle direkt nachvollziehbar ist.
Erweitern wir das Szenario um eine neue Klasse namens Fax auf der gleichen Ebene wie Printer. Dieses Objekt (Klasse) kann auch nur s/w drucken. Daher macht es Sinn auch für dieser Klasse die Membervariable auf Printable actualPrinter = new BWPrinter(); zu setzen.
In der Implementierung der Methode print in der Klasse BWPrinter muss nun auf Attribute etc. vom Objekten des Typ’s Printer oder Fax zugegriffen werden, damit die Methode korrekt arbeiten kann.
Wie kann man das am besten im Sinne eines guten OOP Designs lösen?
Wäre das Fax dann nicht eher ein FaxPrinter?
So dass myPrinter.switchTo(new FaxPrinter()) geschrieben werden kann.
FaxPrinter kann ja dann von BWPrinter erben.
Andernfalls reimplemtierst du ja den Delegat, das verletzt das DRY-Prinzip.