Ersetzen der Bedingung durch Polymorphismus

Một phần của tài liệu AW refactoring improving the design of existing code (Trang 52 - 64)

Der erste Teil dieses Problems ist der switch-Befehl. Es ist eine schlechte Idee, auf- grund der Werte eines anderen Objekts zu verzweigen. Wenn Sie verzweigen müs- sen, dann sollten Sie dies nur auf Basis eigener Daten tun, nicht auf Basis fremder.

class Rental...

double getCharge() { double result = 0;

switch (getMovie().getPriceCode()) { case Movie.REGULAR:

result += 2;

if (getDaysRented() > 2)

result += (getDaysRented() – 2) * 1.5;

break;

case Movie.NEW_RELEASE:

result += getDaysRented() * 3;

break;

case Movie.CHILDRENS:

result += 1.5;

if (getDaysRented() > 3)

result += (getDaysRented() – 3) * 1.5;

break;

}

return result;

}

Hieraus folgt, dass getCharge in die Klasse Movie gehửrt:

class Movie...

double getCharge(int daysRented) { double result = 0;

switch (getPriceCode()) { case Movie.REGULAR:

result += 2;

if (daysRented > 2)

result += (daysRented – 2) * 1.5;

break;

case Movie.NEW_RELEASE:

result += daysRented * 3;

break;

case Movie.CHILDRENS:

result += 1.5;

if (daysRented > 3)

result += (daysRented – 3) * 1.5;

break;

}

return result;

}

Damit dies funktioniert, muss ich die Dauer der Ausleihe (daysRented), die natür- lich ein Attribut der Klasse Rental ist, als Parameter ỹbergeben. Tatsọchlich be- nutzt die Methode zwei Datenelemente: die Dauer der Ausleihe und die Art des Films. Warum ziehe ich es vor, die Dauer der Ausleihe an Rental zu übergeben und nicht die Art des Films? Das liegt daran, dass die vorgeschlagenen Änderun- gen alle mit der Einführung neuer Arten von Filmen zu tun haben. Informationen ỹber Arten von etwas sind anfọlliger fỹr Änderungen. Ändert sich die Art eines Films, so mửchte ich den Dominoeffekt minimieren. Deshalb ziehe ich es vor, den Betrag in der Klasse Movie zu berechnen.

Ich habe Movie mit der neuen Methode umgewandelt und getCharge in der Klasse Rental geọndert, so dass sie die neue Methode verwendet (siehe auch Abbildung 1-12 und Abbildung 1-13):

class Rental...

double getCharge() {

return _movie.getCharge(_daysRented);

}

Nachdem ich die getCharge Methode verschoben habe, mache ich das Gleiche mit der Berechnung der Bonuspunkte (getFrequentRenterPoints). So bleiben beide Berechnungen, die von der Art des Films abhọngen, zusammen in der Klasse, die die Art als Attribut enthọlt.

class Rental...

int getFrequentRenterPoints() {

if ((getMovie().getPriceCode() == Movie.NEW_RELEASE) && getDaysRented() >

1)

return 2;

else

return 1;

}

Class Rental...

int getFrequentRenterPoints () {

return _movie.getFrequentRenterPoints(_daysRented);

}

class movie...

int getFrequentRenterPoints(int daysRented) {

if ((getPriceCode() == Movie.NEW_RELEASE) && daysRented > 1) return 2;

else

return 1;

}

Abbildung 1-12 Klassendiagramm vor dem Verschieben der Methoden nach Movie

1

statement() htmlStatement() getTotalCharge()

getTotalFrequentRenterPoints() Customer

getCharge()

getFrequentRenterPoints() daysRented: int

Rental

priceCode: int Movie

1.4.1 Zu guter Letzt ... Vererbung

Wir haben hier verschiedene Arten von Filmen, die die gleiche Frage verschieden beantworten. Das hửrt sich nach einer Aufgabe fỹr Unterklassen an. Wir kửnnen drei Unterklassen von Movie bilden, von denen jede ihre eigene Version von getCharge haben kann (siehe Abbildung 1-14).

