5.5 Statische und dynamische Klassifizierung 

Bisher gingen wir davon aus, dass ein Objekt seine Klassenzugehörigkeit während seiner gesamten Existenz nicht ändert. Die Beziehung zwischen einem Objekt und der dazugehörigen Klasse war nicht änderbar – die Klassifizierung des Objekts war statisch.
Änderung der Klassenzugehörigkeit
Allerdings: Manchmal haben wir in der Praxis Szenarien, in denen sich die Klassenzugehörigkeit eines Objekts während seiner Existenz ändert. So kann aus einem Interessenten ein Kunde oder aus einem externen Berater ein interner Mitarbeiter werden. In solchen Fällen sprechen wir von einer dynamischen Klassifizierung.
Die dynamische Klassifizierung spielt meistens eine Rolle in den konzeptionellen Modellen, in der Programmierung kann man sie seltener sehen. Ein Grund dafür wird wohl die Tatsache sein, dass die klassenbasierten Programmiersprachen wie Java, C#, C++, Python oder Ruby sie nicht unterstützen. [In C++ wird die Klassenzugehörigkeit eines polymorphen Objekts durch den Pointer auf die Tabelle seiner virtuellen Methoden realisiert. Mit direkter Speichermanipulation ist es daher doch möglich, die Klassenzugehörigkeit eines Objekts in C++ dynamisch zu ändern. Es ist jedoch ein gewagtes Spiel mit dem Feuer, so etwas zu machen. Spiele mit dem Feuer sind aufregend und interessant, und wer sie mag, sollte über die Karriere eine Stuntmans oder eines Zirkusartisten statt eines Softwareentwicklers nachdenken. ]
5.5.1 Dynamische Änderung der Klassenzugehörigkeit 

