Was ist komplexer Code – und wie du ihn vermeiden kannst

von Sergej Kukshaus vor 5 monaten

Das schöne Wort „komplex“ ist für die meisten von uns ein Synonym für Kompliziert.

Dabei haben die beiden Worte bis auf das „Kompl“ nichts miteinander zu tun. Naja, jedenfalls nicht viel.

Etwas Komplexes kann natürlich auch kompliziert sein. Und etwas Kompliziertes ist in den meisten Fällen auch komplex. Sonst wäre es nicht mehr kompliziert.

TL;DR

Wenn du die SOLID Prinzipien befolgt und TDD betreibst, dann hast du keinen komplexen Code. Basta!

Was ist komplex und was ist kompliziert?

Auch wenn ich oft über die Deutsche Sprache meckere, hat diese doch ein Vorteil. Die Wörter sind klar definiert und haben (meistens) eine einzigartige Bedeutung. Die erste Anlaufstelle für „Fremdbegriffe“ ist für mich meist der Duden.

Wann ist eine Sache kompliziert?

Der Duden sagt:

schwierig; verwickelt; [aus vielen Einzelheiten bestehend und daher] schwer zu durchschauen und zu handhaben

duden.de

Ich glaube, ich spreche ein anders „Deutsch“ als alle anderen. Jedenfalls in meiner eigenen Welt 🙂

Kompliziert ist für mich etwas, wofür ich mein Hirn kräftig anstrengen und mich konzentrieren muss nichts kaputtzumachen. Meistens geht das natürlich schief.

Ich habe die Tage einen Termin in einer Auto-Werkstatt. Mein Auto verlangt nach einem Ölwechsel. Warum? Weil ich es nicht selber kann. Es ist für mich zu kompliziert.

Für den Kfz-Mechaniker (pardon Kfz-Mechatroniker) ist das ein Klacks. Er hat es schon unzählige Male gemacht.

Kompliziertheit in ein Maßstab für Unwissenheit.

Wann ist eine Sache komplex?

Hier sagt der Duden:

vielschichtig; viele verschiedene Dinge umfassend; zusammengesetzt; nicht allein für sich auftretend, ineinandergreifend, nicht auflösbar

duden.de

Beim Lesen dieser Begriffe habe ich sofort gedacht „Ich brauche ein neues Nachschlagewerk“.

Besonders gut hat mir der Begriff „vielschichtig“ gefallen.

Ich habe noch nie meine Frau sagen hören:

Schatz, ich habe heute einen komplexen Salat gemacht. Bestehend aus Eisbergsalat, Tomate und Gurke.

Meine Kinder, haben viele Bücher. Diese wurden aus verschiedenen Seiten zusammengesetzt. Diese sind ebenfalls nicht komplex.

Greifen Zahnräder ineinander? Ja. Sind Zahnräder komplex? Nein.

Und was heißt es jetzt nun?

Komplex ist im Grunde genommen alles, was nicht zu 100% wiederholbar ist.

Ein Fußballspiel ist komplex.

Nehmen wir mal die Bilazn FCB vs BVB. Von bisherigen 123 Spielen, hat der BVB „nur“ 33 Spiele gewonnen. Die Bayer 60. Wäre Fußball nicht komplex, würde immer nur eine Manschaft gewissen.

Ist Fußball kompliziert? Nein. Jeder kennt die Regeln (jedenfalls die meisten davon). Ist Fußball komplex? Ja. Vor dem Spiel kennt keiner das Endergebnis.

Komplexität ist ein Maßstab für Überraschungen.

Was ist komplexer Quellcode?

Zunächst einmal, möchte ich kurz sagen, was komplizierter Code ist.

Es ist der Code, den man nicht versteht. Je länger mal liest, desto verständlicher und unkomplizierter wird er.

Code Komplexität ist per Definition eine Kennziffer. Eine Kennziffer für die Anzahl der Verzweigungen. Man nennt das auch „Zyklomatische Komplexität“

