Machen Sie sich keine Sorgen, wenn Sie dieses Buch nicht gleich beim ersten Lesen vollständig verstehen. Wir haben beim ersten Schreiben auch nicht alles verstanden! — [GOF], xiii
Stell’ Dir einen Versandhandel vor. Von einem Kunden geht eine Bestellung ein, die lieferbaren Posten werden ‘rausgesucht, eingepackt und verschickt. Wie wird das Porto berechnet? Dazu gibt es eine Vereinbarung, die sinngemäß diesen Inhalt hat:
- Nachnahme-Sendungen kosten 4,70 Euro zusätzlich. Hier werden keine Ausnahmen gemacht.
- Bei Lieferungen mit einem Wert über 49,99 Euro wird kein Porto berechnet. Ausnahme sind Eintrittskarten, s.u.
- Eintrittskarten werden immer versichert versendet. Die Versicherung kostet 4 Euro, unabhängig von der Anzahl der Karten.
- Der Wert der Eintrittskarten in einer Bestellung trägt nicht zum Wert bei, aufgrund dessen das Porto berechnet wird.
Meine Aufgabe: Programmiere das, so dass erstens der Kunde abfragen kann, welche Kosten auf ihn zukommen und zweitens der Packer sich keine Gedanken machen muss.
Der einfache Teil der Analyse
- Es gibt Waren. Regel “Substantive werden zu Klassen”. Also erschaffe ich eine Klasse “Ware”. Eine Ware hat Name und Preis – den Rest blende ich für dieses Beispiel aus.
- Eine Eintrittskarte ist eine Ware, verhält sich aber ein bisschen anders. Also erschaffe ich eine Klasse “Eintrittskarte”, die von “Ware” erbt.
Damit ist der einfache Teil zuende. Jetzt kommt der philosophische.
Diskussion
Ist es gut, eine Methode “holeVersicherungskosten” für die Klasse “Ware” zu erschaffen? Vorteil, dass ich für eine Ware den Wert 0 zurückgeben kann – einfach und deswegen gut. Und für eine Eintrittskarte überlagere ich die Methode und gebe was zurück? Den Wert 4? Nein, nicht immer, denn nicht jede Eintrittskarte kostet 4 Euro Versicherung, sondern alle Eintrittskarten zusammen. Die eine Eintrittskarte weiss aber nichts von anderen ihrer Art. Der Aufrufer könnte sich merken, ob er schon mal ein Eintrittskarte “in der Hand” hatte, also bei jedem Aufruf das jeweilige Objekt nach seiner Klasse fragen – das ist sehr unelegant. Und ja: Eleganz ist ein wichtiges Kriterium für guten Code. Und ganz sicher will mein Chef guten Code. Wenn er das nicht will, dann wird er nicht lange mein Chef bleiben – und ausserdem ist das hier aysx. Hier wird guter Code gemacht. Zurück zum Thema: Ich komme so nicht weiter. Weil “holeVersicherungskosten” nicht sinnvoll ist für die Klasse “Eintrittskarte” und ich “Ware” und “Eintrittskarte” austauschen können möchte, verbietet es sich die Methode “holeVersicherungskosten” für die Klasse “Ware”. Dieser gerade-heraus-Ansatz ist für die Eintrittskarten-Versicherung also eine Sackgasse. Na, mache ich halt an der anderen Stelle weiter.
Ist es gut, eine Methode “holeLieferungswertPortoGrenzenBeitrag (n)” für die Klasse “Ware” zu erschaffen? Das scheint vielversprechend: Eine Ware gäbe dann ihren Preis multipliziert mit n zurück und eine Eintrittskarte gäbe immer der Wert 0 zurück. Selbst wenn der Auftraggeber auf die Idee kommt, eine neue Kategorie von Ware einzuführen, die nicht in das bestehende Schema passt, bin ich fein ‘raus: Die neue Klasse erbt von “Ware” und implementiert eine eigene Methode “holeLieferungswertPortoGrenzenBeitrag (n)”. Bin ich jetzt wirklich fein raus? Nein. Warum? Weil der Auftraggeber auf die Idee kommen wird, dass für bestimmte Waren neue Regeln gelten. Und wenn diese neuen Regeln dazu führen, dass ich neue Methoden einführen muss, dann habe ich eine Menge Arbeit. Das geht mir gegen den Strich. Nur ein fauler Programmierer ist ein guter Programmierer – Du darfst mich gerne zitieren. Zurück zum Thema: die Idee mit “holeLieferungswertPortoGrenzenBeitrag (n)” ist eine Sackgasse.
Wie wäre es, wenn es ein Dingsbumms gäbe, das die Berechnung der Versicherungskosten kennt? Na gut, aber woher soll das Dingsbumms wissen, welche Berechnung auszuführen ist? Nun, die Ware oder Eintrittskarte – je nach dem, was gerade da ist – “sagt” es dem Dingsbumms. Das Dingsbumms wird sich gefälligst merken, ob es schon einmal von einer Eintrittskarte aufgerufen wurde, damit die Versicherung nur einmal berechnet wird. Und ausserdem weiss es, dass “normale” Waren diesbezüglich keine Kosten erzeugen.
Wie wäre es, wenn es zusätzlich ein Dingsda gäbe, das die Berechnung des Portofreigrenzen-Beitrags übernimmt und das auch von dem jeweiligen Objekt gesagt bekommt, was zu tun ist? Also so ähnlich wie ein Dingsbumms, aber bei den Berechnungen anders. Ich führe jetzt eine Abkürzung ein: Dingsda und Dingsbumms sind BerechnungsDings.
Mit BerechnungsDings werde ich den neuen Anforderungen gerecht. Was passiert, wenn der Auftraggeber eine neue Warenkategorie wünscht? Nicht viel! Ware und Eintrittskarte können bleiben, wie sie sind. Vielleicht müssen Dingsbumms und Dingsda verändert werden – abhängig davon, welche Regeln für die neue Warenkategorie gelten. Und was muss passieren, um eine neue Regel einzuführen? Auch nicht viel! Ich muss ein neues BerechnungsDings erschaffen, vielleicht ein NurMitKreditkarteBezahlbarDings.
Heureka!
In der Software-Entwicklung nennt man das BerechnungsDings “Visitor”, Besucher. Der Visitor hat etwas, das er besucht – hier Ware und Eintrittskarte. Damit der Visitor bei den beiden vorbeischauen kann, stellen Ware und Eintrittskarte je eine Methode zur Verfügung, die meistens “accept” benannt wird. Die Methode hat einen Parameter – den Visitor selbst. Damit der Visitor auf unterschiedliche Objekttypen unterschiedlich reagieren kann, stellt er für jeden Objekttyp eine Methode bereit, deren Name mit “visit” beginnt und mit dem Objekttypen-Namen endet – hier also “visitWare” und “visitEintrittskarte”. Die Visit-Methoden haben einen Parameter, nämlich das zu besuchende Objekt. Ablauf: Die Ware “akzeptiert” den Besucher. Sie ruft bei dem Besucher die für ihre Klasse passende Methode auf und gibt ihre Daten mit. Darauf hin wird der Besucher tätig, hier wird er beispielsweise den Preis abfragen oder ganz untätig bleiben – je nach dem, welche Regel er abbildet.
technische Umsetzung
Ein Interface “WarenVisitor” definiert, was so ein Visitor kann – Ware und Eintrittskarte besuchen. Im Gegenzug akzeptieren Ware und Eintrittskarte jeweils einen WarenVisitor und rufen die passende Methode auf. Vom WarenVisitor gibt es zwei Implementierungen, einen für die PortoFreibetrags-Berechnung, einen für die Versicherungs-Kosten. Das Klassendiagramm zeigt die Zusammenfassung. Die Implementierung ist trivial.
Entia non sunt multiplicanda sine necessitate
Mehr Klassen – ist das der richtige Weg? Hier ist er es, weil ich hier beides bekomme: Flexibilität und Stabilität. Der Visitor-Weg erlaubt, beliebiges Verhalten hinzuzufügen, ohne bestehendes Verhalten zu verändern. Die “Geschäftsklassen” Ware und Eintritsskarte bleiben unverändert und ich kann ihnen “hinter den Kulissen” neues Verhalten unterjubeln.
Literatur
[GOF] Gamma, E./ Helm, R./ Johnson, R./ Vlissides, J.: Entwurfsmuster. Elemente wiederverwendbarer objektorientierter Software. 5., korrigierter Nachdruck, Bonn 2003. (GOF steht für “Gang-Of-Four”. Das Buch ist ein Quell steter Freude. Im Zweifel: kaufen.)