KI-Werkzeuge in der Softwareentwicklung

In der Softwareentwicklung existierten schon immer Werkzeuge und Vereinfachungen wie Autocompletion oder Syntax-Highlighting, die den Entwicklungsprozess effizienter und weniger fehleranfällig machen sollten. Diese Werkzeuge haben es Entwicklern ermöglicht, sich stärker auf die Logik und Funktionalität ihres Quellcodes zu konzentrieren, anstatt sich mit den Details der Syntax oder der Strukturierung von Quellcode herumzuschlagen.

In den vergangenen Jahren hat sich die Landschaft der Softwareentwicklung weiterentwickelt und neue Technologien und Methoden haben Einzug gehalten. Beispielsweise haben Versionskontrollsysteme wie Git die Zusammenarbeit in Teams wesentlich verbessert und Continuous-Integration-/Continuous-Deployment-Pipelines ermöglichen es, Änderungen effizienter in Produktionsumgebungen zu bringen.

KI-Werkzeuge sollen die Entwicklungsarbeit vereinfachen

Aktuell finden immer mehr Werkzeuge, die mit maschinellem Lernen oder großen Sprachmodellen (Large Language Models) arbeiten, ihren Weg in die Praxis. Assistenten wie GitHub Copilot oder Tabnine nutzen hierbei große Mengen an Trainingsdaten, um Entwicklern kontextbezogene Vorschläge anzubieten, die weit über einfache Autocompletion hinausgehen. So können komplexere Code-Snippets vorgeschlagen oder ganze Methoden und Funktionen auf Basis kurzer Beschreibungen generiert werden.

Im Idealfall soll dies die Produktivität erhöhen, auch wenn das letzte Wort hierbei noch nicht gesprochen ist. Doch welche Werkzeuge existieren? Im Rahmen des Artikels soll ein Blick auf spezialisiertere Lösungen zur Entwicklung abseits von ChatGPT und Co. geworfen werden.

Arten von Werkzeugen

Auf dem Markt der KI-Werkzeuge zur Softwareentwicklung existieren Werkzeuge unterschiedlicher Couleur. Neben Integrationen für eine Anzahl von IDEs, existieren Standalone-Tools und auch webbasierte Tools. Viele KI-Werkzeuge sind als Plugins oder Erweiterungen für IDEs wie Visual Studio Code oder IntelliJ IDEA verfügbar. Diese Integrationen ermöglichen es, KI-gestützte Funktionen direkt in der gewohnten Entwicklungsumgebung zu nutzen, was den Arbeitsablauf verbessert.

Einige dieser Werkzeuge bieten spezialisierte Funktionen, die auf bestimmte Aspekte der Softwareentwicklung abzielen, wie Code-Generierung, Fehlererkennung, Optimierung, Review oder Testautomatisierung.

Code-Assistenten

Einer der häufigsten neuen Werkzeug-Typen sind Code-Assistenten, welche es ermöglichen Quellcode zu generieren und diese Fähigkeit in einer Entwicklungsumgebung einzusetzen. Daneben können Fragen zum Quellcode gestellt, Dokumentationen erzeugt, oder Vorschläge für ein Refactoring erzeugt werden.

Bei diesen Code-Assistenten finden sich etliche Schwergewichte der IT, wie Amazon oder Microsoft wieder.

Amazon Q

Als Antwort auf GitHub Copilot stellte Amazon CodeWhisperer vor. Mittlerweile ist dieses Werkzeug in Amazon Q aufgegangen.

Für Entwickler dürfte das Teilprodukt Amazon Q Developer interessant sein. Für dieses sind unter anderem Integrationen für die JetBrains IDEs, VS-Code und Visual Studio verfügbar. Auch eine Version für die Kommandozeile wird geboten.

Amazon Q in einer Jetbrains IDE

Für den Assistenten wird eine AWS Builder ID benötigt. Im begrenztem Rahmen kann der Assistent, damit kostenlos ausprobiert werden.

Sinnvolle Ergebnisse liefert der Assistenz nur bei Anfragen in englischer Sprache. Interessant ist die Möglichkeit, Quelltext zu generieren, der über mehrere Dateien reicht. Hier haben andere Assistenten meist ihre Probleme und erzeugen nur Quellcode an einem Stück.

Gesteuert wird der Assistent über Befehle wie /dev mit einem darauffolgenden Prompt. Angeboten wird neben der kostenlosen Variante, ein Business Lite und ein Business Pro Abonnement.

Insgesamt fühlt sich Amazon Q als generisches KI-Werkzeug zur Entwicklung unzureichend an, allerdings könnte es anders aussehen, wenn eine engere Verzahnung mit AWS und die Nutzung eigener Geschäftsdaten gewünscht wird.

Codeium

Codeium ist ebenfalls ein Code-Assistent, welcher sich in unterschiedlichste IDEs integriert.

Codeium unterstützt eine Reihe von IDEs

Das Plugin verfügt über eine Chat-Funktionalität, welche es ermöglicht Anforderungen bzw. Prompts zu definieren. Negativ fällt auf, dass hier die aktuell genutzte Programmiersprache nicht automatisch erkannt wird, sondern explizit angegeben werden muss.

Auch das Antworten auf bereits erzeugte Nachrichten muss separat erledigt werden. Wird stattdessen direkt im Chatfenster geantwortet, wird eine neue unabhängige Konversation gestartet. Soll auf einen vorherigen Chat Bezug genommen werden, so muss der Continue this chat-Button genutzt werden.

Die Chat-Funktionalität nutzt die falsche Programmiersprache

Interessanter ist die Möglichkeit, relativ unkompliziert Unit-Tests für ausgewählte Methoden zu generieren. Hierfür wird eine Methode ausgewählt und entsprechende Testfälle werden ermittelt und anschließend in Code umgesetzt.

Codium erzeugt Testfälle

Anschließend können die Testfälle in eine Datei übernommen werden. Auch hier fehlt wieder der Kontext, da die Datei standardmäßig einfach im Hauptverzeichnis des Projektes abgelegt wird, zumindest bei der JetBrains-IDE-Integration.

Genutzt werden für Codium die OpenAI-Modelle der GPT-3 und GPT-4 Reihe. Interessant ist Codium für Plattformen, bei denen sonst keine IDE-Integration vorliegt, da Codium hier mit Vielfalt glänzt.

Neben dem Codeassistenten bietet Codium mit Forge auch eine Lösung für das Review von Quellcode an.

Cody

Mit Cody existiert ein KI-gestützter Assistent zur Softwareentwicklung. Nicht verwechselt werden sollte der Assistent mit Cody AI, das sich mehr als KI-unterstützte Suche auf Basis einer Firmen-Wissensbasis versteht.

Neben der Webvariante von Cody werden primär die Entwicklungsumgebungen VS Code und die JetBrains-IDEs unterstützt. Daneben existiert eine experimentelle Unterstützung für Neovim. Andere IDEs wie Eclipse und Emacs sollen in Zukunft folgen.

In der JetBrains-Variante wirkt die Integration ausgereift. So ist nicht nur ein Fenster verfügbar, in dem ein Chat angezeigt wird, sondern es existiert auch eine Integration im Code-Editor.

Anhand des Methodennamens wurde der Inhalt der Methode generiert

Während im Chatfenster der Kontext, wie die aktuell verwendete Programmiersprache nicht erkannt wird, sieht dies im Code-Editor anders aus. Hier wird der Code in der verwendeten Sprache generiert.

Die Modellauswahl im Chat-Fenster

Ein Merkmal, mit dem sich Cody von anderen KI-Assistenten unterscheidet, ist die transparente Auswahl der genutzten Modelle. Das passende Modell kann hierbei einfach ausgewählt werden.

Neben den Möglichkeiten zur Codegenerierung bietet Cody auch die Möglichkeit vorgefertigte Kommandos zu nutzen und mit diesen das Dokumentieren von Quellcode oder Unit-Test zu automatisieren.

CodeSquire

CodeSquire ist eine spezialisierte KI-Assistent-Lösung in Form einer Erweiterung für den Browser Chrome. CodeSquire ist ein Tool für Datenwissenschaftler, das Kommentare in Code umwandelt, SQL-Anfragen aus natürlicher Sprache erstellt, intelligente Codevervollständigung bietet und komplexe Funktionen generiert.

Unterstützt werden aktuell Plattformen wie Google Colab, BigQuery und JupyterLab.

Diese Plattformen zählen zu IDEs, die meist speziell für interaktive Datenanalyse und wissenschaftliches Rechnen genutzt werden. Diese speziellen IDEs kombinieren viele Funktionen, die in traditionellen IDEs zu finden sind, wie Code-Editoren, Terminals und Dateibrowser, mit speziellen Werkzeugen für die Arbeit mit Daten und interaktiven Notebooks.

CodeWP

Ebenfalls zu den spezialisierten Lösungen zählt CodeWP, welches einen Assistenten darstellt, welcher auf WordPress spezialisiert ist.

CodeWP

Die dahinterliegenden Modelle sind darauf trainiert, Code in PHP und JavaScript im Kontext von WordPress zu generieren. So kann mit einem einzelnen Prompt ein einfaches Plugin generiert werden.

Die CodeWP-Website

CodeWP erweckt mit Aussagen wie Proprietary AI und More accurate than ChatGPT sowie der Aussage:

Our Al models are trained to output the best, most modern, secure, simple code for WordPress. So no need to worry about common bugs or issues.

den Eindruck, dass ein eigenes Sprachmodel verwendet wird, ohne auf Mitbewerber wie OpenAI angewiesen zu sein.

Cursor

Cursor versteht sich, im Gegensatz zu den bisher vorgestellten Assistenten, als dedizierte IDE mit einer KI-basierten Unterstützung für Entwicklung.

Technisch handelt es sich um einen Fork von VS Code. Der Grund hierfür, ist nach Aussage des Herstellers, in der besseren Anpassbarkeit der IDE zu finden.

Der Onboarding-Prozess von Cursor

Nach der Installation wird der Nutzer durch einen kleinen Onboarding-Prozess geführt. Dieser führt in die Möglichkeiten ein, Bugs zu identifizieren, spezifische Codestellen zu lokalisieren oder Code von einer Programmiersprache in eine andere zu übersetzen.

Cursor kann natürliche Sprache verstehen und darauf reagieren, was es erleichtern soll, direkt im Code-Editor mit der KI zu interagieren. So können Fragen zu Codebasis gestellt werden, Vervollständigungen angefordert werden oder Code-Snippets generieren werden.

