Wartbare Software durch übersichtlicheren Code

Lesbarer Code, auch noch nach Jahren
Kommentare

In unserer heutigen IT-Landschaft treffen wir immer häufiger auf Altlasten vergangener Tage, auf teilweise unlesbaren Sourcecode und auf Projekte, die nicht mehr zu retten sind. Das liegt neben vielen anderen Gründen auch an der Wartbarkeit des Sourcecodes. In diesem Artikel möchte ich üblichen Fehlern auf die Schliche kommen und versuchen aufzuzeigen, was wir als Entwickler tun können, um unser Produkt übersichtlicher und wartbarer zu gestalten.

Ende letzten Jahres erhielt ich Einblick in ein Projekt mit vermeintlich „modernem“ C#-Code, voller tief verschachtelter Linq-Abfragen, mit massenhaft Linq Extensions und Lambda-Code. Die Software benötigte an manchen Stellen bis zu zwölf Minuten, um eine vergleichsweise kleine Datenmenge von 50 000 Datensätzen zu laden. Nachdem ich rund 30 Prozent des Sourcecodes mit älteren, aber effektiveren Methoden optimiert und refaktorisiert hatte, konnte ich die Performance an den langsamsten Stellen um bis zu 600 Prozent steigern. Solche Projekte sind vielleicht nur Extrembeispiele, zeigen aber dennoch auf, wohin schlechter oder unüberlegter Code führen kann, besonders wenn er sich durch mehr als 200 000 Zeilen zieht.

Diese Probleme entstehen beinahe immer auf dieselbe Art und Weise. Am Anfang gibt es ein Projekt oder Produkt, das in kurzer Zeit und möglichst günstig entwickelt werden muss. Solange sich die Entwicklung noch am Anfang befindet, funktioniert das auch ganz gut und Feature um Feature wird hinzugefügt. Aber im Laufe der Zeit wird die technische Schuld größer und größer, der Aufwand höher und das Risiko einer Verschiebung im Terminplan oder der Kostensteigerung steigt rasant an. Das führt zu dem oben beschriebenen Szenario und wirkt sich auf den Projektplan im letzten Viertel des Projekts stark kontraproduktiv und verlangsamend aus. Eine achtsamere Herangehensweise wirkt dagegen beschleunigend. Die Entwicklungszeit nimmt im Laufe des Projektplans ab, Features sind schneller hinzugefügt und es kann mehr Zeit in die weniger sichtbaren Aufgaben wie z. B. die Dokumentation gesteckt werden.

In diesem Artikel möchte ich im Besonderen auf die Codequalität als solches eingehen und weniger auf architektonisch verursachte technische Schuld.

Besser werden

Vor einigen Jahren schrieb Scott Meyer das Buch „More effective C++“, bestückt mit vielen kleinen, aber hilfreichen Tipps. Heute ist es zwar nicht mehr ganz neu, aber immer noch zeitgemäß. Guter Code ist nicht so sehr abhängig von modischen Strömungen und tollen Neuerungen einer Sprache, sondern soll Software wartbar gestalten. Zwar kommen immer modernere Methoden zu den verwendeten Sprachen hinzu und die Hardware zur Gestaltung dieser Software wird immer leistungsfähiger, aber im Idealfall sollte eine Software etwa fünf Jahre lang gut funktionieren und möglichst schnell durch Bugfixes verbessert werden. Guter Code hilft uns, die Software so zu gestalten, dass sie auch noch in drei Jahren, wenn sich vielleicht die Hälfte des Entwicklerteams geändert hat, effektiv verbessert werden kann. Danach kann man immer noch eine ganz neue Softwareversion auf Basis der alten entwickeln und einen Großteil des Codes in die neue Version einfließen lassen.

Gerade in Zeiten von wunderbaren Tools wie ReSharper oder den Toolsammlungen von RedGate, Telerik oder DevExpress ist sauberer Code kein Zwang mehr, aber immer noch ein Muss. Solche Tools helfen uns, die Übersicht zu bewahren, Code modern zu gestalten und Coderegeln über ganze Projekte hinweg zu führen. Aber wir verlernen dadurch auch in gewissen Maßen, wie man selbst sauber entwickelt. Wer das nicht glaubt, muss nur mal sein Lieblingsprojekt unter Linux oder Mac OS X mit Mono und einem normalem Editor (meinetwegen darf dazu auch Vi verwendet werden) aufrufen. Spätestens jetzt fehlen einem IntelliSense und andere Funktionen. Im weiteren Verlauf möchte ich einige Tipps mit Ihnen teilen, die ich im Laufe der Jahre aus verschiedenen Projekten herausziehen konnte.

this

