Donnerstag, 3. Juni 2021

Iterationen beim Entwickeln

Programmierkunst

Das Entwickeln von Software ist ein schöpferischer Vorgang: es gibt kein Rezept, kein "Schema F", das man einfach nur lernen und anwenden müßte, um eine gegebene Anwendungslogik zu implementieren, also in ein System zusammenarbeitender Objekte und Datenstrukturen umzusetzen . 

Was wir nur haben, ist eine Sammlung von Empfehlungen, von Best Practices, von Erfahrungsregeln, auch von kolossalen Fehlschlägen in der Vergangenheit. So wächst die Kunst des Entwickelns – sowohl des einzelnen Entwicklers als auch des ganzen Berufsstands – mit jedem Erfolg und mit jedem Fehlschlag.

Reflexion

Dieses Wachstum beruht vor allem auf der Reflexion. Der Zugewinn durch eine konkrete Aufgabe entsteht nicht darin, sie einfach nur gelöst zu haben, so daß ich etwas Funktionierendes abliefere. Der Zugewinn entsteht allein dadurch, daß ich mich frage: 

  • Was kann ich von dieser Arbeit mitnehmen?
  • Welche allgemeinen Features, die gar nicht spezifisch für diese Aufgabe sind, kann ich in späteren Aufgaben wiederverwenden?
  • Wie war die Arbeit?
  • Auf welche Probleme bin ich beim Implementieren gestoßen?
  • Was hat mich unverhältnismäßig lange aufgehalten?
  • Wie habe ich die einzelnen Teilaufgaben konkret gelöst?
  • Wie paßt das Produkt in den Gesamtkontext der Entwicklungen im System?
  • Was hätte ich besser machen können?
  • Was kann ich vielleicht auch jetzt, wo die Sache schon gerade auf "Testbereit" gesetzt wurde, aber noch frisch und in meinem Kopf noch maximal präsent ist, an der Lösung verbessern, am internen Zusammenspiel der Klassen, am Datenmodell, an der Effizienz?

Loops

Es hat sich gezeigt, daß Loops wie solche Reflexionsschritte – die Schwungräder des produktiven Arbeitens sind. Eine Loop besteht – allgemein gesprochen – darin, daß zu einem bestimmten Punkt die Arbeit nicht einfach nur linear fortgesetzt wird, sondern das bislang Produzierte aus einem Distanzverhältnis heraus angeschaut und bewertet wird.

Was bedeutet das? Zur Loop gehören:

  • eine Kontrollinstanz, die ich Agent nennen möchte
  • eine konkrete reflektierende Tätigkeit,
  • und ein Ergebnis, das Einfluß auf den Hauptstrom der Tätigkeit hat.

Agenten

Wer ist der Agent? Er steht, wie gesagt, in einem distanzierten Verhältnis zum aktuellen Produktionsprozeß. Dieser Agent kann sein:

  • der Produzent selbst, indem er seine eigene Arbeit aus der Distanz betrachtet
  • eine andere Person,
  • oder eine Maschine.

Die reflektierenden Tätigkeiten

Was für reflektierende Tätigkeiten führt der Agent aus?

  • Er kann die Einhaltung formaler Standards im Produkt überwachen
    (gut geeignet für maschinelle Agenten).
  • Er kann das Produkt auf die beabsichtigte Funktion hin testen.
  • Er kann die konkrete Ausgestaltung des Produkts auf seine Güte hin bewerten
    (etwa nach Kriterien wie Ergonomie, Zugänglichkeit, Performance).
Diese Tätigkeiten sind hier aufsteigend nach Schwierigkeit aufgeführt. Die Bewertung auf Güte ist das Schwierigste, da sie kaum operationalisierbar ist, nur von einem Menschen ausgeführt werden kann und die Kriterien der Bewertung vom Erfahrungswissen des Menschen abhängen, oft nur als implizites Wissen im einzelnen Menschen vorliegen.

