Eine neue Aufgabe liegt vor dir, die Anforderungen wirken überschaubar und eigentlich weißt du schon ungefähr, welche Klassen du brauchst. Also IntelliJ auf, neues Projekt oder neue Klasse erstellen und los geht's. Das fühlt sich produktiv an, schließlich entsteht sofort etwas Sichtbares. Die ersten Methoden sind schnell geschrieben, ein paar Tests funktionieren und nach kurzer Zeit sieht es so aus, als wäre ein großer Teil der Aufgabe bereits erledigt. Genau an dieser Stelle kann es allerdings problematisch werden, denn beim Programmieren besteht die eigentliche Arbeit nicht nur darin, Java-Code zu schreiben. Ein großer Teil der Arbeit besteht darin, ein Problem so weit zu verstehen, dass du überhaupt weißt, welchen Code du schreiben solltest.
Das klingt zunächst nach unnötigem Aufwand. Gerade bei kleinen Aufgaben ist der Gedanke schnell da: Das sind doch nur ein paar Klassen, dafür brauche ich keinen Plan. Manchmal stimmt das sogar. Wenn du etwas ausprobieren, eine API kennenlernen oder nur testen möchtest, wie sich eine bestimmte Methode verhält, spricht überhaupt nichts dagegen, einfach Code zu schreiben. Für solche Experimente ist genau das oft der schnellste Weg. Schwieriger wird es, sobald aus dem Experiment eine richtige Anwendung oder eine konkrete Aufgabe wird. Dann entstehen beim direkten Losprogrammieren schnell Entscheidungen, über die du vorher eigentlich noch gar nicht nachgedacht hast. Welche Klasse ist wofür verantwortlich? Woher kommen die Daten? Wer darf sie verändern? Wie werden Fehler behandelt? Welche Teile könnten später erweitert werden?
Diese Fragen verschwinden nicht dadurch, dass du sofort anfängst zu programmieren. Du beantwortest sie nur nebenbei während der Implementierung. Häufig beantwortest du sie dann nicht bewusst, sondern einfach mit der Lösung, die in diesem Moment am einfachsten erscheint. Das funktioniert erstaunlich lange. Bis du einige Stunden später feststellst, dass eine Klasse plötzlich fünf verschiedene Aufgaben übernimmt, dieselben Informationen an mehreren Stellen verarbeitet werden und eine kleine Änderung Auswirkungen auf Bereiche hat, die damit eigentlich gar nichts zu tun haben sollten. Dann beginnt das Refactoring.
Refactoring gehört zur Softwareentwicklung und ist nichts Schlechtes. Kein vernünftiger Entwickler erwartet, eine Anwendung beim ersten Versuch perfekt zu entwerfen. Problematisch wird es erst, wenn ein großer Teil des Refactorings eigentlich vermeidbar gewesen wäre. Nicht durch mehr Erfahrung, nicht durch komplizierte Architekturdiagramme, sondern manchmal schlicht durch zehn Minuten Nachdenken vor dem ersten public class.
Zerlege zuerst das Problem, nicht den Code
Bevor du darüber nachdenkst, welche Klassen du erstellen möchtest, solltest du zunächst verstehen, was eigentlich passieren soll. Angenommen, du möchtest eine kleine Anwendung schreiben, die Aufgaben verwaltet. Eine Aufgabe hat einen Titel, eine Beschreibung und einen Status. Aufgaben sollen erstellt, bearbeitet, erledigt und gespeichert werden können. Du könntest jetzt direkt eine Klasse TaskManager erstellen und dort anfangen, Methoden einzubauen. Technisch wäre das vollkommen legitim. Interessanter ist aber zunächst die fachliche Sicht: Was ist eine Aufgabe? Was bedeutet "erledigt"? Wo werden Aufgaben gespeichert? Muss die Speicherung austauschbar sein? Was passiert, wenn eine Aufgabe nicht gefunden wird? Darf eine erledigte Aufgabe wieder geöffnet werden?
Du musst daraus keine zwanzigseitige Spezifikation machen. Oft reicht tatsächlich ein Blatt Papier, eine Markdown-Datei oder ein paar Notizen. Wichtig ist nur, dass du das Problem zerlegst. Aus der ursprünglichen Aussage "Ich brauche eine Aufgabenverwaltung" entstehen dadurch mehrere kleinere Verantwortlichkeiten. Es gibt Daten, die eine Aufgabe beschreiben. Es gibt Logik, die bestimmt, was mit einer Aufgabe passieren darf. Und es gibt eine technische Komponente, die Aufgaben dauerhaft speichert. Damit ergibt sich bereits eine wesentlich klarere Struktur.
Eine einfache Klasse könnte beispielsweise so aussehen:
public class Task {
private final long id;
private String title;
private boolean completed;
public Task(final long id, final String title) {
this.id = id;
this.title = title;
}
public void complete() {
this.completed = true;
}
}
Damit ist noch keine Anwendung gebaut. Aber du hast bereits eine fachliche Entscheidung getroffen: Eine Aufgabe kennt ihren eigenen Zustand. Die Speicherung muss deshalb nicht ebenfalls dafür verantwortlich sein, was "erledigt" bedeutet. Und eine Benutzeroberfläche muss diese Logik ebenfalls nicht selbst implementieren. Genau darum geht es bei der Vorbereitung. Du versuchst nicht, bereits jedes Detail festzulegen. Du suchst nach Verantwortlichkeiten und Abhängigkeiten.
Eine gute Frage ist dabei immer: Was müsste ich ändern, wenn sich diese Anforderung verändert? Wenn du beispielsweise von einer SQLite-Datenbank auf eine andere Speicherung wechseln möchtest und dafür deine komplette Fachlogik umbauen musst, sind zwei Dinge wahrscheinlich zu eng miteinander verbunden. Wenn eine Änderung an einer JSF-Seite plötzlich Auswirkungen auf deine Datenbanklogik hat, solltest du ebenfalls genauer hinsehen. Diese Überlegungen kannst du anstellen, bevor der entsprechende Code existiert. Das ist erheblich angenehmer, als dieselben Überlegungen später zwischen mehreren hundert Zeilen bestehendem Code nachzuholen.
Ein kleiner Plan reicht meistens vollkommen aus
Strukturiertes Arbeiten bedeutet nicht, dass du vor jeder kleinen Aufgabe UML-Diagramme erstellen und einen Architekturworkshop veranstalten musst. Das wäre das andere Extrem und genauso wenig sinnvoll. Die Vorbereitung sollte zur Aufgabe passen. Bei einer kleinen Funktion reichen manchmal zwei Minuten. Bei einer Anwendung mit mehreren Komponenten können daraus deutlich längere Überlegungen werden.
Ich gehe bei neuen Aufgaben meistens gedanklich von außen nach innen. Zuerst möchte ich wissen, was am Ende passieren soll. Danach interessiert mich, welche Informationen dafür benötigt werden und woher diese kommen. Anschließend überlege ich, welche Teile der Anwendung welche Verantwortung übernehmen sollten. Erst dann wird interessant, welche Klassen, Interfaces oder Methoden daraus entstehen. Bei einer Maven-Anwendung könnte eine Aufgabe beispielsweise lauten, Abhängigkeiten eines Projekts zu analysieren. Wenn du direkt anfängst, könnte die erste Klasse schnell gleichzeitig die pom.xml lesen, Maven-Koordinaten auflösen, Ergebnisse filtern und einen Report schreiben. Das funktioniert möglicherweise sogar. Spätestens wenn du später statt eines HTML-Reports zusätzlich JSON erzeugen möchtest oder die Analyse nicht mehr nur über eine einzelne pom.xml, sondern über ein Multi-Module-Projekt laufen soll, wird diese Struktur unangenehm.
Wenn du das Problem vorher zerlegst, erkennst du dagegen verschiedene Schritte. Die Projektstruktur muss eingelesen werden. Daraus müssen Abhängigkeiten ermittelt werden. Diese Abhängigkeiten können analysiert werden. Das Ergebnis kann anschließend in unterschiedlichen Formaten ausgegeben werden. Jetzt kannst du über Klassen nachdenken. Vielleicht entsteht ein ProjectReader, ein DependencyAnalyzer und ein ReportWriter. Ob diese Klassen am Ende exakt so heißen, ist gar nicht entscheidend. Vielleicht stellst du während der Implementierung fest, dass eine andere Aufteilung sinnvoller ist. Der wichtige Unterschied ist: Du startest mit einem Modell des Problems.
Dieses Modell darf sich verändern. Das ist sogar ziemlich wahrscheinlich. Planung bedeutet nicht, dass du dich vor dem Programmieren endgültig auf eine Architektur festlegst. Sie gibt dir lediglich eine Richtung. Während der Umsetzung vergleichst du diese Richtung immer wieder mit der Realität. Vielleicht stellst du fest, dass zwei geplante Klassen eigentlich dieselbe Verantwortung besitzen. Dann fasst du sie zusammen. Vielleicht wird eine Klasse deutlich komplexer als erwartet. Dann zerlegst du sie weiter. Vielleicht war eine geplante Abstraktion vollkommen unnötig. Dann fliegt sie wieder raus. Das ist keine gescheiterte Planung, sondern ganz normale Softwareentwicklung.
Ein Plan ist kein Vertrag mit deinem zukünftigen Code. Er ist ein Werkzeug, mit dem du verhinderst, dass jede Entscheidung erst dann getroffen wird, wenn du gerade mitten in der Implementierung steckst. Ein weiterer Vorteil wird häufig unterschätzt: Du erkennst offene Fragen früher. Wenn du eine Aufgabe gedanklich durchspielst und an einer Stelle nicht erklären kannst, was dort passieren soll, fehlt dir wahrscheinlich eine Information. Beim direkten Programmieren würdest du an dieser Stelle möglicherweise einfach irgendeine Annahme treffen und darauf weiter aufbauen. Je mehr Code danach entsteht, desto teurer wird eine falsche Annahme. Darum kann eine simple Notiz wie "Was passiert, wenn keine Konfiguration existiert?" später erstaunlich viel Arbeit sparen.
Fazit
Einfach drauflos zu programmieren ist nicht grundsätzlich falsch. Für Experimente, kleine Tests und technische Spielereien ist es sogar oft genau richtig. Wenn du wissen möchtest, wie eine bestimmte Java-API funktioniert, musst du daraus vorher kein Konzept machen. Schreib zehn Zeilen Code und probiere es aus. Sobald du aber eine konkrete Aufgabe lösen möchtest, lohnt sich ein kurzer Schritt davor. Versuche zuerst zu verstehen, was eigentlich gebaut werden soll. Zerlege das Problem in kleinere Teile. Überlege, welche Verantwortlichkeiten es gibt und welche Komponenten voneinander abhängig sind. Schreib offene Fragen auf. Denk einmal darüber nach, welche Stellen sich wahrscheinlich später ändern könnten. Und dann fang an zu programmieren.
Du wirst trotzdem Fehler machen. Du wirst Klassen wieder umbauen. Du wirst Methoden löschen, von denen du zwei Stunden vorher noch überzeugt warst. Und du wirst gelegentlich feststellen, dass deine ursprüngliche Idee überhaupt nicht funktioniert. Das gehört dazu. Der Unterschied ist, dass du nicht vollkommen blind in die Implementierung startest.
Mit der Zeit passiert dabei noch etwas anderes: Du wirst besser darin, Probleme zu zerlegen. Du erkennst früher, wenn eine Klasse zu viele Verantwortlichkeiten bekommt. Du bemerkst schneller, wenn zwei Komponenten unnötig eng miteinander verbunden sind. Du entwickelst ein Gefühl dafür, welche Entscheidungen wichtig sind und welche du problemlos später treffen kannst. Genau das ist eine der Fähigkeiten, die beim Programmieren langfristig wichtiger wird als die Kenntnis einzelner Java-Klassen oder Frameworks. Syntax kannst du nachschlagen. Eine Maven-Konfiguration kannst du recherchieren. Auch eine WildFly-Einstellung lässt sich finden. Die eigentliche Entwicklungsarbeit beginnt bei der Frage, wie du aus einem Problem eine sinnvolle technische Lösung machst. Und dafür lohnt es sich meistens, den Editor für ein paar Minuten noch geschlossen zu lassen.