Der this-Zeiger ist heute leider zu Unrecht verpönt – abgeschrieben durch uns Entwickler aufgrund einer Vielzahl an hilfreichen Tools und weil er vom Compiler nicht mehr benötigt wird. Viele verwenden this daher nicht mehr, aber das halte ich auch für falsch. this ist zwar nicht mehr unbedingt notwendig, gibt dem Betrachter aber die Möglichkeit, auch ohne vollständige IDE oder installierte Tools (ReSharper etc.) zu erkennen, ob eine Klassenvariable, ein privater oder öffentlicher Member oder eine Methode angesprochen wurde. Zwar können saubere Naming Conventions schon helfen, die Übersicht zu behalten. Dennoch tritt dann wieder das bekannte „Wald vor lauter Bäumen“-Problem auf. Und gegen den Unterschied zwischen statischen Methoden und Members helfen noch nicht einmal die Conventions.

Geschweifte Klammern und Zeileneinrückung

Geschweifte Klammern werden von uns faulen Entwicklern auch gerne weggelassen. Einzeilige if-Abfragen, while-Schleifen oder usings sind so einfach schick und schnell geschrieben. Aber das fördert nicht gerade die Übersichtlichkeit des Codes und die Wartbarkeit der gesamten Software leidet weiter. Saubere Schachtelung und Zeileneinrückung (und bitte nicht mit Leerzeichen, sondern sauber mit vier Zeichen breiten Tabs) erhöhen nicht nur die Codequalität, sondern vor allem die Lesbarkeit.

List someList = new List();

using(SqlConnection connection = new SqlConnection(myConnectionString))
using(SqlCommand command = connection.GetCommand())
using(SqlDataReader reader = command.ExecuteReader())
while(reader.Read())
someList.Add(reader["Spalte1"].ToString())

Die fehlenden Klammern und Zeileneinrückungen in Listing 1 lassen den Block wie eine Ansammlung von einzelnen Aufrufen aussehen. Falls nun jemand diese Funktion erweitern oder vielleicht auf das Ergebnis in Spalte 1 reagieren muss, sei es wegen eines DBNull.Value oder um es als einen anderen Datentyp auszulesen, muss er eigentlich nur wenig machen. Dennoch haben wir hier eine sehr große Fehlerquelle, da schnell die fehlenden Einrückungen sowie Klammern übersehen werden und einfach nur die letzte Zeile bearbeitet wird, was jedoch die Logik des Codeblocks verändert.

List someList = new List();

using(SqlConnection connection = new SqlConnection(myConnectionString))
{
  using(SqlCommand command = connection.GetCommand())
  {
    using(SqlDataReader reader = command.ExecuteReader())
    {
      while(reader.Read())
      {
        // Lies Spalte1 als String aus
        // TODO: Wert ist eigentlich ein Double, bitte anpassen.
        someList.Add(reader[„Spalte1“].ToString())
      }
    }
  }
}

Sehen wir uns den Block nun mit Einrückung und Klammern an (Listing 2). Man sieht sofort, wo welcher Befehl hingehört, weiß, wo man erweitern muss und kann das Ganze durch Kommentare sogar noch sauberer gestalten. Ein „TODO:“-Kommentar erinnert uns dann noch daran, bestimmte Passagen nachträglich anzupassen. Eine Erweiterung des Codes ist ohne Probleme und mit niedriger Fehleranfälligkeit möglich.

! >= == false

Ich habe es jahrelang selbst eingesetzt, aber im täglichen Stress leider auch schon genauso oft beim Debuggen in kritischen Projekten überlesen: das Ausrufezeichen, den !-Operator. Leider wird gerade dieser Operator in einer Ansammlung von booleschen Abfragen zu schnell übersehen (Listing 3).

// Das ! wird schnell überlesen
if(!this.listView.Visible
&& thist._shouldBeVisible
&& !this._isInitialized)
{
  // Tue etwas...
}

// == false sieht man bei der späteren Fehlersuche schneller
if(this.listView.Visible == false
&& thist._shouldBeVisible
&& this._isInitialized == false)
{
  // Tue etwas ...
}

[ header = Seite 2: Equals und Compare ]

Equals und Compare

Was in der Java-Welt schon immer eingesetzt wird (auch wenn es hier aus anderen Gründen passiert), findet langsam auch immer mehr Anklang in der .NET-Welt. Equals und Compare sind Methoden zum Vergleich von Objekten, die semantisch grundsätzlich identisch sind. Anders als bei ==, >, <, >= und <= werden die Objekte hier aber direkt miteinander vergleichen. Das kann unter Umständen dazu führen, dass Projekte, die mit unsicherem Code kompiliert werden, zu weiteren Fehlern führen. Achten wir aber auf diese feinen Unterschiede, erreichen wir eine höhere Codequalität. Unter .NET sollte man allerdings einfach bei den normalen Operatoren == und Co. bleiben.