Die Freemium-Version unterliegt einigen Einschränkungen, welche in den kostenpflichtigen Tarifen aufgehoben werden.

Fraglich ist, ob hierfür eine neue IDE benötigt, und warum nicht auf Integrationen für bestehende Systeme gesetzt wurde. In den meisten Fällen werden Entwickler doch meist auf ihre angestammten Werkzeuge setzen wollen.

GitHub Copilot

Zu den bekannteren Lösungen auf dem Markt zählt sicherlich GitHub Copilot. Dieses Werkzeug ist in allen Varianten (bis auf die Trial-Version) kostenpflichtig.

Neben der Nutzung über die Kommandozeile, existieren eine Reihe von IDE-Integrationen, insbesondere für Visual Studio, VS Code und die JetBrains IDEs. Daneben werden Vim und Neovim, sowie Azure Data Studio unterstützt.

GitHub Copilot in einer JetBrains-IDE

Positiv fällt die Autovervollständigung bzw. die Geschwindigkeit derselben auf. Allerdings ist sie in einigen Fällen auch relativ nervig, da sie bei der Entwicklung zu unnötiger Ablenkung führen kann.

Eine Methode wird generiert

Zumindest in den JetBrains-IDEs gibt es keine Integration über die Quick-Fixes-Funktionalität. Dafür stehen eine Reihe von Kommandos wie /tests, /simplify, /fix oder /explain zur Verfügung.

Diese können in der eingebauten Chat-Funktionalität genutzt werden. Die Ergebnisse werden im Chat angezeigt, können allerdings nicht automatisch ins Projekt übernommen werden, sondern müssen kopiert und wieder eingefügt werden. Besonders nervig ist dies bei der Generierung von Dokumentation für Methoden, wie sich im Vergleich zum Assistenten JetBrains AI zeigt.

Positiv hervorzuheben ist die automatische Übernahme des Kontexts, wenn Themen im Chat angesprochen und genutzt werden.

JetBrains AI

Das tschechische Unternehmen JetBrains ist primär für seine unterschiedlichen IDEs bekannt und bietet mit JetBrains AI einen Assistenten für KI-unterstütze Entwicklung. Auch JetBrains AI muss über ein Abonnement freigeschaltet werden. Wenig verwunderlich ist die Integration von JetBrains AI in die jeweiligen IDEs der Firma sehr gelungen.

Entwicklung mit der JetBrains AI

Neben der bei vielen KI-Assistenten gegebenen Möglichkeiten des Chats mit dem Sprachmodell, bietet JetBrains AI die Möglichkeit von Quick-Fixes in Form von AI Actions, welche unter anderem das Schreiben von Dokumentation oder das Generieren von Unit-Tests vereinfachen sollen.

Neben den vorgefertigten Prompts können eigene Prompts hinterlegt und diese dann ebenfalls über die AI Actions genutzt werden. Angenehm an JetBrains AI ist die Möglichkeit Dokumentation wie Javadoc automatisch für eine Methode generieren und antragen zu können.

Die Einstellungen für JetBrains AI

Automatische Codevorschläge während der Entwicklung sind so gestaltet, dass sie nicht unnötig ablenken und können über die Einstellungen konfiguriert werden.

Daneben findet sich der KI-Assistent noch in anderen Integrationen wieder, wie bei der Umbenennung bzw. der Namensfindung, hier werden neben den klassischen Vorschlägen auch KI-Vorschläge angezeigt.

Durch ein kleines Symbol wird transparent gezeigt, welche Vorschläge von der KI stammen und welche nicht. Grundsätzlich zieht sich diese Transparenz durch JetBrains AI bzw. dessen Implementation.

Auch Fragen zu bestimmten Teilen des Quellcodes können schnell und bequem gestellt werden, indem an der gewünschten Stelle über eine Quick-Action ein KI-Chat zum aktuellen Quellcode gestartet wird.

Weitere Kleinigkeiten sind die Generierung von Commit-Nachrichten, welche ebenfalls von JetBrains AI bereitgestellt werden.

Während im Standard-Abonnement von JetBrains AI nicht gewählt werden kann, welche Sprachmodelle verwendet werden, soll dies später in den Enterprise-Varianten auswählbar sein. Je nach genutzter Funktionalität scheinen im Moment unterschiedliche Modelle genutzt werden.

Neben JetBrains AI, verfügen einige IDEs wie IntelliJ IDEA Ultimate mittlerweile auch über Möglichkeiten zur Codevervollständigung über ein lokales Sprachmodell, welches ohne externe Zugriffe auskommt.

Die IDE-Integration von JetBrains AI wirkt insgesamt sehr ausgereift, insbesondere im Vergleich zu anderen KI-basierten Assistenten. Dafür steht JetBrains AI nur für die entsprechenden IDEs der Firma zur Verfügung.

Tabnine

Die Firma hinter Tabnine existiert schon länger als der aktuelle KI-Hype und hat seit längerem Code-Assistenten zur Unterstützung in der Entwicklung angeboten.

Ursprünglich bekannt als Codota, hat sich das Unternehmen auf die Entwicklung von KI-basierten Werkzeugen für Entwickler spezialisiert. Im Gegensatz zu vielen anderen Lösungen wird bei Tabnine, über Tabnine Enterprise, auch das Selbst-Hosting angeboten.

Interessant ist bei Tabnine die Wahl der Modelle zur Verarbeitung der Anfragen. Hier werden Modelle wie Tabnine Protected angeboten, welche nur mit Quellcodes trainiert wurden, welche eine entsprechende Lizenz besitzen und somit idealerweise z. B. keine Codeschnipsel unter GPL replizieren.

Auch werden je nach Modell gewisse Garantien gegeben, was Themen wie Datenschutz und die Weiterverwendung der Prompts angeht. Daneben werden die Modelle über Tags sinnvoll kategorisiert, sodass die Wahl des passenden Modells aufgrund dieser getätigt werden kann.

Die Auswahl der Modelle

Bei den IDEs unterstützt Tabnine eine Reihe von IDEs, angefangen bei VS Code über die JetBrains-IDEs, bis hin zu Neovim.

Die Fix-Funktionalität von Tabnine

In Bezug auf die IDE-Integration wirkt Tabnine in JetBrains-IDEs recht gut integriert. Dadurch können kontextbasierte Operationen wie das Beheben von Fehlern oder das Dokumentieren von Quellcode effizient durchgeführt werden.

Im Tabnine-Chat wird dabei eine Antwort generiert und dessen Ergebnis kann mit in den Quellcode übernommen werden.

Das manuelle Einfügen fühlt sich allerdings immer etwas umständlich an und aktiviert oft die automatische Codeformatierung nicht, was im schlechtesten Fall immer einen zusätzlichen Bearbeitungsschritt bedeutet.

Die Generation eines Tests schlägt fehl

Andere Operationen, wie die Erstellung eines Testplans, können unter Umständen scheitern, da eine vom Plugin generierte Datei möglicherweise nicht befüllt werden kann, was auf einen Bug hinzudeuten scheint.

Die Testplan-Ideen von Tabnine

Auch wenn die Ideen für den Testplan von Tabnine interessant sind, fühlt sich hier die Integration durch das manuelle Einfügen komplex und fehleranfällig an.

Analyse-Werkzeuge

Neben den allgemeinen Code-Assistenten existieren einige Werkzeuge, welche sich auf die Analyse von Quellcode spezialisiert haben, z. B. für das Review von Quellcode bzw. Pull Requests.

Amazon CodeGuru

Ein von Amazon angebotenes Analyse-Werkzeug ist Amazon CodeGuru. Dieses Werkzeug versteht sich als Scanner, um Sicherheitslücken und Schwachstellen im Code zu finden. Daneben werden auch Vorschläge erstellt wie Anwendungen optimiert bzw. beschleunigt werden können.

Gedacht ist dieses Werkzeug nicht für die direkte Nutzung, sondern eher für die Integration in entsprechende Pipelines.

Neben der Nutzung in AWS CodeCommit (das demnächst eingestellt wird) wird auch die Nutzung von BitBucket- und GitHub-Repositories unterstützt.

Sourcery AI

Sourcery AI versteht sich als Werkzeug für automatisches Reviewing. Verknüpft werden kann dieses Werkzeug unter anderem mit GitHub oder GitLab. Wenn gewünscht, wird so bei jedem Pull-Request ein entsprechender Kommentar hinterlassen.

Sourcery AI erstellt Kommentare zu einem Pull Request

Während die Nutzung für kommerzielle Projekte mit einem Abonnement verbunden ist, können Open-Source-Projekte Sourcery AI ohne weitere Kosten einsetzen.

Neben der Kommentierung des Pull-Requests werden auch Hinweise für den Reviewer und eine Zusammenfassung erstellt.

Snyk

Neben Werkzeugen, die sich auf normale Entwicklungsarbeiten konzentrierten, existiert mit Snyk ein Analyse-Werkzeug, welches Verwundbarkeiten und Sicherheitsprobleme im Code aufdecken soll.

Snyk in einer JetBrains IDE

Snyk positioniert sich als Werkzeug, das durch den Einsatz von maschinellem Lernen sowie dynamischen und statischen Analysen den Quellcode auf diese Problemklasse hin untersucht.

Dabei werden eine Reihe von Produkten angeboten, welche diese Technologie zur Anwendung bringen soll.

WhatTheDiff

Ähnlich wie Sourcery AI ist auch WhatTheDiff ein Werkzeug für automatisierte Code-Reviews.

Im Gegensatz zu Sourcery AI muss die GitHub-Integration vor der Nutzung konfiguriert und aktiviert werden.

Die Repositories müssen aktiviert werden

Nach der Aktivierung werden für Pull Requests automatisch Kommentare erzeugt.

What The Diff erzeugt automatisch Kommentare zu den Pull Requests

Wie bei Sourcery AI werden hier auch Kommentare zur Zusammenfassung und Review-Kommentare am Pull Request erstellt, welche dann bearbeitet werden können.

Weitere Werkzeuge

Neben den größeren Klassen wie Code-Assistenten und Analysewerkzeuge, existieren weitere Werkzeuge, welche KI-basiert einen Mehrwert in der Entwicklung bringen können.

bloop.ai

Unter bloop.ai werden verschiedene Services rund um KI-gestützte Codegenerierung und Nutzung angeboten.

