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.

Offene Übersetzungs-API

Im Bereich der maschinellen Übersetzung gab es in den letzten Jahren viele Fortschritte. So gibt es eine Vielzahl an Übersetzungdiensten, die genutzt werden können, um Texte von einer Sprache in die andere zu übersetzen. Das Problem an den meiste dieser Dienste ist, das sie proprietär sind.

libretranslate.com

Mit LibreTranslate existiert allerdings eine offene Übersetzungs-API, welche von jedermann genutzt werden kann. Die API kann hierbei selbstgehostet werden. Lizenziert ist das Projekt unter der AGPL in Version 3 und damit freie Software. Unter der Haube nutzt das Projekt die Übersetzungsengine Argos Translate.

Self Healing Code

In der Softwareentwicklung stellt sich manchmal das Gefühl ein, von Buzzwörtern umgeben zu sein. Auch Self Healing Code könnte ein solches sein. Doch trägt das Konzept einige interessante Eigenschaften mit sich und sollte nicht vorschnell verworfen werden.

Die grobe Idee hinter diesem Konzept ist es, dass die Anwendung Fehler erkennen und diese im Idealfall auch beheben kann. Dieser Prozess soll ohne menschliches Eingreifen stattfinden.

Neben der einzelnen Applikation kann, sich ein solches Verhalten auf komplexere Systeme und deren Zusammenspiel beziehen. Neben der Fehlerbehebung, während der Laufzeit einer solchen Software, wird der Begriff des Self Healing Code in letzter Zeit auch im Zusammenhang mit generativer KI genutzt.

Definition

Im Bereich der Softwareentwicklung ist Self Healing Code so definiert, dass ein Programm in der Lage ist, Fehler zu erkennen und zu korrigieren. Verwandt damit ist der Begriff der selbstheilenden Systeme, denen die Fähigkeit inhärent ist, aus einem defekten Zustand wieder in einen funktionalen Zustand zu wechseln.

Ein einfaches Model von Self Healing Code

Das Ziel des selbstheilenden Codes ist es, die Notwendigkeit menschlicher Eingriffe zu minimieren und die Betriebszeit und Effizienz der Software zu maximieren. Dies kann durch die Implementierung von Überwachungs- und Diagnosefunktionen erreicht werden, die auf Anomalien oder Fehler hin überprüfen. Sobald ein Problem erkannt wird, wird versucht darauf zu reagieren, indem entweder eine Korrekturmaßnahme ausgeführt oder auf einen vorherigen stabilen Zustand zurückkehrt wird.

Defensive Programmierung

Eng verwandt mit Self Healing Code ist defensive Programmierung. Beides sind Praktiken, die dazu dienen, die Robustheit und Zuverlässigkeit von Software zu verbessern, aber sie tun dies auf unterschiedliche Weisen.

Defensive Programmierung ist eine Methode, bei der der Entwickler davon ausgeht, dass Probleme auftreten werden und daher Vorkehrungen trifft, um diese zu bewältigen. Dies kann beinhalten, dass überprüft wird, ob Eingaben gültig sind, bevor sie verwendet werden, Ausnahmen ordnungsgemäß behandelt werden und der Code so geschrieben wird, dass er leicht zu verstehen und zu warten ist.

Das Ziel der defensiven Programmierung ist es, die Anzahl der Fehler zu reduzieren und sicherzustellen, dass die Anwendung auch bei unerwarteten Eingaben oder Bedingungen korrekt funktioniert.

Selbstheilender Code hingegen geht einen Schritt weiter. Anstatt nur zu versuchen, Fehler zu vermeiden, versucht er, Fehler zu erkennen und zu beheben, wenn sie auftreten.

Bei der Betrachtung von Self Healing Code sollte auch defensive Programmierung Berücksichtigung finden. Diese kann dazu beitragen, die Anzahl der Fehler zu reduzieren, die auftreten können. Self Healing Code kann anschließend dazu beitragen, die Auswirkungen der Fehler zu minimieren, die trotzdem auftraten.

Implementation

Natürlich muss die gewünschte Funktionsweise der Selbstheilung bei der Entwicklung und dem Design einer Applikation und entsprechender Systeme berücksichtigt und implementiert werden.

Der erste Schritt ist die Fehlererkennung. Das bedeutet, dass die Applikation in der Lage sein muss, eventuelle Fehler und Probleme selbstständig zu erkennen. Hier können Applikationslogiken, Logs oder auch Überwachungssysteme genutzt werden.

Wird eine Anwendung oder ein Teil eines Softwaresystems überwacht, so müssen Schwellwerte definiert werden, welche definieren, ab wann die Services sich in einem kritischen Zustand befinden, damit darauf basierend Maßnahmen ergriffen werden können.

Präventiv und reaktiv

Die Fähigkeit der Selbstheilung kann in präventives und reaktives Handeln unterschieden werden. Beim präventiven Handeln werden gewisse Schlüsselindikatoren von der Applikation ausgewertet und darauf basierend eine Handlung ausgelöst.

So könnte eine Server-Applikation keine neuen Verbindungen mehr zulassen, wenn die CPU-Auslastung auf dem eigenen System zu hoch ist und somit einer Überlastung vorbeugen.

Reaktives Handeln ist vonseiten der Applikation immer dann notwendig, nachdem es zu einem Fehler gekommen ist. In diesem Fall muss die Applikation reagieren, um wieder einen funktionsfähigen Ablauf herstellen zu können.

Fehlerbeseitigung

Wurde ein Fehler erkannt, sollte er im nächsten Schritt behoben werden. Hier sind unterschiedliche Möglichkeiten denkbar, wie der Neustart eines Services oder das Ausweichen auf andere Datenquellen.

Dieser Prozess der Erkennung und Beseitigung von Fehlern und Problemen sollte ebenfalls automatisiert sein, sodass er ohne menschliche Einwirkung auskommt. Wird der Mechanismus aktiv, sollte ein Logging vorgenommen werden, damit dies später nachvollzogen werden kann.

Test und Dokumentation

Neben der Implementierung sollten selbstheilende Funktionalitäten auch regelmäßig getestet und gut dokumentiert werden, um bei Bedarf eine schnelle und effiziente Fehleranalyse zu ermöglichen.

Auch auf Sicherheitsaspekte sollte achtgegeben werden. Es sollte sichergestellt werden, dass die Maßnahmen zur Selbstheilung nicht von außen manipuliert oder missbraucht werden können.

Ein einfaches Beispiel

Wie könnte die Anwendung dieses Konzeptes aussehen? Ein heruntergebrochenes Beispiel unter Java könnte sich wie folgt darstellen:

public Connection connectToDatabase() {

    Connection connection = null;

    while (connection == null) {
        
        try {
            connection = DriverManager.getConnection(URL, USER, PASSWORD);
        } catch (SQLException e) {

            try {
                Thread.sleep(5000);
            } catch (InterruptedException e) {
                // Ignore
            }
        }
    }

    return connection;
}

In der Methode connectToDatabase soll eine Datenbankverbindung erstellt und diese anschließend zurückgegeben werden. Tritt beim Aufbau der Verbindung eine Ausnahme auf, so wird versucht nach einer Wartezeit nochmals eine Verbindung aufzubauen, in der Hoffnung, dass der Fehler nur temporärer Natur war.

Damit wird dem Nutzer eine Alternative zu einem völligen Abbruch des Verbindungsversuches geboten. Im Idealfall, auch bei Auftreten eines Fehlers, wird ein verzögerter Aufbau der Verbindung ermöglicht. In der Praxis sollte dieses Beispiel allerdings in der vereinfachten Form nicht genutzt werden, da die Methode connectToDatabase niemals eine Antwort liefern würde, wenn die Datenbank nicht mehr antwortet. Hier ist es nötig, nach einer gewissen Zeit oder einer bestimmten Anzahl an Versuchen abzubrechen.

Methodiken und Pattern

Für die Bereitstellungen selbstheilenden Codes können unterschiedlichste Methodiken innerhalb einer Applikation genutzt werden, um dieses Konzept zum Erfolg zu führen.

Dies führt vom Einsatz von Entwurfsmustern wie dem Circut Breaker über die Möglichkeit zum Failover, den Neustart fehlerhafter Komponenten, oder die Nutzung von Read-Only-Mechanismen, bei verschlechternder Servicequalität.

Circut Breaker