Eine Funktion hat eine Basiskomplexität von eins. Mit jeder Bedingung (if, else, case) und jeder Schleife (for, foreach, while, do-while) und für logische Operatoren wie && und || erhöht sich die Komplexität um eins.

Schauen wir mal ein wenig auf diesen Pseudocode:

function PersonSpeichen(person) {
	verbindung = DatenbankVerbindungHerstellen()
	verbindung.Öffnen()
	
	if (person.NeuePerson) {
		verbindung.Ausführen("INSERT INTO Personen ...");
		
		foreach(person.Kinder als kind) {
			verbindung.Ausführen("INSERT Personen ...");
			kind.Id = verbindung.LetzteIdAuslesen()
		}
		
	} else {
		verbindung.Ausführen("UPDATE Personen SET ... WHERE Id = person.Id");
		
		foreach(person.Kinder als kind) {
			if (kind.WurdeBearbeitet) {
				verbindung.Ausführen("UPDATE Personen SET ... WHERE id = kind.Id");
			}
			if (kind.NeuePerson) {
				verbindung.Ausführen("INSERT Personen ...");
				kind.Id = verbindung.LetzteIdAuslesen()
			}
		}
	}
	
	verbindung.Schließen();
}

Bereits dieses einfache Beispiel hat eine Komplexität von 7.

Doch warum ist das jetzt komplex? Weil es ebenfalls nicht Vorhersehbar ist.

Man kann nicht auf den ersten Blick sagen, ob die Person hinzugefügt oder aktualisiert wird. Ebenso lässt sich auch nicht sagen, ob überhaupt eins der Kinder aktualisiert werden oder nicht.

Auch lässt sich nicht sagen, unter welchen Umständen wurde das Kind neu angelegt?

Je höher die Komplexität, desto höher das Fehlerrisiko

Im Allgemeinen gilt:

  • 1-10: Nicht komplex, geringes Fehlerrisiko
  • 11-20: Mittlere Komplexität, moderates Fehlerrisiko
  • 21-50: Hohe Komplexität, hone Wahrscheinlichkeit eines Fehlers
  • > 50: Sehr hohe Komplexität, sehr hohe Wahrscheinlichkeit eines Fehlers.

Allerdings habe ich schon Funktionen gesehen, die selbst bei einer Komplexität von 10 bereits unwartbar waren. Genauso auch Methoden mit einer Komplexität von über 30, die super verständlich aufgebaut wurden.

Die Bewertung hat nämlich eine Schwachstelle.

Das switch-case. Bei jedem case erhöht sich automatische die Komplexität um 1.

Schauen wir mal das folgende Codebeispiel an:

function BerechneWochentag(int nummer) {
	switch(nummer) {
		case 1: return "Montag";
		case 2: return "Dienstag";
		case 3: return "Mittwoch";
		case 4: return "Donnerstag";
		case 5: return "Freitag";
		case 6: return "Samstag";
		case 7: return "Sonntag";
		default: return "Unbekannt";
	}
}

und dann noch einmal dieses hier:

function BerechneWochentag(int nummer) {
	string[] wochentage = ["Montag", "Dienstag", "Mittwoch",
		"Donnerstag", "Freitag", "Samstag", "Sonntag"];
	
	return wochentage[nummer+1];
}

Beide tun (fast) exakt dasselbe. Doch während das obere eine Komplexität von 8 hat, hat das untere nur eine Komplexität von 1.

Doch was davon ist in deinen Augen lesbarer?

Das paradoxe ist, der untere Quellcode, ist nicht ausfallsicher. Man benötigt 2 if-Bedingungen (Nummer muss zwischen 1 und 7 sein), um den Code abzusichern. Dies wäre also schon eine Komplexität von 3.

Wie vermeidet man komplexen Quellcode?

Aus meiner Sicht, ist die Frage relativ einfach zu beantworten.

Wenn du den Quellcode liest, musst du dich nur Fragen „was passiert hier“ und nicht „wie wird das (unbekannte) Ziel erreicht?“.

