Prinzipien professioneller Softwareentwicklung
Oft sind die fachlichen Anforderungen an Software kompliziert. Daher sollte man versuchen die technischen Lösungen möglichst einfach zu gestalten.
Außerdem soll Software vielmals über länge Zeit entwickelt und weitergenutzt werden. Es sind wenig Wegwerf-Lösungen gefragt.
Softwareentwicklung sollte sich immer an den Anforderungen der Kunden orientieren. Allerdings können sich diese über die Zeit verändern. Darauf muss man immer reagieren können. Daher sollte man auf eine agile Softwareentwicklung setzen.
Eine optimale Software sollte folgende Eigenschaften haben:
- Einfach und klar verständlich
- Gleichbleibende Komplexität
- Akzeptable Build-Zeit
- Geringer Aufwand zur Erstellung von Instanzen
- Performanz in der Ausführung
- Geeignete Dokumentation
Es gibt Techniken, wie Domain Driven Design, agile Softwareentwicklung, DevOps und andere um diese Ziele zu erreichen.
Softwareentwicklung besteht aus einem großen technischen Teil, aber auch aus einem organisatorischem Teil ohne den der technische nicht ausgeführt werden kann.
In diesem Artikel geht es vorwiegend um die technischen Aspekte. Die organisatorischen Aspekte beleuchten wir in einem späteren Artikel.
Viele der hier beschriebenen Aspekte werden bereits in vielen Quellen ausgiebig beleuchtet. Dieser Artikel soll eine knappe Zusammenfassung dieser Aspekte bieten.
Einfach und klar verständlich
Ein guter Maßstab, ob Software einfach und verständlich ist, ist wieviel Aufwand man betreiben muss um neue Entwickler in das Projekt einzuarbeiten.
Eine saubere Implementierung auf Klassenebene ist ebenso wichtig wie die Architektur der Software.
Eigene technische Lösungen müssen immer erklärt und eine Dokumentation auf einem aktuellen Stand gehalten werden. Das sorgt für Aufwand und wird ungern gemacht. Nur in Ausnahmefällen kann so etwas Sinn machen.
Nicht zu vergessen ist, dass das Kompilieren und Starten der Software am eigenen Rechner nicht kompliziert sein sollte.
Selbst der fähigste Entwickler wird irgendwann an seine Grenzen stoßen, wenn der Code zu komplex und unüberschaubar wird. Dann schleichen sich zwangsläufig Fehler ein.
Um dies zu vermeiden sollte der Code nach folgende Maßstäben gestaltet werden:
- Clean-Code anwenden
- Technische Benennungen sollte sich am Wortschatz des Kunden/Domänen-Experten orientieren
- Wenig Abweichungen vom Standard - keine eigenen technischen komplexen Lösungen
- Code in Module und/oder Micro-Services getrennt
- Build Script im Source-Code verwalten
- Gleichbleibende Komplexität
Es ist ungemein wichtig, dass sich neue Features mit gleichbleibendem Aufwand implementieren lassen. Dies kann nur gelingen, wenn man nicht permanent technische Schulden aufbaut.
Es kann Sinn ergeben technische Schulden bewusst aufzubauen, um z.B. Meilensteine zu erreichen oder während der Erprobung von Konzepten. Allerdings muss man dann anschließend auch für einen Abbau dieser Schulden durch Refactorings sorgen.
Es gibt unterschiedliche Arten technischer Schuld:
- Schwer verständlicher Code
- Fehlende technische Infrastruktur
- Unzureichende automatisierte Tests
- Monolithische Architektur
- Fachlichkeit im Client
- Wahl ungünstiger Technologien
- Einsatz veralteter Technologien
- Komplexer Build Mechanismus
- Unzureichende Benennung
- Fehlende Dokumentation
Schwer verständlicher Code
Komplexer und unverständlicher Code ist eine Art der technischen Schuld.
Fehlende technische Infrastruktur
Ohne Versionsverwaltung ist an professionelle Softwareentwicklung nicht zu denken. Viele der hier beschriebenen Lösungen sind ohne gar nicht möglich.
Seit einigen Jahren gibt es keinen Weg an Git vorbei, da es auf der Idee des Branching basiert, was viele Vorteile mit sich bringt.
Ein CI (Continous Integration) System ist zur Gewährleistung der Funktionalität unerlässlich. Jede Code-Änderung sollte automatisch auf Kompilier- und Testfehler geprüft werden.
Noch besser ist ein CD (Continuous Delivery) System bei dem jegliche Code-Änderung auf ein Test-System aufgespielt wird.
Der Einsatz von Containersystemen ist hierbei eine große Hilfe. Diese Maßnahmen gehören in den Bereich des DevOps und erfordern außer den klassischen Softwareentwicklungsskills zusätzliche Kenntnisse über Infrastruktur, Netzwerke und Betriebssysteme.
Unzureichende automatisierte Tests
Wenn es keine ausreichenden Tests gibt, kann der Entwickler nicht feststellen, ob durch die aktuelle Code-Änderung ein bestehendes Feature nicht mehr wie gewünscht funktioniert.
Code-Refactorings sind sehr riskant und aufwändig, da die Funktionalität der Software manuell geprüft werden muss.
Es ist wichtig, dass die Tests in angemessener Zeit ausgeführt werden und aussagekräftig sind.
Es gibt unterschiedliche Ansätze wie BDD (Behaviour Driven Development) und TDD (Test Driven Development), welche zu unterschiedlichen Arten von Tests führen und in Kombination eingesetzt werden sollten.
Regressionstests stellen sicher, dass ursprüngliches Verhalten erhalten bleibt.
Es ist allerdings nicht ganz einfach Tests so zu organisieren, dass sie Refactorings nicht erschweren.
Monolithische Architektur
Eine monolithische Architektur herrscht meist in Legacy-Systemen vor. Es hat sich gezeigt, dass diese Art der Architektur einige Nachteile mit sich bringen.
Dadurch, dass es keine logische Trennung von Klassen und Modulen gibt, werden häufig Abkürzungen im Code genommen und hilfreiche Abstraktionen und Schnittstellen nicht implementiert. Dies sorgt häufig für eine Codestruktur, die zyklische Abhängigkeiten aufweist und dazu dass Code nicht isoliert betrachtet werden kann. Jedes neue Feature wird in den Code eingeflochten. Das macht den Code schwer verständlich und schwer refaktorierbar.
Eine logische Trennung des Codes in Module und Microservices sorgt dafür, dass man viele kleine in sich geschlossene Einheiten getrennt voneinander betrachten kann. Die sinnvolle Trennung orientiert sich an der Fachlichkeit / Domäne.
Das sorgt für eine Vielzahl von Vorteilen:
- Klare Aufgabentrennung - Module bekommen definierte Schnittstellen und Use-Cases
- Module können voneinander getrennt getestet und betrachtet werden
- Die Zusammenarbeit der Module kann übergeordnet betrachtet werden
- An mehreren Modulen kann parallel gearbeitet werden
- Microservices können mit der für den Einsatzzweck geeigneten Technologie umgesetzt werden
- Microservices können getrennt voneinander gehostet und skaliert werden
- Microservices können einzeln ausgetauscht oder die Technologien aktualisiert werden
- Eine Lastverteilung und die parallele Verarbeitung von Daten sind möglich
Fachlichkeit im Client
Fachlichkeiten sollten nicht im Client abgebildet werden. Die Use-Cases und Sanity-Checks müssen immer mit Backend implementiert werden.
Eine der Grundregeln bei der Web-Entwicklung ist "Never trust the client". Das bedeutet, dass das Backend immer alle Prüfungen durchführen muss, da der Client von außen manipulierbar ist.
Außerdem kann es eine ganze Reihe von Clients geben: die Webseite, mobile Anwendungen auf Handys und Tablets.
Die Clients sollen sich mit der Darstellung und der Auswertung von Eingaben beschäftigen und nicht mit der Einhaltung von Business-Regeln.
Allerdings darf und sollte im Client darf Logik implementiert werden, die dem Benutzer bei der korrekten Eingabe hilft. Z.B. Prüfungen ob der eingegebene Text eine Email oder Zahl ist.
Wahl ungünstiger Technologien
Oft wird eine Software mit einem festen Set an Technologien umgesetzt. Obwohl die Software unterschiedlichste Anforderungen zu bewältigen hat, wird alles mit den gleichen Mitteln gelöst, obwohl es viel passendere Technologien gibt.
Zum Beispiel wird oft eine SQL Datenbank verwendet, was für die Datenhaltung von relationalen Objekten Sinn ergeben kann. Muss die Software aber nun zusätzliche Aufgaben, wie das Aufbereiten von - oder das Suchen in - sehr großen Datenmengen wird dann einfach die Datenbank genommen, die sowieso schon da ist. Das sorgt dafür, dass die SQL Datenbank schnell an Limits kommt - nur noch schwer veränderbar ist, viel Speicher braucht, langsam wird, …
Stattdessen sollte eine für den Einsatzzweck geeignete Datenbank verwendet werden. Diese kann man auch gleich auf einem anderen Server hosten und so für eine Lastverteilung sorgen.
Dies kann man auf unterschiedliche technologische Aspekte beziehen.
Allerdings sollte man hierbei Maß halten und keinen unkontrollierbaren Zoo an Technologien aufbauen.
Einsatz veralteter Technologien
Technologien entwickeln sich ständig weiter. Das bedeutet leider auch, dass die verwendeten Technologien einer Softwareentwicklung ständig veralten.
Wenn es nun versäumt wird, diese zu aktualisieren, verpasst man nicht nur neue Features und Performanz-Verbesserungen oder handelt sich unter Umständen Sicherheitslücken ein. Es ist meist einfacher die Updates in kleinen Schritten mitzunehmen, als im Nachhinein einen großen Versionssprung zu machen, da sich mit jedem Update Dinge verändert haben können, die eine Anpassung der eigenen Software erfordern.
Dies betrifft vor Allem die in der Software verwendeten externen Bibliotheken, aber auch die Build-Tools, die Infrastruktur-Services - wie Datenbanken und Ähnlichem - und auch das CI-System, VM Betriebssysteme, Programmiersprachen.
Hierbei sollte man auf ein funktionierendes DevOps-Team setzen.
Komplexer Build Mechanismus
Der Build-Mechanismus sollte möglichst einfach und wenig verzweigt sein. In Legacy-Systemen kommt es häufig vor, dass der Build Prozess ähnlich spektakulär ist, wie der restliche Source-Code. Der Build-Prozess sollte möglichst keine Abhängigkeiten zur Entwicklungsumgebung, das firmeneigene Ordnersystem oder sofern möglich das Betriebssystem haben.
Ähnlich wie bei Code sollte es für den Entwickler einfach sein, den gesamten Build-Prozess nachzuvollziehen.
Es hilft sich an Standards zu halten und Tricks möglichst zu vermeiden. Oftmals sind Tricks nicht notwendig, wenn man das Projekt in sinnvolle Module unterteilt, die jeweils eigene Build-Skripte mit sich bringen.
Unzureichende Benennung
Code sollte sich an den Anwendungsfällen orientieren und auch die Vokabeln des Kunden, bzw Experten verwenden. Jeder der an dieser Software entwickeln möchte, muss sich sowieso mit der Fachlichkeit auseinander setzen. Von daher macht es wenig Sinn eine technische Sicht zu verwenden um die fachlichen Anwendungsfälle umzusetzen.
Im Domain Driven Development Ansatz erfüllt dieser Punkt eine wichtige Bedeutung - Stichwort: Ubiquitous Language.
Akzeptable Build-Zeit
Es ist wichtig, dass der Entwickler nicht zu lange auf den Build-Job, das Ausführen von Tests, das Hochfahren von Application Servern oder Ähnlichem wartet. Der Softwareentwickler ist meist einer der teuersten Faktoren an einer Software. Daher sollte die Entwicklung der Software mit möglichst wenig Wartezeit verbunden sein.
Wenn das Prüfen von Änderungen langwierig ist, dann wird der Entwickler das oft nicht machen.
Wenn die Wartezeit zwischen 30 Sekunden und wenigen Minuten liegt, kann man in dieser Zeit meist nichts Sinnvolles erledigen. Man möchte prüfen ob die letzte Änderung zu dem gewünschten Verhalten führt und dann mit dem nächsten Schritt fortfahren.
Durch die Wartezeit geht zumeist nur Konzentration verloren und der Entwickler wird entnervt und unaufmerksam.
Auch die Ausführung der Tests sollte lokal schnell möglich sein. Sonst führt dies dazu, dass der Entwickler mehr oder weniger blind Code-Änderungen pushen und nachher nur noch schaut ob der Build-Job Fehler geworfen hat. Dann hat er aber schon mit dem nächsten Schritt angefangen und muss erneut den Fokus wechseln.
Geringer Aufwand zur Erstellung von Instanzen
Auch wenn man alle neuen Features durch Tests abgedeckt hat, sollte das Verhalten einer neuen Version der Software innerhalb der Infrastruktur geprüft werden.
Es muss nicht die ganze Software geprüft werden. Aber zumindest die neuen Features und das allgemeine Verhalten sollte durch sogenannte Smoketests überprüft werden. Damit prüft man die Infrastruktur, Migrationsskripte und Drittservices in Kombination. Wenn sich Testsysteme mit wenig Aufwand hochfahren lassen, kann man diese Prüfungen auch mit wenig Aufwand realisieren.
Wenn die Infrastruktur durch Konfigurationen beschrieben werden, kann man mit relativ wenig Aufwand Instanzen der gesamten Struktur hochfahren. Es sollten Updates der Services und Änderung der Infrastruktur einfach geprüft werden können.
Testsysteme zur manuellen Prüfung sollten immer bereitstehen. Sonst wird das Rollout zur Wundertüte.
Am Besten sollte automatisiert mit jedem Feature-Branch eine neue Testumgebung hochgefahren werden. Wenn zu jedem Feature-Branch automatisch eine Testumgebung erzeugt wird, kann jeder Entwickler jederzeit den aktuellen Stand prüfen. Die Umgebung kann zur visuellen Prüfung des Features vor dem Merge in den Hauptzweig verwendet werden. Außerdem bekommt man damit auch die Möglichkeit zum Prototyping - Ausprobieren und Entwickeln von Ideen - geschenkt.
Dazu kann man Tools wie Docker, Gitlab CI, Jenkins in Kombination nutzen.
Performanz in der Ausführung
Zu einer guten Software gehört selbstverständlich dazu, dass die Ausführung ohne Aussetzer und Wartezeiten für den Benutzer durchgeführt wird. Das klingt zunächst einmal trivial und kann teilweise auch durch Hardware erschlagen werden, ist aber dennoch nicht immer einfach umzusetzen.
Die Wahl der geeigneten Programmiersprachen, Datenbanken und Services ist keine einfache Angelegenheit und muss immer individuell nach Anwendungsfall betrachtet werden. Oft kommt es zu Problemen wenn bei der Architektur konzeptuelle Fehler gemacht werden oder wenn die Software so kompliziert wird, dass nicht mehr nachvollziehbar ist, was alles im Hintergrund für Prozesse ablaufen und welche Seiteneffekte zu berücksichtigen sind. Hier hilft es wenn man bei der Entwicklung der Software Konzepte zu einer mögliche Lastenverteilung berücksichtigt.
Geeignete Dokumentation
Dokumentation ist ein wichtiger, aber auch meist unbeliebter Aspekt in der Softwareentwicklung. Oft wird die Dokumentation lieblos ausgeführt und entspricht oft nicht mehr dem Stand der Software - ist veraltet.
Daher ist es ratsam nicht zwanghaft alles zu dokumentieren. Es sollten aber Schaubilder und grundlegende Konzepte dokumentiert werden.
Glossar
Es hilft beim Verständnis der Fachlichkeit wenn man sich auf die sprachliche Verwendung von Begriffen einigt. Daher sollte ein grundlegende Glossar sicherstellen, dass alle (Softwareentwickler, POs, Kunden) mit den Begriffen, die gleichen Dinge meinen - die gleiche Sprache sprechen. Die Begriffe aus dem Glossar sollten sich auch im Code wieder finden (siehe Ubiquitous Language - DDD).
Visualisierung der Infrastruktur
Zum besseren Verständnis der Software hilft es, wenn jeder, der am Projekt beteiligt ist, versteht in welcher Umgebung die Software läuft und welche Komponenten beteiligt sind. Daher sollte ein Schaubild der Infrastruktur vorhanden sein. Dies hilft bei der Einarbeitung neuer Kollegen, bei Erläuterung dem Kunden gegenüber, als auch als Gedankenstütze bei der Erarbeitung von neuen Features.
Erläuterung grundlegender Konzepte
Die grundlegenden Konzepte sollten dokumentiert werden, um einen Leitfaden während der Umsetzung von neuen Features zu haben. Es ist auch sinnvoll DOs und DONTs festzuhalten, um einen Aufbau technischer Schuld zu vermeiden.
Festhalten von Entscheidungen
Oft wurden während der Entwicklung viele Diskussionen um Konzepte geführt worden. Manchmal ist es später nicht mehr offensichtlich warum Entscheidungen damals so getroffen wurden.
Für den Abbau von technischer Schuld ist es aber wichtig, dass man alles in Frage stellen kann. Man kann nicht ständig alle Diskussionen von vorne führen, nur weil das Team nun aus anderen Mitgliedern besteht.
Durch Nicht-Wissen von Entscheidungsfindungen können folgende ungünstige Situationen entstehen:
- Niemand traut sich alte Entscheidungen in Frage zu stellen, obwohl die Gründe von damals vielleicht nicht mehr zu treffen.
- Es werden Entscheidungen ungünstig verändert, obwohl der Sachverhalt bereits beleuchtet wurde.
- Übergangslösungen werden nicht abgelöst, weil der weiterführende Plan in Vergessenheit geraten ist.
Öffentliche Schnittstellen
Öffentliche Schnittstellen sollten akkurat dokumentiert sein. Schnittstellen sollten keine Überraschungen bergen und nicht veraltet sein. Am Einfachsten ist es wenn die Dokumentation im Code erfolgt.
Schlusswort
Softwareentwicklung bietet einen hohen Freiheitsgrad. Das führt dazu, dass man sehr viele unterschiedliche Ansätze verfolgen kann. Leider führt der Prototyping-Ansatz - ich probier erstmal rum - schnell zu Ergebnissen, die sich sehen lassen können. Das blendet schnell die Sicht auf die Nachhaltigkeit und schürt bei Managern falsche Hoffnungen.
Die oben genannten Prinzipien zu berücksichtigen ist nicht einfach, erfordert initialen Aufwand und Softwareentwickler, die sich mit den Problemen und Prinzipien auch auseinander setzen.
Ohne die passende Organisation ist es schwer die technischen Prinzipien umzusetzen. Darum soll es im zweiten Teil dieses Artikels gehen.