![]() |
![]() |
|
Algorithmen als Routinen Bestimmte Konzepte lassen sich allerdings viel einfacher in der Form eines Prozesses als eines Objekts darstellen. Solche Konzepte sind meistens Algorithmen mit einem fest vorgegebenen Ablauf, deren Zustand nur von den Parametern und den Daten, auf die der Algorithmus selbst zugreift, abhängig ist und wobei nur das Ergebnis des Algorithmus für den Rest der Anwendung relevant ist. Die Form einer Routine ist für die Realisierung solcher Algorithmen gut geeignet. Problematisch ist diese Vorgehensweise, wenn das für den Rest der Anwendung relevante Ergebnis eines solchen Algorithmus nicht ein Wert (beziehungsweise ein Objekt), sondern eine ganze Reihe von Werten ist. Sammlungen als Resultat von Routinen In einem solchen Fall muss die Routine das Ergebnis zuerst komplett aufbereiten und es dann als eine Sammlung (engl. Collection) seinem Aufrufer bereitstellen. Dies ist dann unproblematisch, wenn der Aufrufer das komplette Ergebnis tatsächlich auf einmal braucht – es ist aber weniger als optimal, wenn der Aufrufer nur die einzelnen Elemente des Ergebnisses nacheinander bearbeiten möchte. Beschränkte Zahl von Ergebniselementen Und Routinen sind ganz und gar ungeeignet, wenn der Aufrufer nicht alle Elemente der Ergebnisliste braucht, sondern nur eine beschränkte Zahl der Elemente vom Anfang der Liste. Dies ist ein häufiges Szenario, wenn es zum Beispiel um die Anzeige von Listen, die aus einer Datenbank befüllt werden, geht. Eine Datenbankabfrage kann keine, wenige, aber auch sehr viele Einträge zurückgeben. Wenn auf dem Bildschirm nur hundert angezeigt werden können, hat es wenig Sinn, mit der Anzeige erst zu beginnen, nachdem wir alle zehntausend Ergebniszeilen einer Datenbankabfrage übertragen haben. Ein erster Ansatz: IteratorenIn solchen Fällen können wir von den Mechanismen für Kapselung und Polymorphie der objektorientierten Sprachen profitieren. Der Aufrufer soll nicht von der konkreten Implementierung einer gelieferten Sammlung abhängig sein, sondern nur von deren Schnittstelle. Eine Sammlung kann verschiedene Operationen anbieten. Es können Elemente eingefügt werden, diese können wieder entfernt werden, wir können überprüfen, ob ein Element in der Sammlung enthalten ist oder wie viele Elemente die Sammlung enthält. Aber: In unserem Falle brauchen wir das eigentlich gar nicht. Wir brauchen nur die Fähigkeit der Sammlung, ihre Elemente nacheinander zu liefern – sie abzählen zu lassen. Der Nutzer der Sammlung muss also nur imstande sein, eine Abzählung der Elemente der Sammlung zu starten, nacheinander nach dem nächsten Eintrag zu fragen und am Ende festzustellen, dass alle Elemente abgezählt worden sind. Methode next() Also bräuchten wir nur eine Methode next(), die am Anfang das erste und danach immer das nächste Element liefert. Und eine Methode hasNext(), die false zurückgibt, wenn alle Elemente abgezählt worden sind. Nun, es kann mehrere Benutzer der Sammlung geben, die ihre Elemente unabhängig voneinander abzählen möchten. Daher kann der Zustand der jeweiligen Abzählung nicht der Sammlung selbst zugeordnet werden, sondern einem neuen Objekt – einem Iterator.1
Es ist der Iterator, der die Operationen next() und hasNext() anbietet.2 Die Sammlung selbst muss aber eine Operation anbieten, die einen neuen Iterator erstellt und zurückgibt. Sie muss aber nicht, und das ist die Tatsache, die wir uns zunutze machen, alle Werte tatsächlich enthalten. Der gelieferte Iterator kann die Einträge erst bei Bedarf bereitstellen. Ein Schritt weiter: GeneratorenGeneratoren liefern Werte bei Bedarf Ein Iterator, der die gelieferten Werte dynamisch bei Bedarf bereitstellt, wird Generator genannt. Schauen wir uns nun unser bereits angekündigtes Beispiel an, die Berechnung der Fibonacci-Zahlenreihe. Die Fibonacci-Zahlen sind eine mathematische Folge von nichtnegativen ganzen Zahlen. Der Mathematiker Leonardo Fibonacci entwickelte sie 1202, um das Wachstum einer Population von Kaninchen zu beschreiben. Die Fibonacci-Zahlen werden folgendermaßen definiert: F1 = 1; F2 = 1; Fi; i>2 = Fi-2 + Fi-1 Das bedeutet in Worten: Für die beiden ersten Zahlen wird jeweils der Wert Eins vorgegeben. Jede weitere Zahl ist die Summe ihrer beiden Vorgänger. Daraus ergibt sich die Folge zu 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610, 987, 1597, 2584, 4181, 6765, ... Berechnung der Fibonacci-Zahlen Der Algorithmus zur Berechnung der Fibonacci-Zahlen könnte folgendermaßen aussehen: a := 1;
b := 1;
wiederhole:
füge a zum Ergebnis hinzu;
c := a;
a := b;
b := a + b;
Umsetzung mit Python Was hier bereits auffällt: Die Wiederholung hat keine Abbruchbedingung. Wir können die Reihe unendlich lange fortsetzen. Nun sollen wir diesen Algorithmus in ein Programm umwandeln, das eine bestimmte Anzahl der Fibonacci-Zahlen ausgibt. In Listing 7.27 ist eine erste Version zu sehen, diesmal umgesetzt in der Sprache Python. def printFibonacci(count):
a = 1
b = 1
while (count > 0):
print a,
c = a
a = b
b = a + c
count -= 1
Listing 7.27 Berechnung der Fibonacci-Zahlen mit Python Problem 1: Berechnung und Ausgabe vermischt Die Version ist schon ganz in Ordnung, allerdings hat sie einen Makel: Sie vermischt die Beschaffung der Ergebnisse, in unserem Falle die Berechnung der Fibonacci-Zahlen, mit deren Bearbeitung, in unserem Falle deren Ausgabe. Wir sehen hier einen Fall der Kopplung, die wir ja zu minimieren versuchen. Was wäre, wenn wir mit den Ergebnissen des Algorithmus andere Pläne hätten, zum Beispiel einen anderen Algorithmus sukzessive damit aufzurufen? Versuchen wir also, die Berechnung der Zahlen von deren Ausgabe zu trennen. In Listing 7.28 sehen Sie eine angepasste Version unseres kleinen Programms, die zwei separate Routinen verwendet. def computeFibonacci(count):
a = 1
b = 1
result = []
while (count > len(result)):
result.append(a)
c = a
a = b
b = a + c
return result
def printFibonacci(count):
for f in computeFibonacci(count):
print f,
Listing 7.28 Berechnung und Ausgabe getrennt Problem 2: Speicherbedarf abhängig von Anzahl Das sieht schon etwas besser aus. Wir haben den Makel der Kopplung beseitigt und den Algorithmus in der Methode computeFibonacci von der Ausgabemethode entkoppelt. Allerdings haben wir uns ein anderes Problem eingehandelt: Das Ergebnis wird zuerst in einer dynamisch wachsenden Liste gespeichert und erst nach der Berechnung ausgegeben. Das ist unangenehm, wenn die Anzahl der Fibonacci-Zahlen, die wir ausgeben möchten, sehr groß ist. Das ursprüngliche Programm hatte konstanten Speicherbedarf, der Speicherbedarf unserer aktuellen Version ist von der Anzahl der auszugebenden Zahlen linear abhängig. Lösung: Generator für Fibonacci-Zahlen Das können wir besser! Wir schreiben uns einfach einen Generator. class FibonacciGenerator: 1
def __init__(self, count): 2
self.__count = count
self.__a = 1 3
self.__b = 1 3
def next(self): 4
if (self.__count <= 0):
raise StopIteration
self.__count -= 1;
c = self.__a 3
self.__a = self.__b
self.__b = self.__a + c
return c
def printFibonacci(count):
generator = FibonacciGenerator(count) 5
for i in range(count):
print generator.next(), 6
Listing 7.29 Generator für Fibonacci-Zahlen Wir haben die Funktion computeFibonacci in eine Klasse FibonacciGenerator umgewandelt (Zeile 1). Exemplare der Klasse werden in Zeile 2 mit dem Parameter count der ursprünglichen Funktion initialisiert. Außerdem besitzt die Klasse Objektvariablen a, b und c, die in den mit 3 markierten Zeilen verwendet werden. Diese entsprechen den lokalen Variablen der Funktion. In Zeile 4 sehen Sie die Methode next(), die nacheinander die Fibonacci-Zahlen zurückgibt. Wir haben damit den Algorithmus zur Erzeugung der Zahlenreihe als Objekt implementiert. In den Zeilen 5 und 6 wird der Generator konstruiert und verwendet. Entkopplung erfolgreich Um die Zahlen ähnlich wie die Liste, die von der ursprüngliche Funktion zurückgegeben wurde, in einer for-Schleife benutzen zu können, brauchen wir nur eine kleine Erweiterung. Wir müssen eine Sammlung programmieren, die den Generator als ihren Iterator zurückgibt.3 Unser endgültiger Quelltext ist in Listing 7.30 dargestellt. class FibonacciNumbers:
def __init__(self, count):
self.__count = count
class FibonacciGenerator:
def __init__(self, count):
self.__count = count
self.__a = 1
self.__b = 1
def next(self):
if (self.__count <= 0):
raise StopIteration
self.__count -= 1;
c = self.__a
self.__a = self.__b
self.__b = self.__a + c
return c
def __iter__(self): 1
return FibonacciNumbers.FibonacciGenerator(
self.__count)
def printFibonacci(count):
for f in FibonacciNumbers(count): 2
print f,
Listing 7.30 Ein Generator wird von einer Sammlung geliefert. Nun liefert ein Exemplar der Klasse FibonacciNumbers ein Exemplar der Klasse FibonacciGenerator, wenn es nach seinem Iterator befragt wird. Dies wird in Zeile 1 so deklariert. In der for-Schleife von Zeile 2 verwendet Python dann automatisch den Iterator, um die jeweils nächsten Elemente abzuholen. Eine schöne Sache, nicht wahr? Wir haben die Berechnung der Fibonacci-Zahlen von deren Verarbeitung entkoppelt, und der Speicherverbrauch ist konstant wie bei der ursprünglichen Prozedur. Diskussion: Weitere Abhängigkeiten Bernhard: Sieht ja zunächst recht gut aus. Aber so völlig entkoppelt ist das Ganze dann doch noch nicht. Gregor: Was meinst du damit? Unser Generator hat doch nun mit der Ausgabe der von ihm generierten Zahlen überhaupt nichts mehr zu tun. Bernhard: Das ist schon richtig, aus Sicht des Generators haben wir keine Abhängigkeit mehr. Aber wenn wir unsere Methode zur Ausgabe der Zahlen anschauen, ist diese umgekehrt auf den Generator angewiesen. Sie kann nicht verwendet werden, um die Ergebnisse einer anderen Funktion aufzubereiten. Wenn wir die Ergebnisse eines Generators für die Ackermann-Funktion ausgeben wollen, müssen wir dafür eine eigene Methode schreiben. Gregor: Da hast du allerdings Recht. In unserem Fall ist das zwar kein großes Problem, weil unsere Ausgabemethode so einfach ist und ihre Mehrfachverwendung uns nicht sehr viel an Code einsparen wird. Aber wenn die Ausgabe komplexer wäre, zum Beispiel die Ergebnisse als kleines Feuerwerk auf den Bildschirm zaubern, dann hätten wir durchaus ein Problem, diese Ausgabe erneut zu verwenden. Eine mögliche Lösung ist die Verwendung von Funktionen als Parameter für andere Funktionen. In Abschnitt 7.4.4 werden wir Beispiele für die Behandlung von Funktionen als Objekte kennen lernen. Wir haben im obigen Beispiel gesehen, wie wir einen Generator für Fibonacci-Zahlen umsetzen können. Dadurch haben wir elegant eine Umsetzung der Berechnung erreicht und erhalten außerdem die Elemente der Reihe eins nach dem anderen geliefert. Aber nichts auf dieser Welt gibt es völlig kostenlos. Wir haben auch gesehen, dass wir mehr Quelltext für die Implementierung des Algorithmus brauchen. Außerdem ist der Ablauf der ursprünglich prozeduralen Beschreibung des Algorithmus durch die Struktur der Klasse nicht mehr so klar wie vorher. Bei unserem Beispiel mit den Fibonacci-Zahlen mag dieser Nachteil klein erscheinen. Bei komplizierten Abläufen kann es allerdings ein recht großer Aufwand sein, eine Funktion in einen Generator umzuwandeln. Es hängt sehr stark von der Unterstützung durch die Programmiersprache ab, ob sich der Aufwand, einen Generator zu schreiben, rechnet. Betrachten wir zunächst die Möglichkeiten, Generatoren in Java umzusetzen. In Java ist der Aufwand, einen Generator zu programmieren, höher als der Aufwand, den man für eine entsprechende Methode braucht. Daher werden Generatoren in Java kaum verwendet. Eine wichtige Ausnahme sind Objekte, die für Datenübertragung zuständig sind, bei der die Beschaffung der kompletten Liste der Ergebnisse viel zu viel Zeit und/oder Speicher in Anspruch nehmen würde. Die JDBC-Recordsets oder die in Java allgegenwärtigen Streams und Readers kann man als etwas abgewandelte Generatoren betrachten. Doch wir können Generatoren auch in Java in Form einer Methode programmieren. Ein Generator hat zwei Zuständigkeiten. Einerseits stellt er seinen Benutzern den Zugriff auf die gelieferten Werte zur Verfügung, andererseits beschafft er die Einträge. Die Beschaffung der Werte können wir als eine Methode umsetzen, die in einem nebenläufigen Thread läuft und die beschafften Einträge in eine nur ein Element umfassende Queue abstellt, von wo diese vom Benutzer des Iterators abgefragt werden können. Und genauso können wir eine Basisklasse für Generatoren implementieren. Die abstrakte Basisklasse übernimmt die Verantwortung für die Synchronisation mit dem nebenläufigen Thread und die Iteratorenfunktionalität. Die konkreten Implementierungen müssen nur den Algorithmus umsetzen. Doch einen Nachteil hat diese Implementierung: Wenn nicht alle Werte des Generators ausgelesen werden, bleibt der nebenläufige Berechnungsthread in einem suspendierten Zustand und belegt Ressourcen, auch wenn der Generator nicht mehr gebraucht wird. Speicher- und Threadleck ante portas.4 Aber es gibt auch Sprachen, die Generatoren direkt unterstützen und deren Implementierung sehr einfach machen. Python ab der Version 2.2 oder C# ab der Version 2 gehören in diese lobenswerte Kategorie. Generatoren: einfach in Python oder C# In diesen Sprachen entspricht der Aufwand, eine Funktion zu schreiben, genau dem Aufwand, einen Generator zu schreiben. Und die Form des Generator-Quelltextes entspricht auch dem Quelltext der entsprechenden Funktion. Denn die Verantwortung, die Ergebnisse abzählbar zu machen, übernimmt die Programmiersprache selbst. Unser Beispiel würde bei der Verwendung der Generator-Syntax in Python folgendermaßen aussehen: def computeFibonacci(count):
a = 1
b = 1
while (count > 0):
yield a
c = a
a = b
b = a + c
count -= 1
Listing 7.31 Verwendung eines Sprachkonstrukts von Python als Generator Generator als vollwertiges Objekt Ja, der Quelltext ähnelt sehr einem Quelltext einer Funktion und unterscheidet sich von einer Funktion nur durch die Verwendung der Befehls yield, der dem Befehl return entspricht, aber statt einen Ergebniswert zurückzugeben und die Ausführung der Funktion zu beenden, bereitet er einfach einen Rückgabewert für den nächsten Aufruf der Methode next() des Generators vor. Der Generator ist dabei ein vollwertiges Objekt mit eigenem Zustand und eigener Lebensdauer, Objektvariablen und Methoden. Eine Subroutine wird gestartet, und erst wenn sie fertig ist, kann sie ein Ergebnis liefern. Die aufrufende Routine wartet während der Ausführung der Subroutine, bis sie fertig mit ihrer Arbeit ist. Generatoren sind Koroutinen. Ein Generator dagegen kann als eine Routine betrachtet werden, die parallel zu der aufrufenden Routine läuft und immer bei Bedarf und auch wiederholt ein Ergebnis liefert. Aus diesem Grunde werden Generatoren auch Koroutinen genannt. Dabei kommt es nicht darauf an, ob die Koroutinen tatsächlich in einem separaten Thread wirklich parallel ausgeführt werden – dies wäre sogar, wenn es um die Übersichtlichkeit und Einfachheit der Abläufe geht, eher kontraproduktiv. Vorteil: Ergebnismenge muss nicht begrenzt werden. In unserem Beispiel hat der Generator immer eine endliche Liste von Ergebnissen geliefert. Bei einer dem Generator entsprechenden Funktion ist dies eine zwingende Anforderung, es sei denn, Ihr Rechner hat unbegrenzten Speicher und Sie haben unendlich viel Zeit. Bei einem Generator besteht keine Notwendigkeit, die Anzahl der gelieferten Ergebnisse zu begrenzen.5 Der Konsument der Werte kann selbst bestimmen, wie viele Werte er von dem Generator abfragt.6 Hier unser Beispiel mit einem unbegrenzten Fibonacci-Zahlen-Generator: Unbegrenzter Fibonacci-Generator def computeFibonacci():
a = 1
b = 1
while (True):
yield a
c = a
a = b
b = a + c
def printFibonacciTo(limit):
for f in computeFibonacci():
if (f > limit):
break
print f,
Listing 7.32 Unbegrenzter Generator für Fibonacci-Zahlen Diese Version des Generators beschreibt nun nachvollziehbar unseren Algorithmus und erstellt doch die Elemente der Reihe erst auf explizite Anfrage. Im folgenden Abschnitt werden Sie sehen, wie in objektorientierten Systemen Routinen als Objekte abgebildet werden können. Dadurch können andere Objekte mit diesen Routinen parametrisiert werden. 7.4.4 Funktionsobjekte und ihr Einsatz als Eventhandler
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Eventhandler (Ereignisverarbeiter) |
|
Ereignisverarbeiter sind Routinen, die aufgrund eines Ereignisses aufgerufen werden und die Aktion durchführen, die durch das Ereignis ausgelöst werden soll. In objektorientierten Systemen können diese Routinen als Objekte repräsentiert werden. Als Reaktion auf ein Ereignis wird in diesem Fall eine Operation dieses Objekts aufgerufen. |
Wir haben Eventhandler also als Routinen definiert. Wenn Sie sich das Beispiel in Abbildung 7.36 genauer anschauen, stellen Sie fest, dass die Exemplare der Klasse Aktion eine sehr einfache Schnittstelle haben. Sie bieten lediglich eine einzige Operation durchführen() an. Sie sind nur dazu da, um eine Methode bereitzustellen, die von verschiedenen Objekten zu verschiedenen Zeiten aufgerufen werden kann. Damit kapseln diese Objekte lediglich die Routine durchführen().
Daher wäre es eigentlich praktisch, wenn Sie gar keine eigene Klasse für diese Aktionen erstellen, sondern lediglich Routinen definieren müssten, die dann in verschiedenen Kontexten verwendet werden. Diese Möglichkeit bieten die so genannten Funktionsobjekte.
| Funktionsobjekte (Function Objects) |
|
Manche Programmiersprachen ermöglichen es, Routinen direkt als Objekte zu behandeln. Diese Objekte werden als Funktionsobjekte bezeichnet. In Programmiersprachen, die dieses Konzept unterstützen, müssen Sie also keine Klasse Aktion oder Ausdruck selbst programmieren, es reicht, wenn Sie eine Routine ausdruck() erstellen. Diese Routine wird dann als ein Objekt behandelt. Zu solchen Programmiersprachen gehören zum Beispiel Python, Ruby oder JavaScript. In ihnen sind die Routinen (Prozeduren, Funktionen, Methoden oder Blöcke) selbst Objekte. C# bietet unter dem Namen Delegaten (Delegates) ein Sprachmittel, das es ermöglicht, Objekte mit Funktionen zu parametrisieren. |
In Sprachen, die Funktionsobjekte direkt unterstützen, lässt sich unser Anwendungsbeispiel zur Druckfunktion umsetzen, ohne dass Sie eine eigene Klasse für die Aktion Drucken (und möglicherweise jede Menge anderer Aktionen) einführen müssen.
Drei Umsetzungsvarianten
In den folgenden Abschnitten stellen wir drei verschiedene Umsetzungsvarianten für unser Beispiel aus Abbildung 7.36 vor; eine davon in der Sprache Ruby, die Funktionsobjekte direkt unterstützt, in der also Routinen echte Objekte sind. Das zweite Beispiel stellen wir in Java vor. In Java sind Routinen nicht direkt als Objekte behandelbar. Schließlich stellen wir eine weitere Variante in C# vor. C# bietet ein spezielles Konstrukt, die Delegaten, mit denen sich Funktionsobjekte nachbauen lassen.7
Routinen sind echte Objekte
Als Erstes stellen wir eine Umsetzung unseres Beispiels in einer Sprache vor, die Routinen als echte Objekte behandelt. Die Umsetzung unserer Anforderung lässt sich in so einer Sprache direkt vornehmen.
In Listing 7.33 sehen Sie also eine Umsetzung in Ruby, bei der die Aktionen als anonyme Funktionen umgesetzt werden.8
# Eine Taste, die eine Bezeichnung und eine Aktion verwaltet
class Schaltflaeche
def initialize(name, &aktion) 1
@name = name
@aktion = aktion
end
def click 2
@aktion.call 3
end
end
# Im Code des Dokumentfensters
class DokumentFenster
...
def initialize
schaltflaecheAusdruck = Schaltflaeche.new("Drucken"){ 4
@document.print
}
schalteflaecheLoeschen = Button.new("Löschen") { 5
@document.loeschen
}
}
Listing 7.33 Verwendung von Funktionsobjekten in Ruby
Exemplare der Klasse Schaltfläche werden in Zeile 1 mit einer Aktion parametrisiert.9 Bei Aufruf der Operation click, deren Umsetzung in Zeile 2 zu sehen ist, wird die bei der Konstruktion übergebene Aktion über den Aufruf von call ausgeführt (Zeile 3). Wenn nun ein Exemplar der Klasse DokumentFenster erzeugt wird, so werden dort zwei Exemplare von Schaltfläche erstellt. Das erste Exemplar erhält den Namen Drucken 4 und bekommt außerdem eine anonyme Funktion übergeben, die selbst @document.print aufruft. Die zweite Schaltfläche wird in Zeile 5 mit dem Namen Löschen und einer anderen anonymen Funktion parametrisiert, die wiederum @document.loeschen aufruft.
In unserem Beispiel bleiben die Funktionen zum Drucken und Löschen eines Dokuments anonym. Genauso gut könnten Sie diese aber auch einer Variablen zuweisen.
Proc-Objekte in Ruby Python Java
In Java kann man die Methoden der Klassen nicht als Objekte behandeln. Um zum Beispiel einer Schaltfläche eine spezielle Aktion zuzuordnen, müssen Sie tatsächlich eine Klasse Ausdruck programmieren, wie im Beispiel aus Abbildung 7.36.
Da Sie in Java für die Umsetzung der Routinen also eine komplette eigene Klasse erstellen müssen, können Sie natürlich der Klasse Aktion auch mehr Verantwortung zuweisen, als nur eine Routine zu kapseln. Man kann ihr zum Beispiel den im Menü oder auf der Taste dargestellten Text oder das in einer Tastenleiste dargestellte Symbol zuordnen.
Java bietet eine Basisklasse für solche Aktionsklassen unter dem Namen AbstractAction an. Aber auch in Java lässt es sich vermeiden, eine komplett sichtbare eigenständige Klasse für eine solche Aktion umzusetzen. Dafür können Sie wieder die anonymen Klassen verwenden.10
In Abbildung 7.37 sind die verwendeten Klassen dargestellt. Da UML sich mit anonymen Klassen etwas schwer tut, haben wir stellvertretend die Klasse AnonymousAction und ihr Exemplar ausdrucken aufgeführt.

