Galileo Computing <openbook>
Galileo Computing - Programming the Net
Galileo Computing - Programming the Net


Java 2 von Friedrich Esser
Designmuster und Zertifizierungswissen
Zum Katalog
gp Kapitel 6 Interfaces und Pattern
  gp 6.1 Interface: Definition und Regeln
  gp 6.2 Referenz-Konvertierung und -Casting
  gp 6.3 API: Interface vs. Klasse
  gp 6.4 Interface vs. Vererbung
  gp 6.5 Design-Pattern: generelle Entwurfsmuster
  gp 6.6 Interface-Pattern
  gp 6.7 Delegation-Pattern
  gp 6.8 Vererbung, Interfaces und Delegation
  gp 6.9 Immutable
  gp 6.10 Marker-Interface
  gp 6.11 Factory-Pattern
  gp 6.12 Konzeptionelle Schwächen von Interfaces
  gp 6.13 Zusammenfassung
  gp 6.14 Testfragen

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
Abbildung 6.1   Schnittstelle einer Klasse

Icon
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.


Galileo Computing

6.1 Interface: Definition und Regeln  downtop

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

 [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

    gp  Ein leeres Interfaces nennt man Marker-Interface.
2. Alle Modifikatoren innerhalb des Interfaces sind überflüssig, da
    gp  Konstanten immer public static final sind.
    gp  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

gp  stellt eine (Klassen-) Schnittstelle dar.
gp  kann nur benutzt werden, sofern es mindestens eine Klasse gibt, die es implementiert.

Galileo Computing

6.1.1 Beispiele zu den Interface-Regeln  downtop

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.

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
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

erlaubt und offenbart sofort ein Metrik-Problem :: 1.4142135==2.0?

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.



Galileo Computing

6.2 Referenz-Konvertierung und -Casting  downtop

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

gp  Klassen
gp  Arrays
gp  Interfaces

Galileo Computing

6.2.1 Regeln zur Referenz-Konvertierung  downtop

Eine Konvertierung geschieht implizit, wird vom Compiler überprüft und hat die Form

gp  nachTyp= vonTyp
gp  Aufruf von f(vonTyp), wobei die Methode wie folgt definiert ist: f(nachTyp p) {...}

Icon
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
    gp  primitiver (Element-)Typ, so muss nachTyp die Klasse Object oder das Interface Cloneable sein.
    gp  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.


Galileo Computing

6.2.2 Regeln zum Referenz-Casting  downtop

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
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
    gp  Object oder Cloneable sein oder
    gp  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.


Galileo Computing

6.2.3 Exemplarische Konvertierungs- und Cast-Beispiele  downtop

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
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

Galileo Computing

6.3 API: Interface vs. Klasse  downtop

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.


Galileo Computing

6.3.1 Beispiel JDK: Löschen bzw. Ändern im API  downtop

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.


Galileo Computing

6.3.2 Erweiterungen im API  downtop

Eine Erweiterung – das Hinzufügen neuer Methoden – sollte eigentlich harmlos sein, da das vorhandene Protokoll bzw. API ja nur ergänzt wird.

Icon

Hier verhalten sich Interfaces anders als Klassen:

API-Erweiterungen: Interfaces vs. Klassen

gp  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

gp  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.


Galileo Computing

6.4 Interface vs. Vererbung  downtop

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:

gp  Ein Interface ist reine Polymorphie – ein fundamentales Muster!


Galileo Computing

6.4.1 Service-Beziehung auf Basis von Vererbung  downtop

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
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.

Galileo Computing

6.4.2 Service-Beziehung auf Basis von Interfaces  downtop

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
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«.


Galileo Computing

6.4.3 Nachteile der Vererbung  downtop

Icon

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:

gp  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.


Galileo Computing

6.5 Design-Pattern: generelle Entwurfsmuster  downtop

Design-Pattern: eine Orientierung

In der Einführung von 6.4 wurde die Verwendung eines Interfaces bereits als Design-Pattern bezeichnet. 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.

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
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

gp  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, Collections ).

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.

Was als fundamental angesehen werden kann, ist natürlich diskutabel. Beschränken wir uns erst einmal auf zwei allgemein akzeptierte Pattern.


Galileo Computing

6.6 Interface-Pattern  downtop

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

gp  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
Abbildung 6.6   Client-Code basiert auf einem Interface, nicht einer Klasse
gp  Code bzw. Methoden, die auf Interfaces beruhen, sind Templates.

Template-
Definition

gp  Als Template-Methoden/Code bezeichnet man Algorithmen oder Anweisungen für unterschiedliche Klassen, die also keine Klassendetails implementieren, z.B. nur Interfaces benutzen.10 

Galileo Computing

6.6.1 Filter-Template, Kollektion  downtop

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
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
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.


Galileo Computing

6.6.2 Interface eine Firewalldowntop

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.


Galileo Computing

6.7 Delegation-Pattern  downtop

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

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:

gp  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
Abbildung 6.9   Service wird intern an Komponenten delegiert
Galileo Computing

6.7.1 Probleme bei der Vererbung: zwei Beispiele  downtop

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
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
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
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.


Galileo Computing