Der Circuit Breaker ist ein Entwurfsmuster, welches verwendet wird, um Systeme zu schützen. Die Benamung ist nicht ohne Grund so gewählt, da er wie eine Sicherung die Verbindung zu einem System kappt, wenn bestimmte Kriterien erfüllt sind.

Dies kann z. B. eine definierte Fehlerrate in einem bestimmten Zeitraum sein. Ein Beispiel wäre ein Webservice, welcher wiederholt auf Anfragen nicht antwortet. Der Aufrufer könnte nun versuchen immer und immer wieder Anfragen zu senden, was dazu führen kann, dass das System im schlimmsten Fall unter der Last zusammenbricht.

Hier greift der Circut Breaker ein und unterbricht die Verbindung. Indessen kann in der Anwendung auf den Fehler reagiert werden, die Anfrage z. B. zu einem späteren Zeitpunkt wiederholt werden.

Meist wird nach einer Cooldown-Phase die Verbindung zum Service wieder aufgenommen. Treten hierbei wieder Fehler auf, so wird der Circut Breaker die Verbindung erneut trennen und der Prozess beginnt von vorn.

Auch in der Applikation selbst führt dies zu positiven Effekten, da nicht mehr auf die entsprechende Verbindung gewartet werden muss und eventuell dafür genutzte Threads und weitere Ressourcen für diesen Moment abgewickelt werden können.

Failover

Eine weitere Möglichkeit für Self Healing Code ist die Implementierung von Failover-Verfahren unter der Bereitstellung von Redundanz. Grundsätzlich bedeutet dies, dass auf andere Systeme umgeschaltet wird, wenn das angefragte System ausfällt.

Dies kann bedeuten, dass im Falle eines nicht oder fehlerhaft antwortenden Webservices, eine andere Instanz des Webservices genutzt wird.

Ein Rückfall auf einen alternativen Payment-Provider sichert den Geschäftsprozess ab

Daneben sind auch andere Szenarien denkbar. So konnte die eigene Applikation einen Payment-Provider nutzen. Bei einem Ausfall könnte dies ein geschäftskritisches Problem darstellen. Als Failover-Variante kann auf einen zweiten unabhängigen Payment-Provider umgeschwenkt werden, bis der primäre Provider wieder verfügbar ist.

Ziel ist es beim Failover, die Ausfallzeiten zu minimieren und die Verfügbarkeit zu gewährleisten. Aus Sicht eines Nutzers würde ein solcher Ausfall eines Service zu keinem veränderten Ergebnis führen.

Änderung in der Applikationslogik

Eine weitere Möglichkeit innerhalb einer Applikation auf Probleme zu reagieren, ist es Änderung in der eigentlichen Logik vorzunehmen. Beispielhaft könnte von einem externen Webservice eine Route anhand gewisser Parameter geliefert werden.

Fällt dieser Service aus, könnte die Anwendung stattdessen intern eine Route mit einem vereinfachten Algorithmus berechnen. Das Ergebnis ist qualitativ nicht unbedingt mit dem des externen Service zu vergleichen, allerdings kann es aus Sicht des Nutzers trotzdem ausreichend sein, falls der Webservice nicht zur Verfügung steht.

Limiter

Eine weitere Klasse nützlicher Methodiken sind Limiter. So begrenzt ein Rate Limiter die Anzahl der Anfragen, die ein Nutzer in einem bestimmten Zeitraum senden kann. Dies ist besonders nützlich in Szenarien, in denen Systemressourcen begrenzt sind oder um versehentliche Denial-of-Service-Angriffe zu verhindern.

Daneben existieren weitere Limiter, wie der Time Limiter welcher die Zeit begrenzt, die ein bestimmter Prozess zur Ausführung nutzen kann. Wenn der Prozess die zugewiesene Zeit überschreitet, wird er abgebrochen oder eine Ausnahme wird ausgelöst. Dies kann eine Möglichkeit darstellen, Timeouts zu realisieren.

Let It Crash

Aus einer übergeordneten Sicht kann es sinnvoll sein, Services abstürzen zu lassen, wenn es zu schwerwiegenden Problemen kommt. Je nachdem, wie das System gestaltet ist, wird der Service anschließend wieder gestartet und hochgefahren.

Erlang ist ein anschauliches Beispiel für diese Let It Crash-Philosophie. Sie führt dazu, dass sobald ein Prozess auf einen Fehler stößt, dieser beendet wird, anstatt den Fehler zu beheben. Andere, überwachende Prozesse können anschließend entscheiden, wie sie auf den Absturz reagieren, oft indem sie den fehlerhaften Prozess neu starten.

Hier gibt es im Service selbst keinen selbstheilenden Code, sondern es wird sich auf die Gesamtarchitektur des Systems verlassen, welche dafür sorgt, dass der Dienst wieder neu gestartet wird oder das Problem auf andere Art und Weise behoben wird.

Hier muss darauf geachtet werden, dass die Applikationen auf diesen Fall vorbereitet sein müssen. So müssen z. B. Verbindungen wieder aufgenommen werden, nachdem der jeweilige Service wieder verfügbar ist.

Caches

Auch Caches können im Rahmen selbstheilender Anwendungen nützlich sein. Wenn eine Information nicht vom externen Service bezogen werden kann, kann unter Umständen die letzte gecachte Antwort für die Anforderung genutzt werden.

Dies ist natürlich nur in solchen Fällen möglich, in denen der Cache für die gewünschte Anfrage vorhanden ist und sichergestellt werden kann, dass die gespeicherte Antwort den Anforderungen an die benötigte Aktualität gerecht wird.

Veränderung der Servicequalität

Wer Dienste entwickelt und betreibt, kann weitere Funktionalitäten implementieren, um zumindest ein Teil eines Dienstes noch funktionsfähig zu halten. So kann ein entsprechender Service z. B. in einen Read-Only-Modus gesetzt werden, wenn Schreibzugriffe aufgrund eines Fehlers aktuell nicht funktionieren.

Da in den meisten Fällen Lesezugriffe einem Schreibzugriff überwiegen, können in einem solchen Read-Only-Modus viele der Anfragen immer noch erfolgreich beantwortet werden.

Das dahinter liegende Konzept ist es, die Funktionalität, welche noch zur Verfügung steht, dem Aufrufer zur Verfügung zu stellen, anstatt den Betrieb komplett einzustellen. So können einzelne Features im Fehlerfall abgeschaltet werden, anstatt den kompletten Service zu deaktivieren.

Allerdings müssen die aufrufenden Applikationen hierauf vorbereitet sein und damit umgehen können.

Retry

Wie bereits im Beispiel oben demonstriert, können wiederholte Versuche einen Service aufzurufen, eine Möglichkeit sein, Systeme fehlertolerant und selbstheilend zu gestalten. Dies ist vorwiegend bei Problemen temporärer Natur wie kurzzeitigen Netzwerkausfällen oder einer Überlastung nützlich.

Dieses Muster kann jedoch komplexer werden, abhängig von den Anforderungen der Anwendung. Es kann unter anderem notwendig sein, die Wartezeit zwischen den Wiederholungsversuchen zu erhöhen oder bestimmte Arten von Fehlern von den Wiederholungsversuchen auszuschließen.

Auch zu häufige Wiederholungen sollten ausgeschlossen werden, damit z. B. im Falle einer Überlastung der aufgerufene Service nicht weiter belastet wird.

Timeouts

Ein weiterer wichtiger Punkt bei selbstheilendem Code und fehlertoleranten Architekturen sollten Timeouts sein. Operationen wie Netzwerk oder IO sollten immer mit einem Timeout versehen werden, damit niemals der Fall entsteht, dass auf unbestimmte Zeit auf Ressourcen gewartet wird.

Ähnliche Verfahren lassen sich in der Theorie auch anwenden, wenn längere Berechnungen getätigt werden. In einigen Fällen ist es hier sinnvoll, einen Timeout zu definieren.

Grundsätzlich sollte es immer das Ziel sein, endlose Warteschleifen zu vermeiden und Systemressourcen wie CPU oder Speicher wieder freizugeben. Auch erhält die Anwendung durch Timeouts eine gewisse Art an Kontrolle, da durch diese klar wird, wie lange bestimmte Prozesse maximal laufen dürfen.

Aus Sicht des Nutzers sind Timeouts hilfreich, da dieser nicht unnötige Wartezeiten in Kauf nehmen muss und eine zeitnahe Rückmeldung erhält, wenn auch im schlechtesten Fall in Form einer Fehlermeldung.

