Entwurfsmuster unter Java
Um leistungsfähige objektorientierte Modelle zu entwerfen, stehen dem Entwickler heute Entwurfsmuster hilfreich zur Seite. Speziell unter Java kann mittels Entwurfsmuster Entwicklungsaufwand eingespart werden. Da man diese eigentlich nur im Gesamten verstehen kann, ist es schwer, dessen Nutzen direkt zu erkennen. Für diesen Beitrag wurden anhand von verschiedenen Szenerien ihr Anwendungszweck heraus gearbeitet
In jedem objektorientierten System müssen Objekte erzeugt werden. Für einen
guten Entwurf ist es wichtig, konkrete Stellen zu definieren, wo sich Objekte
erzeugen lassen. Alles andere kann kein guter Entwurf sein. Der Erzeugungsprozeß
kann mittels sogenannter Erzeugungsmuster versteckt werden. Diese machen das
Gesamtsystem davon unabhängig, wie die Objekte erzeugt und zusammengesetzt
werden und wie sich repräsentieren. Solche Erzeugungsmuster bestimmen konkret
was, wie und wann etwas im System erzeugt wird. Das klassische Erzeugungsmuster
verwendet Vererbung, um die Klasse des zu erzeugenden Objekts zu variieren. Das
System kann so von oben herab die einzelnen Objekte konfigurieren und hat so einen
zentralen Ansatzpunkt geschaffen, ohne sich auf Details im Gesamtsystem
beziehen zu müssen. Heute bedient sich jedes Rahmenwerk (Framework) dieser
Erzeugungsmuster. Wir beschreiben hier das Fabrikmethoden Muster konkreter.
Der Praxiseinsatz „Fabrikmethode"
Dieses Muster definiert eine Schnittstelle mit Operationen zur Erzeugung
von konkreten Objekten. Jedoch unterscheiden hierbei Unterklassen, in welchen
Fällen und von welchem Typ das erzeugte Objekt ist. Ein Sammelsurium aus
Erzeugungsmethoden ist dazu in den Oberklassen notwendig. Dieser Modellentwurf
ist für Rahmenwerke typisch. Die Außenwelt bedient sich einer Klasse des
Rahmenwerks und erzeugt darüber per Erzeugungsoperationen konkrete Objekte.
Stellen wir uns ein Rahmenwerk für beliebige Arten von Anwendungen vor, welche
die Aufgabe haben, verschiedene Dokumentarten zu verwalten. Das Rahmenwerk
abstrahiert hierzu die Klassen „Anwendung" und „Dokument". Allgemein
sind diese Klassen abstrakt definiert, damit der Klient die eigene
Implementierung per Unterklassenbildung einbringen kann. Um jetzt eine
Photo-Anwendung zu erstellen, realisieren wir unsere eigenen Klassen „PhotoAnwendung"
Lind „PhotoDokument".
Dabei ist die Klasse „PhotoAnwendtmg" allein für die Verwaltung der Dokumente
zuständig. Auf Aufforderung wird das PhotoDokument Objekt per Menüauswahl
erzeugt. Da die Dokumentklasse unterhalb der Anwendungsklasse angesiedelt
ist, weiß diese zwar, dass etwas erzeugt werden soll, aber nicht welcher Typ
von Dokument. Als Lösung kommt das Fabrikmethoden Muster wie gerufen. Es
kapselt das Objekterzeugungswissen der Dokument Unterklasse und lagert es aus
dem Rahmenwerk in die Außenwelt aus. Die Lösung besteht darin, daß eine
Unterklasse von der abstrakten Rahmenwerk Klasse geschaffen wird, weiche die
abstrakte Methode „erzeugeDokument" überschreibt und so die Erzeugung
anstößt. Nur durch Untcrklassenbildung wird die Erzeugung möglich, wobei die
exakte Klasse dem Rahmenwerk nicht bekannt ist. Die „erzeugeDokument" Methode
der Basisklasse nennt man Fabrikmethode, weil diese das Objekt anlegt.Die
Frage ist, wie wir anwendungsspezifische Dokumentklassen anlegen können, ohne
im Rahmenwerk zu anwendungsspezifisch zu werden. Um dies zu erreichen, kann
der Entwickler im Rahmenwerk eine Klasse einführen, welche die Logik kapselt,
um anwendungsspezifische Unterklassen von der Dokumentklasse zu erzeugen.
Damit der Entwickler auch seine gerade benötigte Klasse aufrufen kann, muß das
Rahmenwerk eine klar definierte Schnittstelle bereitstellen, welche der
Entwickler dann implementiert. Lassen Sie uns einen Blick auf das Gesamtszewario
werfen, damit die Arbeitsweise .ar wird. Das Applikationsobjekt ruft hier die
„erzeugeDokument" Methode bei einem Objekt auf, welche die IDokumentFactory
Schnittstelle implementiert hat. Dabei wird ein String übergeben, welcher der
Methode mitteilt welche Unterklasse von der Klasse Dokument erzeugt werden
soll. Die Applikationsklasse braucht nicht zu wissen, welche Klasse des
Objekts den Methodenaufruf tätigt oder welche Dokumentklasse
letztlich instanziiert wird. So ist das Rahmenwerk vollständig unabhängig von
der Anwendung. Das Diagramm zeigt die einzelnen Schnittstellen und Klassen,
die im Fabrikmethoden Muster vorkommen.Die Klasse Dokument ist eine abstrakte
Klasse des Rahmenwerks, welche durch die Fabrikmethode erzeugt wird. Das
konkrete Objekt ist hier „ConcreteDocument", welches durch die
Fabrikmethode instanzüert wird. Die Erzeugungsanfrage kommt von einer
anwendungsunabhängigen Klasse (Applikation), welche die Aufgabe hat,
anwendungsspezifische Klassen anzulegen. Hierbei bedient diese sich einer
Fabrikklasse (Factory class). Die DokumentFactorv ist eine anwendungsunabhängige
~ Schnittstelle. Diese Schnittstelle deklariert eine Methode, welche
vom Erzeugungsobjekt (Applikation) aufgerufen wird, um ein konkretes Objekt zu
erzeugen. In unserem Fall heißt diese „erzeugeDokument". Diese Methode
nimmt dabei Argumente entgegen, um eine bestimmtes Objekt instanziieren zu
können. Die eigentliche `DokumentFactory Klasse ist eine anwendungsspezifische
Klasse, welche die IDokumentFactory Schnittstelle implementiert. Diese verfügt
nun um die Methode, um ein konkretes Produkt zu erzeugen. Ein Nachteil des Fabrikmethoden
Musters ist, daß jeder Klient die Erzeugungsklasse ableiten muß, nur um ein
konkretes Objekt zu erzeugen. Die Bildung solcher Unterklasse ist egal,
solange der Klient die Erzeugungsklasse ableiten n,uß. Ist dies jedoch nicht
so, muß man mit den vorgeschriebenen Entwurf irgendwie zurecht kommen. Das
Fabrikmethoden Muster sollte in folgenden Fällen zum Einsatz kommen:
1) Wenn eine Klasse die Klassen von Objekten, welche diese erzeugen soll, nicht
im voraus kennen kann.
2) Wenn eine Klasse möchte, daß ihre Unterklassen die von ihr zu erzeugenden Objekte festlegt.
3) Wenn Klassen Zuständigkeiten an eine von mehreren Hilfsunterklassen delegieren sollen und das Wissen lokalisieren wollen, an welche Hilfsklasse die Zuständigkeiten delegiert werden.
Der Praxiseinsatz „Brücke"
Besonders wichtig ist, es ein Modell
zu strukturieren, damit die Öffentlichkeit den Aufbau auch verstehen kann. Zur
Strukturierung hat sich mittlerweile das Brückenmuster etabliert. Überall
spricht man davon, wie wichtig es ist, die Schnittstelle von der
Implementierung zu trennen. Auf der Objektebene bedeutet dies nichts anderes,
als dass ein Interface als Typ deklariert und es innerhalb einer Klasse
implementiert. Die Klasse bindet somit das Interface an seine Implementierung.
In der Praxis kommt es jedoch häufig vor, dass ein Typ sich auf verschiedene
Art und Weise implementieren läßt. Lassen Sie uns ein Beispiel skizzieren,
woran der Nutzen des Brückenmusters ersichtlich wird_ Angenommen, wir sollen
für eine Bankanwendung die Konten verwalten. Dort kommen dann Girokonten und Sparkonten
vor. Jedes dieser Kontoarten kann privat oder geschäftlich eröffnet werden und
hat somit verschiedene Ausprägungen. Diese Ausprägungen sind jeweils anders zu
implementieren. Mit Vererbung als Lösungsansatz kommt man hier nicht weit.
Auch Schnittstellen alleine reichen nicht aus. Es muß also ein anderer Weg
her. In solchen Fällen hilft das Brückenmuster. Das Muster teilt die
Schnittstelle und die Implementierung in zwei separate miteinander arbeitenden
Objekte auf. Jedoch handeln diese als ein logisches Objekt mit kombinierter
Schnittstelle und Implementierung. Die zwei Objekte sind dabei Instanzen zweier
separater Klassen (Abstraktion und Implementierer). Dabei repräsentiert die
Abstraktion den Typ zur Außenwelt. Diese Abstraktion implementiert dabei
nicht die Schnittstelle, sondern delegiert alles an die lmplementation. Der
Implementierer implementiert dabei die Schnittstelle, welche die Abstraktion
bereitstellt. Gewöhnlich sind solche Abstraktionen und lmplementierungsklassen
in einer Hierarchie organisiert. Variierende und neu definierte Abstraktion
repräsentieren Untertypen des Basistyps_ Dessen Implementierer sind ebenso
aufgeteilt. Die Schlüsseleigenschaft des Brükkenmusters (Brigde) besteht
darin, daß jede Abstraktion mit jedem Implementierer arbeitet. So kann die
Beziehung zwischen den beiden Hierarchien oberhalb dieser Hierarchie
angesiedelt sein. Deshalb darf auch keine Unterklasse die Schnittstelle ändern
oder erweitern, dies würde die Zusammenarbeitslogik der Hierarchie zerstören.
Nur so kann eine Klasse einer Hierarchie mit jeder Klasse der anderen
Hierarchie zusammenarbeiten. Deshalb ist es wichtig, daß die Klassen in der
lmplementierungshierarchie über die gleichen Schnittstellen verfügen. Nur so
lassen sich die Objekte unabhängig variieren. Wenn eine Klassenabstraktion über
mehrere Implementierungen verfügt, realisiert man dieses mittels der Vererbung.
Dabei definiert eine abstrakte Klasse die Schnittstelle, während die
Unterklassen die jeweiligen Implementierungen aufnehmen. Es gibt jedoch Entwurfssituationen, in denen dieser Ansatz nicht flexibel genug ist. Denn die Vererbung bindet die Implementierung für immer an die Abstraktion. Dies macht es schwierig, die Abstraktion und Implementierung unabhängig zu verändern oder zu erweitern oder geschweige denn diese wiederzuverwenden.
Vererbung nur begrenzt möglich Eine Vererbung ist nicht mehr möglich, wenn eine
Klasse in einer Ausprägung definiert werden soll, die nicht mehr in die
Hierarchie hineinpaßt. Auf unser Konto-Beispiel bezogen kann ich
schließlich nicht sagen, dass die neue Klassen von Girokonto jetzt GirokontoPrivat
und GirokontoGeschäftlich heißen. Dies würde jeder Entwurfslogik widersprechen.
Das Brückenmuster löst dieses Problem, indem es die Abstraktion und dessen
Implementierungen in zwei unterschiedlichen Klassenhierarchien verwaltet. Für
unser Beispiel gibt es eine Klassenhierarchie für die allgemeinen Kontoarten:
Girokonto und Sparkonto und eine davon getrennte Hierarchie für die
verschiedenen Ausprägungen dieser Kontoarten. Diese` Implementierung verfügt
über die Wurzelklasse „Kontolmpl". Die verschiedenen Unterklassen davon
implementieren die Eigenheiten wie privat oder geschäftlich für diese Klassen.
Alle Operationen von diesen Unterklassen sind auf der Basis der abstrakten
Operationen der „KontoImpl" Schnittstelle implementiert. Dies realisiert
eine Entkopplung von der Abstraktion, weil Abstraktion und Implementierung über
eine Beziehung verfügen. So wir eine Brücke zwischen der Abstraktion und
Implementierung gebildet und es so ermöglicht beide unabhängig voneinander zu
verwenden. Daher wird dieses Muster auch Brücken Muster genannt. Der Einsatz
des Brükkenentwurfsmusters ist sinnvoll,
1) wenn eine dauerhafte Bindung zwischen
Abstraktion und Implementierung vermieden werden soll.
2) Ebenso wenn eine Abstraktion mit verschiedenen Implementierungen kombiniert
werden soll und unabhängig crweiterbar sein sollen.
3) Die Änderung in der Abstraktion keine Auswirkungen auf die Klientklassen
hervorruft
4) sobald eine zu starke Vergrößerung der Klassenanzahl auftaucht. Solch eine
Klassenhierarchie weißt schon darauf hin, daß diese aufgeteilt werden muß, um
den Überblick zu wahren. Der Java Kode in Listing 2 zeigt das Brückenmuster in
Detail.
Der Praxiseinsatz „Adapter"
Meist kommt in einem objektorientierten
Modell vor, daß eine Klasse nicht mit einer anderen Klasse kommunzieren kann,
da dessen Schnittstelle der Quellklasse nicht bekannt ist. Im ursprünglichen
Entwurf konnte man sich nicht vorstellen, daß beide je miteinander kommunzieren
müssten. Als Lösung ist das Adapter Muster prädestiniert, da es doch noch eine
kompatible Schnittstelle herstellt. Der Adapter paßt die Schnittstelle einer
Klasse an eine andere von ihreren Klienten erwartete Schnittstelle an. Der
Adapter hat die Aufgabe, die Funktionalitäten bereitzustellen, worüber die
Klient-Klasse nicht verfügt. Das Diagramm 1 zeigt, wie der Adapter dies
ermöglicht. Oft kann in der Praxis eine Klasse einer Bibliothek nicht verwendet
werden, weil dessen Schnittstellen nicht mit dem Anwendungsbereich
übereinstimmt. Stellen Sie sich vor, Sie haben ein Buchhaltungsprogramm, welche
für spezielle Funktionalitäten eine bestimmte Bibliothek verwendet. Nun kommt
kurze Zeit später eine leistungsfähigere Bibliothek heraus, die Sie gerne
nutzen wollen. Wegen den inkompatiblen Schnittstellen ist dies nicht möglich.
Wir könnten jedoch die Klassenschnittstelle so ändern, daß Sie unserem
Programm entspricht. Dies wäre aber nur möglich, wenn wir über die Quelltcxte
der Bibliothek verfügen würden. Jedoch würde auch dies keinen Sinn ergeben,
schließlich paßt mein keine Bibliothek an eine Anwendung an. Wesentlich
eleganter ist es, die Anwendung an die Schnittstelle der neue Bibliothek
mittels einer Adapter Klasse anzupassen. Die Klasse realisieren wir durch
Vererbung. Diese Klasse verfügt zwar gegenüber der Außenwelt über die gleiche
Schnittstelle, reicht aber die Anfrage intern an eine ganz andere
Schnittstelle weiter. Beispielsweise kann eine Klasse mittels der
Methode berechrteSummcU seine Arbeit an eine andere Klasse weiterreichen, wo
die Methode vielleicht berechneGesamtsumme() heißt. Da das Programm
nun mit der neuen Schnittstelle arbeiten kann, kann man die neuen Funktionen
nutzen.
Die Verwendung des Adapter Musters empfiehlt
sich in bestimmten Situationen:
1) Sobald eine existierende Klasse verwendet werden soll, deren Schnittstelle
aber nicht mit der benötigten Schnittstelle übereinstimmt.
2) Eine wiederverwertdbare Klasse realisiert werden soll, die mit unbekannten
Klassen interoperabel sein soll.
Ein Adapter Objekt bietet die Öffentliche Schnittstelle, welche die Außenwelt
verlangt. Dabei hat diese keine Kenntnisse, welche Klasse als Implementierung
für die Schnittstelle verwendet wird. Folgende Rollen nehmen die Klassen in
einem Adapter Szenario ein und werden anhand des Diagramms ersichtlich. Der
Clicnt repräsentiert eine Klasse, welche eine Methode einer anderen Klasse
aufruft mittels der Schnittstelle (TargetInterface). Dies besagt aber noch
nicht, daß eine spezielle Klasse letztlich verwendet und aufgerufen wird. Die
Schnittstelle (Targetlnterface) deklariert all die Methoden, deren sich der
Client bedienen kann. Die Adapterklasse implementiert diese Schnittstelle,
aber nicht deren Funktionalität. Alle Arbeiten delegiert diese an die
Adaptee-Klasse weiter. Die Adaptee-Klasse verfügt über die Methode, die der
Client haben will. Dessen Schnittstelle ist völlig unabhängig von den anderen.
Eine Adapter Klasse hat jedoch weitere Möglichkeiten als nur einen Methodenaufruf
zu delegieren. Diese kann mittels übergebener Argumenten ver
sdiiedene Transformationen vornehmen. Hierüber kann man zusätzlich Logik
implementieren, worüber sich die Unterschiede zwischen dem Aufbau der Methoden
beim Interface und der Adaptee-Klasse verbergen lassen. Jedoch gibt es keine
Einschränkungen, wie komplex so eine Adapter Klasse sein soll. Das Basisziel
ist es aber die Methodenaufrufe an andere Objekte weiterzuleiten mittels der
Adapter Klasse. Die Implementierung einer Adapter Klasse ist eigentlich
ziemlich einfach. Das einzige was zu beachten ist, wie die Adapter Klasse
informiert wird, welche Adaptee-Klasse diese aufrufen soll.
Hierzu gibt es zwei Vorgehensweisen:
1) Indem eine Referenz als Parameter im
Konstruktor oder einer seiner Methoden übergeben wird. Dies erlaubt dem
Adapterobjekt mit jeder Instanz oder mehreren Instanzen der Adaptee-Klasse zu
verwenden.
2) Das Erzeugen der Adapter Klasse als innere Klasse von Adaptec-Klasse. Dies
vereinfacht die Assoziation zwischen dem Adapterobjekt und dem Adapteeobjekt,
da diese automatisch jetzt erfolgt. Gleichzeitig macht solch eine Assoziation
jedoch inflexibel.