Linq Extensions überdenken

Beinahe jede Linq Extension iteriert einmal vollständig durch die abgefragte Collection. Erst ein Any, dann ein Where, ein OrderBy und zum Schluss ein FirstOrDefault oben darauf. Das kann zwar etwas unschöner, aber performanter mit einem einfachen foreach umgesetzt werden, wodurch ein IEnumerable nur einmal iteriert werden muss. Nun gibt es auch einige spezielle Listentypen, die teilweise eine viel bessere Performance liefern als die normale generische Liste. Schleifen und Iterationen sind oftmals schneller, wenn man eine LinkedList anstelle einer normalen List einsetzt. Zwar sollte man dann auch die Knoten der List verwenden und nicht einfach nur iterieren, aber dann bekommt man die volle Performance geliefert.

Dasselbe gilt in verschiedenen Situationen mit anderen Listen und Dictionaries. Eventuell ist ein Dictionary in einer bestimmten Situation schneller, weil man den Key nutzen kann. Solche Ideen kann man einfach im Hinterkopf behalten und gegebenenfalls einsetzen.

Linq ist wirklich toll – wenn es kontrolliert eingesetzt wird

Wie im vorherigen Punkt schon angesprochen, sind die durch den Linq-Namespace eingebrachten Extensions nichts anderes als Methoden, die eine Funktion innerhalb einer Iteration durch ein IEnumerable ausführt. Das passiert wie jede generelle Linq-Abfrage auch nur intern und durch den Debugger unerreichbar. Und das soll hier auch der Punkt sein: Linq ist nicht debuggbar.

Wie anfangs erzählt, hatte ich ein Projekt vorliegen, das mittelgroße Datenmengen verarbeiten muss. Diese Daten wurden mit vielen Kind-Objekten dargestellt. Ich kam während des Refaktoring-Prozesses an vielen Stellen an den Punkt, wo ich die mittels Linq verwendeten Daten debuggen musste. Oft war der Fehler erst im Kind-Objekt des Kind-Objekts des Kind-Objekts des Objekts, das gerade im Iterator aktiv ist, zu finden, oder der Datensatz 13743 enthielt einen minimal fehlerhaften Eintrag, der aber größere Auswirkungen hatte. Solche Fehler sind mit Linq ohne fremde Hilfsmittel beinahe unmöglich zu ermitteln. Conditional Breakpoints sind nicht anwendbar.

Durch Linq muss man sozusagen blind entwickeln, der Code muss direkt funktionieren, eine andere Option gibt es nicht. Und die verarbeiteten Daten werden definitiv als korrekt erwartet, Fehler in diesen Daten werden nicht behandelt.

Pragmatismus vor Schönheit

Ich habe früher stets den Ansatz „Code ist Poesie“ verfolgt. Bis ich Software nachhaltig, wartbar und wiederverwendbar entwickeln musste. Einer erfolgreichen Anwendung ist es egal, ob der Code vor Schönheit, modernen Codekonstrukten und komplexen Abhängigkeiten nur so strotzt. Und dem Kollegen aus der Wartung, der die nächsten zwei Jahre Patches entwickeln darf, ebenfalls. Je einfacher und pragmatischer der Code ist, desto wartbarer wird er. Jeder kann den Code dann lesen, auch der Werkstudent, der Praktikant und der Berufseinsteiger, der ab sofort an deinem Projekt mitarbeiten soll.

In der Medizin gibt es einen Merksatz: „Häufiges ist häufig und Seltenes ist selten“. So sollten wir es auch in der Softwareentwicklung handhaben. Sparen Sie an komplizierten exotischen Konstrukten und bleiben Sie bei einfachen Lösungen. Ihr zukünftiges Ich wird es Ihnen danken.

„var“ vs. echte Typen

Ein großer Vorteil von Programmiersprachen wie Java, C++ und eben auch C# ist die starke Typisierung. Eine gewisse Dynamik existiert in C# allerdings bereits mit dem Datentyp object, da alles in .NET ein Objekt ist, das letztendlich von object ableitet. Seit .NET 3.5 existiert nun der Datentyp var, der einen schwach typisierten Weg verfolgt und eine variable Initialisierung anbietet. Ursprünglich wurde dieser Datentyp für Linq entwickelt, um die dynamischen Ergebnisse einer Linq-Abfrage zu enthalten, denn je nachdem, ob man nun ein group oder ein order by an die Query anhängt, kommt eine andere Liste zurück. Mit der Zeit entwickelte sich hieraus aber der Drang, alle Variablen mit var zu initialisieren, wenn die Zuweisung direkt den Datentyp ebenfalls darstellt. Darunter leidet aber neben der Performance ganz stark die Lesbarkeit und damit die Wartbarkeit des Codes (Listing 4). Manchmal ist der Datentyp auch sehr praktisch, gerade bei unpraktischen Objekten wie KeyValuePair oder Tulpe, aber oft geschieht seine Verwendung eher aus schlichter Faulheit.

