Die Community zu .NET und Classic VB.
Menü

Lesbaren Quelltext schreiben

 von 

Einleitung 

Martin Fowler sagte einst: "Jeder kann Code schreiben, den Maschinen verstehen können, aber nur die wenigsten können Code schreiben, den auch Menschen verstehen können." Diese Aussage bezog sich nicht etwa auf kryptischen Assembler-Code - viel mehr sind damit auch höhere Programmiersprachen gemeint. Oft wird zunächst Wegwerf-Code geschrieben, weil er nur einmalig einen einzigen Zweck erfüllen muss. Anschließend landet er auf der Festplatte oder im Versionskontrollsystem und verbleibt dort, bis er - oft schon weniger Tage später - in ähnlicher Form wieder gebraucht wird. Und so landen Codeschnipsel oft in Projekten, für die sie gar nicht geschrieben wurden. Schlimmer noch: der Code wird nicht nur von dem ursprünglichen Entwickler selbst, sondern auch von seinen Kollegen gelesen.

Dieser Artikel soll vorstellen, was es mit dem Begriff Code-Smell auf sich hat, und was man gegen ihn tun kann.

Viel Spaß beim Lesen
Jochen Wierum

Muffiger Code  

Code-Smell (zu Deutsch "übel riechender Code") beschreibt keine Funktion von Quellcode. Im Gegenteil: oft ist an der Funktionsweise von riechendem Code nichts auszusetzen. Vielmehr geht es darum, ob der Leser den Code ebenfalls versteht. Robert C. Martin (Bekannt als Onkel Bob) schlägt in seinem Buch "Clean Code" vor, Flüche pro Minute beim Lesen von Quellcode als Maß für Code-Smell zu verwenden. Ich will hier sechs Beispiele vorstellen und beispielhaft zeigen, wie man sie beseitigen kann.

Code Smells lassen sich mittels Refactorings lösen. Ein Refactoring bezeichnet das Ändern von Quellcode, ohne dass sich das Verhalten des Codes nach außen ändert. Während das Umbenennen einer lokalen Variable ein Refactoring ist, ist das beheben eines Bugs bereits kein Refactoring mehr.

An vielen Stellen mag die Diskussion eher philosophisch klingen, allerdings führen viele Refactorings (vor allem die komplexeren) nicht nur zu verständlicherem Code, sondern oder auch zu einem flexibleren Programmdesign. Dies geschieht immer dann, wenn die Lösung nicht in der Umbenennung von Methoden oder Variablen liegt, sondern durch Extrahierung und Verschieben von Methoden auch das Objektdesign klarer wird.

Ein erstes Beispiel: Kommentare  

Kommentare sind selbstverständlich kein Code-Smell. Sie sind allerdings oft ein Hinweis darauf, dass ein Code-Smell vorliegt (ein "Deodorant"). Sofern ein Entwickler über einen selbst geschriebenen Kommentar stolpert, sollte er sich zwei Fragen stellen:

  1. Erklärt der Kommentar etwas, was nicht im Quelltext steht?
  2. Kann ich den Kommentar auch als Quelltext formulieren?

Sofern beide Fragen mit "nein" beantwortet werden, hat der Kommentar eine Daseinsberechtigung. Ansonsten kann der Kommentar entweder weggelassen (Fall 1), oder in Form von Code ausgedrückt werden (Fall 2). Zum besseren Verständnis je ein Beispiel:

' Declares an array
Dim value(10) As Integer

' Set default values
value(0) = 1
value(1) = 7
value(2) = 9

Listing 1: Beispiel für Kommentare ohne Mehrwert

Sofern nicht ein Anfänger, welcher die Programmiersprache noch lernt, den Quellcode liest, wird niemand von den Kommentaren profitieren. Anders sieht es mit folgendem Code aus:

' If the user is logged in and has unread messages
If user.state <> 0 And messages.count(2) > 0 Then
' ...
End If

Listing 2: Kommentare erklären Bedeutung von Bedingungen

