Zukunftsfähigkeit I – Korrektheit/Maturität

Unsauberer Code erfüllt Anforderungen. Er stellt gewünschtes Verhalten her. Das ist für den Kunden von höchster Wichtigkeit und vergleichsweise leicht zu überprüfen.

Clean Code erfüllt dieselben Anforderungen – und noch mehr. Clean Code steht dirty code in Sachen Verhalten in nichts nach. Darüber hinaus jedoch erfüllt er auch noch die Anforderung Zukunftsfähigkeit. Die ist dem Kunden (und auch der Softwareentwicklung) allerdings oft nicht bewusst. Außerdem ist sie nicht so leicht zu überprüfen wie das Verhalten.

Verhalten ist eine Laufzeitanforderung. Zukunftsfähigkeit hingegen ist eine Lebenszeitanforderung. Es geht um mehr als das Hier und Heute; es geht um Morgen und Übermorgen.

Clean Code Development bedeutet daher, Software so zu entwickeln, dass sie nicht nur aktuelle Verhaltensanforderungen erfüllt, sondern auch derzeit noch unbekannte in der Zukunft erfüllen können wird.

Dafür muss Software zwei Eigenschaften jenseits von Funktionalität und Effizienz haben und behalten:

  • Korrektheit und
  • Wandelbarkeit

Korrektheit scheint auf der Hand zu liegen. Ist sie nicht sogar eine Verhaltensanforderung? Grundsätzlich ist das richtig, doch die Erfahrung zeigt, dass fehlende Korrektheit sich nicht unbedingt im Moment der Abnahme von Software durch den Kunden zeigt. Inkorrekte Software geht allzu oft in Produktion; Fehler zeigen sich dann in einem Moment, wenn es gar nicht gut passt, weder für den Kunden/Anwender, noch für die Softwareentwicklung. Mangelhafte Korrektheit hat mithin Auswirkung auf die Zukunft.

Wandelbarkeit hingegen liegt nicht auf der Hand. Ist Software ihrer immateriellen Natur nach nicht ohnehin wandelbar? Oder ergibt sich Wandelbarkeit nicht mehr oder weniger, wenn man recht fleißig objektorientiert arbeitet? Kunden, Management und selbst Entwickler stellen diese Fragen. Wie die Praxis früher oder später jedoch zeigt, ist die Antwort darauf ein klares Nein. Nein, Wandelbarkeit, also Offenheit für Veränderungen, ergibt sich nicht von allein, auch nicht durch gut gemeinte Objektorientierung. Wandelbarkeit muss vielmehr ganz bewusst und systematisch hergestellt werden. Solange das nicht geschieht, verdunkelt jede Änderung am Code die Zukunft einer Software etwas mehr. Der Aufwand, Veränderungen vorzunehmen, seien das Bug Fixes oder Erweiterungen, steigt dann unverhältnismäßig.

Maturität als Aspekt der Korrektheit

Korrektheit mag als Anforderung auf der Hand liegen und jeder gewissenhafte Entwickler wird sich auch um sie bemühen. Trotzdem ist es so eine Sache mit ihr. Backlogs voll mit gemeldeten Fehlern sind keine Seltenheit, Teams, die sich auf Wochen ausschließlich mit Bug Fixes beschäftigen könnten, finden sich überall.

Korrektheit sollte daher nicht als selbstverständlicher Aspekt der Umsetzung von Verhaltensanforderungen erachtet werden. Ihre Herstellung erfordert mehr Aufmerksamkeit. Deshalb schlagen wir sie der Zukunftsfähigkeit zu. Ihre Überprüfung ist Sache des Reviews; die Architektenrolle will Korrektheit haben und zieht an ihr.

Dabei beginnt alles mit der Maturität. Die Frage lautet: Ist die Software schon korrekt?

Bedingt durch die Komplexität von Software kann diese Frage allerdings weder in einem Review des Codes, noch bei der Abnahme durch den Kunden abschließend beantwortet werden. Deshalb muss die Frage umformuliert werden: Wurde alles getan, um die Korrektheit für eine Auslieferung sicherzustellen?

Bugs lassen sich nicht komplett vermeiden. Doch es kann zumindest ein enges Netz geknüpft werden, um möglichst viele vor Release zu fangen. Wurde das getan?

Jeder Bug, der nicht gefangen wird und zum Kunden gelangt, wird früher oder später als Beschwerde zurückkommen. Die stellt dann eine Unterbrechung dar und wird den Fluss der Entwicklung von Neuerungen stören. Außerdem reduziert der spätere Bug Fix die Kapazität für Neuerungen. Bug Fixing ist Plaque im Entwicklungsprozess. Bugs führen zu Verschwendung. Geld wird verbrannt in Nachbesserungen. Das reduziert die Zukunftsfähigkeit, die umso größer ist, je mehr Neuerungen umgesetzt werden. Dafür wird Software ja gekauft. Dafür bekommen Softwareentwickler ihr Geld: dass sie Neuerungen einbauen. Der Fluss deren korrekter Herstellung ist zu maximieren.

