Follow my new blog

Samstag, 25. Juli 2009

Partial Classes helfen dem Single Level of Abstraction Prinzip [endlich-clean.net]

Eines meiner Clean Code Developer (CCD) Lieblingsprinzipien ist Single Level of Abstration (SLA). Wenn man es befolgt, dann wird Code ganz einfach viel lesbarer.

Eigentlich. Denn auch wenn bei einer einzelnen Methode durch Fokussierung auf ein Abstraktionslevel die Übersichtlichkeit steigt, so kann sie für eine ganze Klasse sinken. Das habe ich immer wieder bemerkt und auf Abhilfe gesonnen. Nicht immer besteht die nämlich im Single Responsibility Principle (SRP), nach dem eine Klasse bei anhaltender Unübersichtlichkeit vielleicht besser in mehrere zerlegt werden sollte.

Aber jetzt habe ich eine Lösung im Rahmen der verfügbaren Sprach- und IDE-Mittel gefunden, glaube ich. Als Beispiel eine Methode, die eine Textdatei erzeugt und mit Zeilen aus Dummy-Worten füllt. Solche Textdateien brauchte ich neulich mal für Tests eines kleinen Flow-Frameworks.

V0 – Unrefaktorisiert

In der ersten Runde habe ich den Textfile-Generator natürlich einfach erstmal so runtergeschrieben. Nicht sehr clean, wie diese Abbildung zeigt:

image

Ich wollte möglichst schnell Funktionalität auf die Straße bekommen. Und da die Lösung einfach ist, war innere Qualität zunächst nicht so wichtig. Wer kennt das nicht? :-)

Die zentrale Methode Generate() sieht daher sehr typisch aus. Es steckt alles drin, was ein Mann zur Textfile-Generierung braucht ;-) Da werden solange Zeilen bestehend aus Worten erzeugt, bis eine festgesetzte Gesamtzeichenzahl (max_number_of_chars) erreicht ist. Und die Zeilen haben auch eine ähnliche Länge (MAX_CHARS_IN_LINE), die sich aus der Summe von mehreren Worten unterschiedlicher Länge (von MIN_WORD_LEN bis MAX_WORD_LEN) ergibt, die durch Leerzeichen getrennt sind. Alle soundsoviele Zeilen feuert die Routine einen Event, den der Aufrufer für eine Fortschrittsanzeige benutzen kann.

Das ist alles nicht schwer zu verstehen – aber wenn Sie in den Code blicken, müssen Sie schon einen Moment hinschauen, um die Algorithmusbestandteile zu identifizieren. Sie müssen sozusagen ihr geistiges Auge erst scharfstellen, bevor sie den Algorithmus wirklich sehen/verstehen.

Auch mit der obigen Beschreibung dauert das etwas, weil die Bestandteile/Verarbeitungsschritte nicht klar getrennt sind im Quelltext. Ein wenig helfen die Einrückungen der Schleifen. Doch letztlich zeigen sie nur an, dass etwas zusammengehört, aber nicht was es ist.

V1 – Refaktorisieren mit SLA

Viel leichter haben Sie es, wenn die Methode Generate() dem SLA Prinzip folgt:

image

Ah, jetzt sehen Sie klar! Die Dateierzeugung besteht aus drei Schritten: Initialisierung einer Statistik, der eigentlichen Textfile-Generierung und dann einem Abschlussbericht über die Statistik. Diese Methode liegen alle auf demselben Abstraktionsniveau – auch wenn unterschiedlichen Verantwortlichkeiten angehören (Statistik, Textfile-Generierung).

Wenn Sie dann Details interessieren, dann schauen Sie sich die Methoden genauer an, z.B. GenerateFile():

image

Ah, jetzt sehen Sie auch sofort klar! Geschrieben wird über einen StreamWriter() solange die Datei noch nicht groß genug ist. Eine Zeile nach der anderen wird erzeugt und zur Statistik hinzugefügt.