Wirkung

Welchen Einfluß auf den Produktionsprozess hat die Loop?

  1. Die Justierung des Arbeitsschrittes auf das Gesamtziel hin:
    • die Vergewisserung, daß man noch "auf Kurs" ist, oder
    • die Verbesserung des gerade reflektierten Arbeitsabschnitts. Man geht gewissermaßen in der Zeitachse zurück zum Beginn dieses Abschnitts und korrigiert oder verbessert
  2. Man hat etwas gewonnen (Erfahrung, Wissen, wiederverwendbare Teile der Arbeit) und kann es mitnehmen für spätere Aufgaben

Die Arbeit des Entwickelns

Wenn man genauer hinschaut, besteht die Arbeit des Entwickelns bis in die kleinsten Schritte immer aus solchen Loops. Wenn ich in die Entwicklungsarbeit zu irgendeinem Zeitpunkt t hineinzoome, sehe ich beispielsweise folgendes:

  • Ich produziere eine oder mehrere Codezeilen.
  • Ich bekomme beim Aktivieren Syntaxfehler der Methode
  • Ich korrigiere diese.
  • Dasselbe eine Ebene höher: eine syntaktisch korrekte Methode kann immer noch zu Syntaxfehlern auf Ebene der Klasse führen.
  • Auch diese korrigiere ich.
  • Wenn ich das Gefühl habe, die Änderung könnte Risiken für Subklassen haben, wähle ich "KlassePrüfenSyntax (Subklassen)". Das muß ich allerdings aktiv tun, dem ging also die Reflexion über mögliche Seiteneffekte meines gerade produzierten Codes voraus.
  • Wenn ich auf diese Weise einige Zeit gearbeitet habe, ist die Klasse als Ganzes in einem Zustand für weitere Prüfungen (den Zeitpunkt ermittele ich durch Reflexion).
  • Dann füge ich den Reflexionsschritt "Erweiterte Syntaxprüfung" und/oder "Code Inspector" und/oder "Unittests" ein. Dies sind bereits Loops, die einen größeren Teil meiner in vielen kleineren Iterationsschritten geleisteten Arbeit zusammenfassen.
  • Ich bewerte die Ergebnisse dieser Reflexionsschritte: sind die Prüfungen der Erweiterten Syntaxprüfung oder des CodeInspectors wirklich relevant? Oft sind es nur Warnungen, die auf potentielle Probleme hinweisen.
  • Ich bewerte die Ergebnisse der bestehenden Unit Tests auf die Abdeckung des Codes hin: wie gut ist die Methoden- und Zweigabdeckung? Für völlig neue Methoden sollte ich spätestens jetzt erwägen, das beabsichtigte Programmverhalten durch einen Unit Test abzudecken – wenn auch nicht starr: es ist völlig in Ordnung, wenn mich eine Aufwand/Nutzen-Überlegung dazu führt, keinen Unit Test zu schreiben, weil der Aufwand, die gerade entwickelte Codeeinheit mit (weiteren) Unit Tests abzusichern, zu hoch ist im Verhältnis zum Nutzen.
  • Wenn ich auf diese Weise einige Codeeinheiten erstellt habe, will ich ihr Zusammenwirken testen. Dazu führe ich einen Entwicklertest aus. Üblicherweise machen wir das heute noch manuell:
  • wir rufen beispielsweise eine Transaktion auf und schauen, ob das neue Feld korrekt versorgt und fortgeschrieben wird, oder ob die neue Prüfung korrekt in das Transaktionsverhalten integriert ist.
  • oder wir rufen Methoden, Funktionsbausteine oder Sequenzen von Funktionsbausteinen im Einzeltest auf
  • oder wir schreiben sogar kleine Testreports, die ein bestimmtes Feature überprüfen.
    Diesen Testreports messen wir oft keine große Bedeutung zu, betrachten sie als "Wegwerf-Code". Zu Unrecht. Wenn wir uns die kleine Mühe machen würden, diesen Testcode statt in einem Programm in der Methode einer Testklasse zu implementieren, wäre für die Zukunft schon einiges gewonnen. Es zeigt sich nämlich immer wieder, daß jeder, der später auf dieses Thema zurückkommt, genau nach solchen einfachen Run Tests sucht, um das Laufzeitverhalten anzuschauen. Findet er nichts, muß er den "Wegwerfcode" noch einmal neu schreiben.
  • Schlägt der Entwicklertest fehl, analysiere ich das Programmverhalten und gehe dabei geistig zurück zum Punkt t-∆t, als ich den neuen Code eingefügt hatte. Wird er überhaupt aufgerufen? Wird seine Schnittstelle beim Aufruf korrekt versorgt? Werden die übergebenen Daten gemäß der von mir beabsichtigen Logik verarbeitet? Klappt die Ergebnisübergabe an die UI-Objekte?
  • Funktioniert der Entwicklertest, so mache ich mich an die Entwicklung des nächsten Features und verfahre wieder wie eben beschrieben.
  • Habe ich einen gewissen Stand erreicht, so beurteile ich (eigener Reflexionsschritt), ob die Anzahl der entwickelten Features bereits ausreicht, um die Weiterentwicklungen ins Q-System zu transportieren und dort integrativ testen zu lassen.
  • Der Berater testet das neue Feature-Set im Q-System mit seinen Daten, nach seinen Regeln, mit seiner Sicht auf die Prozesse, mit seinen eigenen Akzeptanzkriterien.
  • Der Berater meldet mir seine Ergebnisse zurück, positive wie negative.
  • Negative Ergebnisse versuche ich mit einem Entwicklertest zu reproduzieren.
  • Gelingt mir dies nicht, muß ich das Problem im Q-System analysieren und den Grund herausfinden, warum im D-System das Symptom nicht zu reproduzieren war.
  • Gelingt es mir, muß ich die gerade entwickelten Codeeinheiten auf dieses Symptom hin analysieren (das wäre der Fehlerfall), oder – der häufigere Fall – bislang nicht beschriebene, aber eigentlich gewünschte Features in mein Backlog für die weitere Arbeit aufnehmen. (Aufgrund der Komplexität des Systems ist es meist nicht möglich, alle benötigten Features in allen möglichen Szenarien bereits im Vorfeld zu beschreiben.)