„Die Zukunft beginnt jetzt!“ ist also der Gedanke hinter der Überprüfung der Maturität.

Was sind die Kriterien dafür, dass alles getan wurde, um die Korrektheit für eine Auslieferung sicherzustellen?

Es reicht nicht, wenn in der Reviewrunde gefragt wird, „Habt ihr denn auch alle eure Veränderungen am Code getestet?“ Auch unter der Annahme, dass alle Entwickler gutwillig und bemüht um Qualität sind, wäre eine allseits positive Antwort wenig aussagekräftig. Die Meinung darüber, was ausreichende Tests sind, gehen einfach weit auseinander und verändern sich auch noch unter Druck. Mündliche Zusicherungen sind schlicht nicht nachvollziehbar.

Deshalb gilt es als erstes zu überprüfen, ob für jede Anforderung die zugehörigen Akzeptanztests codiert wurden und automatisiert ausführbar sind und keinen Fehler melden.

Dass es überhaupt Akzeptanztests gibt, ist natürlich Ergebnis einer systematischen Analyse der Anforderungen vor ihrer Umsetzung. Die Umsetzung beginnt nicht, bevor sich nicht Kunde bzw. Stellvertreter und Entwickler auf klare Beispiele korrekten Softwareverhaltens geeinigt haben, die codiert als Tests ausdrücken, dass die wünschten Neuerungen umgesetzt wurden.

Akzeptanztests überprüfen Funktionalität und Effizienz an der Oberfläche einer Software. Sie setzen knapp unter dem UI an, um leicht automatisierbar zu sein. Zur Not „reizen“ sie die Software aber auch durch das UI.

Akzeptanztests sind auf einem Niveau, das der Kunde nachvollziehen kann. Sie stellen für ihn „Relevanzeinheiten“ dar. Auf ihrer Ebene kann der Kunde Feedback geben. Ob durch das Verhalten allerdings auch schon Wert für eine Anwendung in der Praxis entsteht, sei dahingestellt. Feedbackinkremente sind durchaus kleiner als Wertinkremente. Ihr Wert liegt nicht im Auge des Anwenders, sonder im Auge desjenigen, der sich für kontinuierlichen Fortschritt interessiert.

Akzeptanztest sind codiertes Verständnis. Ohne Akzeptanztests ist nur schwer nachvollziehbar, was überhaupt umgesetzt werden soll. Eine auf den Review folgende Abnahme bleibt ohne automatisierte Akzeptanztests schwach und lückenhaft.

Akzeptanztests sind jedoch nur die erste Bastion gegen ungenügende Maturität. Es braucht weitere Maßnahmen, die belegen, dass alles getan wurde, um die Korrektheit für eine Auslieferung sicherzustellen.

Als zweites überprüft der Review daher, ob jede Methode, die Logik enthält und nicht durch eine mechanische Refaktorisierung entstanden ist, mindestens durch einen aussagekräftigen Test überprüft wird.

Das sind Gerüsttests, die das entstehende Softwaregebäude während des Aufbaus umschließen. Mit „jede Methode“ ist daher tatsächlich jede gemeint, die (nicht triviale) Logik enthält, auch und insbesondere solche, die (eigentlich) privat sind. Denn da, wo Logik (Teil-)Verhalten herstellt, besteht Bug-Risiko.

Akzeptanztests leisten ihren Beitrag, um Bugs in allen Methoden aufzuspüren, doch am Ende können in ihnen nicht genügend (Sonder-)Fälle codiert sein, um in alle Winkel der Logik zu leuchten. Es braucht weitere Tests für die vielfältigen Details der ganzen Logik, die hinter dem gewünschten Verhalten einer Neuerung steht.

Der Review überprüft, ob Akzeptanztests und Gerüsttests vorhanden sind. Hat das Softwaregebäude ein Fundament an Tests und wurde es in einem schützenden und stützenden Rahmen hochgezogen?

Doch wie entstehen all diese Tests? Damit der Review sie vorfindet, ist zu empfehlen, sie vor dem Produktionscode zu schreiben. Ein test-first-Vorgehen ist die beste Versicherung dafür, im Review nicht kalt erwischt zu werden. Denn jeder Tests, der im Review fehlt, erfordert eine Nachbesserung, die wertvolle Zeit raubt für Neuerungen und eine Abnahme verzögert.