Laut Quelltext müsste 0 bedeuten, dass der User nicht eingeloggt ist. Die erste Frage sollte sein, ob sich die 0 nicht zumindest über eine Konstante eliminieren ließe. Eine lokale Lösung für das Problem sähe aber so aus (auf den Kommentar kann nun verzichtet werden):

Dim userLoggedIn As Boolean = user.state <> 0
Dim hasUnreadMessages As Boolean = messages.count(2) > 0

If userLoggedIn And hasUnreadMessages Then
    ' ...
End If

Listing 3: sprechende Namen vermeiden Kommentare

Sprechende Namen  

Variablen wie "i" und "j" in Schleifen sind schnell getippt. Sind die Schleifen klein, ist es kein Problem, sich einen Überblick zu verschaffen. Auch Variablen wie "x" oder "y" sind erlaubt, wenn es sich tatsächlich um Koordinaten handelt. Aber wer kann sich merken, dass "m" die Anzahl der Mitarbeiter war? Hier ein Beispiel, welches einem Codeschnipsel des oben erwähnten Buches "Clean Code" nachempfunden ist:

Function getValue(ByVal data As List(Of List(Of Integer))) As Integer
    Dim x As Integer = -1
    Dim y As Integer = Integer.MinValue

    For Each l In data
        If y <= l(3) Then
            y = l(3)
            x = l(1)
        End If
    Next

    Return x
End Function

Listing 4: Variablennamen ohne semantische Bedeutung erschweren das Verstehen

Durch alleiniges Umbenennen der verwendeten Variablen und Einfügen von zwei Konstanten wird der Codeschnipsel lesbarer.

Const PRICE_COLUMN As Integer = 3
Const ID_COLUMN As Integer = 1

Function getMostExpensiveProduct(ByVal products As List(Of List(Of Integer))) As Integer
    Dim productId As Integer = -1
    Dim highestPrice As Integer = Integer.MinValue

    For Each product In products
        If highestPrice <= product(PRICE_COLUMN) Then
            highestPrice = product(PRICE_COLUMN)
            productId = product(ID_COLUMN)
        End If
    Next

    Return productId
End Function

Listing 5: Sprechende Variablennamen erleichtern das Lesen

Dieser Code ist wesentlich leichter zu verstehen. Natürlich kann argumentiert werden, dass obiger Code im Kontext von anderen Methoden auch Sinn ergeben würde. Aber nicht immer will man das gesamte Projekt verstehen müssen, um eine einzelne Komponente untersuchen zu können.

Ein weiteres Beispiel für sprechende Namen sind Maßeinheiten.

If duration < 10 Then
    ' ...
Else
    ' ...
End If

Listing 6: Welche Maßeinheit ist gemeint?

Wenige Klicks später ist die Definition der Variable duration gefunden:

Private duration As Integer ' in minutes

Listing 7: Diese Maßeinheit ist gemeint!

Damit die Suche das nächste Mal nicht erneut beginnt: was spricht dagegen, die Variable in durationInMinutes umzubenennen? Der Code wird durch solche Namen zwar etwas länger, da mittlerweile aber alle gängigen IDEs sehr ausgereifte Möglichkeiten der Quellcodevervollständigung bieten, fällt kaum Tipparbeit an. In diesem Fall sollten dann auch die Vokale ausgeschrieben werden. Zwar lässt sich erahnen, dass hinter "cstmrs" das Wort "customers" steckt, aber spätestens, wenn man dem Kollegen am Telefon mitteilen will, in welcher Variable die registrierten Kunden versteckt sind ist "customers" definitiv die leichtere Variante.

Magic numbers  

Magische Zahlen sind Zahlen, deren Herkunft nicht ersichtlich ist.

If x.StartsWith("Command ") Then
    command = x.Substring(8)
End If

Listing 8: Diese 8 ist eine magische Zahl

Es ist naheliegend, dass die Zahl 8 in diesem Falle die Länge des Wortes "Command" inklusive dem Leerzeichen ist. Ändert sich der String "Command" aber einmal in "cmd" beginnt das große Rechnen. Durch das Auslagern einer Konstante lässt sich Klarheit verschaffen:

Const COMMAND_PREFIX As String = "Command "
If x.StartsWith(COMMAND_PREFIX) Then
    command = x.Substring(COMMAND_PREFIX.Length)
End If

Listing 9: Konstanten eliminieren Magische Zahlen.

Ein weiteres Beispiel ist weiter oben zu finden: die Methode getMostExpensiveProduct nutzt die Konstanten ID_COLUMN und PRICE_COLUMN statt den magischen Zahlen 1 und 3. Zwar bleiben die Zahlen 1 und 3 noch im Quellcode, sie werden aber direkt mit einem sinnvollen Namen in Zusammenhang gebracht.

Parameter  

Hat eine Methode nur einen oder zwei Parameter, so lassen sich die Bedeutungen noch gut erraten, vor allem, wenn der Methodenname sprechend gestaltet ist (z.B. modeFileIntoDirectory(file, dir)). Folgender Aufruf zeichnet eine Linie:

DrawLine(1, 1, 20, 20, Color.COLOR3, LineStyle.STYLE2)

Listing 10: zu viele Parameter stiften Verwirrung

Farbe und Art der Linie lassen sich leicht identifizieren. Doch die ersten vier Parameter sind auf den ersten Blick nicht erkennbar. Lautet die Reihenfolge x1, y1, x2, y2? Oder sind zunächst beide x-Werte aufgeführt? Zwar gibt es für so etwas Konventionen, aber auch die Einführung von neuen Klassen hilft bei dieser Art von Smell (zusätzlich wurden die Konstanten umbenannt):

DrawLine(New Point2d(1, 1), New Point2d(20, 20), Color.RED, LineStyle.DOTTED)

Listing 11: Das Einführen von neuen Klassen erleichtert das Lesen von Parametern

Eine Alternative wäre die Einführung benannter Parameter:

DrawLine(x1:=1, y1:=1, x2:=20, y2:=20, color:=Color.RED, lineStyle:=LineStyle.DOTTED)

Listing 12: Auch benannte Parameter erleichtern die Lesbarkeit

Generell bringt der Verzicht auf primitive Datentypen oft mehr Klarheit. Die vollendete Anwendung dieser Methode versteckt sich hinter dem Pattern Introduce Parameter Object. Dabei wird eine neue Klasse eingeführt, welche alle Parameter ein einem Objekt kapselt:

Dim line As New Line()
line.x1 = 1
line.y1 = 1
line.x2 = 20
line.y2 = 20
line.color = Color.RED
line.style = LineStyle.DOTTED
DrawLine(line)

Listing 13: Parameterobjekte reduzieren die Parameter weiter

Die Länge von Methoden und Klassen  

Lange Methoden lassen sich schwer überblicken. Methoden haben nach Definition im Idealfall nur einen Zweck. Dieser wird im Namen der Methode beschrieben. Fällt es schwer, den Namen einer Methode zu bilden, ist es empfehlenswert vorher zu überlegen, ob die Methode nicht zu viel tut und ob es möglich ist, zwei kürzere Methoden daraus zu machen. Es werden unterschiedliche Methodenlängen empfohlen, die Angaben liegen zwischen 5 und 25 Zeilen. Die einhellige Meinung ist aber: möglichst kurz. Üblich ist es, zunächst einmal das Problem durch eine "normale" Methode zu lösen, und diese in einem weiteren Schritt in mehrere kurze Methoden zu zerlegen. Refactoring-Patterns wie Extract Method helfen hier.

Die Abstraktionsebene wahren  

Ein weiteres Problem ist der Abstraktionslevel. Dazu auch hier zunächst ein Beispiel (einem ähnlichen Beispiel aus dem Clean Code Buch nachempfunden):

Public Function generatePage() As String
    Dim html As New StringBuilder

    html.Append(CreateHtmlHead())
    html.Append("<body><h1>Result</h1>")
    html.Append(CreateHTMLResultTable())
    html.Append("</body>")
    html.Append(CreateHTMLFooter())

    Return html.ToString
End Function

Listing 14: In dieser Methode sind mehrere Abstraktionslevel vermischt