Wie man sieht, besteht die Arbeit letztlich aus lauter kleinen Mikro-Iterationen, die von etwas größeren Iterationen umfaßt werden usw.


In das quantitative Schema eines "Kausalschleifendiagramms" gefaßt (mit Pfeilen vom Typ "je mehr …, desto mehr/weniger…"), ist jede einzelne Iteration, selbst die kleinste, von folgendem Typ:


Man sieht, daß im Reflexionsschritt stets ein Standard von außen dazukommt ("Desired Product Quality"). Ist man selbst der Agent des Reflexionsschritts, setzt man sich gewissermaßen "einen anderen Hut auf", um diese Anforderungen an das Produkt heranzubringen. Das funktioniert, ist aber nicht unbedingt die beste Wahl. Denn ein Außenstehender findet mögliche Schwachstellen eines Produkts oft besser als sein Hersteller.

Das, was hier mit "Desired Product Quality" beschriftet ist, können Syntaxregeln oder Entwicklungsrichtlinien sein, auch weniger formale Anforderungen wie Wiederverwendbarkeit, Wartbarkeit, Intentionalität des Codes, und natürlich die Produktanforderungen, wie sie z.B. in den Use Cases beschrieben wurden. Eigentlich alle Anforderungen an das Produkt neben der einen, die ich gerade implementiert habe (und auch diese eine noch hinsichtlich der Seitenaspekte wie Korrektheit usw., die ich beim Entwickeln nicht im Auge hatte.)

Keine Kommentare :