So wird ein Dienst angeboten, welcher COBOL-Programme in lesbare Java-Applikationen umwandeln soll. Ein weiterer Dienst befasst sich mit einem Sprachmodell, welches direkt COBOL-Quellcode schreiben kann.

bloop indiziert ein Repository

Für den alltäglichen Gebrauch interessanter war die Understand-Funktionalität, die es ermöglicht, Repositories zu laden und anhand dieser Repositories Fragen zum Quellcode zu stellen.

Bloop wird zum Bevy-Projekt befragt

Diese existierte in einer freien Variante sowie in einer kostenpflichtigen Personal-Variante. In der kostenpflichtigen Variante wurde unter anderem die Indizierung mehrerer Branches ermöglicht.

Nach der kürzlich erfolgten Einstellung steht nur noch die freie Variante dieser Funktionalität zur Verfügung. Für den alltäglichen Gebrauch, vorwiegend mit unbekannteren Codebasen, kann dieses Werkzeug eine wertvolle Ergänzung sein.

GitFluence

Wer in der Softwareentwicklung arbeitet, wird oft auch mit Versionskontrollsystemen wie Git arbeiten. Auch hier existieren mittlerweile KI-Tools, welche unterstützen sollen.

GitFluence

Eines dieser Werkzeuge ist GitFluence, das unter der Haube mit der OpenAI-API arbeitet. Gedacht ist das Werkzeug für den Fall, dass eine Git-Aktion beschrieben wird und automatisch ein Git-Kommando dafür erstellt wird.

Dies wirkt allerdings in einigen Fällen eher unausgegoren und lieferte unbrauchbare Ergebnisse, während es sporadisch sinnvolle Antworten liefert.

Grit.io

Der Dienst Grit.io spezialisiert sich auf Code-Migration und automatische Dependency Upgrades. Aktuell ist er nur über eine Warteliste verfügbar, sodass hier eine genauere Beurteilung schwerfällt.

Eines der Beispiele von der Grit.io-Seite

Durch die automatische Aktualisierung von Abhängigkeiten und die Durchführung größerer Migrationen soll eine allgemeine Verbesserung der Codequalität stattfinden.

Mutable AI

Neben Code-Assistenten, die sich auf die Entwicklung spezialisieren, existieren auch solche Assistenten, die sich der Dokumentation und Schaffung einer Wissensbasis zur entwickelten Software verschrieben haben. Zu diesen Diensten gehört Mutable AI.

Eine Mutable AI-Wiki

Nach Abschluss eines Abonnements ist es möglich zu einem Repository ein automatisches Wiki zur Dokumentation zu erstellen. Neben dieser Art der Dokumentation kann die Codebasis auch über einen KI-Assistenten befragt werden.

Die Dokumentation wird automatisch bei Änderungen des Repositories aktualisiert.

SQLAI.ai

Für die Arbeit mit SQL und Datenbanken existieren eine Reihe von KI-Werkzeugen wie SQLAI.ai. Mithilfe dieser Werkzeuge können Abfragen erzeugt, überprüft und auf Fehler untersucht werden.

SQLAI

Im Wesentlichen generieren die meisten dieser Werkzeuge, häufig unter Einbeziehung zusätzlicher Informationen wie des Datenbankschemas, passende Eingaben für das verwendete Sprachmodell. Zusätzliche Metainformationen wie das Datenbankschema, helfen hierbei sinnvolle Ausgaben für die eigenen Projekte zu erzeugen.

Ein ähnliches Werkzeug ist AI Query, das ebenfalls über Werkzeuge zur SQL-Prüfung und Bearbeitung verfügt. Daneben existieren eine Vielzahl anderer Werkzeuge dieser Art wie TEXT2SQL oder AI2sql.

Über den Tellerrand

Neben all diesen Werkzeugen existieren weitere Ansätze und Möglichkeiten, welche die Entwicklung und Prozesse der Softwareentwicklung vereinfachen sollen.

So existiert mit Stepsize AI ein Werkzeug, welches Sprint Reports im Kontext der agieren Softwareentwicklung erzeugen soll oder mit Bugasura ein Bug-Tracker mit KI-Unterstützung.

Neben kommerziellen Lösungen, welche auf entsprechende Modelle von OpenAI und Co. setzen, existieren auch freie Modelle zur Entwicklung von Software.

Eines dieser Modelle ist PolyCoder, welches auf Basis von GPT-2, mit einem Korpus von über zwölf Programmiersprachen trainiert wurde. Ähnliches vermag CodeGeeX zu leisten, welches aus dem asiatischen Raum stammt.

Allerdings lassen sich diese Systeme nicht so einfach nutzen wie die vorkonfektionierten Angebote, kommerzieller Anbieter. Es muss ein entsprechender Setup-Aufwand geleistet werden, bevor die Modelle genutzt werden können. Darüber hinaus ist die Performanz lokal ausgeführter Modelle, aufgrund der genutzten Hardware, oft unzureichend.

Fazit

Sprachmodelle konnten für die Entwicklung bereits genutzt werden, bevor es spezielle Integrationen dafür gab. Dafür musste der Entwickler Prompts definieren und diese mit dem Quelltext in das Modell geben.

Viele Integrationen nehmen dem Entwickler das Schreiben des Prompts in vielen Fällen ab und ermöglichen so eine schnellere Nutzung der Modelle. Bedingt durch die zugrundeliegenden Sprachmodelle werden viele Programmiersprachen auch von den vorgestellten Werkzeugen unterstützt.

Damit können in der Theorie viele Standardaufgaben, wie die Dokumentation, Unit-Tests oder auch komplexere Dinge wie die Konvertierung zwischen zwei Programmiersprachen mehr oder weniger vereinfacht werden. Allerdings sollten die Ergebnisse dieser KI-basierten Assistenzfunktionen immer bewertet und analysiert werden und nicht einfach ungeprüft übernommen werden. Spätestens bei komplexeren Problemen, welche ein umfassenderes Verständnis über die Codebasis benötigen, versagen die KI-Assistenten in vielen Fällen.

Aktuell existieren auf dem Markt eine unzählige Anzahl von KI-Werkzeugen und jeden Tag werden es mehr. Einige dieser Werkzeuge werden wieder verschwinden, während andere Werkzeuge erhalten bleiben. Auch in Zukunft sollen KI-Assistenten weiter integriert werden, wie in XCode von Apple.

Für Code-Assistenten sowie zahlreiche andere Werkzeuge gilt, dass sie im Wesentlichen auf ähnliche Weise funktionieren: Ein beliebiger Prompt wird erstellt, an ein Sprachmodell übermittelt und von diesem verarbeitet.

Hier stechen am Ende nur Lösungen hervor, welche eine gute Integration bieten und es somit dem Entwickler nicht unnötig schwer machen, die Assistenzfunktionen im Arbeitsalltag anzuwenden.

Positiv haben neben der Integration der JetBrains AI die Codesuche über Bloop überrascht, bei welcher zu einer Codebasis Fragen gestellt werden können und diese Codebasis damit genauer und schneller kennengelernt werden kann.

Neben den praktischen Aspekten sollte auch beachtetet werden, dass ein Großteil der aktuellen KI-Lösungen kostenpflichtig sind und ihren Gegenwert einspielen müssen.

Abgesehen von den monetären Aspekten gilt es auch den Datenschutz zu beachten, schließlich werden in vielen Fällen vertrauliche Daten an Drittservices gesendet und dort verarbeitet.

Daneben ist die Datenbasis prinzipbedingt immer leicht veraltet. So können Informationen zu neuen Versionen einer Software z. B. zur Game Engine Bevy über viele Sprachmodelle nicht bezogen werden, da ihr Trainingsdatum vor dem Erscheinungsdatum der neuen Softwareversion liegt.

Ob sich die Technologie in Zukunft einen wirklichen Mehrwert in der Entwicklung bringt, wird sich zeigen. Gegenwärtig scheint es so, dass sich ein Teil der KI-Werkzeuge sich dem Plateau der Produktivität im Hype-Zyklus nähert.

Bei einer guten und niederschwelligen Integration kann damit vielleicht das ein oder andere KI-basierte Werkzeug seinen Weg in den Werkzeugkasten der Softwareentwicklung finden.

Dieser Artikel erschien ursprünglich auf Golem.de und ist hier in einer alternativen Variante zu finden.

Fluent Interfaces unter Java

Der Begriff des Fluent Interface hat mittlerweile knapp zwei Jahrzehnte auf dem Buckel und solche Schnittstellen sind mittlerweile auch in größerem Umfang in der Praxis angekommen und werden dort vielfältig genutzt. Trotzdem scheint es manchmal schwer zu fallen genau zu definieren, was ein solches Fluent Interface ist, was es auszeichnet und wie es in der Praxis sinnvoll genutzt werden kann.

Herkunft

Fluent Interfaces, welche wohl holprig mit einer fließenden Schnittstelle übersetzt werden könnten, sind eine Begrifflichkeit, welche in dieser Form erstmals 2005 von Martin Fowler und Eric Evans definiert wurden.

Martin Fowler ist bekannt für seine Arbeit, am Manifest für Agile Softwareentwicklung und seine Bücher, welche unter anderem das Code-Refactoring in die breite Öffentlichkeit trugen und populär machten.

Eric Evans ist vorwiegend für seine Beiträge rund um das Domain-Driven Design bekannt, dessen Gedanken er erstmals im gleichnamigen Buch Anfang der 2000er-Jahre zusammenfasste und somit den Begriff prägte.

Sinnvoller wäre für Fluent Interfaces allerdings die Bezeichnung als sprechende Schnittstelle, welcher eher der Idee im Kern entspricht. Bezugnehmend auf die Wortherkunft aus dem englischen to be fluent in a language, kann allerdings auch im Deutschen von Fluent Interfaces gesprochen werden.

Ideen des Fluent Interfaces

Dem Namen entsprechend ist eines der Ziele eines Fluent Interfaces, APIs besser lesbar und verstehbarer zu gestalten. Daneben soll die API Funktionalitäten passend zum Objekt bereitstellen können.

Bei Betrachtung einer solchen sprechenden Schnittstelle, soll der Quellcode sich wie eine natürliche Sprache bzw. ein Text in einer solchen anfühlen, anstatt eine Reihe von abstrakten Anweisungen darzustellen. Martin Fowler definierte auch ein Maß für diese Fluidität:

The key test of fluency, for us, is the Domain Specific Language quality. The more the use of the API has that language like flow, the more fluent it is.

Ein schönes Beispiel für die Nutzung einer solchen sprechenden Schnittstelle ist die Anwendung des Mock-Frameworks Mockito:

when(uuidGenerator.getUUID()).thenReturn(15);

Auch ohne genauere Kenntnisse über Mockito kann der Programmierer aus diesem Beispiel herauslesen, dass wenn die Methode getUUID angefordert wird, im Rahmen des Mocks immer der Wert 15 zurückgegeben wird.

Die Zeile kann mit sprachlichen Mitteln einfach gelesen werden und verrät dem Nutzer etwas über die dahinterliegende Funktionalität. Das Ziel, die entsprechenden Schnittstellen einfach lesbar und benutzbar zu machen, wird sich dadurch erkauft, dass es meist aufwendiger ist, solche Schnittstellen zu designen und zu entwickeln.

Dies bedingt sich durch das vorher zu durchdenkende Design und einem Mehraufwand beim Schreiben des eigentlichen Quellcodes, unter anderem in Form zusätzlicher Methoden und anderen Objekten zur Abbildung des Fluent Interface.

Zur Implementierung von Fluent Interfaces werden unterschiedliche Techniken wie das Method Chaining genutzt. Daneben werden häufig Elemente wie statische Factory-Methoden und entsprechende Imports, sowie benannte Parameter genutzt.

Sprechende Schnittstellen ermöglichen es auch Personen einen Einblick in den Quellcode zu geben, welche nicht mit den technischen Details desselben vertraut sind. Auch bei der Abbildung von fachlicher Logik helfen Fluent Interfaces, z. B. über entsprechende Methodennamen, welche sich auf die Fachlichkeit der Anwendung beziehen.

Method Chaining

Eine der Techniken zur Erstellung von sprechenden Schnittstellen ist das Method Chaining, welches im Deutschen treffend mit Methodenverkettung übersetzt werden kann.

Methodenketten sind eine Technik, bei der mehrere Methoden aufeinanderfolgend aufgerufen werden, um ein Ergebnis zu erhalten. Beispielsweise könnte ein Entwickler mehrere Methoden aufrufen, um einen bestimmten Wert aus einem Array zu extrahieren oder einen Alternativwert bereitzustellen. Die Methodenketten ermöglichen es dem Entwickler, mehrere Aufgaben in einer Kette zu erledigen, anstatt sie auf getrennte Statements zu verteilen.

Das bedeutetet allerdings im Umkehrschluss nicht, dass die Verkettung von Methoden gleich einem Fluent Interface entspricht, sodass an dieser Stelle immer differenziert werden sollte.

Am Ende einer Methodenkette wird das benötigte Objekt zurückgegeben oder die gewünschte Aufgabe ausgeführt. Aussehen könnte eine solche Methodenkette, in diesem Fall am Beispiel der Stream-API, wie folgt:

User nathalie = Arrays.stream(users)
        .filter(user -> "Nathalie".equals(user.getForename()))
        .findFirst()
        .orElse(null);

Bei solchen Methodenketten gilt es zu berücksichtigen, dass bestimmte Methoden innerhalb der Kette im schlimmsten Fall null zurückgeben oder Exceptions auslösen. Je nach genutztem Interface müssen diese Fälle entsprechend beachtet werden.

Interessanterweise brechen Methodenketten mit einigen älteren Konventionen. So würden z. B. Setter in einem Builder-Pattern:

User.Builder.newBuilder()
               .name("Mustermann")
               .forename("Max")
               .username("maxmustermann")
               .build();

einen Wert zurückgeben, in diesem Beispiel das Builder-Objekt. Im Normalfall würde der Setter stattdessen keinen Rückgabeparameter aufweisen. Er wäre in einem gewöhnlichen Objekt wie folgt definiert:

public void setName(String name) {
    this.name = name;
}

Diese Konvention rührt vom Command-Query-Separation-Pattern, welches besagt, dass entweder Kommandos oder Queries definiert werden sollten. Abfragen als solche, dürfen keinerlei Auswirkungen am Zustand der Klasse haben, während bei Kommandos entsprechende Auswirkungen explizit erwünscht sind. Dies ist z. B. dann der Fall, wenn Daten geändert werden.

Method Nesting

Eine weitere Technik, welche für die Nutzung und das Design von Fluent Interfaces benutzt wird, ist das Method Nesting bzw. das Verschachteln von Methoden. Ein Beispiel hierfür liefert die Java-Bibliothek REST Assured, welche eine domänenspezifische Sprache zur Abfrage und Validierung von REST-Services darstellt:

when()
  .get("https://api.pi.delivery/v1/pi?start=0&numberOfDigits=100").
then()
  .statusCode(200)
  .body("content", startsWith("31415926535897932384"));

Neben der Verkettung von Methoden werden hier auch Methoden ineinander verschachtelt. In diesem Fall wird die startWith-Methode innerhalb der body-Methode geschachtelt, was dafür sorgt, dass beim Lesen ein besseres Verständnis entsteht, was im Quellcode passiert.

Die startWith-Methode stammt hierbei aus dem Hamcrest-Framework, welches viele solcher Methoden, unter anderem zur Nutzung in Unit-Tests, bereitstellt.

Statische Imports

Damit die Sprachelemente der Bibliothek im obigen Beispiel nicht immer voll referenziert werden müssen, werden statische Imports genutzt:

import static io.restassured.RestAssured.*;
import static org.hamcrest.Matchers.*;

Damit kann eine statische Methode ohne ihren Klassennamen referenziert und genutzt werden. Ohne diesen statischen Import würde obiges Beispiel etwas umfangreicher aussehen:

RestAssured.when()
  .get("https://api.pi.delivery/v1/pi?start=0&numberOfDigits=100").
then()
  .statusCode(200)
  .body("content", Matchers.startsWith("31415926535897932384"));

Damit wird über den Mechanismus der statischen Importe sichergestellt, dass der Quelltext, im Rahmen der Philosophie der sprechenden Schnittstellen, möglichst lesbar bleibt.

Object Scoping

Vor allem in Programmiersprachen mit einem globalen Namensraum und echten Funktionen können Funktionen, welche einzelne Bestandteile einer domänenspezifischen Sprache darstellen, in diesem globalen Namensraum angelegt werden.

Allerdings ist dies in den meisten Fällen unerwünscht, sodass die Bestandteile der domänenspezifischen Sprache, wie im Beispiel REST Assured in entsprechende Objekte verpackt werden und damit den globalen Namensraum entlasten. Dieses Vorgehen trägt den Namen Object Scoping.

Anwendung

Doch wozu können solche Fluent Interfaces in der Praxis genutzt werden? Unterschiedlichste Szenarien werden immer wieder angesprochen, wenn es um die Nutzung dieser Technik geht.

Eine häufigere Anwendung ist das Builder-Pattern und die Nutzung in Form domänenspezifischer Sprachen kurz DSLs bzw. im englischen Original Domain Specific Languages.

Eine solche domänenspezifische Sprache ist eine Programmiersprache, welche für ein bestimmtes Feld, besagte Domäne, entwickelt wird. Häufig werden sie verwendet, um komplexe Aufgaben zu vereinfachen, indem eine auf die Domäne und die zu lösenden Probleme angepasste Syntax genutzt wird, welche meist große Schnittmengen mit der Fachlichkeit der Domäne beinhaltet.

Fluent Interfaces in der Java-Welt

Auch in der Java Class Library existieren APIs, welche als Fluent Interface angelegt sind. Ein prominentes Beispiel hierfür ist die Stream-API:

User aMustermann = Arrays.stream(users)
        .parallel()
        .filter(user -> "Mustermann".equals(user.getName()))
        .findAny()
        .orElse(null);

Über die Stream-API können unterschiedlichste Anforderungen abgebildet werden. In diesem Fall wird ein Array mit Objekten vom Typ User durchsucht und parallel ein Nutzer mit dem Nachnamen Mustermann gesucht. Dieses Objekt wird dann zurückgegeben. Wird kein solches Objekt gefunden, so wird stattdessen null zurückgegeben.

Auch in anderen Frameworks wie Mocking-Frameworks oder solchen Bibliotheken, welche domänenspezifische Sprachen abbilden, wird intensiv Gebrauch von Fluent Interfaces gemacht.

Frameworks, wie Spring, nutzen ebenfalls interne DSLs, um bestimmte Anforderungen und Möglichkeiten zur Konfiguration abzubilden.

Fluent Interfaces im Builder-Pattern

Eine weitere Anwendung für Fluent Interfaces im weiteren Sinne ist das sogenannte Builder-Pattern. Bei diesem existiert eine Klasse und ein dazugehöriger Builder, welcher dazu dient das Objekt zu konstruieren.

Hier wird ersichtlich, dass dies auf den ersten Blick mehr Aufwand bei der Implementation bedeutet. Allerdings müssen „Standardaufgaben“, wie das Erstellen eines Builders nicht unbedingt von Hand erledigt werden, da es für unterschiedlichste IDEs entsprechende Unterstützung in Form von Erweiterungen gibt. Für IntelliJ IDEA wäre ein Plugin für diese Aufgabe der InnerBuilder von Mathias Bogaert.

Ein Plugin für die einfache Erzeugung von Buildern in IntelliJ IDEA

Mithilfe dieser Plugins können Builder-Klassen schnell erstellt und bei Änderungen der eigentlichen Basisklasse angepasst werden, indem die Builder-Klasse über das Plugin neu generiert wird. Dies ermöglicht eine unkomplizierte Änderung von Klassen samt ihrer dazugehörigen Builder.

Manchmal wird beim Builder-Pattern infrage gestellt, ob es sich bei diesem wirklich um ein Fluent Interface handelt. Dabei wird die Unterscheidung getroffen, dass Fluent Interfaces sich mehr auf die Manipulation und Konfiguration von Objekten konzentrieren, während Builder den Fokus auf die Erzeugung von Objekten legen.

Es stellt sich allerdings die Frage, inwiefern diese Gedanken akademischer Natur, in der Praxis relevant sind, da auch Builder sich aus Sicht des Lesers des Quellcodes gut als Fluent Interfaces erschließen lassen.

Builder-Pattern im Beispiel

Wie könnte ein solches Builder-Pattern nun an einem einfachen Beispiel aussehen? Gegeben sei folgende Klasse:

public class User {

    private String name;

    private String forename;

    private String username;

    public User() {
    }
}

Die Klasse mit dem Namen User enthält drei Felder in welchen der Name, Vorname und der Nutzername des Nutzers gespeichert werden. Um die Felder der Klasse zu befüllen, existieren unterschiedliche Möglichkeiten.

So könnte ein entsprechender Konstruktor definiert werden, welcher diese Felder setzt:

public User(String name, String forename, String username) {
    this.name = name;
    this.forename = forename;
    this.username = username;
}

Dieser könnte nun aufgerufen werden, um eine Instanz der Klasse zu erstellen und die Felder zu füllen:

User max = new User("Mustermann", "Max", "maxmustermann");

Soll jedoch ein Wert nicht gesetzt werden, kann dieser auf null gesetzt werden. Allerdings kann es hier auch den Fall geben, dass mit NotNull-Annotationen und einer entsprechenden Validation gearbeitet wird:

public User(@NotNull String name, @NotNull String forename, @NotNull String username)

Durch die Annotation soll verhindert werden, dass die entsprechenden Felder mit dem Wert null initialisiert werden können. In einem solchen Fall ist für die entsprechenden Felder auch die Anwendung des Builder-Patterns nur schwer vorstellbar, z. B. über das Setzen der Werte in der newBuilder-Methode.

Sind solche Einschränkungen nicht vorhanden, könnte alternativ mit unterschiedlichen Konstruktoren gearbeitet werden, in welchen jeweils nur einige der Felder gesetzt werden können.

Spätestens bei komplexeren Objekten wird auffallen, dass diese Methode eher ungeeignet ist. Der klassische Weg ist es hier entsprechende Getter und Setter zum Abrufen und Setzen der Eigenschaften bereitzustellen:

public String getName() {
    return name;
}

public void setName(String name) {
    this.name = name;
}

public String getForename() {
    return forename;
}

public void setForename(String forename) {
    this.forename = forename;
}

public String getUsername() {
    return username;
}

public void setUsername(String username) {
    this.username = username;
}

Zwar können Getter und Setter auch über Annotationen, z. B. mithilfe der Bibliothek Lombok, vor dem Compile-Vorgang, generiert werden, allerdings hält der Autor nicht sonderlich viel von dieser Art von „Präprozessor-Magie“.

Hierbei werden unnötige Abhängigkeiten für doch recht simple Aufgaben, an das Projekt angehangen, welche wiederum bestimmte Funktionsweisen vor dem Entwickler verstecken. Vor allem, wenn der Entwickler nicht mit dem Projekt vertraut ist, kann dies das Verständnis erschweren.

Unter Nutzung der Setter kann ein Objekt nun feingranular mit den gewünschten Werten befüllt werden:

User user = new User();

user.setName("Mustermann");
user.setForename("Max");
user.setUsername("maxmustermann");

Soll z. B. der Vorname nicht gesetzt werden, so kann die entsprechende Zeile einfach weggelassen werden. Diese Nutzung bzw. Schreibweise wird manchmal unter dem Namen Method Sequencing zusammengefasst.

An dieser Stelle könnte stattdessen ein Builder genutzt werden. Bei diesem würde das Ganze wie folgt aussehen:

User user = User.Builder.newBuilder()
        .name("Mustermann")
        .forename("Max")
        .username("maxmustermann")
        .build();

Bei dieser Schreibweise wird der Builder genutzt, um das Objekt zu erstellen und mit seinen Werten zu befüllen. Der Builder liefert dazu die benötigen Methoden:

public static final class Builder {
    private String name;
    private String forename;
    private String username;

    private Builder() {
    }

    public static Builder newBuilder() {
        return new Builder();
    }

    public Builder name(String name) {
        this.name = name;
        return this;
    }

    public Builder forename(String forename) {
        this.forename = forename;
        return this;
    }

    public Builder username(String username) {
        this.username = username;
        return this;
    }

    public User build() {
        return new User(this);
    }
}

In vielen modernen Implementationen des Pattern wird meist auf das Präfix set für die Setter verzichtet und stattdessen nur der Name des Feldes als Methode definiert. Alternativ wird häufig auch das Präfix with genutzt.

Beim Quelltext wird ersichtlich, dass jede Methode, bis auf die build-Methode, eine Instanz des Builders zurückgibt und damit die Verkettung der unterschiedlichen Methoden erlaubt.

Fluent Interfaces zur Definition einer DSL nutzen

Neben dem Entwicklungsmuster des Builders können mithilfe von Fluent Interfaces domänenspezifische Sprachen (DSLs) erstellt werden.

Bei solchen Sprachen handelt es sich um speziell für ein bestimmtes Anwendungsgebiet entwickelte Programmiersprachen. Sie sind oft einfacher und leichter zu verstehen als allgemeine Programmiersprachen und ermöglichen es Entwicklern bzw. den Anwendern der Sprache, effizienter und präziser zu programmieren.

DSLs werden häufig in bestimmten Bereichen wie der Finanzbranche, der Medizin, der Luftfahrtindustrie, der Spieleentwicklung und der Datenanalyse eingesetzt. Sie können dazu beitragen, Prozesse zu automatisieren und zu vereinfachen und ermöglichen Entwicklern, komplexe Aufgaben in kürzerer Zeit zu erledigen.

Zu den bekanntesten domänenspezifischen Sprachen gehört SQL und auch reguläre Ausdrücke können als DSL gesehen werden. Grundsätzlich werden domänenspezifischen Sprachen in externe und interne Sprachen unterteilt.

Bei den internen DSLs wird eine Wirtssprache, in diesem Beispiel Java, genutzt, um die DSL abzubilden. Externe DSLs hingegen stehen für sich allein und nutzen keinerlei Wirtssprache. Deshalb sind sie auf eigens dafür entwickelte Compiler oder Interpreter angewiesen.

Im folgenden Beispiel soll eine DSL für HTML entwickelt und anhand dieser sollen einige Konzepte der sprechenden Schnittstellen in Zusammenhang gebracht werden. Die HTML-DSL soll dazu genutzt werden, HTML-Dokumente zu bauen. Ein Aufruf dieser DSL könnte wie folgt aussehen:

String html = new Html()
        .head()
          .meta("UTF-8")
          .meta("keywords", "fluent, interface")
          .title("Fluent interfaces")
          .finalise()
        .body()
          .text("Fluent interfaces")
          .br()
          .finalise()
        .generate();

Während bei einem einfachen Builder immer der Builder als solcher zurückgegeben wird, wird in diesem Fall nicht immer das Hauptobjekt, die Klasse Html, zurückgegeben. Stattdessen werden teilweise sogenannte Intermediate-Objekte zurückgegeben. Diese haben außerhalb der DSL keinerlei sinnvolle Funktionalität, sondern dienen dazu nur die Methoden anzubieten, welche im Kontext von HTML erlaubt sind.

Über Intermediate-Objekte lassen sich somit auch bestimmten Reihenfolgen erzwingen. Damit kann der Compiler zumindest zur partiellen Überprüfung der Anweisungen benutzt werden und dem Anwender bzw. Entwickler werden nur die Funktionalitäten zur Verfügung gestellt, welche im entsprechenden Kontext sinnvoll sind.

Über die Intermediate-Objekte werden die entsprechenden Methoden zur Verfügung gestellt

Unterstützt wird der Entwickler daneben durch die Autovervollständigung der IDE, welche die Methoden des jeweiligen Intermediate-Objektes darstellen kann. Anschließend dient die Methode finalise dazu, wieder eine Ebene höher in der Hierarchie der Intermediate-Objekte zu wandern.

In der Theorie könnten die entsprechende Verschachtelung beliebig komplex gestaltet werden. So können div-Blöcke als Elemente genutzt werden und innerhalb des Intermediate-Objektes für den div-Block stehen dann ausschließlich solche Elemente, welche im Rahmen eines div-Blockes genutzt werden können.

Bei der Nutzung einer solchen domänenspezifischen Sprache ist es wichtig, dass der jeweilige Aufruf auch sinnvoll beendet werden muss. So kann es bei falscher Nutzung durchaus passieren, dass der Entwickler, wenn er entsprechende Aufrufe vergisst, ein Intermediate-Objekt als Rückgabe am Ende erhält, mit welchem er nicht viel anfangen kann. Dieses Problem wird auch als Finishing-Problem bezeichnet.

Beim Design einer DSL muss immer sichergestellt werden, dass es einen Aufruf in den jeweiligen Intermediate-Objekten gibt, welcher dafür sorgt am Ende ein sinnvolles Ergebnis zu erhalten bzw. wieder zum eigentlichen Objekt zurückführt.

Automatische Generation von sprechenden Schnittstellen

Viele domänenspezifische Sprachen werden von Hand geschrieben, allerdings existieren durchaus Ansätze solche Sprachen auch über eine entsprechende definierte Grammatik zu generieren.

Auch Codegeneratoren wie z. B. für GraphQL-Schnittstellen erzeugen unter Umständen Fluent Interfaces, welche dann genutzt werden können:

return new ProductResponseProjection()
        .categoryId()
        .categoryName()
        .issuingCountry();

In diesem Fall wird aus dem GraphQL-Schema eine entsprechende API plus das dazugehörige Modell generiert, welches sich dann nach Außen hin als Builder– bzw. Fluent Interface darstellt.

Ob und wieweit die Generierung von sprechenden Schnittstellen automatisiert werden kann, hängt immer sehr stark vom jeweiligen Anwendungsfall ab.

Fazit

Fluent Interfaces sind ein Begriff, der nun schon eine Weile durch die Welt geistert und wahrscheinlich finden sich in den Köpfen unterschiedliche Vorstellung davon.

Teilweise ist es schwierig, eine harte Definition von sprechenden Schnittstellen zu finden, wie der StringBuilder unter Java zeigt:

String abc = new StringBuilder()
        .append("ABC")
        .append("DEF")
        .append("GHJ")
        .toString();

Die append-Methode stellt hier eine Art Builder-Pattern in Form einer sprechenden Schnittstelle zur Verfügung und kann damit im Kleinen als solche gesehen werden. Die Bandbreite von Fluent Interfaces im Kleinen, wie dem Builder-Pattern, bis zu ausgewachsenen domänenspezifischen Sprachen mit eigenen Grammatiken ist groß.