Hier klicken, um das Bild zu vergrößern
Anonyme Klassen können innerhalb einer Methode deklariert werden, sie können dabei als eine Unterklasse einer benannten Klasse oder als eine Implementierung einer Schnittstelle erzeugt werden.
In Listing 7.34 ist die Implementierung aufgeführt. In Zeile 1 wird dabei eine anonyme Unterklasse von AbstractAction deklariert und auch gleich ein Exemplar ausdrucken davon erstellt.
Die anonyme Klasse implementiert die Operation actionPerformed() in Zeile 2 so, dass beim Erhalt eines Ereignisses das aktuelle Dokument gedruckt wird.
private void initComponents() {
Action ausdrucken = new AbstractAction("Drucken") { 1
public void actionPerformed(ActionEvent e) { 2
... // Dokument drucken
}
};
JButton printButton = new JButton();
printButton.setAction(ausdrucken); 3
...
}
Listing 7.34 Verwendung einer anonymen Unterklasse von AbstractAction
In Zeile 3 wird die Aktion ausdrucken der neu erstellten Schaltfläche printButton (ein Exemplar von JButton) zugeordnet. So werden über printButton eintreffende Ereignisse die Aktion ausdrucken auslösen.
Routinen können zu Objekten gemacht werden
C# verwendet zur Behandlung von Ereignissen so genannte Delegaten-Klassen.
| Delegaten-Klassen |
|
Delegaten-Klassen sind eine Spezialität von C#. Exemplare von Delegaten-Klassen erhalten bei ihrer Konstruktion eine Methode übergeben, die sie auf Anforderung ausführen. Die Signatur der Methode wird dabei durch die Klassendefinition festgelegt. Delegaten-Objekte sind damit also direkt durch die Übergabe von Methoden parametrisierbar. |
Die Verwendung der Delegaten-Klassen ist deshalb notwendig, weil C# als eine statisch typisierte Programmiersprache die Signatur für die verwendeten Funktionen deklarieren muss. Dies geschieht über die Klassendefinition.
Die Delegaten-Klassen, deren Exemplaren man entsprechende Methoden zuordnen kann, sind ziemlich speziell. Sie haben alle die gleiche Struktur und nur einen Zweck: Methoden zu kapseln. Sie sind so speziell, dass C# für die Deklaration dieser Klassen eine spezielle Syntax und das Schlüsselwort delegate vorgesehen hat.
Schaltflächen in C#
Betrachten wir zunächst unser Beispiel mit den Schaltflächen in einer Variante, die eine Delegaten-Klasse verwendet. In Abbildung 7.38 ist die Verwendung der in C# vordefinierten Klasse EventHandler dargestellt. Diese ist eine Delegaten-Klasse, so dass Sie Exemplaren dieser Klasse Methoden mit der deklarierten Signatur zuordnen können.

