Start

Weniger Komplexität — weniger BugsSimple Fails Less

Ich entwickle nun mehr als 20 Jahre Software und»Simple Fails Less« ist die auf 3 Worte komprimierte, persönliche Erkenntnis daraus. Weil ich diese Erkenntnis für fundamental wichtig halte, habe ich sie zu meinem Firmenmotto gemacht und sie auch auf meine Visitenkarte geschrieben.

Kontrolle von Komplexität

Meine Erkenntnis aus der Software-Entwicklung ist, dass Komplexität eine der größten Risikofaktoren ist. Am Anfang eines neuen Projekts (grüne Wiese) ist noch alles überschaubar und einfach. Jeder überblickt noch alles und ist zuversichtlich. Doch die Software wird über Monate und Jahre weiterentwickelt und damit steigt praktisch immer die Komplexität:

  • Neue Methoden werden in Klassen hinzugefügt.
  • Methoden werden verlängert.
  • Neue Fallunterscheidungen / Ausnahmen müssen ergänzt werden.

Und bevor man sich versieht, überschreitet die Code-Kapazität die Gehirnkapazität.

Wenn die Code-Komplexität größer ist als die Gehirnkapazität

Aber bevor wir über theoretische Sachen sprechen, schauen wir uns doch mal ein konkretes Beispiel an. Man beachte im folgenden Bild die Verschachtelungstiefe mit if-else und foreach. Versuchen Sie doch mal, die folgenden Fragen zu beantworten:

  • Was genau macht der Code?
  • Ist man hier zuversichtlich, dass man alle Eventualitäten überblickt?
  • Hat man hier ein gutes Gefühl, eine Erweiterung einzubauen?
private void Confirm(object input, string classification)
{
    if (input != null)
    {
        var inputType = input.GetType();
        foreach (var pi in inputType.GetProperties())
        {
            var attribute = pi.GetCustomAttributes(false)
                .Where(a => a is DatasetValidationAttribute)
                .Cast()
                .FirstOrDefault();
            var classificationName = classification + "." + pi.Name;
            if (string.IsNullOrEmpty(classification) == true)
            {
                classification = pi.Name;
            }

            var propertyValue = pi.GetValue(input);
            if (propertyValue != null)
            {
                if (pi.PropertyType.IsClass && pi.PropertyType != typeof(String))
                {
                    if (typeof(IEnumerable).IsAssignableFrom(pi.PropertyType) == false)
                    {
                        Confirm(propertyValue, classification);
                    }
                    else
                    {
                        var index = 0;
                        var enumerable = propertyValue as IEnumerable;
                        if (enumerable != null)
                        {
                            foreach (var item in enumerable)
                            {
                                var itemType = item.GetType();
                                if (itemType.IsClass && itemType != typeof(String))
                                {
                                    Confirm(item, classification+$"[{index}]");
                                }
                                else
                                {
                                    if (attribute != null && itemType == typeof(String))
                                    {
                                        StartConfirmation(attribute, classification + $"[{index}]",
                                            item.ToString());
                                    }
                                }

                                index++;
                            }
                        }
                    }
                }
                else
                {
                    if (attribute != null && pi.PropertyType == typeof(String))
                    {
                        StartConfirmation(attribute, classification, propertyValue.ToString());
                    }
                }
            }
        }
    }
}

In diesem — zugegebenermaßen krassen — Beispiel überschreitet die Code-Komplexität die Gehirnkomplexität bei Weitem. Hier wurde sicherlich der Zeitpunkt für eine Refakturierung lange verpasst.

Aber woran genau liegt es, dass dieser Code so komplex ist? Denken Sie mal auf einer ganz hohen Abstraktionsebene darüber nach. Als ich mal mit großem Abstand auf so einen Code geschaut habe, hat es »Klick!« gemacht und ich wusste die Antwort. Es ist ganz einfach: Man sieht den Wald vor lauter Bäumen nicht.

Signal-to-Noise Ratio

In der Physik gibt es die Messgröße Signal-to-Noise Ratio, die ein ähnliches Phänomen beschreibt. In der Praxis möchte man am liebsten ganz viel Signal und wenig Noise haben. Das Signal-to-Noise Ratio gibt an, wie das Verhältnis von »Signal« (will man viel von haben) und »Noise« (will man wenig von haben) ist. Mit anderen Worten: Je höher das Signal-to-Noise Ratio desto besser.

