Es gibt unerwartete Hilfe für das Flow-Design von den objektorientierten Freunden des Clean Code. Innerhalb zweier Tage bin ich über sehr ähnliche, aber unzusammenhängende Aussagen gestolpert.
Da schreibt einerseits der Chefredakteur des ehrwürdigen Dr. Dobb´s Journal, Andrew Binstock, in einem Editorial, wie wichtig er es fände, Klassen klein zu halten:
“Small classes are much easier to understand and to test. If small size is an objective, then the immediate next question is, "How small? Jeff Bay […] suggests the number should be in the 50-60 line range. Essentially, what fits on one screen.”
Und da schreibt andererseits der dänische Fachbuchautor Mark Seemann in seinem Blog:
“Each class is very small, so although you have many of them, understanding what each one does is easy. […] However, when thinking about SOLID code, it actually helps to think about it more like a liquid […]. Each class has much more room to maneuver because it is small and fits together with other classes in many different ways.”
Zwei Autoren stellvertretend für weitere, die dasselbe sagen: Klassen müssen klein sein, um evolvierbaren Code zu erhalten. Das ist ein Schluss, den sie aus anerkannten SOLIDen Prinzipien der Softwareentwicklung ziehen.
Kleine Klassen, nicht nur kleine Methoden
Bevor ich laut darüber nachdenke, was kleine Klassen für den Code bedeuten, möchte ich einen Einwand vorwegnehmen: “Warum sollen Klassen so klein sein und nicht Methoden? Reicht es nicht, wenn jede Methode einer Klassen vielleicht maximal eine Bildschirmseite füllt? Dann ist die doch auch schon viel übersichtlicher als üblich.”
Absolut. Methoden sollten auch nicht umfangreich sein. Jeder Code, der mehr als eine Bildschirmseite umfasst, ist schwieriger zu verstehen. Auf einer Seite können wir Schachtelungen überblicken; wenn wir dafür scrollen müssen, verlieren wir schnell den Zusammenhang.
Aber auch ich finde es nicht genug, wenn nur Methoden klein sind. Denn Methoden sind nicht die für die Laufzeit wesentlichen Funktionseinheiten. In einem objektorientierten Programm geht es eben um Objekte. Wie reden darüber, wie Objekte in Beziehung gesetzt werden. Abhängigkeiten gibt es nicht von Methoden, sondern von Interfaces, d.h. “ganzen” Objekten.
Wenn ich mich durch Code arbeite, will ich daher schnell verstehen, was diese Objekte tun, nicht nur einzelne ihrer Methoden. Ich will die “rekombinierbaren Einheiten” der Software überblicken. Objekte sind mehr als Methoden, da sie Zustand haben können. Das macht ja gerade die Objektorientierung aus: Wir stellen Netzwerke aus zustandsbehafteten Funktionseinheiten her, die Anforderungen erfüllen.
So ein Netzwerk gut zu verstehen, ist die Voraussetzung, es leicht zu verändern. Dazu kommt noch die Granularität seiner Bestandteile, der Objekte. Das ist ja Seemanns Argument: Strukturen aus feingranularen Objekten lassen sich leichter umformen, wenn neue Anforderungen das nötig machen.
Explodierende Abhängigkeiten
Wenn wir uns einig sind, dass Klassen die Funktionseinheiten sein sollten, die klein1 zu halten sind, dann jetzt zu den Folgen.
Die erste offensichtliche Folge ist, dass die Zahl kleiner Klassen sehr viel größer sein muss, als die Zahl der bisherigen. Klassen von mehreren Hundert Zeilen sind keine Seltenheit; solche mit mehreren Zehntausend Zeilen habe ich aber auch schon gesehen. Wenn ich “klein” für den Gedankengang hier einmal mit 100 LOC gleichsetze und übliche Klassen von 300 bis 1000 LOC haben, dann wird die Zahl kleiner Klassen wohl mindestens 5 Mal so groß sein.
Aber nicht nur das. Wenn aus einer Klasse 5 oder mehr werden, dann soll die Summe ja immer noch dasselbe leisten wie vorher. Das heißt, die Funktionalität muss immer noch irgendwie zusammenhängen. Diese vielen Klassen sind also notwendig voneinander abhängig. Wo vorher eine Klasse allein stand…
A
…da ist es zukünftig ein Wald aus Klassen:
A1
B
C
D
A2
B
E
F
D
(Einrückung bedeutet hier Abhängigkeit, d.h. A1 braucht die Dienste von B und C, C wiederum von D usw.)
Selbstverständlich sind diese Abhängigkeiten nicht alle statisch. Grundsätzlich isolierte Testbarkeit wird erhalten durch dynamische Abhängigkeiten und Dependency Injection. Das ist technisch nicht kompliziert – hat aber seinen Preis beim Testen. Abhängige Klassen können entweder nur mit Integrationstests geprüft werden – was die Fehlerfindung erschwert. Oder sie müssen mit Attrappen ihrer Abhängigkeiten ausgestattet werden – was den Testaufbau selbst mit Mock-Frameworks umständlich macht.
Das hört sich nicht gut an, oder? Steigt die Evolvierbarkeit wirklich, wenn die Klassen klein und übersichtlich werden? Wird da der Teufel nicht mit dem Belzebub ausgetrieben? Vorher war der Code unübersichtlich, weil lang – nun ist der Code unübersichtlich, weil die Abhängigkeiten stark zugenommen haben.
Auch wenn ich das nicht so einfach negativ sehe2, spüre ich auch einen Schmerz bei solcher Zunahme der Abhängigkeiten. Da helfen alle DI Container der Welt nicht. Sie verwalten nur das Elend.
Konzeptionelle Klimmzüge
Doch lassen wir die explodierenden Abhängigkeiten einmal außen vor. Sehen wir sie positiv im Sinne der Prinzipien “Lose Kopplung, hohe Kohäsion” und “Single Responsibility”. Das Entwicklerleben ist halt kein Ponyhof; Opfer sind zu bringen für die Evolvierbarkeit.
Was aber mit den konzeptionellen Klimmzügen die Sie im Rahmen der Klassenverkleinerung vollbringen müssen? Da haben Sie in langen Entwurfssitzungen – oder agilen TDD-Impulsen – nun Ihre Klassen geschnitten; jede ist sorgfältig an die Problemdomäne angepasst; alles hat seine Ordnung – nur leider haben alles Nachdenken und auch der TDD-Druck es nicht geschafft, die Klassen wirklich klein zu halten. 500+ LOC sind herausgekommen für die zentrale Domänenmodellklasse oder die Verschlüsselungsklasse oder die Datenzugriffsklasse. Sie haben sich sogar bemüht, die Methoden klein zu halten. SLA rulez! Und nun kommt einer daher und sagt, wahrhaft SOLIDe clean sei Ihr Code erst, wenn die Klassen selbst klein seien. Ja, wie soll das denn gehen? Ein Kunde ist ein Kunde ist ein Kunde. Den kann man nicht so einfach aufteilen. Und die Verschlüsselungsfunktionalität passt auch so schön unter den Hut einer Klasse.
Diesen Einwand verstehe ich auch sehr gut. Wer die Welt in funktionsreiche Akteure benannt mit Substantiven aufgeteilt hat, der tut sich schwer, diese Funktionalität weiter aufzuteilen. In was sollten Sie einen Kunden denn zerlegen? Naheliegende Abspaltungen von Funktionalität werden Sie schon selbst vorgenommen haben; Sie haben substantivische Sinnzusammenhänge selbstverständlich hergestellt. Die Bonitätsprüfung ist schon nicht mehr Bestandteil des Kunden, sondern eine eigene Klasse, von der der Kunde abhängig ist…
public class Kunde
{
…
private IBonitätsprüfung _bp;
public Kunde(IBonitätsprüfung bp) {…}
…
}
…damit man hübsch objektorientiert fragen kann: kunde.HatBonitätFür(100000).
Es geht also nicht mehr kleiner, wenn die Problemdomäne sich noch sinnvoll im Code widerspiegeln soll.
Die wunderbaren Flexibilitätsvorteile, die wahrhaft SOLIDer Code durch kleine Klassen verspricht, erscheinen damit unerreichbar. Sie sind dazu verdammt, unSOLIDe zu arbeiten, um nicht in Abhängigkeiten zu ersticken und/oder sich in einem konzeptionellen Wirrwarr zu verlieren.
So scheint es zumindest…
Erlösung durch Perspektivwechsel
So gern Sie den guten Rat von Binstock und Seemann annehmen würden, Sie werden im Augenblick nicht können. Er käme Ihnen teuer zu stehen. Das ist auch meine Meinung. Aus Ihrer Perspektive der üblichen, der OOAD-geprägten Objektorientierung wäre der Preis für den Gewinn an Evolvierbarkeit durch wirklich kleine zu hoch.
Solange Sie aus dieser Perspektive auf den Ratschlag blicken, kann ich Ihnen nicht dazu raten. Was aber, wenn Sie die Perspektive wechseln? Was, wenn Sie sich nicht mehr vor explodierenden Abhängigkeiten fürchten müssten? Was, wenn es keine konzeptionelle Schwierigkeit gäbe, Klassen zu zerteilen?
Eine solche alternative Perspektive gibt es. Es ist die Perspektive des Flow-Designs (FD) und der Event-Based Components (EBC)3.
Im Flow-Design sind Funktionseinheiten (lies: Klassen) nicht mehr voneinander abhängig. Wenn die eine Klasse Daten lädt, die andere sie verarbeitet und eine dritte sie speichert, dann kennen diese Klassen einander nicht, weder statisch noch dynamisch. Es gibt keinen Abhängigkeitsverhau und auch keinen Testattrappenalbtraum.
In FD/EBC gibt es zwar noch Abhängigkeiten, doch die sind streng systematisiert und unkritisch. Kein Grund für Schlaflosigkeit. Die Evolvierbarkeit leidet unter ihnen nicht. Sie folgen einer grundsätzlichen Separation of Concerns.
Darüber hinaus gibt Flow-Design den vorherrschenden Fokus auf Substantive auf. Funktionseinheiten (lies: Klassen) sind keine Akteure, sondern Aktionen. Denken Sie im Augenblick einfach mal nur “zustandsbehaftete Funktion”. Mit FD versuchen Sie zur Lösung eines Problems nicht ein paar Substantive zu finden, auf denen Sie dann in einem umständlichen Verfahren Funktionalität verteilen. Sie suchen vielmehr “nur” nach Funktionen oder “Verhaltensschritten”. Die können Zustand haben oder nicht. Egal. Vor allem Arbeiten Sie auf Input und erzeugen Output.
Dieser Fokuswechsel von Substantiven/Akteuren/Dingen nach Verben/Aktionen/Verhalten führt automatisch zu sehr kleinen Funktionseinheiten (lies: Klassen). Und wenn nicht, dann lassen sich EBC-Klassen (lies: Verhaltensweisen) sehr viel leichter als OOAD-Klassen (lies: Dinge) in weitere Klassen zerlegen.
Fazit
SOLIDer Rat ist tatsächlich viel Wert – wenn Sie ihn im rechten Kontext beherzigen. Solange der die übliche methodische OOAD-Objektorientierung ist, wird es sie schmerzen. Doch mit Flow-Design und Event-Based Components wird aus vormals festen, unhandlich großen Klassen eine SOLIDe Flüssigkeit. Der Entwurf fließt, die Daten fließen, die Strukturen passen sich fließend an.
SOLID ohne Flow-Orientation ist noch fest. Nomen es omen. Doch SOLID mit Flow-Orientation erzeugt Evolvierbarkeit und Testbarkeit. Oder umgekehrt: Flow-Orientation führt automatisch zu SOLIDem Code.
Spendieren Sie mir doch einen Kaffee, wenn Ihnen dieser Artikel gefallen hat…
Fußnoten
1 Ob “klein” nun heißt 25 Zeilen oder 50 oder 75, das finde ich an dieser Stelle nicht so wichtig. Bildschirmseiten bieten ja je nach Bildschirmauflösung und Font und Monitorgröße unterschiedlich viel Platz. Klassen, deren Methoden sehr unabhängig voneinander sind, verstehe ich auch schnell, wenn nur die Methoden leicht zu überblicken sind; dann wäre es ok, wenn ich ein bisschen scrollen muss. Bestehen unter den Methoden aber vielfältige Abhängigkeiten, dann ist das etwas anderes. Dann möchte ich möglichst viel der Funktionalität auf einen Blick sehen. Wie der Berater so schön sagt: Es kommt darauf an. Eine allgemeingültige Zeilenzahl, die niemals überschritten werden darf, lässt sich nicht definieren. Aber ich kann für mich sagen, dass ich unruhig werden, wenn der Umfang einer Klasse mehr als 2-3 Bildschirmseiten hat. Und ich kann nur an Ihre Sensibilität appellieren, den Schmerz zu spüren, wenn Sie zum Verständnis scrollen müssen.
2 Oder genauer: Nicht die Abhängigkeiten haben zugenommen, sondern sie haben sich verändert. Vorher waren sie weniger sichtbar und einfacher. Die Methoden innerhalb einer solchermaßen geshredderten Klassen waren ja auch voneinander abhängig. Diese Abhängigkeiten sind nun explizit gemacht. Das könnte man auch positiv Entkopplung nennen, zu der ebenfalls positiv eine Zusammenfassung nach Kohäsion in neuen Klassen tritt.
3 Über Flow-Design und EBC habe ich ausführlich in diesem Blog und in der dotnetpro geschrieben. Wer mehr dazu erfahren will, finde auf dieser Ressourcenseite Berge an Material: http://clean-code-advisors.com/ressourcen/flow-design-ressourcen