Hier klicken, um das Bild zu vergrößern
Die Klasse EventHandler legt dabei fest, dass die ihr zugeordneten Methoden die Parameterleiste (object sender, Eventargs e) aufweisen müssen. Die Methode ausdrucken entspricht dieser Festlegung und kann so einem Exemplar der Klasse EventHandler zugeordnet werden. Das Objekt clicked, das in schaltflaecheDrucken enthalten ist, ist ein solches Exemplar.
In Listing 7.35 ist die Umsetzung zu sehen.
class Button {
public event EventHandler clicked;
// ...
}
class DokumentFenster {
// Die Methode zum Ausdrucken des Dokuments
public void ausdrucken(object sender, EventArgs e) { 2
// ... Ausdruck der Datei ...
}
Button schaltflaecheDrucken = new Button("Drucken");
schaltflaecheDrucken.clicked += ausdrucken; 3
}
Listing 7.35 Verwendung der Delegaten-Klasse EventHandler in C#
In Zeile 1 ist zu sehen, dass die Klasse Button ein Exemplar der Klasse EventHandler besitzt. Wird nun wie in Zeile 2 ein Exemplar von Button erstellt, so kann diesem eine Methode zugeordnet werden, die mit der Signatur von EventHandler kompatibel ist. In Zeile 3 wird die Methode ausdrucken zugewiesen. Allgemein werden solche Delegaten-Klassen in C# und der ganzen .NET-Familie verwendet, um Beobachter über interessante Ereignisse zu informieren.
Konverter als Delegaten
Betrachten wir noch ein weiteres Beispiel, bei dem eine eigene Delegaten-Klasse verwendet wird. Um eine Klasse zur Kapselung einer Methode mit der Signatur (String str) und dem Rückgabetyp int zu deklarieren, reicht eine Zeile:
delegate int Converter(String str);
Der Konstruktor der Delegaten-Klasse Converter enthält als Parameter den Namen einer Methode, die bei der Verwendung eines Konverters aufgerufen werden soll. Listing 7.36 zeigt die Delegaten-Klasse im Einsatz.
public static int DecimalConversion(String str) 1
{
return Int32.Parse(str);
}
public static int HexadecimalConversion(String str) 2
{
return Int32.Parse(str,
System.Globalization.NumberStyles.HexNumber);
}
static void Main(string[] args)
{
Converter converter = 3
new Converter(HexadecimalConversion);
Console.Out.WriteLine(converter("100")); 4
}
Listing 7.36 Verwendung der Delegaten-Klasse Converter
In diesem Beispiel sind die zwei Methoden in Zeile 1 und 2 mit der vorher deklarierten Klasse Converter kompatibel. In Zeile 3 wird der Konverter mit einer der beiden Methoden initialisiert. Beim folgenden Aufruf in Zeile 4 wird dann auch die angegebene Konvertierungsmethode verwendet.
In C# können Sie einer Delegatenvariablen nicht nur ein Delegaten-Exemplar zuordnen, sondern gleich mehrere. So können Sie der Variablen converter noch ein anderes Exemplar der Klasse Converter hinzufügen:
converter += new Converter(DecimalConversion);
Jetzt würde C# bei dem Aufruf converter("100") beide Konvertierungsfunktionen aufrufen. Nun, in unserem Beispiel würde das kaum Sinn machen, da wir nur einen Rückgabewert bekommen: den, der von der zuletzt aufgerufenen Methode zurückgegeben wird.
Wenn es aber um die Signalisierung von Ereignissen an mehrere potenzielle Beobachter geht, kann die Zuordnung mehrerer Delegaten-Exemplare mit dem Rückgabetyp void sinnvoll sein.
Wir schicken in diesem Abschnitt zunächst einmal eine Frage vorweg: Wozu benötigen wir eigentlich Kopien von unseren Objekten?
Häufig werden Objekte als Vorlagen verwendet. Dabei wird auf der Grundlage eines bestehenden Objekts ein weiteres, zunächst genau gleiches Objekt erzeugt. Dieses wird dann in der Folge angepasst. So kann zum Beispiel eine Überweisung beim Online-Banking als Vorlage für weitere Überweisungen verwendet werden.
Auf einer technischen Ebene können Sie dagegen Kopien zum Beispiel verwenden, um sich Zustände von Objekten zu merken, die Sie später wiederherstellen wollen oder mit denen ein Abgleich stattfinden soll.
Vorlagen für Überweisungen
Ein Beispiel von Kopien, die von Vorlagen gemacht werden, sind die Überweisungsvorlagen, die Sie zum Beispiel beim Online-Banking verwenden. Dort können Sie eine einmal ausgeführte Überweisung an das Finanzamt als Vorlage speichern. Da Sie wissen, dass diese Überweisung leider nicht die letzte sein wird, können Sie alle Daten des Überweisungsobjekts speichern.
Wenn Sie die nächste Überweisung an das Finanzamt vornehmen, machen Sie auf Basis dieses Objekts eine Kopie, bei der Adressat, Bankleitzahl, Kontonummer und auch Betrag zunächst übernommen werden. In der Regel werden Sie dann aber den Betrag anpassen müssen. Das System wird bei Ausführung dann weitere Attribute wie zum Beispiel das Ausführungsdatum für Sie anpassen.