Und wie wird eine Zeile erzeugt? Drill-down in die Methode GenerateLine() hinein:

image

Ah, alles klar! Eine Zeile wird aus Worten zusammengebaut und dann weggeschrieben.

Und so weiter und so fort… SLA bietet Ihnen auf jeder Ebene einen schnellen Überblick des Zusammenspiels von Bestandteilen (ungefähr) gleicher Abstraktheit. Sie müssen sich nur soweit mit Details konfrontieren, wie für Ihr Informationsbedürfnis in einer Situation nötig ist.

SLA-Problem Unübersichtlichkeit im Großen

Wo Licht ist, da ist auch Schatten. So auch bei SLA. Die Übersichtlichkeit im Kleinen, in der Methode, führt relativ schnell zu einer Unübersichtlichkeit im Großen, in der Klasse. Denn aus einer überschaubaren Klasse mit einer Methode

image

ist eine geworden, die viele Methoden auf unterschiedlichem Abstraktionsniveau hat:

image

Nun ist es einfach zu verstehen, was Generate() tut – aber die Klasse zu betrachten verwirrt eher, auch wenn die Sichtbarkeitsangaben helfen, einen Einstieg in die Interpretation zu finden.

Schlimmer wird es mit der Unübersichtlichkeit, wenn Sie in den Quellcode schauen:

image

Von Abstraktionsebenen keine Spur mehr. Methoden weiter oben mögen auf einem höheren Abstraktionslevel liegen als solche weiter unten. Aber klare Grenzen sind nicht zu erkennen. Ich habe mit Einrückungen experimentiert, aber die bleiben nicht immer erhalten.

Was also tun? Muss ich denn für mehr Übersichtlichkeit gleich SRP in Anschlag bringen und neue Klassen definieren, z.B. eine für die Statistik, eine zum Erzeugen einer neuen Zeile usw. Das fände ich misslich. Denn eine weitere Klasse bedeutet immer mehr Aufwand als eine weitere Methode.

V2 – Partial Classes für mehr Übersicht

Jetzt habe ich allerdings eine Idee gehabt, die als Zwischenstufe helfen mag, die Übersichtlichkeit bei Anwendung von SLA zu erhalten, ohne auf SRP übergehen zu müssen. Ich zerlege meine methodenreichen SLA-Klassen in Partial Classes, die sich jeweils auf ein Abstraktionslevel konzentrieren.

Der “oberste” Klassenbestandteil für das Beispiel sieht dann z.B. so aus:

image

Er zeigt die wesentliche Methode auf höchstem Abstraktionsniveau. Wenn Sie dann mehr wissen wollen, schauen Sie sich den “darunterliegenden” Klassenbestandteil an:

image

Hier finden Sie nun zwei Abstraktionsniveaus vereint. GenerateFile() liegt höher als GenerateLine(). Doch da der Klassenbestandteil insgesamt nicht zu lang ist (1 Bildschirmseite) und die drei Methoden eng zueinander gehören, habe ich keine weitere Aufteilung für nötig befunden.

Insgesamt besteht der TextFileGenerator nun aus 4 Klassenbestandteilen, von denen drei die Textfile-Generierung schrittweise verfeinern und einer die auf verschiedenen Ebenen genutzten Statistikfunktionen zusammenfasst. Im Solution Explorer bekommen Sie also auch schnell einen Überblick über eine Klasse:

image

Ein weiterer Vorteil: Indem ich mit dem simplen SLA anfange, meinen Code zu refaktorisieren, und mit Partial Classes ein unaufwändiges Mittel nutzen kann, um die Übersichtlichkeit zu erhalten, merke ich ganz natürlich und konkret, wann sich das SRP lohnt. In diesem Szenario kristallisiert sich z.B. die Statistik als Kandidat für eine Auslagerung in eine eigene Klasse heraus. Aber das ist ein anderes Thema…