Auf die Programmierung übertragen bedeutet das:

  • Signal: Verständnis, was die eigentlichen Aktionen im Code sind
  • Noise: Alles, was dem Verständnis abträglich ist

Schauen Sie sich mal den nachfolgenden Code an und versuchen Sie zu verstehen, was dieser macht.

public void GeneriereLebenslauf(int benutzerId)
{
    FindeBenutzer(benutzerId)
        .OnSuccess(ErzeugeLebenslauf)
        .OnError(LogErrorMessage);
}

Ich nehme an, dass Sie keine große Schwierigkeiten hatten, zu erkennen, was die Absicht dieses Codes ist. Das liegt vor allem daran, dass ganz viel »Signal« da ist, also Codezeilen, bei denen Sie sofort erkennen, was sie machen sollen. Und es gibt ganz wenig »Noise«, was dem Verständnis abträglich ist.

Stellen Sie sich jetzt vor, dass die Methoden FindeBenutzer() und ErzeugeLebenslauf() ebenfalls nach diesem Muster gebaut sind; dann wird das Lesen und Verstehen des Codes ebenfalls keine Probleme bereiten.

Ich stelle jetzt einfach mal eine These in den Raum, die ich zwar nicht endgültig beweisen kann, die aber durch meine persönliche Erfahrung gestützt ist:

Komplexität ~ Chance auf Bugs

Oder in Worten:

Die Höhe der Komplexität ist proportional zu der Chance auf Bugs

Je höher also die Komplexität des Codes ist, desto mehr steigt die Wahrscheinlichkeit für Bugs. Oder ganz plakativ ausgedrückt:

Komplexität ist der Nährboden für Bugs.

Zwei Arten von Komplexität

Wir haben gesehen, dass Komplexität kontraproduktiv für stabile Software ist. Da liegt es nahe, dass wir analysieren, welche Arten von Komplexität es gibt und wie man diese reduzieren kann.

Zwei Arten von Komplexität

Wir haben also zwei grundlegende Arten von Komplexität:

  • Die Komplexität, die dem Problem an sich innewohnt
  • Die Komplexität, die vom Entwickler beigesteuert wird

An der ersten Komplexität kann man häufig nur schwer drehen, um sie zu verringern. Bei der letzteren Komplexität hat man mehr Einfluss. Und genau darum wollen wir uns jetzt kümmern. Wie können wir die unnötige Komplexität verringern? Die eine Möglichkeit habe ich oben schon beschrieben, indem wir den Code einfach lesbarer gestalten. Darüber hinaus habe ich zwei Komplexitätstreiber identifiziert, die großer Regelmäßigkeit in Projekten für Probleme sorgen.

Zwei weitere typische Komplexitätstreiber

Zwei Komplexitaetstreiber in der Software-Entwicklung

In den meisten Projekten, in denen ich gearbeitet habe, wurden Exceptions und Null durch die Bank weg eingesetzt. Kaum jemand hat irgendein Problem damit gesehen. Und doch kam es praktisch immer zu Problemen mit NullReferenceExceptions oder es traten Exceptions auf, um die sich niemand im Call Stack gekümmert hat. Die folgenden Abschitten beleuchten, warum ich von der Verwendung von Null und Exceptions dringend abrate.

Was ist so schlimm an »Null«?

I call it my billion-dollar mistake. It has caused a billion dollars of pain and damage in the last forty years.

– Tony Hoare

Tony Hoare hat nach eigenen Aussagen der Versuchung nicht widerstehen können, in ALGOL eine Null Reference einzubauen, weil es so einfach zu implementieren war. Dies führte in unzähligen Programmen zu Fehlern, Sicherheitslücken und Systemabstürzen, deren Kosten wohl in Milliradenhöhe waren.

Bis heute erfreut sich Null in vielen objekt-orientierten Programmiersprachen größter Beliebtheit. Warum dem so ist, kann ich beim besten Willen nicht nachvollziehen. Meine Erfahrung lautet:

Wenn Null eingesetzt wird, kommt es zu NullReferenceExceptions — meistens zu den ungünstigsten Zeitpunkten.