Hier klicken, um das Bild zu vergrößern
Nach Auswahl einer Überweisung wird zunächst eine exakte Kopie angelegt, bei der sich anschließend die einzelnen Attribute anpassen lassen (siehe Abbildung 7.40).
Diese Art von Kopie agiert als Prototyp und sollte auch entsprechend verwaltet werden. Wir haben das entsprechende Entwurfsmuster in Abschnitt 7.1 bereits kurz vorgestellt. Sie erstellen dabei auf der Grundlage bereits vorhandener Daten ein neues Objekt und prägen dieses dann konkret aus. Dabei können natürlich auch Kopien von Sammlungen (Collections) angefertigt werden. Andere Arten von Kopien sollten auf einer fachlichen Betrachtungsebene nicht notwendig sein. Müssen Sie sich auf dieser Ebene doch auch mit anderen Arten von Kopien beschäftigen, fehlt oft eine Abstraktionsebene.

Hier klicken, um das Bild zu vergrößern
Wie erstellen Sie nun eine solche Kopie von Objekten? Sie haben bisher zwei technische Möglichkeiten dafür kennen gelernt. In Abschnitt 7.1 haben wir die so genannten Copy-Konstruktoren beschrieben, ebenso die Erstellung von neuen Objekten auf der Grundlage von Prototypen. Ein Copy-Konstruktor erhält bei der Konstruktion ein existierendes Exemplar einer Klasse übergeben und erstellt davon eine Kopie.
Um Kopien von Objekten herzustellen, hat der Copy-Konstruktor allerdings einen Nachteil: Der Aufruf eines Konstruktors kann nicht polymorph erfolgen, ist also nicht abhängig vom konkreten Typ des zu kopierenden Objekts. Es ist also durchaus ein Unterschied, ob Sie
MeinObjekt kopie = original.clone();
oder
MeinObjekt kopie = new MeinObjekt(original);
aufrufen. Auf den ersten Blick sieht es zwar so aus, als würden beide Aufrufe genau das Gleiche machen, nämlich eine exakte Kopie erstellen. Aufgrund der fehlenden dynamischen Polymorphie bei Konstruktoren ist das Verhalten der beiden Varianten aber unterschiedlich.
Wir betrachten zunächst einmal das Verhalten von Copy-Konstruktoren für diesen Fall. Nehmen Sie an, Sie haben eine Klasse Kunde vorliegen, außerdem eine Unterklasse für Geschäftskunden. Beide Klassen haben jeweils einen Copy-Konstruktor. In Abbildung 7.41 sind die Klassen mit ihren Konstruktoren dargestellt.