Erstmal bin ich zufrieden, in einfacher Weise Übersichtlichkeit in Klassen mit Methoden auf unterschiedlichem Abstraktionsniveau herstellen zu können. Partial Classes to the rescue. SLA kann also eines meiner Lieblingsprinzipien bleiben.

Kommentare:

Laurin Stoll hat gesagt…

Hi ralf,

Hm... ich weiss nicht. Eigentlich wäre es für den Sachverhalt, meiner Meinung nach, schon angebracht das SRP zu befolgen. Aber ok - es gibt manchmal einen Mittelweg, dass kenne ich selbst. Aber reichen dann Regions nicht?
Von mehreren Klassen bist du ja nicht weit, wenn du mehrere Partial Classes hast...

viele grüsse

Ralf Westphal - One Man Think Tank hat gesagt…

@Laurin: Ich stimme zu, dass evtl eine eigene Klasse für die Stats sinnig wäre. Aber ich halte es für aufwändiger, darüber zu grübeln, als erstmal einfach die Funktionalität in Methoden auszulagern. Bei einem Prinzip zu bleiben (SLA) ist einfacher. Und es führt eben dahin, dass man Bruchstellen sieht oder "Cluster" von Funktionalität eigener Verantwortlichkeit.

Mit Regions habe ich experimentiert. Die lage für mich auch nahe. Aber ich finde sie nicht so gut, auch wenn da eine Möglichkeit bestünde, unterschiedl Abstraktionsebenen zu schachteln.

Das Problem mit Regions ist einfach, dass sie beim Ausdruck quasi verlorengehen. Zudem tendiert man dazu, Regions zu öffnen und zu öffnen und selten zu schließen beim Explorieren. Am Ende steht man dann wieder da und hat alles auf einer Ebene.

Nein, nach einigem Experimentieren fühle ich mich mit partial classes ziemlich angekommen. Sie sind nicht optimal, aber derzeit für mich das kleinste Übel.

Ein nächster kleiner Schritt sind dann geschachtelte Klassen. Das würde ich mit den Stats-Methoden machen. Die haben über die Klasse des Dateigenerators hinaus derzeit keine Bedeutung; also würde ich sie in eine Klasse rausfaktorisieren, die ich aber weiterhin lokal in der Dateigeneratorklasse lassen würde, um eben ihre begrenzte Relevanz auch in der Form zu dokumentieren.

-Ralf

Michael Ertelt hat gesagt…

Nichts gegen partielle Klassen, auch ich finde die in einigen Fällen sehr nützlich. Aber sind geschachtelte Klassen wirklich nur ein nächster kleiner Schritt, oder das Ziel? Die Statistik Methoden aus dem Beispiel auszulagern ist nur folgerichtig. Vielleicht sogar die internen Generierungsmethoden um sie austauschbar zu machen. Und dann auch noch die Art der Bereitstellung (Schreiben in eine Datei, in eine Datenbank, ...). Wie weit sollten wir hier gehen?

Ralf Westphal - One Man Think Tank hat gesagt…

@Michael: Gute Frage: Wie weit soll man gehen?

Weil ich das auch nicht weiß, habe ich nach der Refaktorisierung mit SLA aufgehört.

Du zeigst valide weitere mögliche Refaktorisierungen für mehr Flexibilität/Evolvierbarkeit auf. An dieser Stelle sehe ich aber nicht, dass ich sie brauche. Es geht ja nicht um einen Framework, den ich da basteln wollte.

Deshalb spüre ich keinen Schmerz, rieche keinen Code-Smell ;-) Statt SoC wende ich daher YAGNI an: ich höre auf dran rumzuschrauben, weil ich nicht weiß, ob ich mehr Evolvierbarkeit brauche.

SLA hat mich erstmal weit genug gebracht für die sichtbaren Anforderungen. Die partiellen Klassen habe ich dann nur eingeführt, weil ich das SLA-Ziel durch Aufspaltung am Ende verfehlt hatte. Der Code war im Detail übersichtlicher, aber nicht mehr im Ganzen.