Dies ermửglicht es mir, den switch-Befehl durch Polymorphismus zu ersetzen.

Leider hat dies einen kleinen Fehler: Es funktioniert nicht. Ein Film (ein Objekt der Klasse Movie) kann seine Klassifizierung wọhrend seines Lebens ọndern. Ein Objekt kann seine Klasse aber wọhrend seines Lebens nicht ọndern. Hierfỹr gibt es aber eine Lửsung, nọmlich das Zustandsmuster (state pattern) [Gang of Four]. Mit diesem Zustandsmuster sehen die Klassen aus wie in Abbildung 1-15.

Abbildung 1-13 Klassendiagramm nach dem Verschieben der Methoden nach Movie

Abbildung 1-14 Einsatz von Vererbung bei der Klasse Movie

1

statement() htmlStatement() getTotalCharge()

getTotalFrequentRenterPoints() Customer

getCharge()

getFrequentRenterPoints() daysRented: int

Rental

getCharge(days: int)

getFrequentRenterPoints(days: int) priceCode: int

Movie

getCharge Movie

getCharge Regular Movie

getCharge Childrens Movie

getCharge New Release Movie

Durch die zusọtzliche Indirektionsebene kann ich nun die Klasse Price spezialisie- ren und den Preis ọndern, wann immer dies notwendig ist.

Wenn Sie mit den Entwurfsmustern der Viererbande vertraut sind, so werden Sie sich fragen: ằIst dies ein Zustand oder eine Strategie?ô Reprọsentiert die Klasse Price einen Algorithmus für die Preisberechnung (dann würde ich sie Pricer oder PricingStrategy nennen) oder reprọsentiert sie einen Zustand des Films (Star Trek X ist eine Neuerscheinung). Zu diesem Zeitpunkt spiegelt die Wahl des Musters (und des Namens) wider, wie Sie sich die Struktur vorstellen. Zur Zeit stelle ich mir dies als einen Zustand des Films vor. Wenn ich spọter entscheide, dass eine Strategie meine Absichten besser vermittelt, werde ich refaktorisieren, indem ich den Namen ọndere.

Um das Zustandsmuster einzuführen, verwende ich drei Refaktorisierungen. Zu- erst verschiebe ich mittels Typenschlüssel durch Zustand/Strategie ersetzen (231) das artabhọngige Verhalten in das Zustandsmuster. Dann verwende ich Methode ver- schieben (139), um den switch-Befehl in die Klasse Price zu verschieben. Zum Schluss verwende ich Bedingten Audruck durch Polymorphismus ersetzen, (259), um den switch-Befehl zu entfernen.

Ich beginne mit Typenschlüssel durch Zustand/Strategie ersetzen (231). Der erste Schritt besteht darin Eigenes Feld kapseln (171) zu verwenden, um sicherzustellen, dass alle Verwendungen durch get- und set-Methoden erfolgen. Da der grửòte Teil des Codes aus anderen Klassen stammt, verwenden die meisten Methoden bereits get-Methoden. Allerdings müssen die Konstruktoren auf den Preisschlüssel (priceCode) zugreifen.

Abbildung 1-15 Einsatz des Zustandsmuster (State pattern) bei der Klasse Movie getCharge

Price

getCharge Regular Price

getCharge Childrens Price

getCharge New Release Price getCharge

Movie

1

return price.getCharge

class Movie...

public Movie(String name, int priceCode) { _name = name;

_priceCode = priceCode;

}

Hier kann ich die set-Methode verwenden.

class Movie

public Movie(String name, int priceCode) { _name = name;

setPriceCode(priceCode);

}

Ich wandle den Code wieder um und stelle sicher, dass ich nichts kaputtgemacht habe. Nun füge ich die neuen Klassen hinzu. Ich platziere die Art des Films im Price-Objekt. Ich mache dies mittels einer abstrakten Methode und konkreten Methoden in den Unterklassen:

abstract class Price {

abstract int getPriceCode();

}

class ChildrensPrice extends Price { int getPriceCode() {

return Movie.CHILDRENS;

} }

class NewReleasePrice extends Price { int getPriceCode() {

return Movie.NEW_RELEASE;

} }

class RegularPrice extends Price { int getPriceCode() {

return Movie.REGULAR;

} }

Nun kann ich die neuen Klassen umwandeln.

Als Nọchstes muss ich die Zugriffsmethode der Klasse Movie so ọndern, dass sie den Preisschlüssel aus der neuen Klasse nutzt:

public int getPriceCode() { return _priceCode;

}

public setPriceCode (int arg) { _priceCode = arg;

}

private int _priceCode;

Dazu müssen der Preisschlüssel durch ein Preisfeld ersetzt und die Zugriffsmetho- den angepasst werden:

class Movie...

public int getPriceCode() { return _price.getPriceCode();

}

public void setPriceCode(int arg) { switch (arg) {

case REGULAR:

_price = new RegularPrice();

break;

case CHILDRENS:

_price = new ChildrensPrice();

break;

case NEW_RELEASE:

_price = new NewReleasePrice();

break;

default:

throw new IllegalArgumentException("Incorrect Price Code");

} }

private Price _price;

Ich kann den Code nun wieder umwandeln und ihn testen und stelle fest, dass die komplexeren Methoden nicht bemerkt haben, dass die Welt sich verọndert hat.

Nun wende ich Methode verschieben (139) auf getCharge an:

class Movie...

double getCharge(int daysRented) { double result = 0;

switch (getPriceCode()) { case Movie.REGULAR:

result += 2;

if (daysRented > 2)

result += (daysRented – 2) * 1.5;

break;

case Movie.NEW_RELEASE:

result += daysRented * 3;

break;

case Movie.CHILDRENS:

result += 1.5;

if (daysRented > 3)

result += (daysRented – 3) * 1.5;

break;

}

return result;

}

Das Verschieben geht ganz einfach:

class Movie...

double getCharge(int daysRented) { return _price.getCharge(daysRented);

}

class Price...

double getCharge(int daysRented) { double result = 0;

switch (getPriceCode()) { case Movie.REGULAR:

result += 2;

if (daysRented > 2)

result += (daysRented – 2) * 1.5;

break;

case Movie.NEW_RELEASE:

result += daysRented * 3;

break;

case Movie.CHILDRENS:

result += 1.5;

if (daysRented > 3)

result += (daysRented – 3) * 1.5;

break;

}

return result;

}

Nach dem Verschieben kann ich damit beginnen, Bedingten Ausdruck durch Poly- morphismus ersetzen (259) anzuwenden:

class Price...

double getCharge(int daysRented) { double result = 0;

switch (getPriceCode()) {

case Movie.REGULAR:

result += 2;

if (daysRented > 2)

result += (daysRented – 2) * 1.5;

break;

case Movie.NEW_RELEASE:

result += daysRented * 3;

break;

case Movie.CHILDRENS:

result += 1.5;

if (daysRented > 3)

result += (daysRented – 3) * 1.5;

break;

}

return result;

}

Ich mache dies, indem ich für jeweils einen Zweig des switch-Befehls eine über- schreibende Methode erstelle. Ich beginne mit RegularPrice:

class RegularPrice...

double getCharge(int daysRented){

double result = 2;

if (daysRented > 2)

result += (daysRented – 2) * 1.5;

return result;

}

Dies überschreibt den switch-Befehl der Oberklasse, den ich lasse, wie er ist. Ich wandle den Code um und teste ihn fỹr diesen Fall, dann nehme ich den nọchsten Zweig, wandle um und teste. (Um sicherzustellen, dass ich den Code der Unter- klasse verwende, baue ich gern extra einen Fehler ein und sehe zu, dass der Test schief geht. Ich bin übrigens nicht paranoid oder anderweitig verrückt.)