Ein interessanter Nebeneffekt ist, dass Timeouts teilweise zur Fehlersuche genutzt werden können. Wenn bestimmte Anforderungen ständig zu Timeouts führen, könnte dies auf ein tiefer liegendes Problem hinweisen, das analysiert werden sollte.

Tooling

Viele der beschriebenen Mechanismen können von Grund auf vom Entwickler implementiert werden. Allerdings existieren eine Reihe von Bibliotheken und Frameworks welche einen Teil dieser Arbeit abnehmen.

In der Java-Welt liefern Frameworks z. B. Spring Boot, Möglichkeiten für eine robuste Fehlerbehandlung. Bibliotheken, wie Resilience4j, bieten Lösungen für Selbstheilungsfunktionen und Fehlertoleranz. Sie ermöglicht es Entwicklern, selbstheilende Muster zu implementieren, wie Circut Breaker oder Fallback-Mechanismen, um Ausfälle effektiv zu behandeln.

Damit wird es einfacher und bequemer, selbstheilenden Code zu implementieren.

Self Healing Code in der Zukunft

Neben den klassischen Methoden, um selbstheilenden Code zu realisieren, werden in letzter Zeit immer mehr Varianten von Self Healing Code in Verbindung mit generativer KI postuliert, wie dem Large Language Model GPT-4.

Während die bisherigen Beispiele selbstheilender Systeme auf die Laufzeit abzielten, existieren auch Verfahren und Ideen, generative KI zu Nutzung bei der Entwicklung einzusetzen, um Quellcode „ohne Mitwirkung des Entwicklers“ zu realisieren.

Mittelfristig sind Systeme im breiten Einsatz denkbar, welche die Codebasis eines Projektes analysieren und basierend darauf Änderungen generieren, welche anhand von Pull-Requests dem menschlichen Entwickler vorgeschlagen werden können.

Ein Workflow zur Erzeugung automatisierter Änderungen via LLM

Damit bei den automatisiert erstellten Änderungen möglichst sichergestellt wird, dass sie auch funktionieren, können die Änderungen durch eine Continuous Integration-Pipeline entsprechenden Tests unterworfen werden. Nur wenn die Pipeline erfolgreich durchläuft, werden die Änderungen dem Entwickler vorgeschlagen.

So nutzt Microsoft mit InferFix ein System zur semiautomatischen Fehlerbehebung, um den Arbeitsablauf für interne Projekte zu verbessern. Auch andere Firmen, wie Stackoverflow, denken ebenfalls über die Nutzung von LLMs und generativer KI im Rahmen der Softwareentwicklung nach.

Automatisiertes Debugging

Solche „selbstheilenden Fähigkeiten“ können auch für die komplett automatische Fehlerbehebung genutzt werden. So existiert mit Wolverine ein Proof of Concept für ein solches System. Wolverine nutzt GPT-4 von OpenAI, um Fehler in einem Python-Skript zu reparieren.

Dabei werden Fehler im Skript in das Sprachmodell gegeben und anschließend die Lösung auf den Quelltext angewendet. Danach wird ermittelt, ob das Skript nach der Änderung funktioniert. Treten erneut Fehler auf, werden diese wieder an das LLM übermittelt und dessen Lösung wieder in das Skript übernommen.

Solche Verfahren könnten weitergedacht und direkt beim Nutzer, im Falle eines Fehlers, ausgeführt werden.

Auch in IDEs ziehen Plugins basierend auf generativer KI ein, wie das AI Assistant-Plugin von Jetbrains. Mit diesem können Fehlermeldungen und Codeteile analysiert und erklärt und eine weitere Interaktion mit den Sprachmodellen durchgeführt werden. Dazu zählen unter anderem das Generieren von Dokumentation, sowie von Namen.

Probleme

Im Idealfall können solche Systeme eine Arbeitserleichterung sein, allerdings führen sich auch zu Problemen. So ist nicht sichergestellt, dass die von der KI gefundenen Lösungen wirklich die geforderten fachlichen Spezifikationen erfüllen. Daneben kann sich die Codequalität verschlechtern, wenn solche Änderung ungeprüft übernommen werden.

Hier könnte es im Laufe der Zeit vorkommen, dass aus Bequemlichkeit dazu übergegangen wird, solche Änderungen automatisiert auf den Quellcode anzuwenden.

Auf lange Sicht kann dies dazu führen, dass die eigene Codebasis immer schlechter verstanden wird, wenn diese generativer KI „gepflegt“ wird und diese Änderungen ohne ein sinnvollen Reviewprozess übernommen werden. Es ist denkbar, dass sich die Verantwortlichkeit von der eigentlichen Implementation des Quellcodes immer mehr in Richtung des Reviews verschiebt.

Wenn es an den Einsatz generativer KI geht, müssen neben solchen Fragen auch datenschutzrechtliche und sicherheitstechnische Aspekte bedacht werden.

Auch sollte beachtet werden, dass generative KI, wie die meistgenutzten Modelle von OpenAI Kosten verursachen, welche meist je Token abgerechnet werden. Werden lokale Modelle genutzt, muss stattdessen Rechenleistung und dahinterstehende Infrastruktur bereitgestellt werden.

Daneben existieren Größenbeschränkungen. Modelle wie solche von OpenAI sind bezüglich der maximal verarbeitbaren Token beschränkt, sodass größere Quelltext auf diese Art und Weise nur schwer am Stück analysiert werden können.

In Bezug auf selbstheilenden Code und Systeme kann ein blindes Vertrauen zu erheblichen Problemen führen.

Beispielhaft könnte ein System nur bestimmte Datentypen verarbeiten. Wenn ein solches Datenfeld vom Typ Integer ist und der Nutzer nun stattdessen Zeichenketten sendet, würde die Anwendung dies ablehnen. In einem solchen Fall könnte ein auf KI basierendes System zur Behebung dieses Fehlers den Typ der Schnittstelle, so ändern, dass auch Zeichenketten erlaubt sind und somit weiteren Problemen die Tür öffnen.

Fazit

Self Healing Code bietet eine Reihe von Vorteilen, im Betrieb der Anwendungen und sollte beim Design entsprechender Applikationen und Systeme berücksichtigt werden.

So sind diese Systeme zuverlässiger, bieten erhöhte Verfügbarkeit, und verringern eventuelle Downtimes. Auch in der Wartung können solche Systeme günstiger sein.

Allerdings sollten die Schwierigkeiten bedacht werden. Die Entwicklung von selbstheilendem Code kann komplex sein. Es kann eine Herausforderung darstellen, effektive Mechanismen zur Fehlererkennung, Diagnosealgorithmen und Strategien zur Fehlerbehebung zu entwickeln, die reibungslos miteinander interagieren.

Daneben bedeutet Self Healing Code in vielen Fällen auch ein Overhead. Unter Umständen werden zusätzliche Systemressourcen, für die Erkennung und die Beseitigung von Problemen, benötigt.

Wenn fälschlicherweise Fehler erkannt werden und die selbstheilenden Mechanismen aktiv werden, kann dies problematisch sein. Dazu gehören die Schwierigkeit, solche Systeme zu testen und zu überprüfen, und die Möglichkeit, dass das System unvorhersehbare oder unerwünschte Änderungen vornimmt.

Neben dem klassischen selbstheilenden Code hält generative KI immer mehr Einzug in unseren Alltag und dies wird auch in Verbindung mit Self Healing Code keine Ausnahme sein. Allerdings sollte hier Vorsicht geboten sein und der Mensch nicht aus dem Loop genommen werden.

Self Healing Code hat das Potenzial, die Zuverlässigkeit und Verfügbarkeit von Systemen zu verbessern und dieses Potenzial sollte nicht brach liegen gelassen werden.

Coding Conventions

Die Entwicklung von Software zeichnet sich in der heutigen Welt oft dadurch aus, dass sie unter Mitwirkung unterschiedlichster Entwickler bewerkstelligt wird. Im Rahmen einer solchen Entwicklung kommt es darauf an, bestimmte Standards und Best Practices einzuhalten.

Neben dem passenden Workflow kommen hier Coding Conventions zum Tragen und bilden einen wichtigen Baustein um Quelltext effizienter, lesbarer und zuverlässiger zu gestalten.

Was sind Coding Convention?

Eine Coding Convention definiert sich über bestimmte Stilregeln und Best Practices, bezogen auf eine Programmiersprache. Innerhalb der Konvention werden viele Aspekte der Programmiersprache und ihrer sprachlichen Elemente behandelt. Dies fängt bei Regeln zur Formatierung an, führt sich fort mit der Benamung von Variablen und anderen Strukturen und erstreckt sich auch auf andere Bereiche, wie Reihenfolgen und Zeilenlängen.