Es gibt allerdings auch Sprachen, die eine Änderung der Klassenzugehörigkeit explizit unterstützen. Eine dieser Sprachen ist das Common Lisp Object System, CLOS. Wir werden deshalb zunächst einmal am Beispiel von CLOS vorstellen, wie wir eine solche Anpassung der Klassenzugehörigkeit vornehmen könnten, wenn die Sprache das unterstützt. Im Anschluss daran werden wir dann zeigen, wie wir einen entsprechenden Mechanismus durch die Verwendung von Strategien auch in den etwas gängigeren Sprachen wie Java umsetzen können.
In einer Sprache wie CLOS sind Mechanismen vorgesehen, die es erlauben, den Typ eines Objekts auf definierte Weise zur Laufzeit eines Programms zu verändern.
Wir machen aus einem A ein B.
Wenn wir Objekt x (ein Exemplar der Klasse A) zu einem Exemplar der Klasse B machen, werden dabei alle Attribute übernommen, die in A und in B vorkommen. Zusätzlich wird (sofern vorhanden) eine Methode update-instance-for-different-class aufgerufen, in der neu zu initialisierende Datenelemente, die in B definiert sind, aber von A nicht bereitgestellt werden, mit Werten belegt werden können. Der Rest ist einfach, da x nun ein Exemplar von B ist, werden auch die entsprechenden Aufrufe von Operationen den Methoden von B zugeordnet.
Geschäftspartner: Gäste, Interessenten und Kunden
Nehmen wir als Beispiel eine Anwendung, in der Geschäftspartner als Gäste, Interessenten oder Kunden klassifiziert werden. Dabei können diese vom Gast zum Interessenten und schließlich zum Kunden werden. Die entsprechende Hierarchie ist in Abbildung 5.70 dargestellt.
Abbildung 5.70 Hierarchie von Geschäftspartnern
Die Definition der Klassen sieht in CLOS dann wie unten stehend aus.
;; Definition der beteiligten Klassen
(defclass geschaeftspartner()
((name :initarg :name :accessor name)
(vorname :initarg :vorname :accessor vorname)
)
)
(defclass interessent (geschaeftspartner)
((erwarteter-umsatz :initarg :umsatz :accessor umsatz))
)
(defclass gast (geschaeftspartner)
((zeitbudget :initarg :zeitbudget :accessor zeitbudget))
)
(defclass kunde (geschaeftspartner)
((einstufung :initarg :einstufung :accessor einstufung))
)
Listing 5.33 Festlegung einer Klassenhierarchie in CLOS
Alle beteiligten Klassen haben unterschiedliche Implementierungen der Operation display. Da in den spezifischen Klassen auch weitere Attribute hinzukommen, ist die Darstellung eines Geschäftspartners in allen abgeleiteten Klassen unterschiedlich.
;; Display-Methoden für die spezifischen Klassen
(defmethod display ((gp geschaeftspartner))
(princ (string-concat "Geschäftspartner " (firstname gp) " " (name gp)))
)
(defmethod display((gp interessent))
(call-next-method) ;; ruft die Methode der Basisklasse
(princ (string-concat " ist ein Interessent mit erwartetem Umsatz " (umsatz gp)))
)
(defmethod display((gp gast))
(call-next-method) ;; ruft die Methode der Basisklasse
(princ (string-concat " ist ein Gast mit Zeitbudget " (zeitbudget gp)))
)
(defmethod display((gp kunde))
(call-next-method) ;; ruft die Methode der Basisklasse
(princ (string-concat " ist ein Kunde mit Einstufung " (einstufung gp)))
)
Listing 5.34 Unterschiedliche Darstellungen für Geschäftspartner
Gast wird zu einem Interessenten.
Nun wollen wir für Exemplare der jeweiligen Klassen deren Beschreibung ausgeben lassen. Dabei lassen wir einfach nebenbei einen Gast seine Klasse auf interessent wechseln. Dies geschieht in CLOS über die Methode change-class.
;; ...
(frieda (make-instance 'gast :vorname "Frieda"
:name "Müller" :zeitbudget "100 Minuten"))
(gerd (make-instance 'interessent :vorname "Gerd"
:name "Müller" :umsatz "200 Euro"))
(anne (make-instance 'kunde :vorname "Anne"
:name "Müller" :einstufung "mittel"))
)
;; ...
(display frieda)
(display gerd)
(display anne)
(change-class frieda 'interessent)
(display frieda)
;; ...
Wir erhalten die folgende Ausgabe vom Interpreter:
Geschäftspartner Frieda Müller ist ein Gast
mit Zeitbudget 100 Minuten.
Geschäftspartner Gerd Müller ist ein Interessent
mit erwartetem Umsatz 200 Euro.
Geschäftspartner Anne Müller ist ein Kunde
mit Einstufung mittel.
Geschäftspartner Frieda Müller
*** – SLOT-VALUE: The slot ERWARTETER-UMSATZ of #<INTERESSENT
#x19F192C1> has no value
Vorbelegen von neuen Attributen
Ups, wir haben Frieda Müller vom Gast zur Interessentin befördert, ohne ihr aber einen Wert für das benötigte Attribut erwarteter-umsatz zu geben. Das heißt, beim Ändern einer Klasse sind spezielle neue Initialisierungen für ein Objekt notwendig. CLOS sieht dafür die generische Funktion update-instance-for-different-class vor.
(defmethod update-instance-for-different-class ((gast gast)
(interessent interessent) &rest initargs)
(setf (umsatz interessent) "100 Euro")
)
Wenn wir die Methode wie oben aufgeführt überschreiben, wird diese beim Wechsel eines Objekts von der Klasse gast zur Klasse interessent aufgerufen. Unsere Ausgabe sieht danach wie unten stehend aus:
Geschäftspartner Frieda Müller ist ein Gast
mit Zeitbudget 100 Minuten.
Geschäftspartner Gerd Müller ist ein Interessent
mit erwartetem Umsatz 200 Euro.
Geschäftspartner Anne Müller ist ein Kunde
mit Einstufung mittel.
Geschäftspartner Frieda Müller ist ein Interessent
mit erwartetem Umsatz 100 Euro.
Wir können das tun. Sollen wir es auch?
Obwohl die Programmiersprache hier einfache und intuitive Mechanismen bereitstellt, um ein Objekt die Klasse wechseln zu lassen, kann ein derartiges Vorgehen die Komplexität von Programmen erhöhen. Auch wenn eine Programmiersprache ein solches Vorgehen zulässt: Besser ist meistens die Anwendung des Entwurfmusters Strategie. Dieses lässt sich auch in Sprachen anwenden, die keine dynamische Klassifizierung unterstützen.
Es besteht nämlich auch ein grundsätzliches konzeptionelles Problem bei der Änderung der Klassenzuordnung zur Laufzeit. Wie bei jeder anderen Änderung eines Objekts müssen wir nämlich auch bei der Änderung seiner Klassenzugehörigkeit dafür sorgen, dass das Objekt alle Versprechen, die es gegeben hat, weiterhin erfüllt. Das Prinzip der Ersetzbarkeit verlangt genau das.
Was bedeutet das? In den statisch typisierten Programmiersprachen bestimmt die Klassenzugehörigkeit den Typ des Objekts. Die Klassenzugehörigkeit bestimmt also, ob eine Variable das Objekt enthalten beziehungsweise referenzieren kann. Ändert sich die Klassenzugehörigkeit des Objekts, müssen wir dafür sorgen, dass nur solche Variablen das Objekt referenzieren, die auch mit seinem neuen Typ kompatibel sind.
Subtypmigration
Es gibt aber eine Möglichkeit, dies zu gewährleisten und trotzdem die gewünschte Änderung des Verhaltens zu erreichen. Sie müssen dafür sorgen, dass die Variablen als ihren Typ nur eine Oberklasse aller möglichen Klassen, zu denen das Objekt im Laufe seiner Existenz mutieren kann, haben können.
Die echte Klassenzugehörigkeit des Objekts darf also nicht sichtbar sein, nur seine Klassenzugehörigkeit zu einer Oberklasse darf bekannt sein. Und diese ändert sich nicht. Es ändert sich nur der von außen unsichtbare Subtyp des Objekts. Das Objekt ändert nicht seine Schnittstelle, es ändert nur die Implementierung.
Im folgenden Abschnitt werden wir sehen, wie wir genau dieses Verhalten durch die Anwendung des Entwurfsmusters »Strategie« erreichen.
5.5.2 Entwurfsmuster »Strategie« statt dynamischer Klassifizierung 

Wenn sich die nach außen sichtbare Klassenzugehörigkeit eines Objekts nicht ändert, kann man die Änderung der eigentlichen Klassenzugehörigkeit auch in einer Programmiersprache umsetzen, die keine dynamische Klassifizierung unterstützt.
Das Mittel der Wahl ist in diesem Fall das Entwurfsmuster »Strategie«.
![]() |
Wenn sich ein Teil des Verhaltens der Exemplare einer Klasse abhängig von ihrem Zustand verändern kann, kann man die verschiedenen Verhaltensweisen in separate Strategieklassen auslagern. Jedes Exemplar der Hauptklasse besitzt zu jedem Zeitpunkt ein Exemplar einer der Strategieklassen, auf das es die Implementierung seines Verhaltens delegiert. Ändert sich der Zustand des Objekts so, dass eine Veränderung der Verhaltens nötig wird, tauscht das Objekt sein Strategieobjekt aus. Durch die Anwendung dieses Musters entkoppelt man die verschiedenen Verhaltensvarianten der Exemplare der Hauptklasse, ohne auf die dynamische Klassifizierung zurückgreifen zu müssen. |
Greifen wir eine etwas modifizierte Variante unseres Beispiels aus Abbildung 5.70 wieder auf, und nehmen wir an, wir schreiben eine E-Commerce-Internetanwendung.
Die Besucher unserer Seite können sich registrieren und Ware bestellen. Der Inhalt der Seite wird für jeden Benutzer speziell aufbereitet. Benutzer, die noch nichts bestellt haben, bekommen andere Werbung und Aktionen präsentiert als die Bestandskunden. Die Neukunden können nur per Vorkasse bezahlen, den Premiumkunden wird eine Ratenzahlung angeboten. Die Zugehörigkeit jedes Benutzers zu diesen Kategorien kann sich mit der Zeit ändern.
Eine mögliche Realisierung ist eine Klasse User, die den aktuellen Status des Benutzers kennt und ihn in ihren Methoden auswertet. Listing 5.35 zeigt eine Java-Implementierung einer solchen Klasse.
public class User {
private enum Status {
PROSPECT, NEW_CUSTOMER, ORDINARY_CUSTOMER, VIP_CUSTOMER
}
private Status status;
public void displayAds() {
switch (status) {
case PROSPECT:
// Werbung für Interessenten
break;
case NEW_CUSTOMER:
// Werbung für Neukunden
break;
... // und so weiter
}
...
}
Listing 5.35 Problematische Lösung: Auswertung von Kundenstatus in Klasse »User«
Wir müssen davon ausgehen, dass solche switch-Befehle nicht nur in der Methode displayAds, sondern in sehr vielen anderen Methoden der Klasse User vorkommen. Das ist nicht besonders übersichtlich und führt zu großem Aufwand, wenn sich die Kategorisierung der Besucher ändert.
So könnten wir in der Zukunft bestimmte Kunden, die gern bestellen, aber ungern zahlen, einer neuen Kategorie zuordnen, die ein neues Verhalten der Internetseite bewirken sollte. Dies würde bedeuten, dass wir viele Methoden und viele switch-Befehle sichten und anpassen müssen.
In Abbildung 5.71 sehen Sie eine Möglichkeit, dieses Problem durch die Anwendung des Entwurfsmusters »Strategie« zu lösen.
Abbildung 5.71 Entwurfsmuster »Strategie« angewendet
Strategieklassen
Unsere Anwendung wird übersichtlicher, wenn wir das Verhalten der unterschiedlichen Benutzerkategorien in unterschiedlichen Klassen unterbringen – in Strategieklassen wie zum Beispiel ProspectStrategy und NewCustomerStrategy. In Abschnitt 5.4.2 haben wir das Konzept der Delegation beschrieben. Exemplare von Strategieklassen sind Objekte, an die der Aufruf von Operationen delegiert wird. Zusätzlich können wir diese Objekte zu definierten Zeitpunkten auswechseln, um darüber das Verhalten eines Objekts zu ändern.
Wenn Sie das Muster »Strategie« auf unser Beispiel anwenden, besitzt jedes Exemplar der Klasse User zu jedem Zeitpunkt genau ein Exemplar einer der Unterklassen von UserStrategy und delegiert die Aufrufe der statusspezifischen Operationen an das Strategieobjekt.
Ändert sich der Status des Benutzers, wird zum Beispiel aus einem Interessenten ein Neukunde, bekommt das User-Objekt ein neues Strategieobjekt. So ändert sich sein Verhalten, nach außen ändert sich der Typ des User-Objekts jedoch nicht.
Beispiel für Strategieklassen
In Listing 5.36 ist die Umsetzung dieses Konzepts für unser Beispiel in Java dargestellt.
public class Guest {
private abstract class Strategy {
// gibt die Werbung aus
public abstract void displayAds();
// bearbeitet eine Bestellung und gibt das neue
// Strategieobjekt des Besuchers zurück
public abstract Strategy makePurchase(...);
... andere Methoden der Strategieobjekte
}
// das aktuelle Strategieobjekt des Besuchers
private Strategy strategy;
private void displayAds() {
// delegiere den Aufruf an das Strategieobjekt
strategy.displayAds();
}
public void makePurchase() {
// delegiere den Aufruf und merke dir das neue
// Strategieobjekt
strategy = strategy.makePurchase();
}
private class ProspectStrategy extends Strategy {
@Override
public void displayAds() {
// Werbung für Interessenten anzeigen
}
@Override
public Strategy makePurchase() {
// die Erstbestellung bearbeiten ...
// Aus einem Interessenten wird jetzt ein Neukunde
return new NewCustomerStrategy(this);
}
...
}
private class NewCustomerStrategy extends Strategy {
public NewCustomerStrategy(ProspectStrategy strategy) {
// Übernehme eventuell die Informationen aus
// dem vorherigen Status
}
@Override
public void displayAds() {
// Werbung für Neukunden anzeigen
}
@Override
public Strategy makePurchase() {
// die weiteren Bestellungen eines Neukunden
// bearbeiten.
// Dies ändert den Status des Kunden nicht, erst der
// Zahlungseingang macht aus einem Neukunden einen
// gewöhnlichen Kunden
return this;
}
...
}
...
}
Listing 5.36 Umsetzung von Strategien für Kunden
Die jeweilige Strategie legt zum einen über die Umsetzung der Operation displayAds fest, welche Werbung der Kunde zu sehen bekommt. Zum anderen legt sie über die Umsetzung der Operation makePurchase auch fest, wie mit einer Bestellung umgegangen wird. Die Operation makePurchase kann auch dazu führen, dass ein Kunde vom Prospect zum Customer befördert wird. Deshalb gibt die Methode makePurchase der Klasse ProspectStrategy ein neues Strategie-Objekt zurück, das dann in Zukunft verwendet wird. ProspectStrategy löst sich damit praktisch selbst ab, sehr uneigennützig.
Das vorgestellte Entwurfsmuster ist besonders geeignet für alle Fälle, in denen Sie zur Laufzeit das Verhalten von Exemplaren einer Klasse ändern wollen.
Ihre Meinung
Wie hat Ihnen das Openbook gefallen? Wir freuen uns immer über Ihre Rückmeldung. Schreiben Sie uns gerne Ihr Feedback als E-Mail an kommunikation@rheinwerk-verlag.de.