Wie ich schon mehrfach erläutert habe, sehe ich Softwareentwicklung als das ständige Ausbalancieren von Prinzipien oder „Daumenregeln“. Es gibt eine ganze Menge solcher Daumenregeln (ich hab mal an die hundert solcher gesammelt) und ich hab vor, diese nach und nach hier mal vorzustellen.
Einige dieser Regeln ergänzen sich, manche sind spezieller als andere oder einfach nur eine andere Sichtweise auf eigentlich das selbe Prinzip. Viele aber widersprechen sich auch. Ein typisches Beispiel wären folgende beiden Prinzipien:
- mache deine Software generisch, damit sie auch leicht mit geänderten Anforderungen klar kommt; kurz: allgemeingültig ist besser als speziell
- mache deine Software einfach, damit sie leicht verständlich ist (KISS); kurz: einfach ist besser als kompliziert
Es wird wohl kaum jemand in Abrede stellen, dass beides wichtige Prinzipien der Softwareentwicklung sind. Wir alle wollen am liebsten einfachen Code der trotzdem sehr viel kann. In der Regel werden wir so etwas aber nicht haben können. Je generischer man versucht zu programmieren, desto komplexer wird der Code. Letztendlich streben beide Prinzipien also in unterschiedliche Richtungen. Es gilt dabei immer einen Mittelweg zu finden.
Man kann diese Daumenregeln auch als „Kräfte“ bezeichnen. Dann wäre das, was wir suchen, eine Art Kräftegleichgewicht. Die Kräfte können unterschiedlich stark sein und so liegt das Gleichgewicht nicht immer in der Mitte. Außerdem gibt es meist mehr als zwei Kräfte, die es zu betrachten gilt.
Oft sind die Gewichtungen aber auch subjektiv. Manche Entwickler werden Einfachheit wichtiger finden, andere finden Generizität wichtiger. Es gibt dann mehrere „richtige“ Lösungen. Aber alle werden darüber übereinstimmen, dass eine Lösung, die beide Prinzipien missachtet, eine schlechte ist. Komplizierte Software, die nur einen ganz bestimmten Spezialfall abdeckt und sich nicht auf andere Probleme anpassen lässt, wollen wir in der Regel nicht haben [1]. Wir suchen also eine Art Pareto-Optimum. Welche pareto-optimale Lösung jetzt für ein konkretes Problem gewählt werden sollte, hängt von vielen Faktoren ab. Zum einen von den konkreten Anforderungen für die zu entwickelnde Software, zum anderen aber auch projektabhängige Einflüsse wie Vorlieben, Kenntnisse und Fähigkeiten der Teammitglieder und sogar der Teamorganisation.
Dieses Ausbalancieren bezeichne ich gerne als „Tradeoff Game“. Leider gibt es im Deutschen kein adäquates Wort dafür. Im Englischen hört man immer „tradeoff“ im Deutschen muss man sich irgendwie mit „Ausbalancieren“ behelfen.
Bei meinem Vortrag auf den Delphi-Tagen letztes Jahr hab ich das auch schonmal anklingen lassen. Netzreporter hat mir vor ein paar Tagen dazu eine sehr gute Frage gestellt:
In meinem Vortrag bringe ich ein Beispiel um das Prinzip „Tell, don’t ask“ zu erläutern. Folgendes ist dazu in meinen Folien zu finden:
Prozedural
1
2
3
4
5
6 if not book.IsLent then
begin
book.IsLent := true;
book.LentTo := user;
book.DueDate := IncDays(Now, 14);
end;Nicht ganz objektorientiert
1
2
3
4 if not book.IsLent then
begin
book.LendTo(user, IncDays(Now, 14));
end;Objektorientiert
1 book.LendTo(user, Now); // alles Weitere in LendTo()
- Das Buch weiß selbst, wann es zurückgegeben werden muss
- Das Buch fängt selbst den Fall, dass es bereits ausgeliehen ist, ab ==> Wirft ggf. Exception
Und die Frage von Netzreporter:
Book.LendTo(User, now) setzt voraus, dass Book weiß, was „User“ und „now“ ist, also Personen und Datum kennt. Da ich eher „realitätsnah“ Objekte kreiere, würde das bei mir nicht zusammenpassen. Mir ist kein Buch bekannt, dass seinen Benutzer kennt und vom heutigen Datum weiß. Ich würde eher denken, es gibt ein Objekt Book, ein Objekt User und ein Objekt Library, die beides miteinander verbindet. Ein Buch kann in die Liste der Library aufgenommen werden und auch nur dann ausgeliehen werden.
Und das ist eine sehr gute Frage (Vielen Dank an dieser Stelle nochmal dafür!). Sie trifft im Kern genau das, was ich unter dem oben erwähnten Tradeoff Game verstehe. Das wird leider aus meinen Folien nicht so ganz deutlich. Deshalb hier nochmal eine ausführlichere Betrachtung des Beispiels:
In diesem konkreten Fall könnte man folgende Kräfte, Prinzipien oder Daumenregeln ausmachen (je nach Sichtweise kann man auch andere finden):
a) Objekte sollten die Realität (die Problemdomäne) möglichst genau nachbilden
b) Code sollte sich so lesen, wie ein englischer Text
c) Methodenaufrufe sollten als Nachrichtenaustausch verstanden werden
d) Objekte sollten eigenes Verhalten haben und keine reine Datensammlung sein
e) Methoden sollten mehr tun als nur delegieren, sie sollten eine sinnvolle Aufgabe haben
f) Es sollte nicht unnötig viele Klassen und Methoden geben
g) Methoden sollten möglichst kurz sein
h) Für jede Information und Entscheidung gibt es eine Klasse, die dafür Experte ist (GRASP-Prinzip Information Expert). Dort und nur dort sollten die Entscheidungen getroffen werden.
Im Einzelnen würden diese Daumenregeln folgendermaßen auf das Problem angewendet:
a) Objekte sollten die Realität (die Problemdomäne) möglichst genau nachbilden
Das ist in etwa die ursprüngliche Idee der Objektorientierung. Manchmal funktioniert das ganz gut. Ich betreue momentan das Softwareentwicklungsprojekt hier an der Uni und da dürfen „meine Studenten“ ein Netzwerk-Skat-Spiel programmieren. Dort kann man das sehr schön machen. Es gibt Spieler und Karten und Stiche und eine Spielliste, etc. Bei anderen Problemen funktioniert das nicht ganz so gut. Siehe z.B. mein Beispiel zu Pipe and Filter.
Das würde hier eher für so etwas sprechen:
1 | library.lendBook(myBook, user, now); |
b) Code sollte sich so lesen, wie ein englischer Text
Das geht durch eine geschickte Wahl der Bezeichner oft recht gut. Problematisch wirds dann wenn nach englischer Grammatik das Objekt mitten in den Bezeichner reingehört. Eigentlich müsste es ja heißen „Library, lend myBook to the user now!“ Also „myBook“ gehört eigentlich mitten rein. Smalltalk erlaubt so eine Syntax direkt. Delphi und viele andere Sprachen nicht. Hier könnte man sich mit Method Chaining behelfen:
1 | library.lend(book).to(user).now; |
Das ist aber aufwändiger zu programmieren.
c) Methodenaufrufe sollten als Nachrichtenaustausch verstanden werden
Bei oben erwähntem Skatprogramm sieht man das sehr deutlich. Man kann hier sowas machen wie nextPlayer().itsYourTurn()
oder player.doYouWantToParticipate()
. Diese Bezeichner tauchen tatsächlich in der Musterlösung auf, die ich gerade schreibe. Das macht das sehr anschaulich.
d) Objekte sollten eigenes Verhalten haben und keine reine Datensammlung sein
Das ist das, was ich mit dem Beispiel aus dem Vortrag sagen wollte. Das wäre hiermit erreicht:
1 | Book.LendTo(User, now); |
Das ist das „Tell, don’t ask“-Prinzip. Wir sagen dem Objekt „mach mal“, anstatt über den Kopf des Objekts hinweg zu entscheiden. Man das auch das „Do-it-myself-Prinzip“ nennen. Ich stell mir da immer kleine Kinder vor, die sagen „Das kann ich aleiiine!“ Mit dem Unterschied, dass die Objekte das wirklich können sollten.
e) Methoden sollten mehr tun als nur delegieren, sie sollten eine sinnvolle Aufgabe haben
Bei obigem Skatprogramm sieht man recht deutlich, dass es es recht viele Methoden gibt, die technisch gesehen unnötig sind. Sie haben schöne sprechende Bezeichner, tun aber nichts anderes als die Aufgabe an andere Objekte zu delegieren ohne einen technischen Mehrwert zu erzielen. Dafür ist die Vorgehensweise aber recht nah an der Problemdomäne.
f) Es sollte nicht unnötig viele Klassen und Methoden geben
Ähnlich wie e). zu viele Klassen machen es schwer den Überblick zu behalten.
g) Methoden sollten möglichst kurz sein
Konträr zu f). Es gibt Entwickler, die es stolz darauf sind, nur Methoden < 10 Zeilen Code zu schreiben. Dafür brauchen sie aber umso mehr.
h) Für jede Information und Entscheidung gibt es eine Klasse, die dafür Experte ist (GRASP Prinzip Information Expert). Dort und nur dort sollten die Enstcheidungen getroffen werden.
Hier wäre der Information Expert wohl die Library, weil diese logisch gesehen festlegt, wie lange ein Buch ausgeliehen wird.
Man sieht: Es gibt hier mehrere Sichtweisen und verschiedene Daumenregeln, die einander widersprechen bzw. miteinander im Wettbewerb stehen. Ich persönlich würde in einem konkreten Projekt, das eine Software für Bibliotheken zu Ziel hat, vermutlich library.lendBook(myBook, user, now);
bevorzugen. Man kann das aber auch anders sehen. Es kommt auf das konkrete Projekt, dessen Rahmenbedingungen, auf das zu entwickelnde Produkt, die Kenntnisse und Vorlieben der Teammitglieder, ja sogar auf die Organistionsstruktur der Entwicklerteams an. Zumindest kann das alles eine Rolle spielen.
Welche Lösung ist davon nun jetzt objektorientiert? Nun, so einfach ist das nicht zu sagen. Die OO ist schon ziemlich alt. Die Idee stammt aus den 60er/70er Jahren und kam mit den Programmiersprachen Simula und Smalltalk. Das was sich die „Väter der OO“ damals gedacht haben und das, was heute unter OO verstanden wird, ist auch nochmal was anderes. Alan Kay einer der Smalltalk-Erfinder meinte mal „Actually I made up the term „object-oriented“, and I can tell you I did not have C++ in mind.“ Letztendlich gibt es keine allgemein anerkannte Definition von Objektorientierung. Wenn man sich mal Ward’s Wiki [2] anschaut, findest man da diverse teils sehr kontroverse Diskussionen zur OO (wobei die meisten schätzungsweise zur Zeit des OO-Hypes in den 90ern, Anfang 2000er waren [3]. Als Ausgangsbasis zum Lesen bzw. stöbern kann man z.B. die Seite DefinitionsForOo verwenden.
Das, was ich anbieten kann, ist also nur „meine“ Sichtweise auf die OO, nicht jedoch eine allgemeingültige. Schlicht und einfach weil es so eine allgemeingültige Sichtweise nicht gibt (egal was man irgendwo über OO erzählt bekommt; man sollte vorsichtig sein, wenn jemand etwas ein für allemal definiert und für wahr™ befindet).
Ich für meinen Teil halte die drei hier unter a), b) und d) vorgestellten Lösungen für objektorientiert. Es finden sich gute Gründe für jede dieser Lösungen. Alle sind „richtig“. Das, was ich in meinem Vortrag als „prozedural“ vorgestellt habe, findet sich zwar auch häufig in „objektorientiertem“ Code, jedoch ist der konkrete Ausschnitt klar prozedural (was nicht heißt, dass er „falsch“ wäre). Das „nicht ganz objektorientierte“ halte ich für eine halbe Lösung. Das finde ich nicht so gut.
[1] Wir ignorieren hier mal andere Prinzipien und Anforderungen. Performance könnte beispielsweise ein Grund für komplexen Spezialcode sein.
[2] Das ist das „Ur-Wiki“. Ward Cunningham hat das Wikiprinzip erfunden und dort umgesetzt. Ward’s Wiki ist ziemlich chaotisch, aber eine wahre Fundgrube mit sehr interessantem Zeug. Man kann da drin stundenlang stöbern.
[3] Das ist nicht ganz so leicht heraus zu finden, weil dort mit Datumsangaben gespart wird.
Permalink
Permalink