Warum werden sie benötigt?

Nun kann sich natürlich die Frage gestellt werden, warum eigentlich Coding Conventions benötigt werden?

Neben offensichtlichen Gründen, dass sie vielleicht eine Anforderung des Kunden sind, gibt es auch andere Gründe für diese Konventionen. So werden die meisten Projekte nicht von einer einzelnen Person betreut und für die Entwickler eines Produktes ist es einfacher, wenn der Quelltext nach identischen Standards entwickelt wurde. Dies ermöglicht eine schnellere Einarbeitung und hilft auch bei anderen Dingen, wie der Verminderung von Merge-Konflikten bei der Arbeit mit Versionskontrollsystemen.

Damit tragen diese Konventionen dazu bei, die Zusammenarbeit zwischen Entwicklern zu erleichtern, indem sie eine einheitliche und konsistente Basis schaffen.

Eine Welt ohne Coding Conventions

Natürlich können Programme auch ohne Coding Conventions geschrieben werden. Dies kann zu interessanten Programmen führen:

#include 

...

yeet Yeet Yeeet yeeeT 
Yeeeet
yEet yEEt yeEt yyeet yeett yeetT
yeeT yet yeetT
yeeeeT

Bei diesem Programm stellen sich bei der Betrachtung mehrere Fragen. In welcher Sprache ist es geschrieben? Ist es überhaupt lauffähig? Und was ist der eigentliche Zweck des Quelltextes?

In diesem Beispiel wurde das C-Programm so gestaltet, dass es möglichst unlesbar ist, indem mit entsprechenden Definitionen gearbeitet wurde, welche anschließend im Quelltext genutzt werden.

#define yeet int
#define Yeet main
#define yEet std
#define yeEt cout
#define yeeT return
#define Yeeet (
#define yeeeT )
#define Yeeeet {
#define yeeeeT }
#define yyeet <<
#define yet 0
#define yeett "Yeet!"
#define yeetT ;
#define yEEt ::

Es zeigt auf, dass ohne einheitliche Coding Conventions im besten Fall Chaos droht. Auf die Spitze treibt das auch der International Obfuscated C Code Contest, bei welchem es darum geht, Quelltext möglichst so zu verschleiern, dass nur schwer zu erraten ist, welche Funktion dieser am Ende in sich trägt.

Eine Implementation des zcat-Kommandos

In diesem Beispiel wird der Befehl zcat zur Darstellung mittels gz-komprimierter Daten implementiert. Auch ohne solche Extrembeispiele würde in einer Welt ohne Coding Conventions eine Menge an inkonsistentem Code entstehen:

int counterServer = 1               ;
int counterClient = 2               ;
int counterDevice = 3               ;
int test1 = 4                       ;

Natürlich kann ein Quelltext so formatiert werden, aber in den meisten Fällen erschwert dies die Lesbarkeit des Quelltextes enorm. Auch die Nutzung von Whitespaces und falscher Einrückung kann zu Problemen führen:

if        (system==true) {
    doSomething()        ;
doFoobar                       ();
}

Auch Richtlinien über Komplexität sind ein wichtiger Bestandteil, solcher Konventionen. Gegeben sei folgendes Programm:

#include

main(){
  int x=0,y[14],*z=&y;*(z++)=0x48;*(z++)=y[x++]+0x1D;
  *(z++)=y[x++]+0x07;*(z++)=y[x++]+0x00;*(z++)=y[x++]+0x03;
  *(z++)=y[x++]-0x43;*(z++)=y[x++]-0x0C;*(z++)=y[x++]+0x57;
  *(z++)=y[x++]-0x08;*(z++)=y[x++]+0x03;*(z++)=y[x++]-0x06;
  *(z++)=y[x++]-0x08;*(z++)=y[x++]-0x43;*(z++)=y[x]-0x21;
  x=*(--z);while(y[x]!=NULL)putchar(y[x++]);
}

Bei diesem handelt es sich um ein einfaches Hello World-Programm, aber das Verständnis wird durch die Umsetzung, genauer gesagt dessen unnötige Komplexität, sehr erschwert.

Auch wenn sich auf Einrückungen geeinigt wird, ist dies nicht immer sinnvoll:

function f() {
  doThings();
  doMoreThings();
             }

Bei diesem Beispiel wird eine Einrückung genutzt, welche ungebräuchlich ist und bei den meisten Entwicklern wahrscheinlich auf Ablehnung stoßen wird und nicht dazu führt, dass der Quelltext übersichtlicher wird.

Die Benamung von Elementen ist ein wichtiger Teil von Coding Conventions:

void doSomeTHING() {
  int test1 = 1;
  int TEST2 = 2;
  int teST3 = 3;
}

void DoSomething() {
  int tEST4 = 4;
}

Wird bei dieser nicht auf Konsistenz geachtet, trägt dies nicht zum besseren Verständnis bei. Auch Kommentare, bzw. das Schreiben derselben sind eine Aufgabe, bei der sorgfältig gearbeitet werden sollte:

try {
  ...
} catch(Exception e) {
  // Gotta Catch 'Em All
}

Natürlich ist Humor im echten Leben wichtig, aber in einem Quelltext sollte er nichts zu suchen haben. Stattdessen sollte sich hier auf die Fachlichkeit bezogen werden.

Auch die Nutzung unüblicher Vorgehensweisen bzw. das Verstecken bestimmter Operationen erschwert das Verständnis eines Quelltextes:

int main() {

  String helloWorld = "Hello World!";
  cout << helloWorld << endl;

  String hello = (helloWorld, 5);
  cout << hello << endl;

  system("pause");
  return 0;
}

Ohne weitere Informationen ist es relativ schwer herauszufinden, was in diesem Beispiel passiert. Hier werden die ersten fünf Stellen der Zeichenkette Hello World! zurückgegeben. In diesem Fall geschieht dies über die Überladung des Komma-Operators:

using namespace std;

#define String string

String operator,(String lhs, int rhs) {
  lhs.erase(lhs.begin() + rhs, lhs.end());
  return lhs;
}

Auch eine schlechte Benennung und ein Sprachmischmasch kann das Verständnis des Quelltextes erschweren:

#include 

gibHalloWeltAus()
{
    // use cout for output
    cout << "Hello World!" << endl;

    // Rückgabereturn
    return 0;
}

Ziele von Coding Conventions

Wenn sich diese Beispiele aus der Welt ohne Coding Conventions angeschaut werden, können aus diesen einige Ziele für entsprechende Konventionen abgeleitet werden.

Es geht darum, dass Coding Conventions bewährte Praktiken abbilden und für einen lesbaren und verständlichen Quelltext sorgen. Sie sollen die Zusammenarbeit im Team erleichtern und eine gewisse Einheitlichkeit herstellen.

Daneben sind Coding Conventions und das Konzept von Clean Code miteinander verbunden. Clean Code ist ein Konzept, das sich auf die Softwareproduktion und das Programmierdesign bezieht. Es legt fest, dass Quelltext so geschrieben werden sollte, dass er einfach zu lesen, zu verstehen und zu warten ist. Die Einhaltung von Coding Conventions kann dazu beitragen, diese Kriterien einzuhalten.

Elemente von Coding Conventions

Doch woraus genau bestehen Coding Conventions im Einzelnen? Im ersten Schritt sollte sich bewusst gemacht werden, dass sich solche Konventionen von Sprache zu Sprache unterscheiden. Auch wenn sich im Laufe der Zeit einige Standards herauskristallisiert haben, können diese nicht immer eins zu eins auf die eigenen Anforderungen angewendet werden.

Unterschiedliche Elemente von Coding Conventions

Im Einzelnen setzen sich Coding Conventions aus Elementen zusammen, welche im folgenden genauer besprochen werden sollen.

Benamung

Ein essenzielles Element ist die Benamung innerhalb eines Entwicklungsprojektes. Dies fängt bei Dateinamen und Verzeichnissen an, zieht sich hin zu Bezeichnern, wie den Namen von Variablen, Klassen und vielen anderen Elementen.

Grundsätzlich sollte bei der Benamung von Elementen immer so viel wie nötig und so wenig wie möglich benannt werden.

Dateinamen

Da Coding Conventions sich von Sprache zu Sprache unterscheiden, existieren bereits Unterschiede auf Ebene der Dateinamen. Während Dateien von C-Programmen meist in Kleinschreibung benannt werden:

main.c
tilerenderer.c

sieht dies bei Java-Applikationen anders aus:

Main.java
TileRenderer.java

Neben den Dateinamen bezieht sich dies auch auf die Benamung und Struktur von Verzeichnissen. In C würde dies wie folgt aussehen:

src
  engine
  renderer
  utils

während in Java meist die Struktur des Packages abgebildet wird. Bei dem Package com.example.transformer.html würde die entsprechende Verzeichnisstruktur wie folgt aussehen:

src
  main
    java
      com
        example
          transformer
            html
            markdown
  test

Eine weitere Eigenart von Java ist, dass die Namen der Packages eine Domain-Struktur abbilden und z. B. mit der Domain der Firma beginnen.

Neben den Coding Conventions für die jeweilige Sprache gehen bei der Strukturierung des Projektes auch noch andere Aspekte ein. Wird z. B. mit dem Build-Werkzeug Maven gearbeitet, so gilt dort Konvention vor Konfiguration.

Bei Maven bedeutet dies, dass eine Reihe von Standardregeln existieren, die vom Benutzer des Werkzeuges befolgt werden müssen, um ein Projekt erfolgreich zu erstellen. So muss ein Projekt in einer bestimmten Struktur organisiert sein, damit Maven es erfolgreich verarbeiten kann. Diese Standards erleichtern es, ein Projekt mit Maven zu erstellen, da der Benutzer nicht jeden einzelnen Schritt konfigurieren muss.

Sprechende Namen

Auch bei der Benamung sollten gewissen Standards eingehalten werden:

int a = getSum();

In diesem Fall wird eine Methode mit dem Namen getSum aufgerufen und das Ergebnis in der Variable a gespeichert. Hier sollte mit sprechenden Namen gearbeitet werden. Solche Namen zeichnen sich dadurch aus, dass sie beim Lesen bereits Aufschluss über ihre Fachlichkeit und deren Bedeutung geben:

int sum = getSum();

Damit wird klar, dass sich in der Variable sum eine entsprechende Summe befindet. Theoretisch kann die Benennung natürlich noch weiter spezifiziert werden:

int sumArticles = getSum();

Unter Umständen können Namen hierbei etwas länger werden, aber dafür wird Klarheit gewonnen. Diese Art der Benamung sollte nicht nur für Variablen, sondern generell für Bezeichner, wie Klassennamen gelten.

Allerdings keine Regel ohne Ausnahme, z. B. bei Exceptions unter Java:

try {
  // Try some funky stuff
} catch(Exception e) {
  // Handle exception
}

Dort hat es sich eingebürgert, einer Exception den Namen e bzw. ex zu geben. Sind solche Konventionen vorhanden und weitverbreitet, sollten diese entsprechend eingehalten werden. Auch hier dient das Einhalten dieser Regeln dazu, die Lesbarkeit und Wartbarkeit des Quelltextes zu erhöhen.

Verbotene Bezeichner

Es gibt eine Reihe von Bezeichnungen, welche in der Theorie, je nach verwendeter Sprache, verwendet werden können, es aber nicht sollten.

So ist es in Sprachen wie C# möglich, mit einem vorgestellten At-Zeichen Schlüsselwörter der Sprache als Bezeichner verwenden zu können. Um Verwirrung und darauf aufbauende Probleme zu vermeiden, sollte dies unterlassen werden.

Andere Bezeichner, wie handle, sollten nur in einem eng begrenzten Kontext oder einer entsprechenden Fachlichkeit benutzt werden.

Auch die Nutzung von Variablen mit dem Namen temp oder tmp sollte unterlassen werden, da meist eine entsprechend sinnvollere fachliche Benamung möglich ist.

Schleifen und Benamung

Wie bei Exceptions haben sich auch bei Schleifen bestimmte Konventionen zur Benamung eingebürgert, an welche sich gehalten werden sollte:

for(int i = 0; i < 10; i++) {

    for(int j = 0; j < 10; j++) {

      // Do stuff
    }
}

So wird die Zählervariable bei Schleifen mehrheitlich mit dem Namen i benannt und wenn in der Schleife weitere Schleifen geschachtelt werden, so werden diese fortlaufend mit j, k und so weiter benannt.

Aber auch hier kann in Ausnahmen davon abgewichen werden. Ein Beispiel hierfür wäre z. B. die Verarbeitung eines Bildes:

for(int y = 0; y < image.height; y++) {

    for(int x = 0; x < image.width; x++) {

      // Do image stuff
    }
}

Hier wird sich auf die x- und y-Achse des Bildes bezogen und durch die entsprechende Benamung kann sinnvoll mit diesen in der eigentlichen Logik der Schleife gearbeitet werden.

Kamele, Dromedare und Schlangen

Bezeichner können wie bei obigem Beispiel einfache Namen bestehend aus einem Wort sein, bestehen aber in vielen Fällen aus mehreren Wörtern.

Unterschiedlichste Schreibvarianten bei zusammengesetzten Bezeichnern

Um diese sinnvoll miteinander zu verbinden werden je nach Sprache unterschiedliche Varianten von Binnenmajuskeln benutzt, welche je nach Verwendung treffende Namen wie CamelCase und Ähnliche tragen. Diese Schreibweise sorgt letztlich für eine bessere Lesbarkeit, da sie einzelne Wörter sinnvoll voneinander abgrenzt.

Binde- und Unterstriche

Neben der Schreibweise mittels Binnenmajuskeln existieren auch andere Schreibweisen, was sich im Beispiel wie folgt darstellt:

do_things_fast();
do-things-fast();

So wird in Sprachen wie C und Perl auch auf Unterstriche zurückgegriffen und auch in PHP war dies bis zur Version 4 der Fall. Die Schreibweise mit dem Bindestrich, welche auch als lisp-case bekannt ist, wurde unter anderem in COBOL und Lisp genutzt.

Auch bei Rust wird teilweise auf Unterstriche als auch auf CamelCase gesetzt.

Ausnahmen bei der Benamung

Je nach Sprache wird damit meist eine bestimmte Schreibweise für Bezeichner wie den Namen von Variablen genutzt, allerdings existieren hiervon auch Ausnahmen bzw. Abweichungen, wie bei der Definition von Konstanten:

public static final String SECRET_TOKEN = "X7z4nhty3287";

Diese werden in vielen Fällen komplett großgeschrieben und meist mit Unterstrichen unterteilt. Auch hier gilt wieder, dass solche Konstanten möglichst sprechend benannt werden sollten und auf Abkürzungen und Ähnliches verzichtet werden sollte.

Prä- und Suffixe

In der Vergangenheit wurden an Bezeichner teilweise Prä- und Suffixe mit angetragen. Begründet war dies mit den damaligen Compilern und der fehlenden Unterstützung in der Entwicklungsumgebung. Durch die Nutzung eines Präfixes konnte so z. B. der Typ einer Variable aus dem Namen ermittelt werden.

Die sicherlich bekannteste Notation ist die Ungarische Notation. Hier werden die Bezeichner aus einem Präfix für die Funktion, einem Kürzel für den Datentyp und einem Bezeichner zusammengesetzt.

Ein Beispiel für einen solchen Namen wäre die Variable idValue, welche anzeigt, dass es sich um einen Index vom Typ Double handelt, welcher den Namen Value trägt.

Mittlerweile wird diese Notation in der Praxis nur noch selten genutzt. Auch Linus Torvalds hatte sich dazu geäußert:

Encoding the type of a function into the name (so-called Hungarian notation) is brain damaged – the compiler knows the types anyway and can check those, and it only confuses the programmer.

Neben der besseren Unterstützung der IDEs gibt es andere Gründe, welche gegen eine Nutzung der ungarischen Notation sprechen. So kann z. B. bestehender Quelltext schlechter migriert werden, wenn sie die Namen nicht ändern dürfen, aber die Typen dies tun. Dies war z. B. der Fall bei der Umstellung der WinAPI auf eine 64-Bit fähige API, bei dem Namen nun nicht mehr auf den korrekten Datentyp hinweisen.

Einrückungen

Neben der Benennung von Bezeichnern ist auch die Einrückung ein unter Umständen recht emotionales Thema.

Dabei geht es hauptsächlich darum, ob Leerzeichen oder Tabulatoren für die Einrückungen genutzt werden. Aus pragmatischer Sicht sollte hier insbesondere die Mischung dieser beiden Varianten verhindert werden.

