Kapitel 6 Interfaces und Pattern
Eine zur Vererbung komplementäre Methodik ist die Verwendung von Interfaces.
Ein Interface ist zumindest für C-ähnliche Sprachen ein neues Konzept, womit aber alle Typ- und Konvertierungs-Regeln komplexer werden. Neben Regeln und Beispielen ist die Frage nach dem Sinn der Interfaces berechtigt.
Die Anwort führt zwangsläufig zu Design-Pattern, sprachunabhängigen Gestaltungsmustern, die immer wiederkehren. Die Benutzung eines Interfaces stellt bereits ein fundamentales Pattern dar.
Der Begriff Schnittstelle ist eigentlich eine gute Übersetzung für Interface, ist aber bereits mit einer festen Bedeutung belegt.
In strukturierten Sprachen versteht man darunter die Funktionen mit ihren Signaturen, die Module bzw. Übersetzungseinheiten zur Verfügung stellen.
Schnittstelle: öffentlich bereitgestellte Methoden
In objektorientierten Sprachen wie C++ versteht man unter einer Schnittstelle die öffentlich bereitgestellten Methoden einer Klasse und formuliert dazu das folgende klassische Prinzip (Abb. 6.1).
Abbildung 6.1 Schnittstelle einer Klasse
![Icon](common/D.gif)
Programmiere gegen die Schnittstelle
Klassen-Design-Prinzip: Erlaube nur das Programmieren gegen Schnittstellen, nicht gegen die Implementation.
|
Interface vs. Schnittstelle
Im Weiteren wird der Begriff »Schnittstelle« im objektorientierten Sinn verwendet, »Interface« jedoch für das spezielle Java-Konstrukt.
6.1 Interface: Definition und Regeln ![down](common/down.gif) ![top](common/15x15leer.gif)
In der Definition eines Interfaces sind die Member-Modifikatoren optional (siehe zweite Regel unten).
Interface-
Deklaration
[public][static][final] constType
constName= value;
...
[public][abstract] ResultType methodName(parameters)
[ThrowsList];
...
}
Interface-Implementation
Eine Klasse implementiert ein oder mehrere Interfaces, indem sie sie im Kopf ihrer Declarations aufführt:
![Icon](common/R.gif)
[cModifiers] class ClassName
[extends SuperClass]
implements
Interface1[,Interface2,...] {...}
Interface-Regeln
Für Interfaces und ihre Verwendung gelten folgende einfache Regeln:
1. |
Ein Interface kann leer sein, d.h., Konstanten- und Methoden-Angaben sind optional. |
Marker-Interface
|
|
Ein leeres Interfaces nennt man Marker-Interface. |
2. |
Alle Modifikatoren innerhalb des Interfaces sind überflüssig, da |
|
|
Konstanten immer public static final sind. |
|
|
Methoden immer public abstract deklarierte Instanz-Methoden sind. |
3. |
Ein Interface hat keine Konstruktoren und somit auch keine Instanzen. |
4. |
Ein Interface ist ein Typ, der für Referenzen, Arrays, Parameter und Argumente verwendet werden kann. |
5. |
Eine Interface-Variable kann immer eine Instanz einer Klasse referenzieren, die das Interface implementiert. |
6. |
Wenn eine Klasse ein oder mehrere Interfaces mit implements deklariert, muss sie alle Interface-Methoden als public erklärte Methoden1 implementieren oder ansonsten abstract erklärt werden. |
7. |
Eine Klasse, die ein Interface implementiert, kann auf alle Konstanten des Interfaces direkt über ihre einfachen Namen zugreifen, ohne den Interface-Namen als Präfix verwenden zu müssen. |
8. |
Besitzt ein Interface Super-Interfaces oder implementiert eine Klasse mehrere Interfaces, so müssen die Signaturen der Methoden in unterschiedlichen (Super-)Interfaces verschieden sein oder – bei gleicher Signatur – denselben Typ als Ergebnis liefern. |
Mehrfachvererbung bei
Interfaces
Die letzte Regel berücksichtigt u.a. die Tatsache, dass bei Interfaces Mehrfachvererbung erlaubt ist. Ein Sub-Interface erbt dann alle Methoden und Konstanten seiner Super-Interfaces.
Kollisionen der Methoden-Signaturen nicht ausgeschlossen!
Wie bei allen Mehrfachvererbungen bzw. -implementierungen kann es zu Kollisionen bei Methoden-Signaturen kommen, die dann vom Compiler erkannt und angezeigt werden.
An der Definition bzw. den Regeln fällt sofort auf, dass Interfaces ohne Klassen-Implementation unbrauchbar sind.
Interfaces sind ohne Klassen-Implementation sinnlos
Denn ein Interface
|
stellt eine (Klassen-) Schnittstelle dar. |
|
kann nur benutzt werden, sofern es mindestens eine Klasse gibt, die es implementiert. |
6.1.1 Beispiele zu den Interface-Regeln ![down](common/down.gif) ![top](common/top.gif)
Zur Verdeutlichung sind bei den Code-Beispielen die Regeln als Kommentare angegeben.
Das erste Code-Fragment zeigt zwei sehr einfache Interfaces ohne Methoden, die von einer noch einfacheren Klasse implementiert werden.2
interface IMarker {}
// 1. Regel
Interface nur mit public static final Feldern
interface IARGB {
// 2. Regel
int RED_MASK= 0xff0000; // alle public static final
int GREEN_MASK= 0xff00;
int BLUE_MASK= 0xff;
}
Einfacher Zugriff auf Interface-
Konstante
class Example implements IARGB, IMarker
{
int redPart (int argb) {
return (argb & RED_MASK) >> 16; // 7. Regel
}
}
Das nächste Code-Fragment zeigt Unverträglichkeiten bei der Mehrfachvererbung:
Kollisions-Beispiel
interface I1 { void f1 (); }
interface I2 { int f1 (); } // void f1(); wäre ok
interface I3 extends I1,I2 { } // 8. Regel:
C-Fehler
oder auch
abstract class C implements I1,I2 {} // 8. Regel:
C-Fehler
Das folgende Interface ILocation besteht aus zwei Methoden, die die x- bzw. y-Position zurückgeben, sowie einer distanceTo()-Methode, die den Abstand zu einem anderen Ort als Ergebnis liefern soll.
Keine statischen Methoden im Interface
interface ILocation {
float getx();
float gety();
float distanceTo(ILocation p); // Abstand
// float distance(ILocation p1, ILocation p2); siehe 2. Regel
}
Symmetrische Operationen sind ausgeschlossen!
Da Interfaces keine statischen Methoden definieren können (2. Regel), macht eine so schöne symmetrische Methode wie distance() leider wenig Sinn. An einer Abstandsbestimmung zwischen zwei Orten wären dann immer drei Instanzen vom Typ ILocation beteiligt:
location1.distance(location2,location3) // 4. Regel
Es folgen drei Implementierungen zu ILocation (Abb.6.2).
Interfaces
werden veerbt
Abbildung 6.2 Verschiedene Implementierungen zu ILocation
Die folgende Klasse Figure kann leer implementiert nur abstract sein:
abstract class Figure implements ILocation {}
// 6. Regel
Konkrete Subklassen von Figure müssen dann eben die Methoden von ILocation implementieren.
Unterschiedliche Implementation einer Interface-Methode
Ein Abstand – präziser eine Metrik – kann unterschiedlich definiert werden. Die Klassen Point und MapPoint implementieren die distanceTo()-Methode einmal als euklidischen Abstand (Luftlinie), ein andermal, wie in Straßenkarten üblich, als Weg entlang der x/y-Koordinaten.
class Point implements ILocation {
private float x,y;
public Point(float x, float y) { this.x=x; this.y=y; }
public float getx() { return x; }
public float gety() { return y; }
public float distanceTo(ILocation p) {
return (float)Math.sqrt((x-p.getx())*(x-p.getx())+
(y-p.gety())*(y-p.gety()));
}
}
Overriding der Metrik-Methode
class MapPoint extends Point { // implementiert
somit ILocation
public MapPoint(float x, float y) { super(x,y); }
public float distanceTo(ILocation p)
{
return Math.abs(x-p.getx()) + Math.abs(y-p.gety());
}
}
Die Methode testCommutation() verwendet nur Interface-Variablen. Sie vergleicht die Abstände zweier ILocation-Argumente, wobei man eigentlich erwartet, dass die Methode distanceTo() kommutativ ist.
Test der Metrik-Methoden
static void testCommutation(ILocation
loc1, ILocation loc2) {
System.out.println(d1.distanceTo(d2)+"=="+
d2.distanceTo(d1)+"?");
}
Ein Metrik-Problem:
nicht kommutativ!
Die Methode testCommutation() kann mit beliebigen Objekten von Klassen aufgerufen werden, die ILocation implementiert haben. Da Point und MapPoint vom Typ ILocation sind, ist also der Aufruf
testCommutation(new Point(0,0),new MapPoint(1,1));
// 5. Regel
![Icon](common/D.gif)
erlaubt und offenbart sofort ein Metrik-Problem :: 1.4142135==2.0?3
Instanz-Methoden sind nicht symmetrisch
Instanz-Methoden sind nicht symmetrisch, da sie immer zum linken Objekt gehören. Dadurch sind Operationen nicht unbedingt kommutativ, was im Design berücksichtigt werden muss. 3
|
6.2 Referenz-Konvertierung und -Casting ![down](common/down.gif) ![top](common/top.gif)
Implizite Konvertierung und explizites Casting wurden bisher nur für primitive Typen besprochen.
Konvertierung und Cast:
Unterscheidung nach Klassen, Arrays, Interfaces
Bedingt durch Vererbung und Interface sind einige Regeln hinzuzufügen, die aufgrund der Kombinatorik nicht mehr so einfach darzustellen sind. Bei den folgenden Konvertierungs-Regeln hat man zu differenzieren zwischen
|
Klassen |
|
Arrays |
|
Interfaces |
6.2.1 Regeln zur Referenz-Konvertierung ![down](common/down.gif) ![top](common/top.gif)
Eine Konvertierung geschieht implizit, wird vom Compiler überprüft und hat die Form
|
nachTyp= vonTyp |
|
Aufruf von f(vonTyp), wobei die Methode wie folgt definiert ist: f(nachTyp p) {...} |
![Icon](common/R.gif)
Konvertierungs-Regeln: Nur der Compiler prüft!
Die Regeln für gültige Konvertierungen unterschiedlicher Typen vonTyp und nachTyp lassen sich wie folgt zusammenfassen:
1. |
Ist vonTyp eine Klasse, so muss nachTyp eine Superklasse oder ein Interface sein, das vonTyp implementiert hat. |
2. |
Ist vonTyp ein Interface, so muss nachTyp ein Super-Interface oder aber die Klasse Object sein. |
3. |
Ist vonTyp ein Array und ist vonTyp ein |
|
|
primitiver (Element-)Typ, so muss nachTyp die Klasse Object oder das Interface Cloneable sein. |
|
|
nicht primitiver (Element-)Typ, so müssen sich die Elemente des Typs nachTyp in vonTyp implizit konvertieren lassen. |
Streng genommen lässt sich der erste Punkt der dritten Regel aus der ersten Regel ableiten, da Arrays vom Typ Object sind und damit implizit Cloneable implementieren.
Der Aufruf von
testCommutation(new Point(0,0),new MapPoint(1,1));
im letzten Beispiel aus 6.1.1 fällt somit unter die erste Regel.
6.2.2 Regeln zum Referenz-Casting ![down](common/down.gif) ![top](common/top.gif)
Cast-Prüfung:
meist erst zur Laufzeit möglich
Casting geschieht explizit (bei unterschiedlichen Typen) und hat die Form
(nachTyp)vonTyp
Der Compiler steht vor dem Dilemma, diese Umwandlung nur sehr unvollständig prüfen zu können. Nur in aussichtslosen Fällen signalisiert er bereits einen Fehler.
![Icon](common/R.gif)
Cast-Regeln, die der Compiler prüft
Die drei Regeln, bei denen der Compiler ein Cast als gültig anerkennt, entsprechen den drei Regeln von 6.2.1, nur eben stark relaxt:
1. |
Sind beide Typen Klassen, müssen sie aus derselben Hierarchie sein. |
2. |
Ist ein Typ ein Interface, kann der andere eine Klasse oder ein Interface sein. |
3. |
Ist ein Typ ein Array, muss der andere Typ |
|
|
Object oder Cloneable sein oder |
|
|
ein Array sein, und ein Cast der Referenz-Elemente vonTyp in nachTyp muss erlaubt sein. |
Die JVM prüft aufgrund der
Konvertierungs-Regeln zur Laufzeit
In allen Fällen muss aber letztendlich die JVM zur Laufzeit anhand des Objekts, das durch vonTyp referenziert wird, entscheiden, ob das Cast wirklich gültig ist. Hierzu gelten dann wieder die Regeln aus 6.2.1.
6.2.3 Exemplarische Konvertierungs- und Cast-Beispiele ![down](common/down.gif) ![top](common/top.gif)
Zu allen Regeln alle kombinatorischen Möglichkeiten zu testen, ist aufwendig und auch langweilig. Für einige wichtige Beispiele reichen Marker-Interfaces und leere Klassen.
Konvertierungen und Casts anhand einer Interface- bzw. Klassen-
Hierarchie
Abbildung 6.3 Beispiel zu Konvertierungs- und Cast-Regeln
Die zu Abb. 6.3 zugehörigen Deklarationen sind dann wie folgt:
Leere Interfaces und Klassen reichen zur Prüfung
interface I1 {}
interface I2 {}
interface I3 extends I1,I2 {}
class B {}
class C extends B implements I3 {}
class D {}
final class F {}
Mögliche und unmögliche Casts und Konvertierungen
Im folgenden Code-Fragment werden mittels Referenz-Variablen diverse Zuweisungs-Kombinationen getestet und die zugehörigen Regeln angegeben:
I1 i1Ref= null; I1[] i1Arr=null;
I2 i2Ref= null; I2[] i2Arr=null;
I3 i3Ref= null; I3[] i3Arr=null;
Object o= null; Cloneable clone= null;
B bRef= null; C cRef= null;
D dRef= null; F fRef= null;
bRef= cRef; // 6.2.1, 1. Regel
i1Ref= i3Ref; // 6.2.1, 2. Regel
o= i1Ref; // 6.2.1, 2. Regel
i1Arr= i3Arr; // 6.2.1, 3. Regel
clone= i1Arr; // 6.2.1, 3. Regel
// cRef= bRef; C-Fehler: 6.2.1, 1. Regel
cRef= (C)bRef; // 6.2.2, 1. Regel
// bRef= (B)dRef; C-Fehler: 6.2.2, 1. Regel
bRef= (B)i3Ref; // 6.2.2, 2. Regel
// i3Ref= i1Ref; C-Fehler: 6.2.1, 2. Regel
i3Ref= (I3)i1Ref; // 6.2.2, 2. Regel
i1Ref= (I1)i2Ref; // 6.2.2, 2. Regel
// i1Arr= i2Arr; C-Fehler: 6.2.1, 3. Regel
i1Arr= (I1[])i2Arr; // 6.2.2, 3. Regel
// i1Arr= clone; C-Fehler: 6.2.1, 3. Regel
i1Arr= (I1[])clone; // 6.2.2, 3. Regel
// i1Ref= (I1)fRef; C-Fehler: 6.2.2, 2. Regel
// fRef= (F)i1Ref; C-Fehler: 6.2.2, 2. Regel
6.3 API: Interface vs. Klasse ![down](common/down.gif) ![top](common/top.gif)
Eine wichtige Frage im Zusammenhang mit der Weiterentwicklung von Packages und Klassen ist die Abhängigkeit des darauf aufbauenden Codes.
API-Problematik: Reaktion auf Löschen, Ändern und Erweitern
Interfaces sowie Basisklassen in Hierarchien definieren durch ihre Methoden ein Protokoll bzw. API (Application Program Interface), das von den abgeleiteten Klassen eingehalten werden muss.
Jede Änderungen oder Erweiterungen des APIs muss also daraufhin untersucht werden, inwieweit es den Code der darauf aufbauenden Klassen »brechen« kann, d.h., dass damit Benutzer-Klassen inkompatibel werden und Applikationen nicht mehr laufen.4
6.3.1 Beispiel JDK: Löschen bzw. Ändern im API ![down](common/down.gif) ![top](common/top.gif)
deprecated: Schutz des Legacy-Codes
Mit jeder weiteren Version hat das JDK des Unternehmens Sun mit obsoleten, veralteten oder unliebsamen Methoden zu kämpfen.
Sun belegt solche Methoden dann mit dem Hinweis »deprecated«, löscht sie aber nicht, da sie ansonsten den Code von unzähligen »alten« Anwendungen (Legacy-Code) brechen würden, die diese Methoden verwenden.
Die Signaturen oder Rückgabewerte von Methoden können natürlich aus den gleichen Gründen genauso wenig geändert werden.
6.3.2 Erweiterungen im API ![down](common/down.gif) ![top](common/top.gif)
Eine Erweiterung – das Hinzufügen neuer Methoden – sollte eigentlich harmlos sein, da das vorhandene Protokoll bzw. API ja nur ergänzt wird.
![Icon](common/D.gif)
Hier verhalten sich Interfaces anders als Klassen:
API-Erweiterungen: Interfaces vs. Klassen
|
Eine Klasse, die durch neue nicht abstract deklarierte Methoden erweitert wird, kann ohne Modifikation von allen anderen Klassen weiter benutzt werden. |
Interfaces: Eine neue Methode »bricht« das API
|
Ein Interface, dem neue Methoden hinzugefügt werden, lässt alle Klassen, die dieses Interface implementiert haben, abstract werden. |
Ein API, das auf Interfaces beruht, kann also nur durch neue Interfaces erweitert werden, die Sub-Interfaces der bereits vorhandenen sind. Der alte Code bleibt dann korrekt, und neuer Code kann durch Implementation der neuen Sub-Interfaces realisiert werden.
6.4 Interface vs. Vererbung ![down](common/down.gif) ![top](common/top.gif)
Sinn eines Interfaces:
Polymorphie »pur«
Der erste Teil dieses Kapitels wird nicht unbedingt den Wert eines Interfaces klar gemacht haben. Mit Ausnahme von ein paar Konstanten kann ein Interface nichts Sinnvolles implementieren, und um es zu verwenden, benötigt man ohnehin Klassen. Wo liegt dann ihr Sinn? Eine sibyllinische Antwort ist:
|
Ein Interface ist reine Polymorphie – ein fundamentales Muster! |
|
6.4.1 Service-Beziehung auf Basis von Vererbung ![down](common/down.gif) ![top](common/top.gif)
Die Kommunikation zwischen Objekten läuft immer über ihre Schnittstellen oder sollte es zumindest (siehe Klassen-Design-Prinzip!).
Jede Art von Zusammenarbeit in einem klassischen OO-System basiert auf Klassen-Assoziationen. Objekte von Klassen benutzen einander, indem sie andere Objekte erzeugen und/oder deren Methoden aufrufen.
Methodenaufrufe basieren auf
Assoziationen
Kurz, auf Basis von Klassen-Assoziationen werden Algorithmen bzw. wird Code realisiert (Abb. 6.4).
Abbildung 6.4 Assoziation gilt für Klassen-Hierarchien
Methodenaufrufe und die Substitutions-Regel
Assoziationen zwischen Klassen können nach der Substitutions-Regel von allen Objekten der Subklassen genutzt werden. Methodenaufrufe und Code auf Basis der Superklassen gilt universell in der Hierarchie.
Vorteile der Vererbung
Vorteile der Vererbung für Code und Methoden
1. |
Code, der auf den Beziehungen bzw. Methoden von Superklassen basiert, läuft generell für alle Objekte in der Klassen-Hierarchie. |
2. |
Eine Subklasse erbt die Implementierung der Superklasse. |
3. |
Passt eine Methode der Superklasse nicht so ganz, kann sie überschrieben werden. Dank der Polymorphie wird zur Laufzeit dann automatisch die zum Objekt passende Methode ausgewählt. |
6.4.2 Service-Beziehung auf Basis von Interfaces ![down](common/down.gif) ![top](common/top.gif)
Interface, eine Indirektions-Stufe für Clients
Das Einfügen von Interfaces bringt erst einmal eine Indirektions-Stufe mehr, was nicht unbedingt ein Vorteil ist (Abb. 6.3).
Abbildung 6.5 Assoziation auf Basis von Interfaces
Betrachten wir, ob die Vorteile der Vererbung (in 6.4.1) geblieben sind.
Interfaces:
Was bleibt von den o.a. Vorteilen der Vererbung?
ad 1: ok, gilt auch für Interfaces.
ad 2: nicht ok, jede Klasse muss ihre Implementierung selbst schreiben.
ad 3: muss man beim Overriding alles neu implementieren, gilt ein uneingeschränktes ok, kann man die Implementation teilweise nutzen, hat man bei Overriding weniger Arbeit, d.h. also »fast ok«.
6.4.3 Nachteile der Vererbung ![down](common/down.gif) ![top](common/top.gif)
![Icon](common/Kaktus.gif)
Betrachtet man die Vorteile (in 6.4.1) genauer, so erkennt man:
Vererbung: Verletzung des Klassen-Design-Prinzips
1. |
Der zweite Vorteil basiert auf der Verletzung des Klassen-Design-Prinzips. Denn wird in der Superklasse die Implementation einer Methode geändert, müssen alle Subklassen, die den ursprünglichen Code nutzen, darauf untersucht werden, ob sie mit dem neuen Code korrekt arbeiten. |
Vererbung impliziert zwangsläufig eine Is-A-Beziehung
2. |
Will man mit einem eigenen Klassensystem den einzigen echten ersten Vorteil nutzen, muss man sich in das andere System als Subklasse einhängen. |
Das erste Problem, der Bruch des Klassen-Design-Prinzips, ist keineswegs trivial:
|
Wie findet man zu einer public erklärten Kasse alle Subklassen im Universum? |
Falsche Is-A-Beziehungen bringen Probleme!
Das zweite Problem ist die recht seltene Is-A-Beziehung. Die eigenen Klassen müssen echte Spezialisierungen der anderen sein. Wenn nicht, erbt man nur Schrott, den andere locker über die Instanzen des eigenen Klassensystems aufrufen können.5
6.5 Design-Pattern: generelle Entwurfsmuster ![down](common/down.gif) ![top](common/top.gif)
Design-Pattern: eine Orientierung
In der Einführung von 6.4 wurde die Verwendung eines Interfaces bereits als Design-Pattern bezeichnet.6 Der Oberbegriff »Design« deutet bereits an, dass es um Gestaltungs-Modellierung geht, nur eben mit speziellem Sinn.
Seit dem »Flagship«-Buch Design-Patterns von E. Gamma et al. von 1995 werden Pattern und Anti-Pattern überall gesichtet. Die Zahl der Entdeckungen geht – nach unseriösen Schätzungen – in die Hunderte.
Aber Entwurfsmuster werden nicht erst seit 1995 in der Software verwendet, und in anderen Wissenschaften sind sie schon lange etabliert.7
Die Ausbildung von Ingenieuren und Architekten ist geprägt von Mustern, die vorschreiben, wie aus Basiselementen funktionstüchtige Einheiten zusammenzubauen sind.
Design-Pattern: sprachunabhängiges Muster
Im Software-Engineering versteht man unter Design-Pattern eine besondere Objektstruktur oder Interaktions-Beziehung zwischen Objekten, die bei ähnlichen Problemen so immer wiederkehrt.
![Icon](common/D.gif)
Was ist ein Pattern?
Ein Indiz für ein Pattern ist, dass man es sprachunabhängig mittels einer Entwurfssprache wie der UML beschreiben kann, und – als Knockout-Kriterium – dass es jemand anders nützlich findet und benutzt.
|
Neben Pattern tauchen natürlich auch Begriffe wie Idiom und Framework auf. Beide Begriffe sind bodenständiger.
Idiom: sprachabhängiges Muster
|
Idiome sind nützliche oder notwendige, aber sprachabhängige Muster. |
Ein Idiom in C++, welches z.B. mit Speicherbereinigung zu tun hat, gibt es in Java oder Smalltalk erst gar nicht.
Framework:
Klassen-Hierarchien mit dedizierter Aufgabe
Frameworks sind sprachabhängige detaillierte Klassen-Hierarchien, die dedizierte Aufgaben erledigen (Beispiel: Java 2D, Collections8 ).
Fundamentale Pattern
Um den Überblick zu wahren, werden Pattern in Kategorien eingeteilt. Besonders interessant ist die kleine Gruppe der fundamentalen Pattern.
Fundamentale Design-Pattern gehören zum Handwerkszeug
Fundamentale Design-Muster gehören wie Idiome zum Handwerkszeug eines Programmierers und können nur schwer im Nachhinein bzw. isoliert vom Erlernen einer Sprache vermittelt werden.9
Was als fundamental angesehen werden kann, ist natürlich diskutabel. Beschränken wir uns erst einmal auf zwei allgemein akzeptierte Pattern.
6.6 Interface-Pattern ![down](common/down.gif) ![top](common/top.gif)
Wie kann man die beiden Nachteile der Vererbung (6.4.3) vermeiden und die Vorteile (6.4.1) so weit wie möglich erhalten?
Die Erfindung von Interfaces in C++
Erklärt man alle öffentlichen Methoden in der Superklasse abstract, so ist der erste Nachteil verschwunden. Den zweiten Nachteil vermeidet man mit Hilfe von Mehrfachvererbung, da man die eigene Hierarchie korrekt als Is-A-Beziehung verwenden und sich trotzdem als Subklasse von einer anderen ableiten kann, um deren Service zu verwenden.
![Icon](common/P.gif)
|
Damit wurden übrigens gerade Interfaces für C++ eingeführt! |
Hinter Interfaces steckt ein fundamentales Design-Pattern:
Interface-
Assoziationen
Interface-Assoziationen: Werden Assoziationen mit Hilfe von Interfaces entworfen, können Objekte jeder Klasse, die dieses Interface implementieren, im System verwendet werden.
|
Das Template-Prinzip
Template-Prinzip (ein Korollar): Programmiere gegen ein Interface, nicht gegen eine Implementierung (Abb. 6.6).
|
Abbildung 6.6 Client-Code basiert auf einem Interface, nicht einer Klasse
|
Code bzw. Methoden, die auf Interfaces beruhen, sind Templates. |
Template-
Definition
|
Als Template-Methoden/Code bezeichnet man Algorithmen oder Anweisungen für unterschiedliche Klassen, die also keine Klassendetails implementieren, z.B. nur Interfaces benutzen.10 |
6.6.1 Filter-Template, Kollektion ![down](common/down.gif) ![top](common/top.gif)
Collection-
Framework: Operationen sind meist Idiome bzw. Templates
Kollektionen sind ein Glücksfall. Das Mini-Framework beruht nur auf Interfaces. Die Verwendung wird immer anhand von vielen kleinen Code-Fragmenten gezeigt, deren Algorithmen allesamt Idiome sind.
Die Klasse FilterTemplate bietet eine Methode filter() an, die aufgrund einer Filterbedingung isInFilter() des Interfaces IFilter aus einer alten eine neue »selektierte« Kollektion erstellt (siehe Abb. 6.7).
FilterTemplate: eine kleine Template-Klasse
Abbildung 6.7 Client benutzt Template-Methode
Einmal abgesehen von dem realen Objekt HashSet, das aber nur in Form einer Interface-Referenz nach außen geliefert wird, ist filter() ein typischer Vertreter einer Template-Methode.
interface IFilter { boolean isInFilter(Object o);
} // 11
Eine Template-Methode
class FilterTemplate {
Callback-Parameter IFilter enthält Client-Methode
// eine wiederverwendbare Template-Methode
public static Collection filter(Collection
c, IFilter f) {
Collection fc= new HashSet();
Object o;
for (Iterator i= c.iterator(); i.hasNext();)
if (f.isInFilter(o=i.next())) fc.add(o);
return fc;
}
}
BookService verwendet als Client die Template-Methode, um aus einer Kollektion von Büchern (Books) als Array die Bücher (Book) zu liefern, bei denen ein bestimmter String im Titel vorkommt (siehe Abb. 6.8).
Verwendung der Template-Klasse bzw. -Methode
Abbildung 6.8 Buch-Service, der das filter()-Template nutzt
Minimale Klassen:
Book bzw. Books
class Book {
private String title;
public Book(String title) { this.title= title; }
public String getTitle() { return title; }
}
class Books {
protected Collection bcol;
Books() { bcol= new HashSet(); }
void addBook(String title) { bcol.add(new Book(title)); }
}
BookService nutzt Template
class BookService implements IFilter {
private Books bLib;
private String sTitle;
public BookService(Books bLib) { this.bLib= bLib;
}
Callback-Methode bereitstellen
public boolean isInFilter(Object b) {
return ((Book)b).getTitle().indexOf(sTitle)>= 0;
}
public Book[] searchBooks(String sTitle) {
if (sTitle==null) return null; // zumindest diesen Fehler!
this.sTitle= sTitle;
filter() nutzen
return (Book[])FilterTemplate.filter(bLib.bcol,this).
toArray(new Book[0]); ¨
}
}
Es fehlt noch ein kleiner Test des Buch-Lookup-Services:
BookService-Test
public class Test {
public static void main(String[] args) {
// eine Bibliothek mit drei Büchern
Books bLib= new Books();
bLib.addBook("Java in 21 hours"); // 1
bLib.addBook("C++ for smarties"); // 2
bLib.addBook("C# for Java dummies"); // 3
// Durchsuchen der Bibliothek nach "Java"-Bücher
Book[] barr= new BookService(bLib).searchBooks("Java");
for (int i=0; i<barr.length;i++)
System.out.println(barr[i].getTitle()); // liefert 1,3
}
}
Erklärung: Der Code ist schlicht. Es sind nur die absolut notwendigen Methoden aufgeführt, es wurden praktisch keine Fehler abgefangen und auf das Standard-Interface Comparable verzichtet.12
An zwei Stellen sind (ohne Prüfung) Casts notwendig.
Collection. toArray() mit und ohne Cast
Zu ¨: Die Instanz-Methode Collection.toArray() liefert ein Object-Array zurück, das keinen Cast nach Book[] erlaubt.
Mit Hilfe von Collection.toArray(Object[] arr) hat man jedoch eine Kontrolle über den Laufzeittyp des Arrays, indem man ein Array des passenden Typs als Argument übergibt. Somit ist ein Cast nach Book[] erfolgreich.
6.6.2 Interface – eine Firewall? ![down](common/down.gif) ![top](common/top.gif)
Schützen Interfaces ihre Klassen vor unerlaubtem Zugriff?
Frage (siehe Abb. 6.6): Ist ein Interface eine Art Firewall?13
Dazu ein Code-Fragment als Test:
void anInterfaceIsNoFirewall (I iref) {
if (iref instanceof AnyClass) // z.B. Test und
(AnyClass)iref).anyClassMethod(); // Cast reicht!
}
Antwort: Nein, man kann immer einen Cast durchführen.
6.7 Delegation-Pattern ![down](common/down.gif) ![top](common/top.gif)
Nachdem die Vererbung zum dominierenden Paradigma wurde, brauchte es einige Zeit, bis die Euphorie sich legte.
Delegation:
komponentenorientierte
Entwicklung
Erst das kommerziell recht erfolgreiche komponentenorientierte COM-Modell14 , welches nach außen völlig auf Veerbungs-Mechanismen verzichtet, rückte die Dinge wieder zurecht.
![Icon](common/P.gif)
Die Nutzung bzw. Wiederverwendung von Code per Aggregation/Komposition von Objekten, mit dem Ziel, den Service nach außen von passenden inneren Komponenten erledigen zu lassen, ist eigentlich die Norm und viel genereller einsetzbar als Vererbung (siehe Abb. 6.9).
Design-Heuristik:
im Zweifelsfall delegieren!
Die heuristische Design-Strategie lautet:
|
Benutze das Delegation-Pattern im Normalfall, um den externen Service an spezielle Komponenten zu delegieren, und nur ausnahmsweise Vererbung (nur im Kontext Is-A). |
|
Abbildung 6.9 Service wird intern an Komponenten delegiert
6.7.1 Probleme bei der Vererbung: zwei Beispiele ![down](common/down.gif) ![top](common/top.gif)
Ein Blick über den Tellerrand: Wie machen es die anderen?
Zuerst ein Seitenblick auf andere Ingenieurwissenschaften. Komplexe Produkte wie z.B. PKWs delegieren den nach außen zur Verfügung gestellten Service an unzählige Komponenten. Niemals würde ein Ingenieur auf die Idee kommen, Vererbung zu benutzen und einen PKW als speziellen Motor zu bezeichnen oder gar als solchen zu entwickeln.
In der virtuellen Welt ist es dagegen nicht immer leicht, im Design zwischen Vererbung und Komponenten-Bau zu unterscheiden (deshalb ja auch die o.a. Heuristik).
Dazu zwei ausgewählte Beispiele, wobei das erste geometrische nur aufgrund seiner Anschaulichkeit verwendet wird.15
Punkt – Kreis – Ellipse
Ein Evergreen:
Punkt, Kreis und Ellipse – wer erbt von wem?
Frage: Wie sieht eine dazu passende Vererbung aus?
Eine recht übliche Lösung ist rein pragmatisch: Jeder geometrischen Figur wird zumindest ein Basispunkt vererbt. Ein Kreis benötigt dann noch zusätzlich (zum Mittelpunkt als Basispunkt) einen Radius, eine Ellipse noch einen zusätzlichen Radius (Abb. 6.10).
Hierarchie auf Basis benötigter Felder
Abbildung 6.10 Vererbungslösung auf Basis der benötigten Felder
Katastrophe: Lösungen auf Basis benötigter Felder sind in Hinblick auf die Substitutions- bzw. Widing-Convertion-Regel (siehe 5.1) fatal. Oder sieht etwa die nachfolgende Anweisung gut aus?
Punkt p= new Ellipse(1,3); // die beiden Radien
sind 1 und 3
Nachfolgend eine Lösung, die Enthaltensein und Vererbung ernst nimmt (Abb. 6.11).
Eine Is-A-Vererbungslösung mit Komponente
Abbildung 6.11 Lösung, die auf Komponenten und Vererbung basiert
Problem:
unpassende Schnittstelle der Superklasse
Die letzte Lösung ist – geometrisch gesehen – sauber, hat aber leider eine
unpassende Schnittstelle: Allgemeine Ellipsen-Felder und -Methoden machen für einen Kreis keinen Sinn. Zumindest die öffentlichen Ellipsen-Methoden müssten durch spezielle Kreis-Methoden ersetzt werden.16
Ein Kreis erbt nicht nur einen Radius zu viel, er erbt auch noch überflüssige Methoden der Ellipse wie z.B. die Getter und Setter setA(), setB(), getA() und getB(), die so keinen Sinn machen (siehe hierzu 6.7.2).
Anwort: Nein, sie sieht nicht so gut aus!
Beispiel: Kunde
Personen in der Juristerei
Die Kunden eines Unternehmens können natürliche Personen oder eigenständige Unternehmen – Kapitalgesellschaften als juristische Personen – sein. Natürliche sowie juristische Personen sind die beiden Spezialformen einer rechtsfähigen Person. Für den Geschäftsverkehr ist die Unterscheidung der Kunden wichtig. Also muss eine Kunden-Modellierung dies berücksichtigen.
Kunden-Lösung mit Hilfe von Mehrfachvererbung?
Abbildung 6.12 Kunde: ein Entwurf auf der Basis von Mehrfachvererbung
Mehrfachvererbung: hohe Komplexität, wenig Nutzen!
Abgesehen davon, dass die Lösung in Abb. 6.12 in Java nicht möglich ist, ist sie auch ein wenig pathologisch. Ein Kunde kann nicht gleichzeitig natürliche und juristische Person sein. Die Spezialisierungen sind disjunkt. Ein Kunde erbt also Eigenschaften und Methoden von beiden, aber nur ein Erbe darf gültig sein. Eine Vererbungslösung – zumindest diese – ist nicht sinnvoll.
6.7.2 Delegation vs. Vererbung ![down](common/down.gif) ![top](common/top.gif)
![Icon](common/D.gif)
Delegation kann Vererbung nachbilden
Betrachtet man Vererbung und Delegation aus Sicht der Implementation, so stellt man fest, dass Delegation der generellere Mechanismus ist:
|
Mit Delegation lässt sich Vererbung nachbilden. Die Umkehrung gilt nicht. |
|
Handelt es sich um eine Is-Kind-Of- oder Is-A-Beziehung mit unpassender Schnittstelle, so ist ein Delegations-Pattern besser geeignet. |
|
Fairerweise sollte man Folgendes hinzufügen:
|
Bei einer Is-A-Beziehung, in der auch die Schnittstelle der Basisklasse für die Subklassen Sinn machen, ist Vererbung das eindeutig elegantere und bessere Konzept. |
Verifizieren wir die Punkte an zwei Minimal-Beispielen (Abb. 6.13).
Evergreen-Beispiel: Kreis-Ellipse-Lösung mit Delegation
Abbildung 6.13 Implementation von Delegation
Code-Vergleich: Vererbung vs. Delegation
Der zugehörige Code sieht dann etwa wie folgt aus:
class A{ public float service(float x) { return
x*x; } }
// Vererbung, elegant und minimal
class B extends A { }
// Delegation, mehr bodenständig
class B {
A a= new A(); // Komponente, an die Service delegiert wird
public float service(float x) { return a.service(x); }
}
Selektive Nutzung des Services von Komponenten
Delegation kann Vererbung nachbilden, ohne den gefährlichen Weg der Verletzung des Klassen-Design-Prinzips zu beschreiten. Es hat zusätzlich seine Stärke darin, selektiv Services von Basis-Komponenten zu nutzen.
class Ellipse {
private Point p;
private float a, b;
void setCenter(Point p) { this.p=p; }
void setA(float a) { this.a=a; }
//...
}
Beispiel: Kreis delegiert an Komponente Ellipse
class Circle { // Nachbildung der Vererbung in Abb.
6.11
private Ellipse e= new Ellipse(); // delegiert an Ellipse
void setCenter(Point p) { e.setCenter(p); }
void setRadius(float r) { e.setA(r); e.setB(r); }
//...
}
Delegation nicht um jeden Preis
Die Kosten der Delegation sind manchmal zu hoch
Delegation ist aufwändig. Hat eine Klasse mehr als hundert Methoden und alle, bis auf zwei oder drei, sind in einer neuen Klasse sinnvoll, sollte man eine Subklasse verwenden, denn Delegation ist nicht gerade angenehm.
![Icon](common/T.gif)
Die sinnlosen Methoden kann man dann so überschreiben, dass sie bei Benutzung unschädlich sind oder eine Ausnahme (RuntimeException) auslösen.17
Alternative: eine pragmatische Vererbungslösung
Will man z.B. bei einer Subklasse Circle bleiben, weil ansonsten unzählige Methoden von Circle nach Ellipse umgeleitet werden müssten, gibt es Alternativen, die nicht schön, aber praktikabel sind:
class Ellipse { /* 100+ Methoden */ }
class Circle extends Ellipse {
// Alternative 1: a und b immer in Synch
void setA(float r) { super.setA(r); super.setB(r); }
//..
// Alternative 2: neue Methode, alte unbrauchbar
überschreiben
void setRadius(float r) { super.setA(r); super.setB(r); }
void setB(float b) {throw new UnsupportedOperationException();}
}
|
Variationen von Delegation tauchen in vielen anderen Pattern auf. |
Nahe Verwandte von Delegation sind Adapter-, Decorator-, Façade- und Proxy-Pattern.
6.8 Vererbung, Interfaces und Delegation ![down](common/down.gif) ![top](common/top.gif)
![Icon](common/D.gif)
Es wurden ein Trio von grundlegenden Design-Konzepten vorgestellt, auf denen praktisch alle Patterns, Frameworks und Idiome in Java aufbauen:
VID-Design:
Basis für Java- Entwicklungen
|
Vererbung ist das eleganteste Konzept zur Wiederverwendung, aber leider aufgrund der Is-A-Beschränkung sehr restriktiv. |
|
Interfaces sind Generalisten. Sie gestatten Template-Programmierung, Algorithmen und Codierung ohne konkrete Klassen. |
|
Delegation erlaubt die Wiederverwendung von Code ohne Vererbung durch komponentenorientierte Entwicklung. |
|
Es folgen noch zwei nützliche Idiome und ein wichtiges Pattern.
6.9 Immutable ![down](common/down.gif) ![top](common/top.gif)
![Icon](common/P.gif)
Immutable – nicht veränderbar nach der Anlage – ist mehr ein Idiom als ein Pattern, aber zumindest fundamental. Es bezieht sich nur auf eine Klasse, nicht auf Klassenbeziehungen und ist sehr eng mit der Sprache verknüpft.
Immutable Objekte suggerieren Werte-Semantik
|
Immutable Objekte verhindern die Nachteile der Referenz-Semantik um den Preis des Ressourcenverbrauchs. |
Primitive Typen gehören in Java immer zu einem Objekt, sie haben Werte-Semantik bei der Zuweisung und können nicht geteilt werden.18
Parallele Nutzung von Objekten über mehrere Referenzen
Durch Zuweisungen oder Argument-Übergabe gibt es dagegen zu einem Objekt immer mehrere Referenzen. Dadurch ist ein Client eines Objekts – mit Ausnahme der strikten Komposition – nie sicher, ob nicht parallel andere Clients das Objekt ändern.
Hier stellt das Immutable-Idiom sicher, dass beliebig viele Clients ein Objekt nutzen können, ohne dass Änderungen des einen Fehler beim anderen provozieren.
Immutable Java-Strings vs. mutable C-Strings
Die wohl wichtigsten immutable Objekte sind Strings. Java-Strings unterscheiden sich damit grundsätzlich von mutable Strings in C/C++.
Abbildung 6.14 Immutable Klasse
![Icon](common/i.gif)
Nur Konstruktoren setzen Werte, keine Setters erlaubt
Immutable Objekte erschafft man in Java alternativ mit Hilfe von
|
blank final Instanz-Variablen oder |
|
Konstruktoren, die ausschließlich die Werte der Instanz-Variablen setzen können (siehe Abb. 6.14). |
|
Der erste Weg ist nur in einfachen Fällen gangbar bzw. sinnvoll, der zweite ist der generelle und übliche. Neben den Konstruktoren gibt es dann nur noch Methoden in der Klasse, die lesend auf die Felder zugreifen.
Immutable bedeutet Ressourcenverbrauch
Muss ein immutable Objekt geändert werden, erzeugt man einfach ein neues. Und genau das erzeugt den Ressourcenverbrauch, d.h., Ausführungszeit und Speicherverbrauch für die Erzeugung neuer Objekte nehmen zu.
![Icon](common/D.gif)
Einsatz von immutable Objekten
Das Immutable-Idiom sollte benutzt werden,
|
wenn Objekte überwiegend von vielen Clients ohne Zustandsänderung benutzt werden |
|
um thread-sichere Objekte zu erschaffen19 |
|
In einfachen Fällen kann man immutable Objekte durch interne Verwendung von blank final Variablen erzeugen (siehe hierzu 5.5.2).19
6.10 Marker-Interface ![down](common/down.gif) ![top](common/top.gif)
![Icon](common/i.gif)
Marker-Interfaces führen zu einer Zwei-Klassengesellschaft
Pure Marker-Interfaces sind leer, d.h. definieren weder Methoden noch Konstanten. Der einzige Sinn des Marker-Idioms ist, Klassen in zwei disjunkte Mengen zu unterteilen und eine davon zu markieren. Objekte können dann mit Hilfe des instanceof-Operators darauf getestet werden, ob sie zur markierten Menge der Klassen gehören.
Drei wichtige Vertreter von Marker-Interfaces sind java.lang.Clonable, java.io.Serializable und java.rmi.Remote.
Markieren von Methoden
Abbildung 6.15 Markieren von Klassen bzw. Methoden
Interessant ist das Remote-Interface, weil es dazu benutzt wird, Methoden zu markieren (Abb. 6.15).
![Icon](common/D.gif)
Für Marker gilt eine einfache Design-Regel:
Einsatzmöglichkeiten von Marker-Interfaces
|
Das Marker-Interface-Idiom kann für eine (unbekannte) Menge von Klassen eingesetzt werden, die sich in einem besonderen Merkmal von anderen unterscheiden. |
|
Beispiel: ILocation
Im ILocation-Beispiel aus 6.1.1 (Abb. 6.2) zeigte testCommutation() die Asymmetrie von Instanz-Methoden mit ihren recht unerwünschten Folgen.
ILocation, ein Design-Fehler mit Folgen
Natürlich war das Design von ILocation falsch! Eine Metrik gehört zum Raum, nicht zu einzelnen Punkten im Raum.
Aber trotzdem eignet sich dieses fehlerhafte Beispiel recht gut, mittels eines Marker-Interfaces nur jeweils eine passende Metrik bzw. Abstandsbestimmung zu wählen, damit dieses »abscheuliche« Ergebnis wie in testCommutation() vermieden wird.
Euclidian-
Marker-Interface, eine Lösung des Problems?
Abbildung 6.16 Point, markiert als euklidische Metrik
Markiert man Point als Euclidian (Abb. 6.15), so sieht ein Code-Fragment, welches nur euklidische Metriken erlaubt, etwa wie folgt aus:
interface Euclidian {};
Point wird markiert
class Point implements ILocation, Euclidian { /*...*/
}
//...
static void onlyEuclidianMetric(ILocation d1, ILocation
d2) {
if (!(d1 instanceof Euclidian))
if (d2 instanceof Euclidian) {
ILocation temp=d1; d1=d2; d2=temp;
}
else { /* Fehler: z.B. Ausnahme */ }
System.out.println(d1.distanceTo(d2));
}
Problem: Dieses Design hat das Problem, dass es überhaupt nicht wirkt:
onlyEuclidianMetric(new Point(0,0),new MapPoint(1,1));
// :: 1.41
onlyEuclidianMetric(new MapPoint(1,1),new Point(0,0));
// :: 2.0
Marker-Interfaces markieren automatisch auch alle Subklassen
Erklärung: Marker-Interfaces werden wie alle Interfaces vererbt, es besteht keine selektive Markierung in einer Hierarchie.
Wird eine Klasse markiert, werden zwangsläufig auch alle Subklassen markiert.
Im ILocation-Beispiel ist das Design leicht zu korrigieren (was das Design natürlich überhaupt nicht besser macht!):
Marker-Interfaces: die Auswahl der richtigen Klasse ist entscheidend
Abbildung 6.17 MapPoint, markiert als NonEuclidian
interface NonEuclidian {};
class MapPoint extends Point implements NonEuclidian
{ /*..*/ }
static void onlyEuclidianMetric (ILocation d1, ILocation
d2) {
if (d1 instanceof NonEuclidian)
if (!(d2 instanceof NonEuclidian)) {
ILocation temp=d1; d1=d2; d2=temp;
}
else { /* Fehler: z.B. Ausnahme */ }
System.out.println(d1.distanceTo(d2));
}
6.10.1 Sticker-Interface: Erweiterung des Marker-Interfaces ![down](common/down.gif) ![top](common/top.gif)
![Icon](common/i.gif)
Sticker-Interfaces: Aufkleber für die individuelle Markierung von Klassen
Als »Sticker« wird ein konstanter Aufkleber bezeichnet, der einer Klasse »angeheftet« werden kann, und eine individuelle Selektion erlaubt:
|
Ein Sticker-Interface kann für selektives Markieren in Klassen-Hierarchien oder für die Unterscheidung von mehr als zwei Mengen von Klassen eingesetzt werden, die sich in einem Merkmal unterscheiden. |
|
Das Sticker-Interface vereinbart Konstanten, die die möglichen Eigenschaften der Menge widerspiegeln und eine Methode getX(), die die Eigenschaft liefert.
Sticker-Interface: Konstanten unterscheiden Klassen
Abbildung 6.18 Nicht leeres Marker-Interface
Im Fall ILocation wäre ein Sticker-Interface etwa (Abb. 6.18):
interface Metric {
int EUCLIDIAN= 0; int MAPMETRIC= 1;
int getMetric();
}
Klasse
identifiziert sich
class Point implements ILocation, Metric {
//...
public int getMetric() { return EUCLIDIAN; }
}
//...
Leider ist die Abfrage, zu welcher Menge eine Klasse, Methode oder ein Objekt gehört, nicht mehr ganz so einfach wie beim Marker-Interface:
static void onlyEuclidianMetric(ILocation d1, ILocation
d2) {
M: {
if (!(d1 instanceof Metric) ||
((Metric)d1).getMetric()!=Metric.EUCLIDIAN)
if (d2 instanceof Metric)
if (((Metric)d2).getMetric()==Metric.EUCLIDIAN) {
ILocation temp=d1; d1=d2; d2=temp;
}
else break M;
else break M;
System.out.println(d1.distanceTo(d2)); return;
}
System.out.println("Fehler");
}
6.10.2 Marker-Interface Cloneable ![down](common/down.gif) ![top](common/top.gif)
Object implementiert clone()-Operation, aber nicht Cloneable
Nach der o.a. Vererbungs-Regel ist klar, dass die Urklasse Object das Marker-Interface Cloneable nicht selbst implementieren kann.20
Sie stellt aber für alle Klassen eine nützliche Klon-Operation bereit:
protected native Object clone()
throws CloneNotSupportedException;
Die Methode clone() ist in Object so implementiert, dass sie zuerst eine Instanz der abgeleiteten Klasse erzeugt und dann alle Felder des neuen Objekts mit den Werten des alten initialisiert.
Shallow-Copy vs.
Deep-Copy
Shallow-Copy: Eine Operation wie clone() nennt man Shallow-Copy, da es bei Referenzfeldern nur die Zeigerwerte kopiert und nicht für jedes interne Objekt die Klon-Operation wiederholt.
Deep-Copy: Die Semantik von Deep-Copy verlangt dagegen eine rekursive Klon-Operation für alle internen Objekte.
Arrays sind
Cloneable
|
Für alle Arrays gilt Cloneable im Sinne von Shallow-Copy als implementiert. |
Beispiele
Klonen von
primitiven Arrays
Nachfolgend wird ein int-Array geklont und sortiert.
int[] ia= {3,2,1}, ja= (int[]) ia.clone();
Arrays.sort(ia); // sortiert ia
System.out.println(ia[0]+" "+ia[1]+" "+ia[2]); // :: 1 2 3
System.out.println(ja[0]+" "+ja[1]+" "+ja[2]); // :: 3 2 1
Mutable vs. immutable: Verpacken wir eine ganze Zahl einmal in ein mutable Int-Objekt und in ein immutable Integer-Objekt:
class Int { // mutable, da
int i=0; // i nicht private
Int(int i){ this.i=i; }
}
//...
Klonen von mutable bzw. immutable
Objekt-Arrays
Int[] i1A= { new Int(1), new Int(2), new Int(3)
},
j1A= (Int[]) i1A.clone(); // shallow copy
Integer[] i2A= {new Integer(1),new Integer(2),new
Integer(3)},
j2A= (Integer[]) i2A.clone(); // dito
Arrays
implementieren Shallow-Copy
Am Array i1A und j1A wird die Wirkung von Shallow-Copy deutlich, denn es gibt nur drei Int-Instanzen, die von i1A und j1A referenziert bzw. geändert werden können. Dies zeigt ein Code-Fragment:
i1A[0].i=3; i1A[2].i=1;
System.out.println(i1A[0].i+" "+i1A[1].i+" "+i1A[2].i); // :: 3 2 1
System.out.println(j1A[0].i+" "+j1A[1].i+" "+j1A[2].i); // :: 3 2 1
Auch im zweiten Fall zeigen i2A und j2A auf dieselben drei Integer-Objekte, aber da diese immutable sind, ist in diesem Fall das »Objekt- Sharing« harmlos.
Implementation von Cloneable
Verwendung von clone() in eigenen Klassen
Will eine Klasse die Default-Klon-Operation zur Verfügung stellen, muss sie neben implements Cloneable die Klon-Operation explizit implementieren, da sie in Object protected erklärt ist und somit nur im Package java.lang im Zugriff steht.
Eine einfache Shallow-Copy-Implementierung übernimmt einfach die Deklaration von Object, wobei protected native durch public überschrieben wird.
Da eine Ausnahme beim native-Klonen mittels Object nicht vorkommen sollte, fängt man die Ausnahme besser ab, was sicherlich angenehmer für die Benutzung ist.
Muster einer clone()-Implementation
![Icon](common/i.gif)
class Any implements Cloneable {
//...
public Object clone() {
try { // Ausnahmen: siehe 7. Kapitel
return super.clone(); // shallow copy durch Object
} catch(CloneNotSupportedException e) {
return null; // sollte nie passieren!
}
}
}
![Icon](common/R.gif)
Abschließend noch eine kleine Semantik-Regel:
clone()-Overriding muss die Semantik von equals() beachten
|
Klon-Operationen können in jeder gewünschten Weise überschrieben werden, nur die Semantik sollte erhalten bleiben. |
Zur Semantik zählt u.a. die Frage, ob die equals()-Operation für Klone true liefern soll, da per Default gilt:
o.clone().equals(o) == false
6.11 Factory-Pattern ![down](common/down.gif) ![top](common/top.gif)
Das Factory/Fabrik-Pattern ist in Bezug auf die Erzeugung von Objekten fundamental. Dies hängt vor allem mit Interfaces zusammen.
Problem:
Objekt-Erzeugung
Interfaces haben keine Konstruktoren, und damit kann man keine generellen Template-Operationen schreiben, die Objekte erzeugen müssen.
![Icon](common/P.gif)
Im Template muss man zwangsläufig »hinter« ein Interface schauen (siehe Abb. 6.6) und Objekte von den zugehörigen Klassen direkt erzeugen.
Fundamentales Pattern zur Erzeugung von Objekten
Dies muss man aber nicht, wenn man ein Factory-Pattern benutzt:
|
Der Sinn eines Factory-Patterns besteht darin, die konkreten Klassen vor den Clients, die sie erzeugen, zu isolieren. |
|
Alle Clients – auch Template-Klassen – erzeugen Objekte nur über eine Factory, die idealerweise selbst wieder ein Interface mit einer create()-Methode ist. |
|
Zentrale Objekt-Erzeugung an nur
einer Stelle
Ein weiterer Vorteil des Factory-Pattern besteht darin, dass die Erzeugung von Objekten auf eine Stelle konzentriert wird und nicht über alle (unbekannten) Clients hinweg verstreut wird. Dies macht das Design Änderungen gegenüber wesentlich robuster.
Unter dem Factory-Pattern firmieren verschiedene ähnliche Lösungen, die sich darin unterscheiden, wie die Factory realisiert wird.
Muster auf Basis eines Factory-Interfaces
Abbildung 6.19 Factory-Pattern, basierend auf einem Factory-Interface
Die in Abb. 6.19 dargestellte konkrete Factory verbirgt sich vor dem Client wieder hinter einem Factory-Interface und ist daher sehr allgemein. Die Information, welche Klasse erzeugt werden soll, wird in typeInfo vom Client an die Factory übergeben.
6.11.1 Ein »Teile-Fabrik«-Beispiel ![down](common/down.gif) ![top](common/top.gif)
Teile-Fabrik: ein zusammenfassendes Beispiel
Der Einsatz einer Factory soll abschließend anhand des zentralen Datenobjekts Teil für Industrieunternehmen gezeigt werden.
Betriebliche Sicht
Die Rolle von Teil – ein polymorphes Objekt – ist geschäftsbereichabhängig
Unter dem Begriff Teil fasst man so unterschiedliche Objekte wie Erzeugnisse, Baugruppen, Halbfabrikate, Einzelteile, Rohstoffe, Hilfs- und Betriebsstoffe zusammen.
Jede Abteilung sieht das Objekt Teil unter einem anderen Aspekt. Die Sichten wechseln, abhängig von Konstruktion, Fertigung, Lager, Vertrieb, Einkauf, Service oder Rechnungswesen.
Der Entwickler ist also mit folgender betrieblichen Realität konfrontiert:
|
Eine konkrete Implementierung des Objekts Teil wird in nahezu jedem Programm verwendet. |
|
Jede Applikation benötigt aufgrund des Einsatzgebietes unterschiedliche Eigenschaften und Methoden des Objekts Teil. |
|
Eine einheitliche Klasse Teil, selbst die Implementierung in einer »schönen« Klassen-Hierarchie, kämpft gegen die betriebliche Komplexität und den permanenten Wandel der Anforderungen diverser Fachabteilungen. |
Vererbung »springt ´mal wieder zu kurz«
Bevor man also angesichts dieser Situation »blind« Vererbung implementiert, lohnt sich das Nachdenken über Interfaces und eine »Teile-Fabrik«.
Factory-Design zu Teil
Interface ITeil verbirgt diverse konkrete Teile-Objekte
Eine Applikationen (nachfolgend kurz App-Klasse) soll zwar angeben, welche Art von Teil sie benötigt, arbeitet aber immer nur mit Interface-Objekten wie ITeil, hinter denen sich beliebige reale Teile-Implementationen »verstecken« können. Die Vorteil sind klar:
Vorteile eines ITeil-Interfaces
|
Verwenden App-Klassen ausschließlich ITeil-Referenzen in ihrem Code, kann zu jedem Zeitpunkt eine Teile-Hierarchie verändert oder ergänzt werden, sogar wenn eine neue Teile-Klasse nicht zur alten Hierarchie gehört. |
|
Entstehen neue Anforderungen, die durch das alte Interface ITeil nicht abgedeckt sein sollten, kann ein ergänzendes Interface eingeführt werden. Bereits bestehende Teile-Klassen können dieses zusätzlich implementieren, ohne dass die bestehende Lösung geändert werden muss. |
Erzeugung von Teilen: realisiert durch ein
Factory-Pattern
Der erste Punkt kann mit einem Factory-Pattern realisiert werden, welches ITeil auf Anforderung der App-Klassen erzeugt.
Um das Beispiel ein wenig interessanter zu gestalten, übernimmt ein Container Teile das Abspeichern, Löschen etc. von Objekten des Typs ITeil.
Das gesamte
Applikations-Modell
Abbildung 6.20 Abstrakte Fabrik zu Teil (Factory-Pattern)
Zur Vereinfachung wird noch eine abstrakte Klasse Teil eingeführt, die die Gemeinsamkeiten der bisher bekannten Teile-Familie aufnehmen kann. Das reduziert den Programmieraufwand in diesem Beispiel.
Das Interface TeileArt enthält als Konstanten die möglichen Varianten der Teile-Familie. Eine Implementierung von TeileArt ist nicht unbedingt notwendig, erlaubt aber den direkten Zugriff auf die Konstanten.
Implementation
Interface TeileArt:
Eine Konstante für jede konkrete Teile-Klasse
Die Implementation ist »straightforward«, die Methoden sind trivial und ohne besondere Fehlerbehandlung.
public interface TeileArt
{ // Zur Zeit verwendete Teilearten
int ERZEUGNIS= 1;
int BAUGRUPPE= 2;
int EINZELTEIL= 3;
int ROHSTOFF= 4;
//...
}
Der Sinn von TeileArt ist die Bereitstellung von Konstanten, die die verschiedenen Teile-Arten symbolisieren.
ITeil: Clients sehen Teil nur als
Interface
public interface ITeil
{ // Interface aller konkreten Teile
void setId(String id);
String getId();
void setName(String name);
String getName();
//...
boolean isIdOk(); // true: gültiges Id für Primärschlüssel
boolean isVKTeil(); // true= Verkaufsteil, sonst nicht
}
Spezialisierte Teile-Interfaces machbar
Das Interface ITeil enthält die Methoden, die von allen Teilen – unabhängig von ihrer Art – angeboten werden. Sicherlich können bestimmte Teile noch andere, für sie spezifische Interfaces implementieren.
Teil:
abstrakte
Oberklasse für gemeinsame Methoden
public abstract class Teil
implements ITeil { // abstrakt
private String id;
private String name;
public void setId(String id) { this.id= id;
}
public String getId() { return id; }
public void setName(String name) { this.name= name; }
public String getName() { return name; }
}
Konkrete
Teile-Klassen
Die Klasse Teil dient nur der vereinfachten Implementierung der konkreten Klassen. Durch Spezialisierung brauchen nur noch die fehlenden Methoden hinzugefügt werden.
public class Erzeugnis
extends Teil { // konkret
private final static String idPrefix= "01-";
public boolean isIdOk() {
return getId().length()>2?
getId().substring(0,3).equals(idPrefix):false;
}
public boolean isVKTeil() { return true; }
}
public class Rohstoff
extends Teil { // konkret
private final static String idPrefix= "04-";
public boolean isIdOk() {
return getId().length()>2?
getId().substring(0,3).equals(idPrefix):false;
}
public boolean isVKTeil() { return false; }
}
Die Klassen Erzeugnis bzw. Rohstoff sind zwei exemplarische konkrete Klassen, die implizit das Interface ITeil implementieren.
Factory-Interface
produziert neue Teile
public interface ITeileFabrik {
ITeil create (int art);
}
ITeileFabrik ist ein kleines, aber machtvolles Interface, welches die Aufgabe hat, für alle App-Klassen Objekte vom Typ ITeil zu erzeugen.
Konkrete Factory-Klasse –
hier Produktion ohne Prüfung
public class TeileFabrik implements ITeileFabrik,
TeileArt {
public ITeil create(int art) {
switch (art) {
case ERZEUGNIS: return new Erzeugnis();
//...
case ROHSTOFF: return new Rohstoff();
default: return null;
}
}
}
Nur die Klasse TeileFabrik erzeugt reale Objekte und liefert sie als ITeil zurück. Hierzu benötigt sie eine Konstante zur Identifikation.
Container Teile:
speichert Teile durch Delegation an eine Map
class Teile {
private Map tmap= new HashMap(); // Delegation an Komponente
public boolean insert(ITeil t) {
if (tmap.containsKey(t.getId()) || !t.isIdOk()) return false;
tmap.put(t.getId(),t); return true;
}
public boolean delete(String s) {
if (tmap.containsKey(s)) return true;
tmap.remove(s); return true;
}
public ITeil get(String s) { return (ITeil)tmap.get(s); }
}
Die Klasse Teile ist ein Container, der Objekte vom Typ ITeil intern in einer Map speichert. Map ist ein Interface und wurde bereits in 4.2.5 vorgestellt. Die Klasse HashMap ist eine konkrete Implementation.
Delegation statt Vererbung: Teile sind kein HashMap!
Anstatt Vererbung –
class Teile extends HashMap {...} // falsch, keine
Is-A-Relation
– wurde hier bewusst Delegation an eine HashMap-Komponente eingesetzt.
Vorteil der Delegation (Freiheit der Implementation):
Delegation bringt Freiheit
Die Klasse Teile ist frei in der Wahl des eingesetzten Containers und der von ihr angebotenen Methoden und deren Namen. Denn Teile steht zur HashMap nicht in einer Is-A- sondern nur in einer Is-Kind-Of-Beziehung.
TestApp:
ein triviales Testprogramm
public class TestApp
implements TeileArt {
static void println (ITeil t) {
System.out.println(t.getId()+
(t.isVKTeil()?": [V] ":": [-] ")+t.getName());
}
public static void main (String[] args) {
ITeileFabrik neuTeil= new TeileFabrik();
Teile ts= new Teile();
// Anlage und Einfügen eines Erzeugnisses
ITeil t= neuTeil.create(ERZEUGNIS);
t.setId("01-003-01");
t.setName("Tischlampe Tizian");
System.out.println(ts.insert(t));
// Anlage und Einfügen eines Rohstoffs
t= neuTeil.create(ROHSTOFF);
t.setId("04-070-10");
t.setName("Grundierung A-1");
System.out.println(ts.insert(t));
// Teile über Schlüssel aus dem Container
holen
println(ts.get("01-003-01"));
println(ts.get("04-070-10"));
}
}
In den Klassen Teile und TestApp werden konkrete Objekte wie HashMap und TeileFabrik nicht über Referenzen der zugehörigen Klasse bearbeitet, sondern über die zugehörigen Interfaces Map bzw. ITeileFabrik.
»Wiederholungen sind wie Werbung, langweilig, aber wirksam«
![Icon](common/i.gif)
Die Umsetzung des Template-Prinzips mit Hilfe von Interfaces:
Synergieeffekt:
Interfaces in Templates
|
Hat man die Wahl, programmiert man immer gegen ein Interface. Eine unnötige Beschränkung des Codes auf bestimmte Klassen-Objekte wäre töricht, denn »Interfaces sind Polymorphie pur«. |
|
6.12 Konzeptionelle Schwächen von Interfaces ![down](common/down.gif) ![top](common/top.gif)
Die Einführung von Interfaces war ein guter erster Schritt. Sie folgen dem KISS21 -Prinzip. Interfaces berücksichtigen allerdings nicht unbedingt das Einstein-Prinzip: »Mache es so einfach wie möglich, aber nicht einfacher.«
6.12.1 Constraints (Zusicherungen) ![down](common/down.gif) ![top](common/top.gif)
Ein Interface beschreibt nur die Signatur und den Rückgabe-Typ von Instanz-Methoden.
Wird ein Interface oder ein Framework auf Basis von Interfaces geschrieben, muss sehr ausführlich dargelegt werden, welche Intention jede Methode hat und was der Programmierer bei der Implementation beachten sollte.22
Werden informelle Kommentare nicht verstanden und eine Interface-Methode bremsen() wird als »beschleunigen« implementiert, dann ist die Not groß.
![Icon](common/Kaktus.gif)
|
Interfaces müssen Zusicherungen enthalten können, die bei der Implementation einzuhalten sind. |
|
Constraints: Zusicherungen, die Interfaces fehlen
Besonders wichtig wäre die Ergänzung von Interfaces um
|
Preconditions – Bedingungen, die vor der Ausführung einer Methode gewährleistet sein müssen |
|
Postconditions – Bedingungen, die nach der Ausführung einer Methode gewährleistet sein müssen |
|
Invarianten – Bedingungen, die für alle Methoden gelten müssen |
6.12.2 Interfaces bieten keinen Zugriffsschutz ![down](common/down.gif) ![top](common/top.gif)
![Icon](common/Kaktus.gif)
Wie bereits in 6.6.2 angesprochen, bieten Interface-Referenzen keinen Schutz vor direktem Zugriff auf die referenzierten konkreten Objekte.
Interface: kein Schutz,
nur Wohlverhalten
Die gesamte Programmierung auf Basis von Interfaces beruht auf dem Wohlverhalten des Anwendungsprogrammierers.
Bei bestimmten Applikationen muss allerdings sichergestellt werden, dass konkrete Objekte – die zumindest teilweise dem Role-Pattern23 folgen – nur über ein Interface benutzt werden können.
6.12.3 Ein Template-Dilemma ![down](common/down.gif) ![top](common/top.gif)
![Icon](common/Kaktus.gif)
Betrachtet man das Mini-Framework Collection bzw. sieht sich dazu Tutorials an, so tauchen überall Idiome und Templates auf, die in dieser Form – wie sollte es auch anders sein – in vielen Applikationen direkt verwendet werden könnten.
Templates: in Klassen oder per Cut and Paste
Was schlagen die Java-Entwickler vor: vielleicht Cut and Paste? Diese Art der Wiederverwendung von Code ist irgendwie bekannt.
Die Alternative dazu sind Klassen, die Idiome und Template-Code in sich aufnehmen und die dann universell benutzt werden sollen.
Aber wollte man nicht gerade klassenunabhängig schreiben? Und wie soll man sie universell nutzen – mittels Mehrfachvererbung? Oder sollen etwa alle Templates und Idiome grundsätzlich static sein?
It´s a long way ...
6.13 Zusammenfassung ![down](common/down.gif) ![top](common/top.gif)
Interfaces stellen eine neue Qualität dar, die zwangsläufig mit neuen Definitionen und Regeln verbunden sind. Diese werden am Anfang des Kapitels vorgestellt. Wichtig sind vor allem die Konvertierungs- und Cast-Regeln.
Im Vergleich zur restriktiven Vererbung sind Interfaces wesentlich flexibler, wobei sie die meisten Vorteile des Vererbungskonzepts wie die Substitutions-Regeln behalten.
Die Verwendung eines Interfaces folgt den fundamentalen Regeln eines Patterns, das Programmieren nach dem Template-Prinzip erlaubt.
Wichtig ist dabei, Referenzen möglichst nur von Interfaces zu verwenden und die Erzeugung von realen Objekten nach dem Factory-Pattern auf eine konkrete Klasse zu beschränken, die wiederum nur über ein Interface angesprochen wird.
Delegation, das komponentenorientierte Programmieren, ist ein weiteres Prinzip, welches in Verbindung mit Vererbung und Interfaces die Grundlage für flexible und robuste Java-Programmierung darstellt.
Als spezielle Idiome wurden immutable und das Marker- bzw. das erweiterte Sticker-Interface vorgestellt.
Abschließend wird anhand einer »Teile-Fabrik« das Factory-Pattern, das Template-Prinzip und die Delegation am Beispiel demonstriert.
Last, but not least wird kurz erörtert, warum Interfaces in der vorliegenden Form noch entwicklungsbedürftig sind.
6.14 Testfragen ![top](common/15x15leer.gif) ![top](common/top.gif)
Zu jeder Frage können jeweils ein oder mehrere Antworten bzw. Aussagen richtig sein.
nachTyp= vonTyp;
(nachTyp)vonTyp
5. |
Welche Aussagen sind zu folgenden Klassen- und Interface-Beziehungen richtig? |
interface I1 { void f(); }
interface I2 {}
abstract class A implements I1,I2 {}
class B extends A {
public void f() { System.out.println("B"); }
}
I2 i2= new B(); System.out.println(i2 instanceof
I1);
interface I1 {}
interface I2 extends I1 {}
class A implements I2 {}
I1[] i1arr;
I2[] i2arr= {new A(), new A()};
i1arr= i2arr; // 1
i2arr= i1arr; // 2
Object o= i2arr; // 3
System.out.println(((I2[])o)[0] instanceof A); // 4
1 Siehe Access Widening, 5.4.3.
2 Marker-Interfaces werden gesondert in 6.10 behandelt.
3 ILocation bzw. das Metrik-Problem wird noch einmal in 6.10 aufgegriffen.
4 Diese Erscheinung ist aufgrund der DLL-Logik von Windows hinlänglich bekannt und tritt mit zwanghafter Regelmässigkeit bei Neuinstallationen und Updates auf. Unter Unix kann dies Problem auch auftauchen, aber administrativ besser gelöst werden.
5 Denn man kann public-Methoden einer Superklasse nicht zu private machen. Anmerkungen von C++-Enthusiasten sind unerwünscht. Die spielen in einer unteren Liga.
6 Für Design-Pattern steht an sich mit Entwurfsmuster ein treffendes deutsches Wort zur Verfügung. Aber das Wort »Pattern« hat sich schon so weit international bei den Programmierern durchgesetzt, dass es hier bevorzugt wird.
7 Schachspieler und Feldherren kennen seit Jahrhunderten Eröffnungs-, Stellungs- und Endkampf-Muster.
8 Siehe hierzu Beispiel in 6.6
9 Als Warnung mögen die unzähligen unglücklichen Basic-Programmierer dienen, die sich mit 1000 Tipps, Tricks und Secrets zu VB herumschlagen müssen.
10 Templates werden in C++ direkt auf Sprachebene unterstützt.
11 Eine Art Interface Comparable, nur einfacher (siehe auch 14.3.5).
12 Das Abfangen von Fehlern ist Thema des nächsten Kapitels.
13 Firewall, hier im Sinn von: Schutz der Klassen, die das Interface implementiert haben, vor einem direkten, unerlaubten Zugriff.
14 COM: Component Object Model oder – nach Microsoft-Lesart – the Common Object Model.
15 Es ist ein Paradebeispiel in allen OO-Lehrbüchern, wobei jedoch die Lösungen je nach Autor divergieren.
16 Dies geht aber aufgrund der Access-Widening-Regel nicht (siehe 5.4.3).
17 siehe hierzu Kapitel 7, Ausnahmen
18 Bei C und C++ kann man durchaus auch primitive Typen mit Hilfe von Zeigern
»teilen«.
19 Zu Threads, insbesondere in Verbindung mit immutable Objekten, siehe 9.8.1.
20 Clonable würde dann schlechthin alles markieren.
21 KISS: Keep it straight and simple.
22 Im Fall des einfachen Collection-Frameworks z.B. von den Sun/Java-Designern.
23 Rollen-Muster sind sehr dynamisch und würden Interfaces »umkrempeln«.
|