-Ralf

Rainer Schuster hat gesagt…

@Ralf: Was hällst du von der Idee, die Zerlegung der Partial Klassen nicht mit einem Folder zu machen, sondern wie z.B. bei einer WinForm mit entsprechenden Unterdateien.

Was ist dazu notwendig? Eine Datei steht im *.proj File als:

<Compile Include="CSharpClass.cs"/>

Um sie als zugehörig zu deklarieren benötigt sie noch ein DependentUpon Property, das ihm die Funktionalität signalisiert. Wäre dann aber nicht VS-Standard müsste über ein eigenes AddIn, oder z.B. R# realisiert werden. Notwendiger Code:

<Compile Include="CSharpClass_Refined.cs"/>
<DependentUpon>CSharpClass.cs&lt/DependentUpon>
</Compile>

Fände ich persönlich "hübscher". Wenn dir die Idee auch gefällt, viel Spaß damit.

Viele Grüße,
Rainer

Ralf Westphal - One Man Think Tank hat gesagt…

@Rainer: Den Gedanken an "eingeschachtelte" Dateien hatte ich auch. Aber ich habs nicht hingekriegt :-( Deshalb der Folder. Der ist ne Krücke, keine Frage.

Also: Wie kann ich einfach Dateien anderen in einem VS-Projekt unterordnen? Wie sieht dafür ein Makro oder sowas aus?

Wer hilft?

-Ralf

Mike Hummel hat gesagt…

Hallo,
ich möchte nicht schon wieder gegen reden, aber...

Ich halte den SLA Ansatz für nicht besonders geeignet um Code leserlicher zu machen. Die eigentliche Funktionalität wird dadurch auseinander gehackt. Funktionen du definieren macht dann Sinn, wenn der Code darin mehr als einmal benutzt wird, wenn der Inhalt der Funktion nicht nur aus einer Zeile besteht.

Ist es nicht ein besserer Weg die gewonnene Abstraktion wieder in eine Funktion zu vereinen und durch Kommentare voneinander zu trennen? In den Kommentaren kann man auch gleich wiedergeben was in den Abschnitten geschieht. Außerdem ist die Funktionalität als gesamtes erfassbar, ohne permanent durch sinnlose Funktionssprünge unterbrochen zu werden.

Mit Gruß, Mike

Ralf Westphal - One Man Think Tank hat gesagt…

@Mike: Tut mir leid, dass ich wieder nicht mit dir einer Meinung bin, Mike.

Aber Kommentare sind eher ein Problem als eine Lösung. Kommentare verletzen leicht das DRY Prinzip. Da wird etwas gesagt, was der Code selbst eigentlich schon sagt - oder eben durch einen Funktionsnamen ganz leicht ausdrücken könnte.

Insofern geht es eben nicht (!) um Wiederverwendbarkeit (bei SLA). Wiederverwendbarkeit sollte (zunächst) kein (!) Ziel der Softwareentwicklung sein. Sie wird zwar immer als Ideal ausgerufen - aber die Praxis zeigt, dass sie kaum zu erreichen ist und insofern vom Wesentlichen ablenkt.

Methoden nicht zwar auch ein Mittel, um Wiederholungen zu vermeiden. Vor allem (!) sind sie jedoch ein Mittel, um zu strukturieren und damit lesbarer zu machen. SLA nimmt Methoden da sehr ernst.

-Ralf

Uli hat gesagt…

Hallo Ralf, würdest du das heute nochmal so umsetzen?

Ralf Westphal - One Man Think Tank hat gesagt…

@Uli: Warum nicht. Das ist in C# ein legitimes Strukturierungsmittel.

Inzwischen habe ich aber ein Tool gefunden, um Dateien ohne Projektordner untereinander in VS anzuordnen.

Anonym hat gesagt…

Hallo Ralph,
ist das ein freies Tool? Welches ist es?
Gruß
Robert