Für Leerzeichen spricht, dass die Einrückungen bei allen Nutzern identisch aussehen. Im Gegensatz zu Tabulatoren benötigen Leerzeichen, mehr Speicher. Vier Leerzeichen belegen 4 Byte, ein Tabulator nur ein Byte.

Bei Tabulatoren kann der Einzug in der Entwicklungsumgebung individuell konfiguriert werden, was aber gleichzeitig den Nachteil ergibt, dass der Quelltext bei unterschiedlichen Mitarbeitenden anders aussehen kann.

Persönlich würde der Autor an dieser Stelle immer Leerzeichen empfehlen. Damit ist ein Quelltext gewährleistet, welcher bei jedem Entwickler identisch aussieht. Der zusätzliche Speicherbedarf kann hierbei vernachlässigt werden.

Einrückungstiefen

Bei der Frage der Leerzeichen stellt sich auch die Frage, mit wie vielen Leerzeichen soll ein Block eingerückt werden. Hier ergibt sich die Möglichkeit, dies mit zwei Leerzeichen je Block zu machen:

void main() {
  doSomething();
}

Der Standard bei vielen Projekten sind hingegen vier Leerzeichen:

void main() {
    doSomething();
}

Allerdings sind auch acht Leerzeichen gebräuchlich, z. B. beim Linux-Kernel. Wirklich bemerkbar wird dies allerdings erst dann, wenn mehrere Blöcke ineinander verschachtelt werden:

void main() {

    for(int i = 0; i < 10; i++) {

        for(int j = 0; j < 10; j++) {

            doSomething();
        }
    }
}

Je nach Ausgabeformat, z. B. beim Ausdruck oder in Präsentationen ist es sinnvoll auf zwei Leerzeichen zu setzen, aber im Allgemeinen sollten vier Leerzeichen genutzt werden.

Whitespaces und Leerzeilen

Neben der Einrückung sind auch die Whitespaces im Quelltext selbst, sowie Leerzeilen ein Element zur Strukturierung des Quelltextes.

Leerzeilen stellen ein wichtiges Element zur Strukturierung dar. Natürlich kann ein Quelltext ohne Leerzeilen geschrieben werden und leider ist dies in der Praxis oft zu sehen. Sinnvoll ist es aber, den Quelltext etwas weiträumiger zu gestalten:

int getResult(int a, int b) {

    int sum = getSum();
    int ret = 0;

    for(int i = 0; i < 10; i++) {
        ret += sum;
    }

    return ret;
}

Die Trennung einzelner Bestandteile des Quelltextes durch Leerzeilen sollte anhand der funktionalen Blöcke bzw. nach der Fachlichkeit vorgenommen werden.

Neben den Leerzeilen, sind auch Whitespaces ein essenzieller Teil der Formatierung eines Quelltextes. Whitespaces definieren sich allgemein als Leerstellen in Text, Code oder Schrift, die zwischen Zeichen, Wörtern, Zeilen oder Absätzen liegen. In der Programmierung werden Whitespaces auch als Formatierung verwendet, um den Quelltext leserlicher zu machen und den Code übersichtlicher zu strukturieren.

Whitespaces verbessern die Sichtbarkeit und das Verständnis der Syntax:

int sum=a+b;

for(int i=0;i<10;i++) {
    doSomething();
}

Bei diesem Beispiel wäre es wesentlich sinnvoller, Leerzeichen zum Strukturieren zu nutzen und dem Quelltext eine gewissen Luftigkeit zu geben:

int sum = a + b;

for(int i = 0; i < 10; i++) {
    doSomething();
}

Dies erhöht die Lesbarkeit und sorgt letztlich für ein besseres Verständnis. Natürlich kann auch an dieser Stelle übertrieben werden:

for ( int i = 0; i < 10; i++ ) {
    doSomething ( ) ;
}

So werden hier auch Leerzeichen rund um die Klammern gesetzt, was im Normalfall nicht sonderlich hilfreich ist und deshalb unterlassen werden sollte.

Blockklammern

In vielen Programmiersprachen wird mit Blöcken gearbeitet. Ein Block definiert sich als eine Gruppe von Anweisungen, die als eine Einheit behandelt werden. So wird über den Block z. B. der Gültigkeitsbereich von Variablen definiert. Ein Block beginnt normalerweise mit einer öffnenden geschweiften Klammer und endet mit einer schließenden Klammer gleichen Typs.

Beispielsweise kann ein Block zu einer if-Anweisung gehören, in der eine Reihe von Anweisungen ausgeführt werden, wenn die Bedingung wahr ist. Hier kann natürlich die Frage nach der Notwendigkeit gestellt werden, wie in diesem Stück Java-Code:

if(something == true)
    doFooBar();

So würde dieses Beispiel ohne Probleme kompilieren und wenn die Bedingung zutrifft, die Methode doFooBar aufgerufen werden. Problematisch wird dieses Konstrukt allerdings dann, wenn der Quelltext an dieser Stelle erweitert wird:

if(something == true)
    doAnotherThing();
    doFooBar();

Nun würde nur noch die Methode doAnotherThing ausgeführt werden. Die andere Methode hingegen nicht mehr. Aus dem Quelltext ist dies allerdings nicht ohne Weiteres ersichtlich. Aus diesem Grund sollte immer mit Blockklammern gearbeitet werden, auch wenn nur eine einzelne Anweisung folgt:

if(something == true) {
    doFooBar();
}

Dadurch werden Fehler vermieden und die Intention des Quelltextes wird sofort ersichtlich.

Position der Klammern

Für die Positionierung der geschweiften Blockklammern gibt es in der Praxis zwei verbreitete Varianten, diese zu setzen. Bei der ersten Variante sind sie beide auf der gleichen Ebene zu finden:

boolean get()
{
    int a = 7;
    int b = 42;

    int result = doFooBar(7, 42);

    if(result == 23) 
    {
        return false;
    }

    return true;
}

Der Vorteil an dieser Variante ist, dass sofort zu sehen ist, wo ein Block beginnt und wo sich die entsprechende schließende Klammer des jeweiligen Blockes befindet. Als Nachteil wird bei dieser Variante oft aufgeführt, dass damit etwas Platz verschwendet wird.

Bei der anderen gebräuchlichen Variante wird die öffnende Klammer eines Blockes direkt hinter die Anweisung gesetzt, welche zum öffnenden Block gehört:

boolean get() {

    int a = 7;
    int b = 42;

    int result = doFooBar(7, 42);

    if(result == 23) {
        return false;
    }

    return true;
}

Dies erschwert zwar die Zuordnung zwischen dem Beginn des Blockes und dem Ende, allerdings zeigen die meisten modernen IDEs diese Zuordnung prominent an.

In der Theorie wird bei dieser Variante eine Zeile eingespart, allerdings ist es sinnvoll nach der öffnenden Blockklammer eine Leerzeile zu setzen, um die Übersichtlichkeit zu erhöhen.

Häufig wird noch ein Unterschied zwischen einzeiligen und mehrzeiligen Blöcken gemacht:

if(something == true) {
    doFooBar();
}

Dort wird die Leerzeile weggelassen, während sie bei mehrzeiligen Blöcken immer eingefügt wird:

if(something == true) {

    doFooBar();
    doSomething();

    for(int i = 0; i < 10; i++) {
        doThings();
    }    
}

Blöcke per Einrückung

Neben Sprachen mit solchen Blockklammern existieren auch Sprachen wie Python, welche andere Wege zur Strukturierung von Blöcken nutzen:

import sys
import random

running = True

while running:

    randomNumber = random.randint(0,8)

    if randomNumber == 0:
        break;
        
    else:
        print(randomNumber)

Hier wird die Zuordnung zu einem Block über die entsprechende Einrückung vorgenommen. Damit entfällt die Frage nach der Position der Blockklammern.

Reihenfolgen

In vielen Programmiersprachen gibt es Schlüsselwörter, wie Modifikatoren für die Sichtbarkeit. Für diese empfiehlt es sich auch eine entsprechende Reihenfolge zu definieren und diese einzuhalten.

Am Beispiel von Java wäre dies die Sichtbarkeit, dann eine eventuelle static-Definition gefolgt von einer final-Definition und am Ende der eigentlichen Definition:

public int a = 7;
public final int b = 24;
public static final int c = 42;

Auch bei Systemen zur statischen Codeanalyse, wie Sonarlint, sind solche Regeln hinterlegt.

Reihenfolge im Quelltext