Alles in allem spielen sprechende Schnittstellen vorwiegend bei komplexen Objekten und entsprechenden Use Cases ihre Stärken aus. Sie sorgen für mehr Verständnis und nutzen die Möglichkeiten des Compilers und der IDE zur Unterstützung aus.

Trotz der Vorteile, welche solche sprechenden Schnittstellen bieten, sollte vor der Implementation immer genau überlegt werden, ob für den jeweiligen Anwendungszweck wirklich ein solche benötigt wird. Hier sollte die erhöhte Komplexität in der Implementation und Pflege mit der Notwendigkeit abgewogen werden.

Der Quellcode der einzelnen vorgestellten sprechenden Schnittstellen kann über GitHub eingesehen und ausprobiert werden.

Dieser Artikel erschien ursprünglich auf Golem.de und ist hier in einer alternativen Variante zu finden.

Workflows im Entwicklungsalltag

Einen Entwicklungszweig anlegen oder doch lieber nicht? Wann geht es zum Test? Workflows im Entwicklungsumfeld sind vielschichtig und sollten zum jeweiligen Projekt passen.

Wer Software entwickelt, der kann dies klassisch bewerkstelligen und seine Software einfach in einem Ordner auf der Festplatte entwickeln und gelegentlich eine Kopie des Ordners erstellen. So finden sich am Ende unzählige Versionen der Software verteilt über verschiedene Verzeichnisse auf der Festplatte.

Auch wenn nicht ausgeschlossen werden kann, dass so in der einen oder anderen Firma entwickelt wird, so werden heutzutage meist Versionskontrollsysteme genutzt und mithilfe dieser ein Arbeitsablauf abgebildet. Kombiniert werden die Versionskontrollsysteme dabei größtenteils mit einem Ticketsystem und entsprechenden Wikis.

Im Rahmen des Artikels wird hierbei besonders auf das Versionskontrollsystem Git Bezug genommen. Git legt den Nutzer nicht auf einen Arbeitsablauf fest, sondern gibt dem Nutzer die Möglichkeit einen gewünschten Arbeitsablauf zu implementieren, ohne dass Git dem Nutzer eine bestimmte Arbeitsweise aufzwingt.

Anforderungen

In den meisten Fällen wird Software anhand von Anforderungen entwickelt. Eine Anforderung entsteht, entweder beim Kunden oder innerhalb der Firma. Diese Anforderung wird definiert und nachdem sie (hoffentlich) ausdefiniert wurde, dem Entwickler vorgelegt.

Während Tickets die Anforderungen festhalten und die Kommunikation zur Anforderung über diese läuft und festgehalten wird, dienen Wikis vorwiegend als Wissensbasis.

Im Entwicklungsalltag ermöglicht die Kombination dieser Systeme unterschiedlichste Arbeitsabläufe, welche dann auch über entsprechende Tickets abgebildet werden können. Auch die Arbeit mit Boards und ähnlichen Hilfsmitteln hilft es den Arbeitsablauf sinnvoll zu visualisieren.

Ein entsprechendes Board

Dabei kann mit unterschiedlichen Spalten wie Geplant, In Arbeit, Review, Test und Fertiggestellt gearbeitet werden. Eine Vertiefung würde an dieser Stelle den Rahmen des Artikels sprengen, da sie auf den Zusammenhang der Arbeitsabläufe mit dem Versionskontrollsystem konzentriert werden soll.

Versionskontrollsysteme

Versionskontrollsysteme bieten für die Entwicklung von Software unterschiedlichste Vorteile. Wie in der Einleitung kurz angerissen, kann die Entwicklung ohne solche Systeme in einer Mischung aus Chaos und Redundanz ausarten. Änderungen können verloren gehen und die Frage, was wurde von Stand A zu Stand B geändert kann nur schwer beantwortet werden.

Im Grunde definieren sich Versionskontrollsysteme über einige Eigenschaften: Protokollierung, Wiederherstellung, Archivierung, Koordinierung sowie die Bereitstellung von Entwicklungszweigen.

Aus Sicht der Arbeitsabläufe in einem Unternehmen ist die Eigenschaft der Koordinierung wichtig. Softwareentwicklung ist in den meisten Fällen Teamwork. Und so muss der Zugriff unterschiedlicher Personen auf den gleichen Quelltext gemanagt werden und entsprechende Methodiken zur Zusammenführung unterschiedlicher Arbeitsstände müssen bereitgestellt werden.

Natürlich kann auch in einem solchen Fall mit Ordnern und einem Netzlaufwerk gearbeitet werden, allerdings wird dies früher oder später zu Problemen führen. So könnten Mitglieder des Teams versuchen, die gleiche Datei des Projektes zu bearbeiten und somit eine Änderung eines Kollegen die Arbeit eines Anderen überschreiben.

Neben der Koordinierung ist die Bereitstellung von Entwicklungszweigen eine der wesentlichen Funktionalitäten von modernen Versionskontrollsystemen. Das bedeutet, dass es nicht nur eine Variante des Quellcodes existiert, sondern ein Repository, mit seinen Entwicklungszweigen, Branches genannt, eine Baumstruktur darstellt.

Unterschiedliche Branches in einem Repository bilden eine Baumstruktur

Mithilfe solcher Entwicklungszweige können z. B. Features entwickelt werden, ohne den aktuellen Hauptentwicklungszweig zu stören oder entsprechende Release- und Bugfix-Zweige verwaltet werden.

Branchen und Mergen

Heutige Versionskontrollsysteme kommen ohne einen zentralen Server aus und verfügen meist über einfachen Mechanismus zur Erstellung und der Zusammenführung von Entwicklungszweigen.

Dies sollte nicht als Kleinigkeit abgetan werden. Während dies bei althergebrachten Systemen wie Subversion und CVS zwar technisch möglich war, wurde dieses Feature dort praktisch nicht genutzt.

Bei Systemen wie Git gehören diese Features zu den Grundlagen, während es bei CVS, Subversion und Co. eher als Expertentätigkeit verstanden wurde. Damit wurde die Erstellung und Zusammenführung von Entwicklungszweigen etwas Natürliches. Niemand musste mehr, vor unauflösbaren Konflikten bei der Zusammenführung von Entwicklungszweigen, Angst haben.

Erst dadurch konnten bestimmte Arbeitsabläufe, welche intensiv von Entwicklungszweigen Gebrauch machten, in unsere heutige Entwicklungslandschaft einziehen.

Branches, Branches, Branches

Während der Entwicklung existiert meist ein main– bzw. master-Branch, welcher eine stabile Version enthält und der develop-Branch in welchem sich die Entwicklungsversion befindet.

Mithilfe von Entwicklungszweigen können unterschiedlichste Entwicklungsmodelle realisiert und Features relativ unabhängig voneinander entwickelt werden. Auch die Pflege unterschiedlicher Releases, welche mit Support bedacht werden müssen, ist mithilfe von Entwicklungszweigen möglich.

Neben Entwicklungszweigen existieren in Versionskontrollsystemen sogenannte Tags. Mit diesen können unter anderem Release-Versionen markiert werden und im späteren Verlauf schnell gefunden oder aber Entwicklungszweige vom entsprechenden Tag abgeleitet werden. Dies ist z. B. dann der Fall, wenn ein Bugfix auf einer älteren Release-Version entwickelt werden soll, für welche kein Release-Branch existiert.

Aus Sicht des Entwicklers ermöglichen Entwicklungszweige daneben das, was unter dem Begriff Commit early and often bekannt ist. In einem separaten Feature-Branch können Änderungen schnell festgeschrieben werden, ohne Rücksicht darauf nehmen zu müssen, ob die Änderung das Release in dieser Phase stören würden.

Namensgebung

Bei der Benennung von Entwicklungszweigen sollte nach einem festgelegten Schema gearbeitet werden, welches für den entsprechenden Arbeitsablauf definiert werden sollte.

Wird mit Ticketsystemen gearbeitet, so sollte die Ticketnummer im Branch enthalten sein. Bei Commits gilt das Gleiche für die entsprechende Nachricht, welche dem Commit beigefügt wird. Ticketsysteme nutzen diese Informationen, um zugehörige Entwicklungszweige und Commits direkt am Ticket anzuzeigen.

Je nach gewähltem Arbeitsablauf kann die Benennung aber auch einfach nur der Fachlichkeit geschuldet sein und muss auf keinerlei Ticket verweisen.

Bei der Benennung von Release-Entwicklungszweigen kann mit Zeitangaben oder Versionsangaben, je nach Erfordernis und Struktur der Releases gewählt werden. Ein solcher Entwicklungszweig könnte z. B. den Namen release/2022.04 tragen.

Feature- und Bugfix-Entwicklungszweige können entsprechend benannt werden. Ein Feature könnte dann z. B. so aussehen: feature/tiff-support oder wenn ein Ticket mit im Namen des Entwicklungszweiges kodiert werden soll: feature/id-1234-tiff-support. Bei Bugfixes kann genauso vorgegangen werden, z. B. bugfix/id-1235-header-corruption.

Pull Requests

Wünschenswert bei der Arbeit mit Ticketsystemen in Verbindung mit Versionskontrollsystemen ist die Verknüpfung miteinander. So können am entsprechenden Ticket bereits der zugeordnete Entwicklungszweig, die entsprechenden Commits sowie offene Pull Requests angezeigt werden.

Daneben bieten die Systeme die Möglichkeit, Entwicklungszweige über das Ticket anzulegen und Pull Requests zu erstellen und somit das Entwicklungsergebnis etwas effizienter zu gestalten.

Ein solcher Pull Request (auch als Merge Request bekannt) war ursprünglich eine Mail mit einem angehängten Patch und der Bitte um Integration. Der Entwickler mit einem Zugriff auf das Repository konnten diesen Pull Request dann akzeptieren und den entsprechenden Code zu integrieren.

Teilweise wird dies auch heutzutage noch so gehandhabt, wie bei der Entwicklung der Linux-Kernels.

Ein Pull Request unter GitHub

Im Rahmen der Arbeitsabläufe sind Pull Request, wie sie von den unterschiedlichen Systemen wie GitHub oder Bitbucket angeboten werden, ein probates Mittel Änderungen zu reviewen, Feedback zu geben und die Änderungen schlussendlich zusammenzuführen.

Daneben existieren Systeme wie Gerrit, welche komplexere Prozesse für die Integration des Quellcodes in das Repository ermöglichen. Hier können Änderungen kommentiert, diskutiert und abgestimmt werden.