// Beispiel 1. Unterschiede in der Initialisierung:
// Mit var:
var myValue = 0;
// Mit Standardinitialisierung:
double myValue = 0;
// Mit starker Initialisierung:
double myValue = 0d;

// Beispiel 2. Eine Liste aus einem Repository abfragen:
// Mit var:
var result = UserRepository.GetAllUser();
// Mit expliziter Initialisierung:
List result = UserRepository.GetAllUser();

// Beispiel 3. Mehrere Variablen:
var value = 0;
var success = false;
var type = "Hallo Welt!";
// Beispiel 4. foreach eines Dictionaries
// Mit var:
foreach(var pair in myDictionary)
{}
// Mit KeyValuePair
foreach(KeyValue pair in myDictionary)
{}

Der Datentyp var hat aber auch viele Vorteile:

  • Wenn eine temporäre Variable benötigt wird, kann man mit var das Importieren eines nur für diese temporäre Variable benötigten Namespace verhindern.
  • var bringt den Entwickler dazu, seine Variablen stärker zu benennen. Aus UserAccountDataMapper mapper wird var userAccountDataMapper, was im späteren Kontext des Codes wiederum der Lesbarkeit zugutekommt.
  • var kann so genanntes „Rauschen“ und unsauberen Code durch Vereinfachung verhindern, wie z. B. um durch ein Dictionary zu iterieren.

Daher gilt die Einschränkung nicht durchgehend. Solange sich der Einsatz von var und typisierten Variablen die Waage hält, ist man auf dem richtigen Weg.

Abstraktion richtig einsetzen

Es gibt Projekte, die ausschließlich aus Interfaces bestehen. Dahinter steckt zwar immer eine Implementation, diese ist aber nicht sichtbar, wenn in der Anwendung selbst nur auf die Interfaces zugegriffen wird. Properties werden in Form von IIrgendwas oder List verwendet. Dadurch wird die Entwicklung behindert (z. B. durch eingeschränkte Nutzbarkeit von IntelliSense) und insbesondere auch das Debugging. Interfaces sind zur Beschreibung von Klassen da und nicht zur Abstrahierung. Dafür sind abstrakte Klassen wesentlich besser geeignet.

Test-First

Testen ist oft eines der Übel, denen wir uns als Entwickler stellen müssen, Übel deshalb, weil wir die Testfunktionen meist zu spät ins Projekt einbinden und ihre Implementierung dann meist nur noch Fleißarbeit ist. Dabei muss das nicht so laufen. Wenn die Entwicklung nach dem Test-First-Prinzip durchgeführt wird, ist der Test kein Übel, sondern ein hilfreiches Werkzeug. Die Architektur der Anwendung gewinnt an Struktur und Stabilität, der Code wird sauberer entwickelt und die Wartbarkeit dadurch sukzessiv erhöht.

Clean Code Development

Die von Ralph Westphal in Deutschland gestartete Initiative „Clean Code Developer“ darf und sollte uns allen ein Leitfaden im tagtäglichen Entwicklerdasein sein. Die verschiedenen farblichen Bänder sind dabei ein Kreislauf, den wir immer wieder durchlaufen sollten und der uns zu besseren und gewissenhafteren Entwicklern macht. Sich an den Leitfaden zu halten, ist nicht immer leicht und nicht jede Aufgabe kann durchgeführt werden, aber wenn wir auch nur einen Bruchteil davon berücksichtigen, steigern wir unsere Arbeitsergebnisse.

Fazit

Wir haben gesehen, dass wir schon mit wenig Aufwand unsere Arbeit verbessern können und damit nachfolgenden Kollegen und auch uns selbst helfen, Wartungsaufgaben mit weniger Zeit- und Kostenaufwand umzusetzen. Nicht jeder Tipp ist in jeder Situation sinnvoll, aber die Summe kann als Leitfaden herhalten.

Ich habe alle in diesem Artikel aufgeführten Methoden sowie viele weitere in einem Beispielprojekt bereitgestellt. Der Quelltext steht in meinem GitHub Repository und auf meiner privaten Webseite zum Download zur Verfügung.

Unsere Redaktion empfiehlt:

Relevante Beiträge

Meinungen zu diesem Beitrag

X
- Gib Deinen Standort ein -
- or -