Neben den Namen der Bezeichnern sind je nach Sprache auch bestimmte Reihenfolgen der einzelnen Elemente gewünscht. Unter Java ist dies vornehmlich folgende Reihenfolge: Konstanten, private Variablen, private Methoden, Getter und Setter und anschließend öffentliche Methoden.

Allerdings kann es valide sein, Public-Methoden und Private-Methoden zusammenzuhalten, wenn diese z. B. nach Funktionalität gruppiert sind.

Zeilenlänge und Umbrüche

Früher gab es relativ strenge Regeln, was die maximale Zeilenlänge innerhalb eines Quelltextes anging. Meist waren dies 80 Zeichen pro Zeile, bedingt durch die 80 Spalten in der Hollerith-Lochkarte von IBM. Daneben haben sich mittlerweile Zeilenlängen von 80 über 100 bis zu 120 Zeichen pro Zeile eingebürgert.

Auch in Zeiten größerer Bildschirme und höherer Auflösungen, sollten Zeilen trotzdem nicht unendlich lang gestaltet werden, sondern mit Zeilenumbrüchen gearbeitet werden. Für solche Umbrüche existieren unterschiedliche Variante, welche in gewisser Hinsicht Geschmacksache sind.

public int calculate(int valueA,
                     int valueB,
                     int valueC,
                     int valueD,
                     int valueE,
                     int valueF,
                     int valueG) {
    return 0;
}

Grundsätzlich sollten keine Umbrüche mitten in einer Parameterliste vorgenommen werden, sondern die Parameter einzeln umgebrochen werden. Auch bei Fluent Interfaces wird mit Zeilenumbrüchen gearbeitet:

CarBuilder carBuilder = new CarBuilder()
        .withWheels(4)
        .withEngine(400, Fuel.DIESEL)
        .withWindows(5)
        .build();

Die Umbrüche verbessern, richtig eingesetzt, die Lesbarkeit und Verständlichkeit des Quelltextes.

Kommentare

Für einen verständlichen Quelltext sind in vielen Fällen Kommentare in diesem nötig und wichtig.

Je nach Sprache werden unterschiedliche Möglichkeiten für Kommentare bereitgestellt. Vorwiegend sind dies Zeilenkommentare und Blockkommentare.

Blockkommentare sind eine Reihe von Kommentaren, die durch ein vorangestelltes /* und ein abschließendes */ angezeigt werden, sodass mehrere Zeilen Text zusammen kommentiert werden können. Zeilenkommentare sind Kommentare, die nur eine einzelne Zeile betreffen und mit // beginnen. Sie können am Ende einer Codezeile oder auf einer eigenen Zeile platziert werden. Beide Kommentartypen sind nützlich, um das Verständnis des Codes zu erleichtern, indem sie Erklärungen zu bestimmten Codeabschnitten bereitstellen.

In den meisten Fällen sollten innerhalb eines Quelltextes den Zeilenkommentaren der Vorrang eingeräumt werden, entweder zum Auskommentieren von Quellcode oder zum Dokumentieren innerhalb des Codes:

// Create system temporary directory
Path tmpdir = null;

// log.error(tmpdir);

Block-Kommentare werden oft für die Dokumentation von Methoden, z. B. mittels JavaDoc genutzt:

/**
 * This method returns an Optional that holds a String containing
 * the file extension of the passed filename.
 * @param filename The file name whose extension is to be determined.
 * @return Optional filled with a String with the file extension if 
 * the file has an extension, or an empty optional if it has no extension.
 */

Grundsätzlich gilt bei Kommentaren, dass sie fachlicher Natur sein sollten und dass nicht unnötig kommentiert wird. Als Sprache bietet sich hier wie bei der Benamung von Bezeichnern Englisch als kleinster gemeinsamer Nenner an. Unnötige Kommentare sollten vermieden werden:

// Calculate sum and store in sum
int sum = getSum(a, b);

Der Inhalt des Kommentars ergibt sich bereits aus der sprechenden Bezeichnung der Variablen und der dazugehörigen Methode, sodass dies nicht noch einmal mit einem Kommentar untermauert werden muss.

Interessanter wäre es hier, wenn der Kommentar noch etwas zur Fachlichkeit beiträgt:

// Calculate sums of base articles
int sum = getSum(a, b);

Auch das beliebte Auskommentieren von Code wird mittels der Sprachmittel des Kommentars ermöglicht. Im Normalfall sollte auskommentierter Quellcode am Ende immer entfernt werden und nicht im Quelltext verbleiben.

Allgemeine Regeln

Neben Regeln für spezielle Konstrukte existieren eine Reihe von allgemeinen Regeln, welche auch in Coding Conventions Einzug gefunden haben.

So gilt, dass pro Zeile genau eine Anweisung bzw. ein Statement kodiert wird, eine Funktion bzw. eine Methode genau eine Aufgabe erledigen und Klassen und Methoden eine gewisse Größe nicht überschreiten sollten.

Bei Klassen definiert sich hier meist eine maximale Größe von 2000 Zeilen, während bei Methoden gerne gesagt wird, dass eine Methode als Ganzes auf den Bildschirm passen sollte.

Aufgaben für Methoden

Auch die Beschränkung von Methoden auf eine Aufgabe ist eine sinnvolle Regel. So verheißt eine Methode mit dem Namen doItAll() schon wenig Gutes. Hingegen definiert folgende Methode:

int getSum(int a, int b)

schon anhand ihres Namens klar, welche Aufgabe sie wahrnimmt und mit welchem Ergebnis zu rechnen ist.

Dadurch, dass Funktionen bzw. Methoden sich nur auf eine Aufgabe beschränken, sind sie besser wiederverwendbar und verhindern in vielen Fällen doppelten Quelltext. Auch das Review solcher fachlich eng abgestimmten Methoden ist einfacher möglich, da die Komplexität verringert ist.

Coding Conventions

Während bis hierhin viele einzelne Elemente beschrieben wurden, sollen diese nun zu einer Coding Convention zusammengeführt werden. Solche Coding Conventions sind relativ umfangreiche Werke. In vielen Fällen ist es nicht nötig das Rad neu zu erfinden, da für viele Sprachen Standard-Konventionen existieren, welche genutzt werden können.

Alternativ sollte sich zumindest an diesen Konventionen orientiert werden. Auch die jeweiligen Entwicklungsumgebungen, setzten über die Code-Formatierung gewisse Teile von Coding Conventions direkt um.

Sprachspezifische Konventionen

Wer sich umschaut, wird feststellen, dass eine Reihe von Coding Conventions für unterschiedliche Sprachen existieren. Dies sind unter anderem die .NET: Microsoft Coding Conventions, für Java die Code Conventions for the Java Programming Language und für PHP: PSR-1 und PSR-2.

Allerdings werden manche dieser Konventionen wie für Java mittlerweile als veraltet angesehen und büßen damit auch an Verbindlichkeit ein. Bei anderen Styles wie PSR-2 werden diese direkt für die Entwicklung des Frameworks genutzt und sind somit verbindlich.

Übergreifende Konventionen

Daneben existieren noch andere Coding Conventions wie die Apple Coding Convention und der Google Style Guide.

Der Google Style Guide deckt Konventionen für unterschiedlichste Sprachen, wie C++, Objective-C, Java, Python, R, HTML, JavaScript und weitere ab und kann online eingesehen werden.

Lizenziert ist der Google Style Guide unter der Creative-Commons-Lizenz CC-BY. Neben der eigentlichen Beschreibung werden auch Dateien mit den entsprechenden Konfigurationen für die Entwicklungsumgebung mitgeliefert.

Dokumentation

Auch wenn ein Hauptaugenmerk bei Coding Conventions auf dem Quelltext liegt, sollte die Dokumentation ebenfalls beachtet werden. So sollte nur das notwendige dokumentiert und tote und inkorrekte Dokumentation gelöscht werden.

Auch hat es sich eingebürgert, eine entsprechende Datei ins Wurzelverzeichnis des Projektes zu legen. Diese trägt meist den Namen README bzw. README.md

In diesem Dokument wird erklärt, um welches Projekt es sich handelt und ein kurzer Überblick über das Projekt gegeben. Daneben werden weiterführende Links bereitgestellt und erklärt, wie das Projekt gebaut werden kann.

# WordPress2Markdown

WordPress2Markdown is a tool that convert WordPress eXtended RSS (WXR) into markdown. Export the WordPress site via
backend and use the WordPress eXtended RSS (WXR) with this tool.

## Usage

WordPress2Markdown is a command line tool.