Ich habe mal das obere Beispiel etwas umgeschrieben. Nun sieht das so aus:

function PersonSpeichen(person) {
	PersonAnlegenOderAktualisieren();
	KinderSpeichern(person.Kinder);
}

function PersonAnlegenOderAktualisieren(person) {
	if (person.NeuePerson) {
		PersonAnlegen(person);
	} else {
		PersonAktualisieren(person);
	}
}

function PersonAnlegen(person) {
	if (person.NeuePerson) {
		verbindung.Ausführen("INSERT Personen ...");
		person.Id = verbindung.LetzteIdAuslesen()
	}
}

function PersonAktualisieren(person) {
	if (!person.NeuePerson) {
		verbindung.Ausführen("UPDATE Personen SET ... WHERE Id = person.Id");
	}
}

function KinderSpeichern(person) {
	foreach(person.Kinder als kind) {
		PersonAnlegenOderAktualisieren(kind)
	}
}

Schau dir mal die erste Funktion an. Du weißt sofort was passiert. Wenn dich das wie interessiert, kannst du gerne in die Methode navigieren.

Den Verbindungsaufbau habe ich jetzt mal entfernt, da es nicht die Aufgabe der Funktion ist, eine Verbindung aufrechtzuerhalten. Es soll nur speichern.

Wenn keine Verbindung vorhanden ist, muss der Aufrufer entsprechend (durch eine Exception :-)) darauf hingewiesen werden.

Und was ist mit Legacy Systemen?

Vor einiger Zeit hielt ich ein sehr schönes Buch (Working Effectively with Legacy Code) in den Händen. Auf der Rückseite standen die inspirierenden Worte:

Is your code easy to change? Can you get nearly instantaneous feedback when you do change it? Do you understand it? If the answer to any of these questions is no, you have legacy code, and it is draining time and money away from your development efforts.

MICHAEL C. FEATHERS

Laut dieser Aussage, ist jeder Code, den man nicht versteht (Komplizierter Code) gleichzeitig Legacy Code. Im Umkehrschluss heißt es, man muss dafür sorgen, dass jeder den Quellcode versteht.

Dies Erreicht man nur, wenn man die Komplexität auf ein Minimum reduziert.

Richard Bellairs hat einen lesenswerten Artikel zum Thema Legacy Code geschrieben.

Refactoring gehört irgendwo zu meinen Lieblingsaufgaben. Ich weiß nicht wieso, ich mag es einfach 🙂

Das Thema an sich ist jedoch viel zu komplex (haha) für diesen Beitrag. Dies würde ich dann in einem separaten Artikel behandeln wollen.

Zu guter Letzt fällt mir noch ein Spruch ein, den ich immer wieder meinen Azubis und Praktikanten sage:

Wie isst man einen Elefanten? – Stück für Stück.

Eine Software, die aus mehreren Hundert Tausend Zeilen Quellcode besteht, kann man nicht von heute auf Morgen komplett schön machen. Dafür benötigt man Zeit. Viel Zeit.

Wenn man jedoch nicht damit anfängt, wird es auch immer ekeliger Code bleiben. Schlimmer noch. Der Code wird immer schlimmer und schlimmer.

Fazit – Code Komplexität

Kommen wir langsam zum Schluss.

Einen komplexen Code kann man nicht auf den ersten Blick durchschauen. Man liest mehrere Befehle und überlegt: „Was könnte der Entwickler sich hierbei gedacht haben?“.

Um dies zu vermeiden, muss man den Quellcode sauber halten.

Am besten macht man das, indem man die Methoden klein hällt.

Ist das Kind schon in den Brunnen gefallen, versucht man trotzdem die Methoden zu verkleinern.

Immer und immer wieder. Und das so lange, bis man irgendwann mal sehr schönen Quellcode hat.

Sei der erste und teile deine Meinung mit der Welt!
... und was meinst du dazu?
Deine E-Mail-Adresse wird nicht veröffentlicht.