Die Methode CreateHtmlHead() ist abstrakter, als der anschließende String "<body>...". In diesem Beispiel ist obiger Code zwar noch verständlich, in komplexeren Szenarien entzieht sich die Verständlichkeit aber schnell. Eine einheitliche Abstraktionsebene wird dadurch erreicht, dass die Teile mit der niedrigsten Abstraktion in neue Methoden verschoben werden:

Public Function generatePage() As String
    Dim html As New StringBuilder
    
    html.Append(CreateHtmlHead())
    html.Append(CreateHTMLBody())
    html.Append(CreateHTMLResultTable())
    html.Append(CreateHTMLBodyFooter())
    html.Append(CreateHTMLFooter())
    
    Return html.ToString
End Function

Listing 15: Jetzt ist eine einheitliche Abstraktionsebene erreicht

Eventuell können nun die Create-Methoden sogar in eine separate Klasse ausgelagern werden. So können Patterns wie das Builder-Pattern/ nach und nach umgesetzt werden.

Weitere Smells  

Es ergibt keinen Sinn, alle möglichen Smells aufzulisten und dieser Artikel kann natürlich nur einen Einstieg in die Problematik schaffen. Die hier vorgestellten Code-Smells lassen sich allesamt relativ leicht beheben. Andere Regeln stellen Forderungen, welche tiefere Änderungen herbeiführen. Beispielsweise darf eine Klasse nur genau einen Zweck erfüllen. An dieser Stelle sei auf Wikipedia und auf Coding Horror verwiesen. Martin Fowler schreibt in seinem Buch über Refactorings über viele Code-Smells, deren Ursprung und was man dagegen tun kann.

Die Clean-Code Initiative setzt ebenfalls an diesen Problemen an und fragt, was einen Professionellen Softwareentwickler ausmacht. Dazu gehören nicht ausschließlich Code-Smells, sondern auch viele andere Handwerkszeuge und Prinzipien.

Aller Anfang ist schwer  

Wer es bis hier hin geschafft hat, wird die Idee von Code-Smells nicht schlecht finden. Offen bleibt aber die Frage, ob das alles die Arbeit wirklich wert ist. Ich persönlich durfte mehrfach die Erfahrung machen, dass sich das ganze relativ schnell auszahlt. Vor allem bei Software, die regelmäßig Änderungen erfährt, ist lesbarer Code mit Zeitersparnis gleichzusetzen. Code ist dann lesbar, wenn ein beliebiger Codeschnipsel ohne viel Zeit zum Denken Sinn ergibt, und nicht der komplette Kontext herangezogen werden muss.

Viele Refactorings führen nebenbei ganz automatisch zu einem verbesserten Programmdesign. Dabei werden viele Funktionalitäten aufgeteilt und Klassen umpartitioniert. Auch dies hält Software automatisch offener für Änderungen.

Wichtig ist die Erkenntnis, dass im ersten Versuch selten perfekte Lösungen erarbeitet werden. Refactorings helfen nach dem Ausarbeiten einer Lösung, diese lesbar und wiederverwendbar zu gestalten. Viele dieser Refactorings lassen sich leicht automatisieren, müssen aber händisch angestoßen werden. Am besten von Autor des geschriebenen Codes, weil er am leichtesten verifizieren kann, ob das Ergebnis seiner Vorstellung von erklärendem Code entspricht.

Eine wichtige Pfadfinderregel lautet: "Hinterlasse einen Ort immer sauberer, als du ihn vorgefunden hast". Wenn diese Regel auch von Programmierern eingehalten wird, macht die Arbeit mit Quelltext viel mehr Spaß! Versprochen ;-)

Ihre Meinung  

Falls Sie Fragen zu diesem Tutorial haben oder Ihre Erfahrung mit anderen Nutzern austauschen möchten, dann teilen Sie uns diese bitte in einem der unten vorhandenen Themen oder über einen neuen Beitrag mit. Hierzu können sie einfach einen Beitrag in einem zum Thema passenden Forum anlegen, welcher automatisch mit dieser Seite verknüpft wird.