> java -jar WordPress2Markdown.jar -i wordpress-export.xml -s DATETIME -o /home/seeseekey/MarkdownExport

### Parameter

The options available are:

    [--author -f value] : Filter export by author
    [--authors -a] : Export authors
    [--help -h] : Show help
    [--input -i value] : Input path
    [--output -o value] : Output path
    [--scheme -s /POST_ID|DATETIME/] : Scheme of filenames

## Conversion

WordPress2Markdown converted the following html and other tags:

* \<em\>
* \<b\>
* \<blockquote\>
* \<pre\>
* \<img\>
* \<a\>
* Lists
* WordPress caption blocks ()

All other tags are striped.

## Developing

Project can be compiled with:

> mvn clean compile

Package can be created with:

> mvn clean package

## Authors

* seeseekey - https://seeseekey.net

## License

WordPress2Markdown is licensed under AGPL3.

Eine weitere wichtige Datei ist das Changelog, bzw. die Datei CHANGELOG.md. Diese Datei dokumentiert Änderungen am Projekt und informiert den Leser somit über Änderungen, neue Funktionalität und Ähnliches.

# Changelog

This changelog goes through all the changes that have been made in each release.

## [1.2.0-SNAPSHOT]() - 2022-03-04

### Added

* Implement simple conversion for CSV files

### Changed

* Update project to Java 17
* Rework changelog
* Update dependencies
* Change license from GPL3 to AGPL3

### Fixed

* Fix some SonarLint code smells
* Small optimizations

## [1.1.0](https://github.com/seeseekey/Convert2Markdown/releases/tag/v1.1) - 2019-10-13

### Added

* Implement conversion of MediaWiki dump files
* Add statistical information for export
* Add support for exporting author (#1)
* Add filter to export only a specific author

### Changed

* Rename tool to Convert2Markdown
* Update documentation
* Rebuild HTML to markdown conversion with HTML parser

## [1.0.0](https://github.com/seeseekey/Convert2Markdown/releases/tag/v1.0) - 2019-03-26

* Initial release

Versionierung

Im weiteren Sinne gehört auch die Versionierung des Projektes zu den Coding Conventions. Gerne genutzt wird hierbei die semantische Versionierung. Dabei liegen den Zahlen der Versionnummer z. B. 2.4.7 eine bestimmte Bedeutung zugrunde.

So handelt es sich bei der ersten Zahl um die Major-Version, welche nur dann erhöht wird, wenn zur Vorgängerversion inkompatible Änderungen oder andere signifikanten Änderungen vorgenommen wurden.

Die zweite Zahl ist die sogenannte Minor-Version, welche meist bei neuer Funktionalität hochgezählt wird. Die letzte Zahl bezeichnet die Bugfix-Version und wird bei entsprechenden Fehlerbereinigungen hochgezählt.

Daneben existieren auch andere Versionierungen, wie das relativ beliebte Schema Jahr.Monat z. B. 2023.04 als Versionnummer, welche bei neuen Releases basierend auf der Version gerne um eine dritte Nummer erweitert werden z. B. 2023.04.1.

Umsetzung

Neben der eigentlichen Definition einer Coding Conventions ist es wichtig, dass diese im Entwicklungsalltag berücksichtigt und genutzt wird. Hier stellt sich dann die Frage nach der organisatorischen Umsetzung.

Grundlegend sind es einige Schritte auf dem Weg bis zu Nutzung und Umsetzung. So sollte sich im ersten Schritt auf eine Coding Convention geeinigt werden. Nachdem dies geschehen ist, mitsamt aller diesbezüglicher Regeln, wie zur Benennung oder der gewünschten Komplexität, müssen diese Coding Conventions entsprechend kommuniziert werden.

So ist es problemlos möglich, entsprechende Templates für die Einstellungen der jeweiligen IDEs zur Verfügung zu stellen. Auch sollte die Überprüfung der Coding Conventions beim Review kontrolliert werden.

Daneben können die entsprechenden Coding Conventions auch per Software überprüft und z. B. das Pushen in ein entferntes Git-Repository nur erlaubt werden, wenn die Coding Conventions eingehalten wurden. Allerdings sollte auch nicht versucht werden, soziale Probleme, welche sich bei der Einführung der Konventionen ergeben, durch rein technische Ansätze zu lösen.

Umstellung

Eine weitere Frage ist die Umstellung der bestehenden Projekte auf neue Coding Conventions. Bestimmte Dinge wie die Formatierung des Quelltextes können meist automatisch auch für größere Projekte bewerkstelligt werden.

Daneben sollte bestehender Code Stück für Stück auf die Konventionen angepasst werden, z. B. bezüglich der Benamung. Dies kann immer dann geschehen, wenn an einer entsprechenden Stelle im Rahmen einer Anforderung gearbeitet wird.

Probleme

Natürlich kann es bei der Nutzung und Einführung von Coding Conventions Probleme geben. Dies kann sich in Widerstand aus der Entwicklerschaft oder in Problemen mit der technischen Seite wie unterschiedlicher IDEs ausdrücken.

Vor allem bei einer Neueinführung kann es schwierig sein, sich an entsprechende Konventionen und Regeln zu gewöhnen, wenn vorher ohne gearbeitet wurde. Das Gleiche gilt, wenn eine Konvention nicht den eigenen Präferenzen entspricht.

Es kann passieren, dass einige Zeit dafür aufgebracht werden muss, die Konventionen zu verinnerlichen und umzusetzen. Deshalb ist es wichtig, die Konventionen klar zu kommunizieren, ihre Nutzung verpflichtend zu machen und dies im Entwicklungsprozess wie beim Review auch zu beachten.

Fazit

Coding Conventions sind ein wesentlicher Bestandteil der Softwareentwicklung und bieten viele Vorteile. Sie helfen dabei, Quelltexte einfacher lesbar, verständlich und wiederverwendbar zu machen. Dadurch wird die Wartbarkeit verbessert und die Qualität der Software erhöht. Dies kann zu einer höheren Produktivität und einem schnelleren Entwicklungsprozess führen.

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

Audio-Dateien mittels ffmpeg normalisieren

Für einen Audio-Workflow wollte ich unter anderem Audio-Dateien normalisieren, damit diese mit einer identischen Lautheit verfügbar sind. Dazu kann das freie ffmpeg genutzt werden:

ffmpeg -i local.flac -filter:a loudnorm local.wav

Allerdings wird bei der Normalisierung eine 2-Pass-Lösung empfohlen. Dabei wird im ersten Durchlauf die Datei analysiert, um die entsprechenden Werte für die qualitativ hochwertige Normalisierung zu ermitteln:

ffmpeg -i local.flac -filter:a loudnorm=print_format=json -f null NULL

Für die entsprechende Datei wird dann eine Ausgabe erzeugt:

{
	"input_i" : "-33.65",
	"input_tp" : "-8.11",
	"input_lra" : "7.10",
	"input_thresh" : "-44.96",
	"output_i" : "-25.28",
	"output_tp" : "-2.00",
	"output_lra" : "4.50",
	"output_thresh" : "-36.36",
	"normalization_type" : "dynamic",
	"target_offset" : "1.28"
}

Mithilfe dieser Informationen kann dann der zweite Durchlauf gestartet werden:

ffmpeg -i local.flac -filter:a loudnorm=linear=true:i=-16:lra=7.0:tp=-2.0:offset=0.50:measured_I=-33.65:measured_tp=-8.11:measured_LRA=7.10:measured_thresh=-44.96 -ar 44100 local.wav

Da die Lösung mittels ffmpeg etwas umständlich ist, wurde ffmpeg-normalize entwickelt. Dieses Python-Tool kann über den Python-Paketmanager installiert werden:

pip3 install ffmpeg-normalize

Anschließend kann die Normalisierung und Konvertierung vorgenommen werden:

ffmpeg-normalize local.flac -o local.wav -ar 44100

In diesem Fall wird die FLAC-Datei normalisiert und in eine Wave-Datei konvertiert und die entsprechende Samplerate auf 44100 gestellt. Um das Kommando einfacher zugänglich zu machen, kann eine Bash-Funktion definiert werden. Unter macOS muss diese in der entsprechenden Konfigurationsdatei der Z shell angelegt werden:

nano ~/.zshrc

Anschließend kann die entsprechende Funktion in der Datei definiert werden:

normalize ()
{
  ffmpeg-normalize "$1" -o "$1".wav -ar 44100
}

Nachdem das Terminal neu gestartet wurde, kann das Kommando wie folgt benutzt werden:

normalize local.flac