Jeder Programmierer von OOP sollte doch ein großes Interesse an Objekten haben und nervös werden, wenn es keine Objekte gibt. Null ist nichts, auch kein Objekt. Auf Objekten kann man Operationen auslösen; wenn man das auf null macht, fliegt einem das um die Ohren. Warum verwenden dann so viele Programmierer Null? Antwort: Ich habe keine Ahnung. Erfolg kann es nicht sein.

Auch die Übersichtlichkeit leidet darunter, weil man ja immer auf null testen muss, wenn eine Methode ein Objekt zurückliefert, das null sein kann. Und dann sind wir wieder beim Signal-to-Noise Ratio.

Dass so viele Programmierer null lieben, ist auch vor einem anderen Hintergrund schwer nachzuvollziehen. Die meisten C#-Programmierer lieben LINQ und wie man mit Fluent-Notation IEnumerables bearbeiten kann. Man stelle sich vor, da könnte auch null zurückgeliefert werden und man müsste darauf prüfen. Die Eleganz wäre sofort dahin!

Wer weitere Gründe haben möchte, warum man Null aus seinem Repertoire streichen sollte, dem empfehle ich meinen Extra-Artikel über Null zu studieren.

Was ist so schlimm an »Exceptions«?

Für Exceptions gilt dasselbe wie für Null — nur noch schlimmer. Eine Exceptions reißt den natürlichen Programmablauf aus dem Raum-Zeit-Kontinuum und spült einen im Call Stack nach oben in der Hoffnung, irgend jemand kümmert sich darum. Ja mehr noch: man hofft, dass diese Instanz auf kompetent genug ist, eine sinnvolle Ausnahmebehandlung zu machen.

Je weiter man im Call Stack nach oben kommt, desto unwahrscheinlicher ist es, dass die Ausnahme gut verarztet werden kann. Nicht selten findet man ganz oben in main() ein try/catch all, das streunende Exceptions davon abhalten sollen, an die Oberfläche zu kommen.

Ich kenne keinen Entwickler, der in seinen Programmen goto verwendet, dafür viele, die ohne mit der Wimper zu zucken Exceptions einsetzen. Wenn man es genau nimmt, sind Exceptions noch schlimmer als goto, weil man bei goto wenigstens genau weiß, an welcher Stelle das Programm fortgesetzt wird.

Eine gruselige Anwendung von Exceptions, die gar nicht so selten ist, sieht man hier:

public Benutzer SucheBenutzer(string suchtext)
{
    var benutzer = _controller.SucheBenutzer(suchtext);
    if(benutzer == null)
    {
        throw new UserNotFoundException(suchtext);
    }
    return benutzer;
}

Wenn man überhaupt Exceptions verwendet, sollte man sie nur für tatsächliche Ausnahmen verwenden; wenn also das System mit etwas konfrontiert wird, mit dem es nicht gerechnet hat und auch nicht mehr weitermachen kann. Dass man nach einem Benutzer sucht, den es nicht gibt, ist ein erwartetes Ergebnis. Dieses sollte man nicht mit einer Exception quittieren.

Der Aufruf einer solchen Methode erfordert es jedes Mal, ein try/catch zu verwenden. Und dann sind wir wieder beim Signal-to-Noise Ratio.

Wer weitere Gründe haben möchte, warum man Exceptions aus seinem Repertoire streichen sollte, empfehle ich meinen Extra-Artikel über Exceptions zu studieren.

Software-Entwicklung ist immer mit Risiko verbunden. Selbst wenn ein Programm seit Monaten stabil läuft, kann der Tag kommen, an dem es mit einem Fehler aussteigt. Die Frage ist nicht, ob das passiert, sondern wann.

Zusammenfassung

Über das Thema Komplexität in der Software-Entwicklung könnte man sicher ganze Bücher schreiben. Ich habe mich bei diesem Artikel auf ein paar wenige Aspekte konzentriert, die ich in meiner täglichen Praxis in Projekten antreffe. Die wichtigsten Erkenntnisse sind:

  • Komplexität ist der Nährboden für Bugs.
  • Komplexität muss kontrolliert werden, bevor es zu spät ist.
  • Zwei Komplexitätstreiber sind »null« und »Exceptions«.
  • Weniger Komplexität — weniger Bugs
  • Weniger Bugs — mehr Akzeptanz, weniger Kosten, mehr Profit

Firma

di - IT Consulting
Dirk Illenberger
Bismarckstr. 24
61169 Friedberg

© 2021 di - IT Consulting, Dirk Illenberger