6.7.2 Delegation vs. Vererbung  downtop

Icon
Delegation kann Vererbung nachbilden

Betrachtet man Vererbung und Delegation aus Sicht der Implementation, so stellt man fest, dass Delegation der generellere Mechanismus ist:

gp  Mit Delegation lässt sich Vererbung nachbilden. Die Umkehrung gilt nicht.
gp  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:

gp  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
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

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();}
}
gp  Variationen von Delegation tauchen in vielen anderen Pattern auf.

Nahe Verwandte von Delegation sind Adapter-, Decorator-, Façade- und Proxy-Pattern.


Galileo Computing

6.8 Vererbung, Interfaces und Delegation  downtop

Icon

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

gp  Vererbung ist das eleganteste Konzept zur Wiederverwendung, aber leider aufgrund der Is-A-Beschränkung sehr restriktiv.
gp  Interfaces sind Generalisten. Sie gestatten Template-Programmierung, Algorithmen und Codierung ohne konkrete Klassen.
gp  Delegation erlaubt die Wiederverwendung von Code ohne Vererbung durch komponentenorientierte Entwicklung.

Es folgen noch zwei nützliche Idiome und ein wichtiges Pattern.


Galileo Computing

6.9 Immutable  downtop

Icon

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

gp  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
Abbildung 6.14   Immutable Klasse

Icon
Nur Konstruktoren setzen Werte, keine Setters erlaubt

Immutable Objekte erschafft man in Java alternativ mit Hilfe von

gp  blank final Instanz-Variablen oder
gp  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
Einsatz von immutable Objekten

Das Immutable-Idiom sollte benutzt werden,

gp  wenn Objekte überwiegend von vielen Clients ohne Zustandsänderung benutzt werden
gp  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 


Galileo Computing

6.10 Marker-Interface  downtop

Icon
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
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


Für Marker gilt eine einfache Design-Regel:

Einsatzmöglichkeiten von Marker-Interfaces

gp  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
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
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));
}

Galileo Computing

6.10.1 Sticker-Interface: Erweiterung des Marker-Interfaces  downtop

Icon
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:

gp  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
Abbildung 6.18   Nicht leeres Marker-Interface

Im Fall ILocationre 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");
  }

Galileo Computing

6.10.2 Marker-Interface Cloneable  downtop

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

gp  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

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

Abschließend noch eine kleine Semantik-Regel:

clone()-Overriding muss die Semantik von equals() beachten

gp  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

Galileo Computing

6.11 Factory-Pattern  downtop

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

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:

gp  Der Sinn eines Factory-Patterns besteht darin, die konkreten Klassen vor den Clients, die sie erzeugen, zu isolieren.
gp  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
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.


Galileo Computing

6.11.1 Ein »Teile-Fabrik«-Beispiel  downtop

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:

gp  Eine konkrete Implementierung des Objekts Teil wird in nahezu jedem Programm verwendet.
gp  Jede Applikation benötigt aufgrund des Einsatzgebietes unterschiedliche Eigenschaften und Methoden des Objekts Teil.
gp  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

gp  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.
gp  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
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

Die Umsetzung des Template-Prinzips mit Hilfe von Interfaces:

Synergieeffekt:
Interfaces in Templates

gp  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«.


Galileo Computing

6.12 Konzeptionelle Schwächen von Interfaces  downtop

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.«


Galileo Computing

6.12.1 Constraints (Zusicherungendowntop

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

gp  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

gp  Preconditions – Bedingungen, die vor der Ausführung einer Methode gewährleistet sein müssen
gp  Postconditions – Bedingungen, die nach der Ausführung einer Methode gewährleistet sein müssen
gp  Invarianten – Bedingungen, die für alle Methoden gelten müssen

Galileo Computing

6.12.2 Interfaces bieten keinen Zugriffsschutz  downtop

Icon

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.


Galileo Computing

6.12.3 Ein Template-Dilemma  downtop

Icon

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 ...


Galileo Computing

6.13 Zusammenfassung  downtop

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.


Galileo Computing

6.14 Testfragen  toptop

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«.

  

Perl – Der Einstieg




Copyright © Galileo Press GmbH 2001 - 2002
Für Ihren privaten Gebrauch dürfen Sie die Online-Version natürlich ausdrucken und speichern. Ansonsten unterliegt das <openbook> denselben Bestimmungen wie die gebundene Ausgabe: Das Werk einschließlich aller seiner Teile ist urheberrechtlich geschützt. Alle Rechte vorbehalten einschließlich der Vervielfältigung, Übersetzung, Mikroverfilmung sowie Einspeicherung und Verarbeitung in elektronischen Systemen.
Die Veröffentlichung der Inhalte oder Teilen davon bedarf der ausdrücklichen schriftlichen Genehmigung von Galileo Press. Falls Sie Interesse daran haben sollten, die Inhalte auf Ihrer Website oder einer CD anzubieten, melden Sie sich bitte bei: stefan.krumbiegel@galileo-press.de


[Galileo Computing]

Galileo Press GmbH, Gartenstraße 24, 53229 Bonn, fon: 0228.42150.0, fax 0228.42150.77, info@galileo-press.de