Review und Test?

Einige Entwickler halten Review und Tests für optional. Das sollten sie allerdings niemals sein und dementsprechend auch in keinem Arbeitsablauf fehlen.

Es gibt unterschiedliche Gründe für die jeweiligen Verfahren. Beim Review geht es einmal darum, den Quellcode auf offensichtliche Schwachstellen und Fehler abzuklopfen. Macht der Quellcode das, was die fachliche Anforderung definiert? Werden Coding Guidelines eingehalten und gibt es entsprechende Unit-Test?

Neben den offensichtlichen Vorteilen durch das Review hat der Prozess noch anderen entscheidenden Vorteil. Er trägt zum Knowledge Sharing bei. Nach einem Review wurde eine Stelle im Quellcode bzw. eine Funktionalität nicht nur von einem, sondern von mindestens zwei Entwicklern gesehen und verstanden.

Innerhalb des Arbeitsablaufs lässt sich das Review beliebig gestalten. So kann es z. B. erforderlich sein, bei komplexen Änderungen mehrere Entwickler mit dem Review zu betrauen. In manchen Firmen wird eine Entwicklung im Pair Programming automatisch als gereviewter Code gesehen.

Auch der Test ist wichtig, um die Funktionalität abzusichern und eventuelle Fehler vor der Auslieferung abstellen zu können. Hier stellt sich natürlich die Frage, an welcher Stelle der entsprechende Test eingebaut werden kann und sollte.

Workflows

Nach diesen Vorbetrachtungen der meist technischen und organisatorischen Gegebenheiten können sich nun einzelne Arbeitsabläufe im Detail angeschaut werden.

Dabei wird im Grunde mehr oder weniger zwischen zwei Arten der Softwareentwicklung unterschieden. Einmal zwischen releasebasierten Arbeitsabläufen, bei denen auf ein Release hinaus entwickelt wird und einmal die kontinuierliche Entwicklung, bei der an einer Applikation entwickelt wird, welche keine klassischen Releases mehr kennt. Ein Beispiel für letzteres wäre eine Webapplikation, welche praktisch immer in der aktuellen Version ausgeliefert wird.

Diese Fragen entscheiden am Ende darüber, wie der jeweilige Workflow für das Team respektive das Produkt am Ende aussieht.

One branch to rule them all

Der einfachste Arbeitsablauf besteht sicherlich darin, einfach die entsprechenden Commits im main-Branch seines Projektes zu erstellen.

Alle Commits landen direkt im main-Branch

Bei diesem Arbeitsablauf mischen sich die Implementierungen unterschiedlicher Features und andere Dinge im schlimmsten Fall zu einem instabilen Gemisch. Features landen Stück für Stück im main-Branch und experimentelle Entwicklungen sind ebenfalls schwierig in diesem Model unterzubringen.

Auch ein Review und ein Test sind nur nachträglich möglich, wenn das Kind bereits in den Brunnen gefallen ist. Daneben stellt sich die Frage, wie definiert wird, dass der main-Branch stabil ist und ein entsprechendes Release erstellt werden kann.

Eine Aufgabe: ein Branch

Ein simpler Arbeitsablauf könnte nun so aussehen, dass für jede Änderung, sei es ein Feature, ein Bugfix oder etwas anderes ein Entwicklungszweig angelegt wird und dieser, wenn die Entwicklung abgeschlossen wurde, wieder mit dem Hauptzweig zusammengeführt wird.

Ein solcher Entwicklungszweig wird meist Ticket-Branch, Feature-Branch oder auch Topic-Branch genannt.

Jede Entwicklung findet in einem separaten Branch statt

Im Detail sind bei dieser Variante allerdings einige Fragen ungeklärt. Wann findet das Review statt, wann der Test? Wer gibt den Merge in den Hauptentwicklungszweig frei? Wann ist der main-Branch als stabil anzusehen?

Aus diesem simpleren Arbeitsablauf, bzw. der Idee des Branchings für einzelne Anforderungen ergeben sich in der Realität unterschiedlichste Arbeitsabläufe, von deinen einige im Detail vorgestellt werden sollen.

Entwicklungs- und stabile Linien

Bei diesem Arbeitsablauf wird mit zwei Entwicklungszweigen gearbeitet, einmal dem main-Branch und einmal dem develop-Branch. Soll eine neue Entwicklung stattfinden, so wird ein Entwicklungszweig basierend auf dem develop-Branch erstellt und auf diesem das entsprechende Feature entwickelt.

Der main-Branch enthält die stabile Variante der Software dar

Ist die Entwicklung abgeschlossen, kann ein Pull Request erstellt werden, welcher als Ziel den develop-Branch trägt. Im Rahmen des Pull Requests kann der Reviewer den Quellcode reviewen und entsprechende Anmerkungen antragen. Der Entwickler kann entsprechende Änderungen, welche sich aus dem Review ergeben, im Feature-Branch vornehmen.

Ist das Review erfolgreich abgeschlossen, so könnte die Änderung vor der Zusammenführung der Entwicklungszweige oder danach getestet werden. Wird im Feature-Branch getestet, so sollte anschließend auch im develop-Branch getestet werden, um die erfolgreiche Integration des Features sicherzustellen. Hier bietet sich ein Modell an, nur besonders kritische Änderungen vor und nach dem Merge zu testen, während gewöhnliche Änderungen nur nach der Zusammenführung der Entwicklungszweige getestet werden.

Im Falle von Fehlern, welche im Test gefunden werden, sollten diese im Feature-Branch behoben werden und anschließend wieder ein Pull Request erstellt werden.

Der develop-Branch wird regelmäßig, immer dann, wenn er für stabil befunden wird, in den main-Branch gemergt, sodass dieser in der Theorie eine stabile Version der Software enthält.

Natürlich kann nicht wirklich sichergestellt werden, dass die entsprechende Version stabil ist und auch das Arbeiten mit mehreren Releases ist mit einem solchen Workflow nicht praktikabel möglich.

Der Vorteil, der sich aus dieser Vorgehensweise ergibt, ist, dass der main-Branch immer relativ stabil bleibt, da nur getestete Änderungen in ihn wandern. Außerdem können mehrere Features parallel entwickelt werden.

Auch können Tags hier wieder genutzt werden, um Releases zu definieren und für spätere Änderungen verfügbar zu halten.

Releases aus dem main-Branch

Eine andere Variante ist es, direkt auf dem main-Branch zu arbeiten. Hier kann optional mit Feature-Branches gearbeitet werden.

Für ein nahendes Release wird nun vom main-Branch ein Release-Branch angelegt und dieser entsprechend stabilisiert; bis das Release schlussendlich ausgeliefert werden kann.

Nachteilig ist hier, dass nicht wirklich mit parallel mit Releases gearbeitet werden kann. Auch die Sicherstellung, dass ein bestimmtes Feature in einem bestimmten Release landet, ist nur mit größerem Aufwand zu realisieren.

Neue Releases werden aus dem main-Branch abgeleitet

Während der Stabilisierungsphase im Release besteht, daneben das Problem, dass entsprechende Bugfixes auch in den main-Branch übernommen werden müssen und dies einen zusätzlichen Aufwand bedeutet. Daneben nehmen die Stabilisierungsphasen größere Zeiträume ein und verhindern somit die eigentliche Weiterentwicklung der Software.

Von Release zu Release

Soll mit mehreren Releases gearbeitet werden, so kann ein Arbeitsablauf gewählt werden, welcher unabhängig vom main-Branch ist und stattdessen nur noch aus Release-Branches besteht.

Release-Branches werden nach oben gemergt

In diesem Modell wird für jedes Release ein Entwicklungszweig angelegt. Wenn die Software z. B. monatlich deployt wird, könnten die entsprechenden Branches wie folgt aussehen: release/2022.04, release/2022.05, release/2022.06 usw.

Jede Anforderung wird nun einem Release zugeordnet. Wird ein Feature entwickelt, so wird ein Feature-Branch vom entsprechenden Release-Branch gezogen und auf diesem Feature-Branch die Entwicklung durchgeführt.

Wird das Feature rechtzeitig zum Release fertig entwickelt, wird es im Rahmen des Prozesses (Review, Test) mit dem Release-Branch zusammengeführt.

Kann ein Feature nicht zum zugeordneten Release fertiggestellt werden, wird das zugeordnete Release im Ticket geändert und der Feature-Branch auf Basis der neuen Release-Version fertiggestellt.

Hierzu wird der neu zugeordnete Release-Branch in den Feature-Branch gemergt und anschließend mit der Entwicklung fortgefahren.

Der Feature-Branch kann auch zwischenzeitlich mit dem zugeordneten Release-Branch aktualisiert werden, sodass die eventuellen Änderungen des Release-Branches sich auch im Feature-Branch niederschlagen.

Die Release-Branches selber werden regelmäßig bzw. automatisch von den älteren zu den neueren Branches gemergt. Damit sind Änderungen, welche einem älteren Release zugeordnet waren, automatisch in den neueren Releases enthalten.

Vorteilig an diesem Entwicklungsmodell ist, dass parallel an mehreren Releases gearbeitet werden kann und Verschiebungen von Features im Rahmen der Entwicklung aus technischer Sicht kein Problem darstellen.

Auch können alte Releases bei diesem Arbeitsablauf problemlos über einen längeren Zeitraum betreut werden und mit Bugfixes versehen werden. Diese Bugfixes landen durch den Merge-Prozess der Release-Branches untereinander immer wieder in den neueren Releases.

Bei der Arbeit mit den einzelnen Releases kann die Arbeit noch in Entwicklungs- und Stabilisierungsphasen unterteilt werden, um möglichst stabile und getestete Releases abzuliefern.

Dabei werden neue Features nur innerhalb der Entwicklungsphase in den entsprechenden Release-Branch gemergt. Ist diese Phase ausgelaufen, wird das zugeordnete Release in der Anforderung respektive dem Ticket auf das nächste Release verschoben.

In der Stabilisierungsphase werden nur noch Bugfixes durchgeführt, welche zur Stabilisierung des Releases beitragen.

Git Flow

Ein weiteres relativ beliebtes Modell ist der Arbeitsablauf Git Flow, welcher von Vincent Driessen erdacht wurde und sich neben der guten Skalierbarkeit auch durch seine Vorteile in der Zusammenarbeit auszeichnet.