Sobald jedoch alle Akzeptanztests vorhanden sind und die Gerüsttests nachweisen, dass auch im Detail sorgfältig gearbeitet wurde, kann die Maturität bescheinigt werden. Die Software ist, was diesen Aspekt der Korrektheit angeht, aus Sicht der Softwareentwicklung schon bereit für ein Release.

Die Gerüsttests können anschließend abgebaut oder deutlicher: gelöscht werden. Sie dienten nur dem Aufbau korrekten Codes. Da der nun als korrekt eingestuft wurde und von allein nicht mehr kaputt geht, sind die Gerüsttests überflüssig. Alle privaten Methoden, die zum Zwecke des Tests eine größere Sichtbarkeit hatten, werden nun wirklich auf privat gesetzt. Nur so werden die Grenzen zwischen Modulen deutlich gezogen.

Würden alle Gerüsttests stehenbleiben, wäre der Code bald in einem engen Korsett von Tests eingeschnürt, das weitere Veränderungen erschwert. Gerüsttests sind Whitebox-Tests. Sie testen Details, die eigentlich nicht sichtbar sein sollen. Sie laufen dem Grundprinzip der Kapselung/losen Kopplung entgegen.

Also werden Gerüsttests am Ende eines Reviews gelöscht. Im Repository sind sie ja aber noch vorhanden. Wer also später einmal darauf zurückgreifen will… der kann sie dort suchen.

Das mag sich für Sie rigoros anhören, doch Sie werden es als Erleichterung empfinden, nicht alle Tests behalten zu müssen, wie es sonst in der Literatur empfohlen wird.

Und es ist auch nicht so, dass alle Tests, die nicht Akzeptanztests sind, Gerüsttests darstellen. Sie sind allerdings nicht Thema der Überprüfung der Maturität.

Beispiel

Zumindest mit einem kleinen Beispiel soll die Facette Maturität konkretisiert werden. Nehmen wir an, Sie sollen eine Funktion umsetzen, die ganze Zahlen in einer Textdatei addiert.

Analyseergebnis

Der Dateiaufbau ist simpel: in jeder Zeile steht eine Zahl.

Als Akzeptanztest wünscht sich der Kunde diesen Dateiinhalt für numbers.txt:

1
22
333

in Verbindung mit dieser Summe: 356.

Außerdem war nach den „Verhandlungen“ mit dem Kunden klar, wie die „Oberfläche“ der Software aussehen soll: eine Funktion mit der Signatur int Sum(string filename) in einer Klasse Aggregator.

Review

Der Review schaut zuerst auf den Akzeptanztest. Ist der vorhanden? Ja, das ist er:

[Test()]
public void Akzeptanztest() {
    var sut = new Aggregator();
    var result = sut.Sum("numbers.txt");
    Assert.AreEqual(356, result);
}

Die referenzierte Datei hat den vereinbarten Inhalt.

Der Test ist auch erfolgreich. Ihm steht eine soweit fehlerfreie Umsetzung gegenüber:

public class Aggregator {
    public int Sum(string filename) {
        var numbers = Load(filename);
        return Calculate_sum(numbers);
    }

    internal int[] Load(string filename) {
        var lines = System.IO.File.ReadAllLines(filename);
        var numbers = new List<int>();
        for (var i = 0; i < lines.Length; i++)
            if (!string.IsNullOrWhiteSpace(lines[i]))
                numbers.Add(int.Parse(lines[i]));
        return numbers.ToArray();
    }

    internal int Calculate_sum(IEnumerable<int> numbers) {
        var sum = 0;
        foreach (var n in numbers)
            sum += n;
        return sum;
    }
}