class ChildrensPrice

double getCharge(int daysRented){

double result = 1.5;

if (daysRented > 3)

result += (daysRented – 3) * 1.5;

return result;

}

class NewReleasePrice...

double getCharge(int daysRented){

return daysRented * 3;

}

Nachdem ich mit allen Zweigen fertig bin, deklariere ich die Methode Price.getCharge als abstrakt:

class Price...

abstract double getCharge(int daysRented);

Nun kann ich das ganze Verfahren auf getFrequentRenterPoints anwenden:

class Movie...

int getFrequentRenterPoints(int daysRented) {

if ((getPriceCode() == Movie.NEW_RELEASE) && daysRented > 1) return 2;

else

return 1;

}

Zunọchst verschiebe ich die Methode in die Klasse Price: Class Movie...

int getFrequentRenterPoints(int daysRented) {

return _price.getFrequentRenterPoints(daysRented);

}

Class Price...

int getFrequentRenterPoints(int daysRented) {

if ((getPriceCode() == Movie.NEW_RELEASE) && daysRented > 1) return 2;

else

return 1;

}

In diesem Fall mache ich die Methode der Oberklasse aber nicht abstrakt. Statt- dessen erstelle ich eine überschreibende Methode für Neuerscheinungen und lasse die definierte Methode (als Default) in der Oberklasse:

Class NewReleasePrice

int getFrequentRenterPoints(int daysRented) { return (daysRented > 1) ? 2: 1;

}

Class Price...

int getFrequentRenterPoints(int daysRented){

return 1;

}

Die Einführung des Zustandsmusters war aufwendig. Hat sie sich gelohnt? Der Gewinn besteht darin, dass ich, wenn sich irgendein Verhalten von Price ọndert – neue Preise oder preisabhọngiges Verhalten hinzukommen – die Änderungen viel einfacher vornehmen kann. Der Rest der Anwendung weiò nichts vom Einsatz des Zustandsmusters. Für das bisschen Verhalten, was ich bisher habe, ist das kein groòer Aufwand. In komplexeren Systemen mit dutzenden von preisabhọngigen Methoden macht es aber viel aus. Jede Änderung war ein kleiner Schritt. Diese Vorgehensweise mag Ihnen langsam erscheinen, aber ich musste nie den Debug- ger ửffnen und von daher ging es tatsọchlich flott voran. Es dauerte viel lọnger, dieses Kapitel des Buchs zu schreiben, als den Code zu ọndern.

Ich habe nun die zweite wesentliche Refaktorisierung abgeschlossen. Es ist jetzt viel einfacher, die Klassifikationsstruktur von Filmen und die Regeln für die Abrechnung und die Bonuspunkte zu verọndern. Abbildung 1-16 und Abbildung 1-17 zeigen, wie das Zustandsmuster mit der Preisinformation arbeitet.

Abbildung 1-16 Interaktionen unter Verwendung des Zustandsmusters

aCustomer aRental aMovie

* [for all rentals] getCharge getTotalCharge

getCharge (days) statement

* [for all rentals] getFrequentRenterPoints

getFrequentRenterPoints (days) getTotalFrequentRenterPoints

aPrice

getCharge (days)

getFrequentRenterPoints (days)

Abbildung 1-17 Klassendiagramm nach Ergọnzung des Zustandsmusters statement() htmlStatement() getTotalCharge()

getTotalFrequentRenterPoints() name: String

Customer

getCharge()

getFrequentRenterPoints() daysRented: int

Rental getCharge(days: int)

getFrequentRenterPoints(days:int) title: String

Movie

1

getCharge(days:int)

getFrequentRenterPoints (days: int) Price

1

getCharge(days:int) ChildrensPrice

getCharge(days:int) RegularPrice

getCharge(days:int)

getFrequentRenterPoints (days: int) NewReleasePrice

Một phần của tài liệu AW refactoring improving the design of existing code (Trang 52 - 64)

Tải bản đầy đủ (PDF)

(468 trang)