Git Flow zielt hauptsächlich auf Software ab, bei welcher mehrere Versionen parallel unterstützt werden müssen und sollte nicht als Allheilmittel betrachtet werden, wie der Entwickler selbst schreibt:

This model was conceived in 2010 […] In those 10 years, git-flow […] has become hugely popular in many a software team to the point where people have started treating it like a standard of sorts — but unfortunately also as a dogma or panacea.

This is not the class of software that I had in mind when I wrote the blog post 10 years ago. If your team is doing continuous delivery of software, I would suggest to adopt a much simpler workflow (like GitHub flow) instead of trying to shoehorn git-flow into your team.

Das Regelwerk, welches hinter diesem Arbeitsablauf steht, wirkt auf den ersten Blick wesentlich komplizierter als das anderer Arbeitsabläufe. Das beginnt damit, dass mit unterschiedlichsten Entwicklungszweigen gearbeitet wird. So existieren in diesem Flow die Branches main, develop und separate Branches für Features, Hotfixes und Releases.

Bei Git Flow existieren eine Reihe von Branchtypen

Im Rahmen des Arbeitsablaufs ist geregelt, aus welchen Entwicklungszweigen weitere Entwicklungszweige erstellt werden sollen und wie und in welche Richtung die jeweiligen Zweige zusammengeführt werden dürfen.

Auch wird in diesem Modell gewünscht, dass Feature-Branches nur lokal beim Entwickler liegen und nicht zum zentralen Server hochgeladen werden.

Grundsätzlich wird bei Git Flow mit Feature-Branches gearbeitet, welche auf dem develop-Branch basieren. Aus diesem Branch wiederum werden entsprechende Release-Branches gezogen.

Ist ein Release veröffentlicht, werden eventuelle Bugfixes, über entsprechende Bugfix-Branches basierend auf dem Release-Branch erstellt und dort der Bugfix entwickelt.

Release-Branches werden in den main-Branch gemergt und im Falle von Änderungen des Release-Branches auch in den develop-Branch.

Sollte ein Hotfix notwendig sein, so wird dieses auf Basis des main-Branches in einem Hotfix-Branch entwickelt und anschließend wieder in den main-Branch und in den develop-Branch eingepflegt.

GitHub Flow

Ein weiterer bekannter Arbeitsablauf ist der GitHub Flow. Bei diesem wird davon ausgegangen, dass der main-Branch grundsätzlich immer deploybar ist.

Bei einer neuen Entwicklung wird ein Entwicklungszweig basierend auf dem main-Branch erstellt und ein entsprechender beschreibender Name benutzt, z. B. tiff-support und auf diesem Entwicklungszweig entwickelt.

Ist die Entwicklung so weit abgeschlossen, dass der Entwicklungszweig in den main-Branch eingepflegt werden kann, wird ein Pull Request erstellt und der Reviewer kann sich diesen anschauen und anschließend den Merge durchführen.

Daneben dienen Pull Requests in diesem Arbeitsablauf der allgemeinen Klärung, so können Pull Requests auch erstellt werden, wenn Feedback oder andere Hilfe benötigt wird.

Damit ist das Verfahren ähnlich dem Arbeitsablauf, bei welchem für jede Entwicklung ein eigener Entwicklungszweig erstellt und dieser wieder eingepflegt wird. Im Gegensatz zum obigen Arbeitsablauf definiert der GitHub Flow einige Rahmenbedingungen genauer und sieht weitere Schritte nach dem einpflegen des Zweiges vor.

Hier fordert der Arbeitsablauf ein, dass nach der Zusammenführung der Entwicklungszweige der aktuelle Stand des main-Branches sogleich deployt wird. Hintergrund ist hier, dass damit das Verständnis geschaffen wird, dass wenn die eigenen Code-Änderungen nicht stabil sind, das System bricht. Und aus diesem Grund die Entwickler besondere Sorgfalt darauf legen (sollten) wirklich stabile Änderungen einzubringen.

Trunk Flows

Neben diesen branchbasierten Workflows werden in letzter Zeit vermehrt sogenannte trunk-basierte Flows genutzt.

Bei diesem Arbeitsablauf soll möglichst darauf verzichtet werden, langlebige Entwicklungszweige zu halten. Damit sollen unter anderem mögliche Merge-Konflikte verhindert werden. Stattdessen sollen Commits hier direkt im trunk; im Fall von Git also im main-Branch, erstellt werden.

Die Commits sind bei diesem Verfahren erfahrungsgemäß eher klein. Außerdem hat der Entwickler sicherzustellen, dass der Build ordnungsgemäß funktioniert, bevor er den Commit zum Server hochlädt.

Für Reviews können kurzlebige Entwicklungszweige angelegt werden und entsprechend für Pull Requests und das anschließende einpflegen in den main-Branch genutzt werden.

Schleicht sich dennoch ein Fehler ein, so wird der Commit auf dem main-Branch zurückgerollt.

Continuous Integration-Systeme spielen in diesem Prozess eine wichtige Rolle, indem sie bei Änderungen sofort entsprechende Tests durchführen und den Entwickler informieren, wenn der Build nicht den Anforderungen an die geforderte Qualität entspricht.

Je nach Konfiguration rollen die Continuous Integration-Systeme die problematischen Commits automatisch zurück.

Diese durchgeführten automatisierten Tests, sowie Code-Reviews sollen sicherstellen, dass der main-Branch immer deploybar ist; vergleichbar zu dem GitHub Flow.

Wenn der Test fehlschlägt

Egal in welchem Arbeitsablauf, stellt sich immer die Frage, wie mit Features umgegangen wird, die sich bereits im main-, develop– oder release-Branch befinden und Fehler festgestellt wurden.

Grundsätzlich sollte hier die Entwicklung wieder aufgenommen und der Fehler beseitigt werden. Je nach Arbeitsablauf kann diese Entwicklung im Feature-Branch erfolgen oder z. B. direkt im main-Branch gearbeitet werden.

Problematisch wird es an der Stelle, wenn das Feature bereits gemergt ist, es zeitlich aber nicht mehr realistisch ist, etwaige Fehler bis zum Release zu beseitigen.

In einem solchen Fall existieren unterschiedliche Möglichkeiten. So kann unter anderem mit Feature-Flags gearbeitet werden, welche es ermöglichen das entsprechende Feature abzuschalten.

Je nach verwendeter Programmiersprache können hier Möglichkeiten der jeweiligen Sprache bzw. des jeweiligen Toolings benutzt werden. Unter Rust könnte z. B. über Cargo ein entsprechendes Feature definiert sein und dieses im Fehlerfall wieder abgeschaltet werden.

Feature-Flags werden auch bei trunk-basierenden Workflows als Ergänzung gesehen, um das Feature langsam zu entwickeln. Allerdings stellt sich hierbei die Frage nach der Testbarkeit solange das Feature-Flag noch nicht aktiv ist.

In der Praxis ist es nicht immer möglich, mit solchen Feature-Flags zu arbeiten. In einem solchen Fall können die entsprechenden Commits rückgängig gemacht werden und somit aus dem entsprechenden Branch, welcher zum Release führt, entfernt werden.

Einfacher ist ein solcher Revert, wenn die Änderungen aus einem Feature-Branch per Squash zu einem einzelnen Commit zusammengeführt werden und erst dann gemergt werden.

Damit tauchen die kompletten Änderungen eines solches Feature-Branches nur noch als ein einzelner Commit, im Branch in den die Änderung eingepflegt wird, auf. Positiv wirkt sich dies auch auf die Übersichtlichkeit in diesem Branch aus. Allerdings bedeutet es je nach Automatisierungsgrad des Arbeitsablaufs zusätzlichen Aufwand.

Merge-Hölle?

Bei vielen der oben beschriebenen Arbeitsabläufe wird mit Entwicklungszweigen gearbeitet. Damit mit diesen gearbeitet werden kann, müssen sie schlussendlich in ihre Zielzweige eingepflegt werden.

Bei der Zusammenführung zweier Entwicklungszweige können entsprechende Probleme auftreten, dass zwei Änderungen nicht mehr zusammenpassen. Ein solcher Merge-Konflikt muss behoben werden, um die entsprechende Zusammenführung der Entwicklungszweige erfolgreich abzuschließen.

Meist ist die Behebung solcher Konflikte trivial. Um die Gefahr für solche Konflikte zu verhindert, hilft es Anforderungen und damit verbundene Code-Änderungen klein zuhalten.

Auch Coding Guidelines helfen solche Probleme zu minimieren, da sie dafür sorgen, dass die Formatierungen über das Projekt und die beteiligten Entwickler identisch sind. Damit können entsprechende Probleme wegen unterschiedlichen Einzugsbreiten und Ähnlichem gar nicht erst auftreten.

Merges sind nichts, wovor Entwickler Angst haben sollten. Sie gehören zum Tagesgeschäft dazu und verlaufen in einem Großteil der Fälle problemlos ab und Konflikte können auch dank des Toolings schnell gelöst werden.

Fazit

Arbeitsabläufe gibt es viele und im Grunde hängt der genutzte Arbeitsablauf vom Team, dem Projekt und den damit verbundenen Anforderungen ab. Den einen richtigen Arbeitsablauf gibt es nicht.

Vor allem im agilen Kontext, sollte jedes Team für sich entscheiden, mit welchem Arbeitsablauf es arbeitet und diesen an die eigenen Gegebenheiten und Besonderheiten anpassen.

Wichtig ist, dass die Arbeitsabläufe die Entwicklung nicht behindern oder erschweren, sondern ein Gerüst bieten, damit die Anforderungen der Entwickler, Produktmanager und Kunden schnell abgebildet und sinnvoll umgesetzt werden können und ein stabiles Produkt ausgeliefert werden kann.

Aus Sicht des Autors bietet sich der Release zu Release-Workflow in den meisten Fällen eines releasebasierten Produktes an, da er viele Vorteile bietet; bei relativ geringem Aufwand.

Zudem hängt viel davon ab, was für ein Produkt entwickelt wird und wie die Release-Zyklen aussehen. So ist es auch nicht ungewöhnlich Services regelmäßig z. B. täglich oder sogar mehrmals täglich zu deployen. In solchen Fällen sind Arbeitsabläufe wie der Github Flow sinnvoller zu nutzen.

Dieser Artikel erschien ursprünglich auf Golem.de und ist hier in einer alternativen Variante zu finden.