(Übersehen Sie für den Moment, dass es mit C# auch viel einfacher gegangen wäre. Um für das simple Szenario etwas Code-Fleisch auf die Knochen zu bekommen, ist der Code umständlicher als nötig.)

Als nächstes fragt der Review nach den Gerüsttests.

Gibt es weitere Funktionen über den vereinbarten Einstiegspunkt Sum() hinaus? Das ist der Fall. Und diese Funktionen sind sogar nicht durch Refaktorisierungen entstanden, sondern schon während des Entwurfs als Aspekte (eigenständige Verantwortlichkeiten) erkannt worden.

Sind diese Funktionen mit Tests versehen? Ja, das ist der Fall.

[Test]
public void Gerüttest_Load_with_empty_lines() {
    var sut = new Aggregator();
    var result = sut.Load("numbers_with_empty_lines.txt");
    Assert.AreEqual(new[]{1,2,3} , result);
}

[Test]
public void Gerüttest_Load_with_whitespace() {
    var sut = new Aggregator();
    var result = sut.Load("numbers_with_whitespace.txt");
    Assert.AreEqual(new[] { 1 }, result);
}

[Test]
public void Gerüttest_Calculate_sum() {
    var sut = new Aggregator();
    var result = sut.Calculate_sum(new[] { 1, 2, 3 });
    Assert.AreEqual(6, result);
}

Auch diese Tests sind alle grün.

Für das Laden der zu verarbeitenden Zahlen gibt es sogar zwei Tests. Der Entwickler hat sich nicht darauf verlassen, dass die Dateien wohlgeformt sind. Er hat Load() robuster gemacht, als der Kunde es sich zumindest mit dem Akzeptanztest gewünscht hat.

Ist das ein Widerspruch zum YAGNI-Prinzip? Das kommt darauf an, ob die Fälle „Datei enthält Leerzeilen“ und „Zahlen sind von Whitespace umschlossen“ mit dem Kunden diskutiert wurden.

War das nicht der Fall, hat der Entwickler zwar qualitätsbewusst gedacht – doch potenziell vorzeitig und unnötig optimiert. Das hat Zeit gekostet und somit Verschwendung erzeugt.

Hat es jedoch eine Absprache mit dem Kunden gegeben, dann liegt keine Verschwendung vor. Allerdings ist zu fragen, warum diese Aspekte nicht Eingang in Akzeptanztests gefunden haben. Auch die Sonderfälle hätten mit nur einem Akzeptanztest abgedeckt werden können, indem numbers.txt etwas „vielfältiger“ gestaltet worden wäre.

Wie nun mit diesen Testfällen verfahren? Es wird entschieden, den Akzeptanztest zu erweitern. numbers.txt bekommt folgenden Inhalt (Punkte stehen für Whitespace der einen oder anderen Art):

...
1..
 ..22
333
.

Der Review ist erfolgreich. Die Software ist korrekt, sie hat die gewünschte Maturität.

Damit haben die Gerüsttests ihre Schuldigkeit getan. Sie können gelöscht werden und die Detailfunktionen verschwinden aus der Öffentlichkeit. Ihre Sichtbarkeit wird auf private gesetzt.

Der resultierende Code im Überblick:

[TestFixture()]
public class Test {
	[Test()]
	public void Akzeptanztest() {
	    var sut = new Aggregator();
	    var result = sut.Sum("numbers.txt");
	    Assert.AreEqual(356, result);
	}
}


public class Aggregator {
    public int Sum(string filename) {
        var numbers = Load(filename);
        return Calculate_sum(numbers);
    }

    private int[] Load(string filename) {
        var lines = System.IO.File.ReadAllLines(filename);
        var numbers = new List<int>();
        for (var i = 0; i < lines.Length; i++)
            if (!string.IsNullOrWhiteSpace(lines[i]))
                numbers.Add(int.Parse(lines[i]));
        return numbers.ToArray();
    }

    private int Calculate_sum(IEnumerable<int> numbers) {
        var sum = 0;
        foreach (var n in numbers)
            sum += n;
        return sum;
    }
}

Zusammenfassung

Review ist „die erste Entwicklerpflicht“. Ohne Review besteht kein Verlass, dass Code nicht nur Verhaltensanforderungen erfüllt, sondern auch zukunftsfähig ist.

Während des Reviews durch die Architektenrolle ist zuerst Augenmerk auf die Korrektheit zu legen. Ist die Software schon korrekt? Hat sie die erforderliche Maturität zur Auslieferung?

Nachvollziehbar ist die Maturität nur anhand von codierten Tests. Das sind einerseits die Akzeptanztests, die ausdrücklich mit dem Kunden vereinbart sind. Sie setzen an der Oberfläche der Software an und sind tendenziell grob. Ihre Aufgabe ist es, Grundvertrauen herzustellen und die Umsetzung für den Kunden fühlbar zu machen.

Darüber hinaus braucht es aber weitere Tests für Detailaspekte der Logik. Die sind zumindest nötig während der Umsetzung, um dem Entwickler schrittweise Sicherheit zu geben, korrekt zu arbeiten. Sie ergänzen u.U. auch Akzeptanztests zumindest temporär. Denn am Ende des Reviews werden solche Gerüsttests auf eigentlich privaten Funktionen gelöscht. So stehen sie zukünftigen Refaktorisierungen, die unter der Oberfläche stattfinden, nicht im Wege.

Zukunftsfähigkeit ergibt sich nicht nebenbei. Sie will systematisch geplant, umgesetzt und überprüft werden. Der Ort für die Überprüfung ist der Review. Ihn regelmäßig und umfassend durchzuführen, ist die erste Maßnahme auf dem Weg zu Clean Code.

Weitere Artikel in dieser Serie:

Veröffentlicht in Allgemein und verschlagwortet mit .