Hier klicken, um das Bild zu vergrößern
Aufgerufen wird der Konstruktor der Klasse Kunde in einer Methode neuerKunde der Klasse KundenGenerator. Diese erhält ein Exemplar der Klasse Kunde als Wert für den Parameter alterKunde übergeben. Da Geschäftskunde eine Unterklasse von Kunde ist, kann natürlich auch ein Exemplar der Klasse Geschäftskunde übergeben werden.
In Listing 7.37 ist die Java-Umsetzung der Operation neuerKunde und deren Verwendung gezeigt.
class KundenGenerator {
Kunde neuerKunde (Kunde alterKunde) { 1
return new Kunde(alterKunde);
}
public static void main(String[] args) {
KundenGenerator generator = new KundenGenerator();
Kunde kunde1 = new Kunde("Zuiop","Qwert"); 2
Geschäftskunde kunde2 = 3
new Geschäftskunde("Zuiop","Qwert","HRB 112244");
Kunde neuerKunde1 = generator.neuerKunde (kunde1); 4
Kunde neuerKunde2 = generator.neuerKunde(kunde2); 4
}
Listing 7.37 Kopien durch Copy-Konstruktor
Die Methode neuerKunde in Zeile 1 erstellt eine Kopie eines Exemplars der Klasse Kunde, indem sie den Copy-Konstruktor aufruft. In den Zeilen 2 und 3 wird dann jeweils ein Exemplar der Klasse Kunde und eines der Klasse Geschäftskunde erstellt. Beide werden nacheinander in den mit 4 markierten Zeilen als Parameterwert an neuerKunde übergeben.
Nur Exemplar der Basisklasse
Wenn Sie danach die Klassenzugehörigkeit der beiden neu erstellten Objekte erfragen, erhalten Sie folgende Antwort:
Klasse des neuen Kunden kunde1: Kunde
Klasse des neuen Kunden kunde2: Kunde
Auch die Kopie des Geschäftskunden hat lediglich ein Exemplar der Klasse Kunde erzeugt, da unser Copy-Konstruktor nicht polymorph auf der Grundlage des übergebenen Objekts agiert.
Ein anderes Verhalten zeigt sich, wenn für die Kopien eine eigene Operation verwendet wird, um auf Basis eines existierenden Objekts eine Kopie davon zu erzeugen. Eine solche Operation wird als Clone-Operation bezeichnet. In Abbildung 7.42 sind die modifizierten Klassen dargestellt.

Hier klicken, um das Bild zu vergrößern
Wenn Kopien über die Clone-Operation erzeugt werden, ändert sich das Verhalten im Vergleich zu den Copy-Konstruktoren. Nun haben also sowohl die Klasse Kunde als auch die Klasse Geschäftskunde eine Operation clone zugeordnet. Exemplare der Klassen können damit also Kopien von sich selbst erzeugen.
Die Methode neuerKunde in der Klasse KundenGenerator ruft in dieser Variante keinen Copy-Konstruktor mehr auf, sondern die Operation clone auf dem übergebenen Exemplar der Klasse Kunde:
Kunde neuerKunde(Kunde alterKunde) {
return alterKunde.clone();
}
Wird mit dieser veränderten Variante erneut der Code von Listing 7.37 durchlaufen, weisen die kopierten Objekte die korrekte Klassenzugehörigkeit auf:
Klasse des neuen Kunden kunde1: Kunde
Klasse des neuen Kunden kunde2: Geschäftskunde
Durch das Erstellen einer Kopie über die Operation clone erhalten Sie also eine korrekte Kopie unserer Kundenobjekte, auch wenn es sich um Exemplare von Unterklassen handelt.
Die Operation clone in Java
| Die Operation clone in Java |
|
Die Behandlung der Clone-Operation in Java ist nicht völlig geradlinig. Die von der Klasse Object zur Verfügung gestellte Methode clone ist als protected deklariert. Sie kann damit von abgeleiteten Klassen, die eine öffentliche Operation für das Kopieren ihrer Exemplare zur Verfügung stellen, genutzt werden, um flache Kopien von Objekten zu erzeugen. Erst wenn abgeleitete Klassen eine Operation für das Erstellen von Kopien bereitstellen, kann diese auch genutzt werden. So weit, so verständlich. Allerdings gibt es nun zwei weitere Randbedingungen. Damit ein Objekt die Methode clone der Klasse Object nutzen darf, muss die zugehörige Klasse die Schnittstelle Cloneable implementieren. Die Dokumentation zu dieser Schnittstelle enthält die folgende Beschreibung: Bitte beachten Sie, dass diese Schnittstelle die Methode clone nicht enthält. Deshalb ist es nicht möglich, ein Objekt nur auf Grundlage der Tatsache, dass es die Schnittstelle implementiert, mittels clone zu kopieren. Auch wenn die clone-Methode über Reflection aufgerufen wird, besteht keine Garantie, dass dies erfolgreich sein wird. Objekte, welche die Schnittstelle Cloneable implementieren, weisen die Gemeinsamkeit auf, dass sie eine Kopie von sich anfertigen können. Sie können diese Eigenschaft aber nicht nutzen, weil in der Schnittstelle die entsprechende Operation nicht festgelegt wird. Eine Klasse könnte ihre Methode zur Erstellung einer Kopie also durchaus zum Beispiel reproduce nennen. Dies schränkt zum Beispiel sehr stark die Möglichkeiten ein, Sammlungen von Objekten über einen generischen Mechanismus zu kopieren. Für eigene Klassen gibt es natürlich die Möglichkeit, eine Erweiterung der Schnittstelle Cloneable zu definieren, die dann auch die clone-Methode spezifiziert. |
Betrachten wir jetzt auch einmal die formalen Eigenschaften, die wir einer Kopie zuschreiben:
| Die Kopie ist nicht identisch mit dem Original. |
| Die Kopie gehört zur selben Klasse wie das Original. |
| Die Kopie ist gleich dem Original, sofern wir keine Änderungen daran vorgenommen haben. |
| Änderungen an der Kopie ändern nicht die Daten, die dem Original gehören. |
Letztere Forderung bezieht sich auf die Unterscheidung einer Kopie des Inhalts gegenüber einer Kopie der Referenz. Wenn etwas als Bestandteil eines Objekts betrachtet wird, dann sollte auch nur dieses Objekt selbst die Möglichkeit haben, diesen Bestandteil zu ändern. Ob ein referenziertes Objekt nun in diesem Sinn Bestandteil eines anderen Objekts ist, kann aus der entsprechenden Klassendefinition nicht entnommen werden. Diese Einordnung gehört zur Metainformation, also zur Information über die entsprechende Klasse.
Wir müssen diese Information beim Erstellen von Kopien aber berücksichtigen und entscheiden, ob wir von einem referenzierten Objekt wiederum eine Kopie erstellen oder die Referenz einfach auf dasselbe Objekt setzen können.
Greifen wir unser Beispiel der Überweisungen, die als Vorlagen für andere Überweisungen dienen, nun wieder auf. Die Überweisung referenziert dabei eine Bankverbindung. Diese wiederum verweist auf eine Bank. In Abbildung 7.43 sind diese Beziehungen dargestellt.

Hier klicken, um das Bild zu vergrößern
Wenn Sie nun eine Kopie anlegen, dann wollen Sie sicherlich die Bankverbindung komplett kopieren. Wenn Sie hier einfach nur den Verweis auf das Objekt Bankverbindung übernehmen, würden mysteriöserweise in unserer Vorlage Änderungen an der Kontonummer auftauchen. In diesem Fall soll eine so genannte tiefe Kopie der Bankverbindung erstellt werden. Gilt das aber auch für die über die Bankverbindung referenzierte Bank? Wenn Sie diese auch kopieren würden, wäre der Effekt doch eher merkwürdig. Es gäbe dann zum Beispiel die Kreissparkasse Stormarn mehrfach in Ihrem System. In diesem Fall würden Sie also beim Kopieren der Bankverbindung nur den Verweis auf die referenzierte Bank kopieren. Sie legen eine flache Kopie des Objekts an.
| Flache und tiefe Kopien |
|
Beim Anlegen einer flachen Kopie eines Objekts werdem alle Datenelemente, die Basisdatentypen enthalten, kopiert. Weitere Objekte, die referenziert werden, werden aber nicht mitkopiert, sondern lediglich die Referenz auf diese Objekte. Beim Anlegen einer tiefen Kopie eines Objekts werden alle Datenelemente und alle referenzierten Objekte kopiert. Damit entstehen auch von den referenzierten Objekten Kopien, die anschließend verändert werden können, ohne dass diese dabei das Original modifizieren. Ob ein Objekt flach oder tief kopiert werden muss, ist meist eine fachliche Entscheidung. Wenn die Kopie modifiziert werden muss, ohne das Original zu verändern, so muss eine tiefe Kopie erstellt werden. |
Ein Problem, das uns beim Kopieren plagen kann, ist das Auftreten von zyklischen Referenzen. Es kann niemand ausschließen, dass unsere Kopiermethode über eine Referenz auf ein Objekt trifft, das sie bereits einmal kopiert hat. Nur: Woher soll sie das wissen? Wenn Sie an dieser Stelle einfach weitermachen, haben Sie eine klare Endlosschleife vorliegen, unser Kopiervorgang wird zu keinem Ende kommen.
In manchen Anwendungen können Sie Annahme machen, dass es fachlich nicht notwendig ist, potenziell zyklische Beziehungen zu kopieren.
Nehmen Sie als Beispiel den Fall, dass ein Auftrag eine Liste von Positionen enthält, die jeweiligen Positionen wiederum eine Referenz auf den Auftrag. In diesem Fall ist es klar, dass das die erste Beziehung eine Kompositionsbeziehung ist. Ein Auftrag setzt sich aus den Positionen zusammen. Bei der Position wäre aber der referenzierte Auftrag nicht Bestandteil, so dass ein Kopieren nicht zulässig wäre.
Zyklen durch fehlerhafte Modellierung
Aber auch wenn Sie diese Annahme machen, so können zumindest durch fehlerhafte Modellierungen Zyklen auftreten. Sie würden diese Fehler aber nur sehr schwer finden können, da Ihr Programm sich in so einem Fall einfach nicht beendet. Um Zyklen erkennen zu können, müssen Sie beim Aufruf der Methode mitführen, welche Objekte Sie auf dem Weg zur aktuellen Aufrufstelle bereits kopiert haben. Dies können Sie zum Beispiel tun, indem Sie bei einem rekursiven Aufruf der Kopiermethode eine Liste mitgeben, in die das aktuelle Objekt mit aufgenommen wird. Mit der Methode clone(), die wir bereits diskutiert haben, ist das allerdings nicht möglich, da diese keine Parameter hat.
Betrachten wir nun ein einfaches Beispiel, bei dem Sie zyklische Verweise zwischen Objekten vorliegen haben. Nehmen Sie an, sie haben die Klassen A, B und C aus Abbildung 7.44 vorliegen, die sich gegenseitig referenzieren. Die Methode clone kopiert dabei jeweils das referenzierte Objekt mit.

Hier klicken, um das Bild zu vergrößern
In Listing 7.38 ist die Umsetzung der Operation für die drei Klassen aufgeführt.
class A implements Cloneable {
B b;
public A clone() throws CloneNotSupportedException {
A a = (A)super.clone();
b = b.clone();
return a;
}
}
class B implements Cloneable {
C c;
public B clone() throws CloneNotSupportedException {
B b = (B)super.clone();
c = c.clone();
return b;
}
}
class C implements Cloneable {
A a;
public C clone() throws CloneNotSupportedException {
C c = (C)super.clone();
a = a.clone();
return c;
}
}
Listing 7.38 Umsetzung der Operation clone für A, B und C
Die verschiedenen clone-Methoden rufen sich also wechselseitig auf, die Methode der Klasse A ruft zum Beispiel die Operation clone auf dem referenzierten Exemplar von B auf.
Endlosschleife
Wenn Sie nun versuchen, eine Kopie eines Exemplars von A zu erstellen, landen Sie in einer Endlosschleife:
C c = new C();
B b = new B();
A a = new A();
a.b = b;
b.c = c;
c.a = a;
A another = a.clone();
Mitführen von kopierten Objekten
Der Aufruf von a.clone wird nämlich auch b.clone aufrufen, dieses wiederum a.clone und so weiter. Der Aufruf wird also zu keinem Ende kommen. Diese Situation kann zum Beispiel durch eine Prüfung gegen eine mitgeführte Liste von Objekten korrigiert werden.
public A safeclone(LinkedList list)
throws CloneNotSupportedException {
if (list.contains(this)) return this;
LinkedList newlist = (LinkedList)list.clone();
newlist.add(this);
A a = (A)super.clone();
b = b.safeclone(newlist);
return a;
}
Listing 7.39 Prüfung auf zyklische Referenzen bei Kopien
In Listing 7.39 wird an die Methode safeclone() eine Liste übergeben, in der alle Objekte, die Sie gerade kopieren, enthalten sind. Wenn das aktuelle Objekt hier bereits auftaucht, was Sie über die Prüfung mit list.contains(this) feststellen, ist ein Kopieren nicht mehr notwendig, und Sie können direkt die Referenz zurückgeben. Im anderen Fall müssen Sie die Liste lokal um das aktuelle Objekt erweitern und übergeben diese dann in modifizierter Form an die weiteren Aufrufe der Kopiermethoden.11
Eine Aufgabenstellung, die neben dem Kopieren häufig auftaucht, ist das Sortieren von Objekten nach einem bestimmten Kriterium. Möglicherweise wollen Sie eine Liste von Objekten sortiert anzeigen oder einfach den Zugriff auf eine Sammlung von Objekten effizienter gestalten. Eine Suche in sortierten Sammlungen ist wesentlich effizienter als eine Suche in unsortierten Sammlungen. Ein anderer Grund kann es sein, eine Abarbeitungsreihenfolge für eine Sammlung von Objekten festzulegen.
Verantwortung für Vergleich
In der Praxis gibt es zwei gängige Möglichkeiten, um einen Vergleich bezüglich der Sortierung zweier Objekte durchzuführen:
| Sie können die Verantwortung für den Vergleich einem der beiden Objekte zuordnen. |
| Sie bringen ein weiteres Objekt ins Spiel bringen, das den Vergleich durchführt (einen Vergleicher oder Komparator12 ). |
Beide Varianten haben ihre Vor- und Nachteile.
Schauen wir uns dazu jeweils Beispiele in der Programmiersprache Java an.
Java
Wir haben dabei eine Klasse Kunde vorliegen, die eine Eigenschaft prio (für die Priorität des Kunden) und eine Eigenschaft name deklariert.

Hier klicken, um das Bild zu vergrößern
Dadurch, dass die Klasse die Schnittstelle Comparable implementiert, ist sie in vielen Kontexten einsetzbar, die eine Sortierung erfordern. In Listing 7.40 Umsetzung einer Vergleichsoperation für Kunden dargestellt.
class Kunde implements Comparable {
Prioritaet prio;
String name;
public int compareTo(Object obj) throws ClassCastException
{
Kunde andererKunde = (Kunde)obj;
if (prio.value < andererKunde.prio.value) return –1;
if (prio.value > andererKunde.prio.value) return 1;
return name.compareTo(andererKunde.name);
}
}
Listing 7.40 Umsetzung einer Vergleichsoperation für Kunden
Ein Vergleich unter Verwendung der Methode compareTo() wird Ihre Kunden nun nach Priorität sortieren. Nur wenn die Prioritäten gleich sind, wird weiter nach dem Namen sortiert. Sind auch die Namen gleich, dann erfolgt keine weitere Sortierung mehr.
Es ist meistens vernünftig, die Methode, die eine Sortierung unterstützt, so zu implementieren, dass sie 0 zurückgibt, wenn der Vergleich der zwei Objekte mit der Methode equals() den Wert true zurückgibt, also dann, wenn die Objekte vollständig gleich sind. In unserem Beispiel ist das gegeben, denn bei vollständiger Gleichheit wird die Methode compareTo() der Klasse String das Resultat 0 liefern, wenn sie für das Attribut name aufgerufen wird.
Nehmen Sie nun aber an, sie wollen Ihre Kunden in einer anderen Situation nicht nach Priorität, sondern einfach nach alphabetischer Reihenfolge des Nachnamens sortieren. Es gibt ja nicht nur die Vertriebssicht auf die Kunden.
Die bereits umgesetzte Methode compareTo() können Sie in diesem Fall nicht mehr verwenden.
Besser fahren Sie mit der Anwendung einer Vergleichsstrategie, einem Komparator-Objekt, das die Vergleiche zwischen zwei Objekten durchführt.13 Java zum Beispiel bietet uns dafür bereits die Schnittstelle Comparator an, die von einer Komparator-Klasse implementiert werden kann. Damit verlagern Sie die Verantwortung für den Vergleich vom zu vergleichenden Objekt auf einen eigenständigen Komparator.
Der Komparator benötigt allerdings Zugriff auf die für einen Vergleich relevanten Daten der beiden betroffenen Objekte. In Abbildung 7.46 ist eine Variante unseres Beispiels dargestellt, die einen Komparator verwendet, anstatt die Vergleichsoperation der Klasse Kunde zuzuordnen.

Hier klicken, um das Bild zu vergrößern
In der Abbildung ist neben der Umsetzung der compare-Methode auch eine Verwendung des Komparators angegeben. Die Klasse Collections bietet eine statische Methode an, die eine übergebene Liste mit Hilfe eines ebenfalls übergebenen Komparators sortiert. In der Übersicht sehen Sie auch, dass über einen Komparator nicht nur eine Sortierung über compare() vorgenommen werden kann, sondern dass auch die Prüfung auf Gleichheit über die Methode equals() an ihn delegiert werden kann.
Komparator ist flexibler.
Diese Modellierung ist nun wesentlich flexibler, weil Sie das Sortierkriterium austauschen können, ohne in die Klasse Kunde eingreifen zu müssen. Sie hat allerdings auch einen Nachteil: Sie müssen dem Komparator Zugriff auf die vergleichsrelevanten Daten der Klasse Kunde ermöglichen. Im Fall der Priorität ist das kein Problem, da diese wahrscheinlich ohnehin zur Schnittstelle eines Kunden-Objekts gehört. In anderen Fällen kann es aber notwendig sein, dass eigentlich interne Daten für den Komparator offen gelegt werden.
Komparator oder Vergleichsmethode
| Sortierung und Vergleich über Komparator |
|
Bei der Abwägung, ob Sie eine Vergleichsoperation einem Objekt selbst oder einem Komparator zuordnen, sollten Sie zunächst darauf achten, ob es eine klare vorgegebene Reihenfolge für die Sortierung gibt. Bei der Klasse Datum gibt es beispielsweise eine intuitiv gültige Sortierung, die Datumsobjekte in eine Reihenfolge bringt. In solchen Fällen ist es sinnvoll, die Operationen für Vergleich und Sortierung dem Objekt selbst zuzuordnen. Wenn es allerdings ein solches eindeutiges Kriterium nicht gibt, sollten Sie eine Komparator-Klasse verwenden. Dadurch können Sie auch später weitere Sortierkriterien hinzufügen, ohne die Klasse der zu sortierenden Objekte anpassen zu müssen. |
1 In der Sprache PHP werden die Elemente eines Arrays mit der Funktion each abgezählt. Der Zustand der Abzählung wird nicht in einem externen Iterator, sondern in dem Array selbst verwaltet. Daher kann ein Array in PHP nicht gleichzeitig von mehreren Benutzern abgezählt werden. Ein unschönes Element einer sehr nützlichen Programmiersprache.
2 Die Operationen next() und hasNext() sind nur eine Möglichkeit, wie man die Schnittstelle eines Iterators gestalten kann. Eine andere Möglichkeit wäre, auf die Methode hasNext() zu verzichten und am Ende einer Abzählung von next() den Wert NULL zurückgeben zu lassen oder eine Exception zu werfen. Wieder eine andere Möglichkeit wäre, dass die Methode next() nicht den nächsten Eintrag liefert, sondern true, wenn es einen nächsten Eintrag gibt, und false, wenn die Abzählung beendet ist. Auf den aktuellen Eintrag könnte man dann mit einer anderen Operation zugreifen. Auch wenn das Konzept eines Iterators ziemlich einfach ist, sind die Designmöglichkeiten vielfältig.
3 Iteratoren sind Bestandteile der Sprache Python ab der Version 2.2.
4 Wir haben die beschriebene Umsetzung eines Generators in Java hier nicht im Source-Code gelistet. Durch die mangelnde Unterstützung von Java für Generatoren ist die Lösung alles andere als elegant. Wer die Umsetzung trotz dieser Warnung sehen möchte, findet sie auf der Webseite zum Buch.
5 Diese Behauptung gilt nicht für unsere Implementierung in Java – es sei denn, Sie können den nebenläufigen Thread für die gesamte Restdauer der Anwendungslaufzeit tolerieren.
6 Wenn Sie einen unbegrenzten Generator in einer for–Schleife verwenden, denken Sie immer daran, dass Sie die Schleife durch ein break- oder ein return-Statement beenden sollten.
7 In C++ ist mit den so genannten Funktionszeigern ein Konstrukt verfügbar, mit dem eine vergleichbare Funktionalität wie mit Funktionsobjekten umgesetzt werden kann.
8 Anonyme Klassen und Methoden haben Sie bereits in Abschnitt 5.2.3, kennen gelernt.
9 Die Eigenschaft aktion ist vom Ruby-Typ Proc. Procs sind Objekte, die eine Routine referenzieren können, die erst durch den Aufruf der Operation call ausgeführt wird.
10 Siehe auch Abschnitt 5.2.3, Anonyme Klassen.
11 Die vorgestellte Methode soll nur das generelle Prinzip illustrieren. Durch das jeweils vorgenommene Kopieren der kompletten Liste mit bereits behandelten Objekten arbeitet diese Version nicht sonderlich effizient.
12 Vergleicher ist zwar auch ein schöner deutscher Begriff. Wir rechnen uns aber durch die Verwendung des Begriffs Komparator höhere Chancen dafür aus, dass wir die Filmrechte dieses Buchs nach Hollywood verkaufen können.
13 Dies ist ein weiterer Anwendungsfall für das Entwurfsmuster Strategie, das wir in Abschnitt 5.5.2, Entwurfsmuster Strategie statt dynamischer Klassifizierung, vorgestellt haben.
| << zurück |
|
||||||||||||||
|
||||||||||||||
|
||||||||||||||
|
||||||||||||||
Copyright © Galileo Press 2006
Für Ihren privaten Gebrauch dürfen Sie die Online-Version natürlich ausdrucken. 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.