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.

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.

MQTT unter Java nutzen

Für den Datentransfer zwischen Systemen existieren in der IT-Welt unzählige Protokolle und Verfahren. Mit MQTT existiert ein Protokoll, welches sich unter anderem für Kommunikation im IoT-Bereich gut eignet.

Zugutekommt MQTT hier, dass es unter anderem für die Nutzung über Verbindungen mit geringen Datenraten, z. B. die Nutzung über Satellitensysteme, optimiert wurde.

Neben den Grundlagen und einem Verständnis für das Protokoll, ist auch die Nutzung interessant. Aus diesem Grund soll im Rahmen dieses Artikels, eine kleine MQTT-Umgebung unter Verwendung von Java implementiert werden und mit dieser einige Konzepte und Möglichkeiten rund um MQTT dargestellt und erläutert werden.

Das große Ganze

Als Szenario für eine beispielhafte Implementation wird ein Smart-Home-System angenommen.

Die Räumlichkeiten für das Smart-Home-Szenario

In diesem Szenario existieren Räume, in diesen ein paar Lampen, einige Sensoren und Schalter. All diese Geräte kommunizieren über MQTT mit einem Broker und sind so miteinander verbunden. Auch das Steuerungssystem des Smart-Home-Systems ist per MQTT über den Broker angebunden.

Die Struktur des MQTT-Clients und des Brokers untereinander

Am Ende steuert das Smart-Home-System anhand von Eingaben, z. B. der Nutzung eines Schalters, die entsprechenden Deckenlampen.

Abgebildet werden die Geräte über die entsprechenden Topics im MQTT-Broker. Ein solches Topic könnte z. B. bad/deckenlampe sein und adressiert somit eine Nachrichtenquelle bzw. einen Empfänger.

Das Smart-Home-System abonniert einen Großteil dieser Topics und erhält damit die Daten der Geräte und kann basierend darauf neue Nachrichten an den MQTT-Broker und die entsprechende Topics verschicken.

Broker

Für MQTT zwingend notwendig ist ein Broker. Dieser bildet das zentrale Herzstück für die MQTT-Kommunikation. Er stellt Topics bereit, welche abonniert werden können und zu welchen Nachrichten gesendet werden können. Diese Funktionalitäten hören im MQTT-Kontext auf die Namen Subscribe und Publish. Jeder Client, welcher ein solches Topic abonniert, enthält anschließend die entsprechenden Nachrichten.

Alle Clients sind mit dem Broker verbunden

Auf dem Markt existieren eine Reihe von Brokern z. B. HiveMQ oder Mosquitto. Bei diesen Brokern handelt es sich meist um dedizierte Applikationen. In den meisten real existierenden Szenarien wird ein solcher zentraler Broker aufgesetzt und genutzt.

Daneben existieren auch Broker, welche direkt in eine Applikation integriert werden können, wie Mosquette; welches im beschriebenen Szenario zur Anwendung kommt.

Szenario

Als Anwendungsszenario des fiktiven MQTT-Systems soll besagtes virtuelles Smart-Home-System erstellt werden. In diesem existieren unterschiedlichste Endgeräte, welche mit dem Broker kommunizieren und entsprechende Topics abonnieren bzw. ihre Nachrichten an ein solches Topic senden.

Im Großen und Ganzen werden dazu drei kleine Projekte erstellt, ein Gerätesimulator, welcher die MQTT-Nachrichten der Sensoren und Schalter simuliert, ein MQTT-Broker und das Smart-Home-System, welches die entsprechende Steuerung vornimmt.

Broker selbst gebaut

Da es in diesem Artikel um die Einführung in die Nutzung von MQTT unter Java gehen soll, wird auf den Aufbau eines größeren Services verzichtet und stattdessen mit einem relativ minimalen Starterprojekt begonnen.

Bei diesem Projekt handelt es sich um ein minimales Java-Projekt, welches einige häufig genutzten Abhängigkeiten mitbringt und uns als Startpunkt dienen soll. Es setzt auf Java 17 auf und nutzt Maven als Build-Werkzeug und für das Paketmanagement.

Die drei Projekte sollen die Namen Broker, Devices und System tragen. Im ersten Schritt wird mit dem Broker-Projekt ein Projekt für den MQTT-Broker erstellt. Genutzt wird hierfür Moquette, welcher embedded genutzt werden kann.

Zu Beginn wird die pom.xml des Projektes um eine neue Abhängigkeit erweitert:

<!-- MQTT broker for communication -->
<dependency>
    <groupId>io.moquette</groupId>
    <artifactId>moquette-broker</artifactId>
    <version>0.16</version>
</dependency>

Diese neue Abhängigkeit wird im Dependencies-Block der Datei eingetragen. Damit wurde der Moquette-Broker eingebunden, welcher direkt im Projekt integriert ist und es uns damit ermöglicht seine Funktionalität zu nutzen.

Einbindung

Nachdem die Abhängigkeit eingebunden wurde, kann damit begonnen werden, die Broker-Funktionalität zu nutzen. Dazu wird eine Klasse namens Broker erstellt, in der der Broker mitsamt weiterer Funktionalität gekapselt wird.

Neben der Instanz der Klasse Server, für den MQTT-Broker ist das Herzstück der Klasse die Methode startServer:

public void startServer() {

    // Load class path for configuration
    IResourceLoader classpathLoader = new ClasspathResourceLoader();
    final IConfig classPathConfig = new ResourceLoaderConfig(classpathLoader);

    // Start MQTT broker
    LOG.info("Start MQTT broker...");
    List userHandlers = Collections.singletonList(new PublisherListener());

    try {
        mqttBroker.startServer(classPathConfig, userHandlers);
    } catch (IOException e) {
        LOG.error("MQTT broker start failed...");
    }

    // Publishing topics
    LOG.info("Pushing topics...");

    List lines = Resources.getLines("config/topics.conf");

    for(String line: lines) {
        pushTopic(line);
    }

    LOG.info("Topics pushed...");
}

Bei der Bereitstellung der Konfiguration wird ein InterceptHandler mit dem Namen PublisherListener definiert. Dieser verfügt über keinerlei Funktionalität für die Nutzung des Brokers, sondern dient dazu, entsprechende Meldungen über empfangende Payloads der MQTT-Nachrichten im Log des Brokers anzuzeigen:

INFO org.example.broker.mqtt.PublisherListener - Received on topic: multisensor/temperatur / Content: {"temperature":15.7,"unit":"°C"}

Anschließend wird der Broker gestartet und die Topics werden geladen und an den Broker gepusht, ergo erstellt. Hierfür dient die Methode pushTopic:

public void pushTopic(String topic) {

    LOG.info("Push topic: {}", topic);

    MqttPublishMessage message = MqttMessageBuilders.publish()
            .topicName(topic)
            .retained(true)
            .qos(MqttQoS.EXACTLY_ONCE)
            .payload(Unpooled.copiedBuffer("{}".getBytes(UTF_8))).build();

    mqttBroker.internalPublish(message, "INTRLPUB");
}

In dieser Methode wird eine MQTT-Nachricht erstellt und mit dieser Nachricht wird das entsprechende Topic über die interne Publishing-Methode an den Broker versendet.

Konfiguration für den Broker

Damit der Broker erfolgreich hochfahren kann, wird eine entsprechende Konfiguration benötigt. Eine minimale Konfiguration könnte hierbei wie folgt aussehen:

##############################################
#  Moquette configuration file. 
#
#  The syntax is equals to mosquitto.conf
# 
##############################################

port 1883

host 0.0.0.0

allow_anonymous true

Neben dem zu nutzenden Port, wird eine IP-Adresse definiert, an welche sich der Broker binden soll, sowie der anonyme Zugriff erlaubt.

Diese Konfiguration wird im Pfad src/main/resources/config des Broker-Projektes in der Datei moquette.conf hinterlegt. Im gleichen Pfad wird ebenfalls eine Datei mit dem Namen topics.conf erstellt.

Diese erhält die Topics, welche der Broker anlegen soll:

bad/deckenlampe
kueche/deckenlampe
wohnzimmer/deckenlampe
multisensor/temperatur
multisensor/bewegung
schalter1/status
schalter2/status
schalter3/status

Shutdown-Handler und Einsprungspunkt

In der main-Methode der Klasse Starter, welche unseren Einsprungspunkt für die Broker-Applikation darstellt, wird die Broker-Klasse instanziiert, der Broker gestartet und ein Shutdown-Hook definiert.

public static void main(String[] args) {

    // Init and start broker
    LOG.info("Init broker...");

    Broker broker = new Broker();
    broker.startServer();

    // Bind a shutdown hook
    LOG.info("Bind shutdown hook...");

    Runtime.getRuntime().addShutdownHook(new Thread(() -> {
        LOG.info("Stopping broker...");
        broker.stopServer();
    }));
}

Der Shutdown-Hook dient dazu, es zu ermöglichen, den Broker wieder sauber herunterzufahren. In der Konsole kann dies z. B. durch einen Druck auf die Tasten Strg + C ausgelöst werden. Damit würde der Broker entsprechend gestoppt und die Applikation beendet.

Erster Test

Damit ist der Broker im Grunde, dank der Nutzung von Moquette, fertiggestellt und kann einem ersten Test unterzogen werden.

Über MQTT Explorer wird sich mit dem Broker verbunden

Für diesen Test kann MQTT Explorer verwendet werden, um einen ersten Request per MQTT zum neuen System zu senden. Nachdem sich mit dem Broker über MQTT-Explorer verbunden wurde, können die Topics in diesem eingesehen werden.

Nach der erfolgreichen Verbindung können die Topics des Brokers eingesehen werden

Auch lassen sich über den MQTT Explorer Nachrichten an die entsprechenden Topics senden. Allerdings ist dies wenig zielführend, da im Moment, bis auf MQTT Explorer, niemand die Topics abonniert.

Gerätesimulation

Damit die Topics, welche der Broker bereitstellt bespielt werden, soll im nächsten Schritt ein Projekt aufgesetzt werden, welches dies bewerkstelligt.

Im Rahmen des Szenarios, einer Smart-Home-Umgebung, werden hierbei einige Schalter und ein Multisensor simuliert. Im Einstiegspunkt des Projektes Devices sieht, das Ganze wie folgt aus:

public static void main(String[] args) throws InterruptedException {

    LOG.info("Init Dummy device simulator...");

    // Create list of Dummy devices
    List devices = new ArrayList<>();

    devices.add(new Multisensor("multisensor/bewegung", "multisensor/temperatur"));
    devices.add(new Switch("schalter1/status"));
    devices.add(new Switch("schalter2/status"));
    devices.add(new Switch("schalter3/status"));

    while(true) {

        LOG.info("Send dummy data...");

        for(Device device: devices) {
            device.sendData();
        }

        // Sleep 15 seconds
        Thread.sleep(15000);
    }
}

Im Grunde werde einige virtuelle Geräte definiert, welche entsprechend mit ihren Topics verbunden werden und an diese Topics jeweils eine Payload senden sollen. Dies geschieht für die Simulation alle 15 Sekunden.

Schon an dieser Stelle fällt auf, dass die Schalter hier nur ein Topic benötigen, während an den Multisensor mehrere Topics übergeben werden.

Hier wird sich der hierarchische Aufbau der Topics in MQTT zunutze gemacht. Technisch wäre es kein Problem nur das Topic multisensor zu definieren und an dieses eine entsprechende Payload auszuliefern:

{
  motion: true,
  temperature: 24.6,
  unit: "°C"
}

Stattdessen werden in diesem Szenario die Topics:

multisensor/temperatur
multisensor/bewegung

definiert. Dadurch können Applikationen genau auf die Topics zugreifen, die sie interessieren. So kann ein Gerät, welches mehrere Funktionalitäten vereint, diese über separate Topics einzeln zur Verfügung stellen.

In der Praxis sollte sich das Design der Topics an den Anwendungsfällen orientieren. Werden die Daten für Bewegung und Temperatur immer in Verbindung benötigt, so könnten sie auch über ein Topic ausgeliefert werden.

MQTT-Client unter Java

Damit die virtuellen Geräte ihre Daten an den Broker senden können, wird ein entsprechender MQTT-Client benötigt. Auch hier ist die Auswahl groß.

In diesem Beispiel wird der HiveMQ-Client genutzt, da er neben dem etablierten MQTT-Protokoll in Version 3 auch die relative neue Version 5 unterstützt. Nachdem die entsprechende Abhängigkeit in der pom.xml definiert wurde:

<!-- MQTT-Client -->
<dependency>
    <groupId>com.hivemq</groupId>
    <artifactId>hivemq-mqtt-client</artifactId>
    <version>1.3.0</version>
</dependency>

kann der Client genutzt werden. Der Client unterstützt blockierende und asynchrone APIs. Im Falle der Gerätesimulationen wird auf die blockierende API mit der MQTT-Version 3 gesetzt.

Die simulierten Geräte implementieren ein Interface mit dem Namen Device, welches eine entsprechende Methode mit dem Namen sendData vorschreibt. In der Klasse, welche für Schalter zuständig ist, ist diese wie folgt implementiert:

public void sendData() {

    if (client == null) {
        // Create MQTT client
        client = Mqtt3Client.builder()
                .identifier(UUID.randomUUID().toString())
                .serverHost("localhost")
                .buildBlocking();

        client.connect();
    }

    client.publishWith()
            .topic(topic)
            .qos(MqttQos.AT_LEAST_ONCE)
            .payload(getSwitchPayload().getBytes())
            .send();
}

In der Theorie könnte der Client für alle Geräte global definiert werden, dies wird hier aber aus einer Erwägung, auf welche später noch eingegangen wird, nicht getan. Stattdessen verfügt jedes simulierte Gerät über einen einzelnen MQTT-Client.

Dieser wird beim erstmaligen Aufruf instanziiert und verbindet sich anschließend mit dem entsprechenden Server, welcher auf Localhost lauscht. Anschließend wird mit der publishWith-Methode des Clients eine Nachricht erzeugt, diese mit einem Topic und einer Quality of Service-Stufe versehen.

MQTT beherrscht drei verschiedene Stufen des Quality of Service (QoS). Stufe 0 ist vom Modell her Fire-and-Forget; die Nachricht wird einmal versendet und danach vom Broker vergessen. Ob sie ankommt, ist auf dieser QoS-Stufe nicht relevant. Bei Stufe 1 garantiert der Broker, dass die Nachricht mindestens einmal zugestellt wird, sie kann aber auch mehrfach bei den Clients ankommen. Stufe 2 hingegen garantiert, dass die Nachricht exakt einmal ankommt. Bei den QoS-Stufen muss beachtet werden, dass jede Stufe mehr Overhead erzeugt als die vorherige Stufe.

Nachdem die Payload erzeugt und übergeben wurde, wird die entsprechende Nachricht an den Broker und dort an das gewählte Topic versendet. Die Payload ist in diesem Fall eine JSON-Struktur:

{
  "enabled":false
}

In der Payload einer MQTT-Nachricht können beliebige Daten versendet werden, von Text bis zu Binärdaten. Grundsätzlich sollten hier die Limits von MQTT berücksichtigt werden, so ist die Länge eines Topics auf 64 Kilobyte beschränkt und die Länge der Payload ist auf 256 Megabyte beschränkt.

Dabei handelt sich allerdings nur um theoretische Werte, gemäß der Spezifikation, welche im jeweils gewählten Broker bzw. dessen Einstellungen abweichen könnten. Die Payload sollte hier nach der Faustregel, so viel wie nötig, so wenig wie möglich designt werden.

Die Topics werden von der Gerätesimulation befüllt

Damit ist die Gerätesimulation implementiert und die entsprechenden Topics werden nun mit sinnvollen Werten befüllt. Damit wird der Broker zwar genutzt, aber die entsprechenden Topics werden bisher nur geschrieben, niemand abonniert diese bisher.

Smart-Home-System

Im letzten Schritt soll das Smart-Home-System implementiert werden. Dieses abonniert Topics und führt basierend auf diesen Topics Aktionen durch. Während diese bei einem praxisnahen System konfigurierbar wären, sind sie in diesem Beispiel fest kodiert.

Auch in diesem Projekt wird wieder der HiveMQ-Client genutzt und entsprechend als Abhängigkeit dem Projekt hinzugefügt. Nachdem dort der Client erstellt wurde, kann die Verbindung aufgebaut werden:

client = Mqtt3Client.builder()
        .identifier(UUID.randomUUID().toString())
        .serverHost("localhost")
        .buildBlocking();

// Connect to MQTT server
client.connect();

Anschließend werden bestimmte Topics abonniert, das bedeutet, die Nachrichten der Topics werden vom Broker empfangen und sollen anschließend verarbeitet werden:

// Subscribe to topics
client.toAsync().subscribeWith()
        .topicFilter("schalter1/status")
        .qos(MqttQos.AT_LEAST_ONCE)
        .callback(Starter::switchMessageReceived)
        .send();

client.toAsync().subscribeWith()
        .topicFilter("schalter2/status")
        .qos(MqttQos.AT_LEAST_ONCE)
        .callback(Starter::switchMessageReceived)
        .send();

client.toAsync().subscribeWith()
        .topicFilter("schalter3/status")
        .qos(MqttQos.AT_LEAST_ONCE)
        .callback(Starter::switchMessageReceived)
        .send();

client.toAsync().subscribeWith()
        .topicFilter("multisensor/bewegung")
        .qos(MqttQos.AT_LEAST_ONCE)
        .callback(Starter::multisensorMotionMessageReceived)
        .send();

In diesem Fall sind es die Topics für die drei Schalter sowie das Topic für die Bewegung im Multisensor. Jedem Topic, welches abonniert wird, wird eine entsprechende Callback-Methode mitgegeben. Für den Multisensor wäre dies z. B. der Callback zur Methode multisensorMotionMessageReceived:

private static void multisensorMotionMessageReceived(Mqtt3Publish mqtt3Publish) {

    LOG.info("Receive message: {}", mqtt3Publish);

    String payload = getPayloadAsString(mqtt3Publish.getPayload().get());
    LOG.info("Payload: {}", payload);

    if (payload.length() <= 2) { // Ignore empty JSONs, from publishing topic
        return;
    }

    Motion motion = new Gson().fromJson(payload, Motion.class);

    client.publishWith()
            .topic("bad/deckenlampe")
            .qos(MqttQos.AT_LEAST_ONCE)
            .payload(getLampPayload(motion.motion).getBytes())
            .send();
}

In dieser Callback-Methode wird die Payload mittels der Methode getPayloadAsString entpackt:

private static String getPayloadAsString(ByteBuffer buffer) {
    byte[] payload = new byte[buffer.remaining()];
    buffer.get(payload);
    return new String(payload, StandardCharsets.UTF_8);
}

Hier wird der ByteBuffer genauer gesagt sein Inhalt, welcher vom Client geliefert wird, in einen String konvertiert. Anschließend wird aus der Payload über die Serialisierungs- und Deserialisierung-Bibliothek Gson, ein Java-Objekt aus der Payload erzeugt und mit diesem weitergearbeitet.

In diesem Beispiel wird der Wert des Bewegungsmelders weitergeleitet an den Topic bad/deckenlampe, um damit die Deckenlampe zu schalten.

Bei den Schaltern wird ähnlich verfahren, allerdings wird hier für alle Schalter die gleiche Callback-Methode genutzt:

private static void switchMessageReceived(Mqtt3Publish mqtt3Publish) {

    LOG.info("Receive message: {}", mqtt3Publish);

    String payload = getPayloadAsString(mqtt3Publish.getPayload().get());
    LOG.info("Payload: {}", payload);

    if (payload.length() <= 2) {// Ignore empty JSONs
        return;
    }

    Switch switchStatus = new Gson().fromJson(payload, Switch.class);

    // Define link between switch and lamp
    String targetTopic;

    switch (mqtt3Publish.getTopic().toString()) {
        case "schalter1/status" -> {
            targetTopic = "bad/deckenlampe";
        }
        case "schalter2/status" -> {
            targetTopic = "kueche/deckenlampe";
        }
        case "schalter3/status" -> {
            targetTopic = "wohnzimmer/deckenlampe";
        }
        default -> {
            LOG.info("Ignore unknown topic...");
            return;
        }
    }

    client.publishWith()
            .topic(targetTopic)
            .qos(MqttQos.AT_LEAST_ONCE)
            .payload(getLampPayload(switchStatus.enabled).getBytes())
            .send();
}

Stattdessen wird in der Methode das Topic extrahiert und anhand dieses eine Entscheidung zum passend verknüpften Zieltopic getroffen und an dieses eine neue Nachricht geschickt.

Der letzte Wille

Die Geräte, wie Schalter und der Multisensor, senden Nachrichten an den MQTT-Broker und diese Topics werden von unserem Smart-Home-System abonniert.

Nun könnte in einem beispielhaften Fall einer der Schalter die Nachricht an das Topic senden, dass der Schalter aktiviert wurde. Damit würde dann über das Smart-Home-System die entsprechende Lampe eingeschaltet werden.

Wenn dieser Schalter jedoch keine Verbindung mehr mit dem MQTT-Broker aufnehmen kann oder schlicht und ergreifend defekt ist, würde das Licht in diesem Szenario immer aktiv blieben.

Hier bietet MQTT, ein Feature, das sogenannte Testament bzw. den letzten Willen. Meldet sich ein Gerät bzw. allgemeiner ein Client beim Broker an, kann dieser ein solches Testament hinterlegen. Infolgedessen erhielten die virtuellen Geräte im Gerätesimulator jeweils ihren eigenen Client. Im Kontext der Switch-Klasse im Gerätesimulator würde dies wie folgt aussehen:

// Create MQTT client
client = Mqtt3Client.builder()
        .identifier(UUID.randomUUID().toString())
        .serverHost("localhost")

        // Last will
        .willPublish()
        .topic(topic)
        .payload(getSwitchPayload(false).getBytes())
        .applyWillPublish()

        .buildBlocking();

client.connect();

Beim Testament wird ein Topic gesetzt und eine entsprechende Payload. Im Fall des Schalters würde somit die Payload, welche signalisiert, dass der Schalter abgeschaltet wurde, an die Clients geschickt, welche das entsprechende Topic abonniert haben.

Das Testament wird hierbei nicht bei einer normalen und gewünschten Trennung der Verbindung gesendet, sondern nur im Falle einer ungewollten Trennung des Clients.

Diese kann auftreten, wenn der Broker nicht mehr mit dem Client kommunizieren kann oder die Netzwerkverbindung getrennt wird, bevor eine entsprechende DISCONNECT-Nachricht beim Broker eingetroffen ist.

Was in dem Beispielszenario eher geringere Auswirkungen hat, kann in industriellen Anwendungen von Belang sein, da hier über das Testament Geräte, im Falle von Problemen, in definierte Zustände gebracht werden können.

Jenseits von Java

Nachdem bisher alle Beispiele für das Smart-Home-System in Java umgesetzt worden sind, kann das MQTT-Protokoll auch auf vielen anderen Geräten und Sprachen genutzt werden.

So könnte z. B. eines der virtuellen Geräte mit einem Arduino nachgebaut und dort die Daten des Gerätes per MQTT an den Broker gesendet werden. Hierfür stehen für unterschiedlichste Sprachen und Umgebungen entsprechende Bibliotheken zur Verfügung.

MQTT 5

Daneben wurde die etablierte Version 3 von MQTT in diesem Beispiel genutzt, da Moquette aktuell noch an einer Umsetzung für MQTT 5 arbeitet. 2019 wurde die Spezifikation für die Version 5 von MQTT ratifiziert und sollte, wenn möglich, in neuen Projekten genutzt werden.

In die Version 5 sind Verbesserungen eingeflossen, die unter anderem für eine Verbesserung bei der Skalierbarkeit sorgen, der Erkennung der Fähigkeiten des Servers dienen, sowie Erweiterungsmechanismen im Rahmen des Protokolls beinhalten.

Sollte es sich also anbieten, sollten Projekte idealerweise mit der Unterstützung für MQTT in Version 5 begonnen werden.

Retained Messages

Auch könnte das gezeigte System um weitere Möglichkeiten von MQTT erweitert werden. Es ist es z. B. möglich vom Broker eine Nachricht zu erhalten, sobald ein Topic abonniert wird.

So könnte für den Multisensor die Temperatur als Retained Message bereitgestellt werden. Damit erhält der Client, welcher das Topic abonniert, sofort einen Status für das entsprechende Topic und muss nicht erst auf eine neue Meldung des Temperatursensors warten.

Erstellt wird eine solche zurückbehaltende Nachricht, indem bei der Erstellung der Nachricht, das Retain-Flag gesetzt wird:

client.publishWith()
        .topic(temperatureTopic)
        .retain(true)
        .qos(MqttQos.AT_LEAST_ONCE)
        .payload(getTemperaturePayload().getBytes())
        .send();

Wichtig ist es zu beachten, dass immer nur eine Retained Message pro Topic erlaubt ist und eine neue Nachricht mit dem Retain-Flag eine alte Nachricht ersetzt.

Fazit

Im Rahmen eines fiktiven Beispiels wurde ein Broker aufgesetzt und im Zusammenspiel mit virtuellen Geräten ein minimales Smart-Home-System implementiert. Damit wurde die Zusammenarbeit zwischen den Subscribern und den Publishern in einem MQTT-System gezeigt. Der Quellcode der kompletten Projekte kann über GitHub eingesehen und ausprobiert werden.

Allerdings ist MQTT nicht auf solche Anwendungsszenarien beschränkt. So kann es z. B. auch als Event-System genutzt werden, um z. B. Exporte zu triggern, welche, sobald auf dem Topic zu Einlieferung neue Daten auftauchen, diese in andere Formate exportieren und wiederum eine entsprechende Nachricht versenden.

Auch sind Sicherheitsaspekte in diesem Szenario nicht weiter bedacht. So können z. B. neue Topics von jedem Client angelegt werden. Daneben bietet MQTT noch weitere Features, welche je nach Einsatzzweck genutzt werden können. Dazu gehören persistente Sessions, welche unter anderem verhindern, dass Nachrichten verloren gehen, wenn der Client zum Zeitpunkt der Nachricht nicht mit dem Broker verbunden war.

MQTT bzw. der nachrichtenbasierende Workflow kann genutzt werden, um Systeme voneinander zu entkoppeln und bietet für zukünftige Erweiterungen Platz. Je nach Anwendungszweck sollten die Möglichkeiten von MQTT möglichst sinnvoll in eigenen Projekten genutzt werden.

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

OpenJDK wird von IntelliJ IDEA nicht erkannt

Nach der Installation von OpenJDK über Homebrew unter macOS mittels:

brew install openjdk

wollte ich das JDK unter IntelliJ IDEA nutzen. Allerdings wurde es dort nicht erkannt. Nachdem ich eine Neuinstallation über Homebrew gestartet habe, fiel mir eine entsprechende Meldung auf:

==> Caveats
For the system Java wrappers to find this JDK, symlink it with
  sudo ln -sfn /opt/homebrew/opt/openjdk/libexec/openjdk.jdk /Library/Java/JavaVirtualMachines/openjdk.jdk

Nach der Ausführung des entsprechenden Befehls:

sudo ln -sfn /opt/homebrew/opt/openjdk/libexec/openjdk.jdk /Library/Java/JavaVirtualMachines/openjdk.jdk

funktionierte die entsprechende Erkennung in der IDE wieder und das OpenJDK konnte genutzt werden.

Minecraft auf dem Steam Deck installieren

Da Minecraft nicht über Steam installiert werden kann, läuft es im ersten Moment nicht auf dem Steam Deck. Allerdings kann hier schnell Abhilfe geschaffen werden. Im ersten Schritt muss hierzu in den Desktop-Modus gewechselt werden. Dieser wird erreicht in dem im Steam Deck-Menü der Punkt Ein/Aus ausgewählt wird. Dort findet sich dann der Punkt Zum Desktop wechseln.

Die Installation des GDLauncher

Im Desktop-Modus angekommen, sollte die Softwareverwaltung (Discover) gestartet werden und dort nach der Applikation GDLauncher gesucht werden. Wird im Desktop-Modus eine Tastatur benötigt, so kann diese über einen Druck auf die Steam-Taste in Verbindung mit dem X-Button aktiviert werden. Anschließend sollte die Applikation installiert werden.

Nach einigen Minuten ist der Launcher installiert und kann gestartet werden. Im ersten Schritt möchte der Launcher Java installieren. Hier empfiehlt es sich Automatic Setup auszuwählen. Anschließend werden die benötigten Java-Versionen heruntergeladen und installiert. Danach kann sich über den GDLauncher in den Account eingeloggt werden. Bei bereits umgestellten Konten sollte hier auf Sign in with Microsoft geklickt werden. Nachdem Nutzername und Passwort eingegeben wurde und die App autorisiert wurde, kann über den GDLauncher die passende Minecraft-Version installiert werden.

Dazu müssen einige einführende Worte weggeklickt und anschließend über den Plus-Button eine neue Minecraft-Version installiert werden. Der Download der entsprechenden Version sollte nach einigen Minuten abgeschlossen sein. Nach der Installation sollte das Menü im Desktop-Modus wieder aufgerufen werden und dort nach GDLauncher gesucht werden. Nach einem rechten Mausklick auf das Symbol kann dort Add to Steam ausgewählt werden. Mit der Verknüpfung Return to Gaming Modus, welche sich direkt auf dem Desktop befindet, kann wieder in den normalen Standardmodus des Steam Deck zurückgekehrt werden.

Die Controllereinstellungen müssen für Minecraft sinnvoll definiert werden

Nachdem GDLauncher, als Icon hinterlegt wurde, muss im nächsten Schritt eine sinnvolle Controllereinstellung definiert werden. In meinem Fall habe ich das Community-Layout Minecraft Xbox Style von Rasin Bar genutzt. Anschließend kann der GDLauncher gestartet werden und dort dann die gewünschte Minecraft-Version ausgewählt und gestartet werden. In Minecraft selbst können entsprechende Einstellungen wie die gewünschte Auflösung vorgenommen werden.

Minecraft auf dem Steam Deck

Direkt auf dem Steam Deck sollte diese immer 1280 x 800 Pixel betragen und kann somit direkt über die Einstellungen des GDLauncher vorgenommen werden. Dies ist auch die sinnvollere Variante um Letterbox-Effekte zu verhindern. Da Minecraft von sich aus keine sinnvolle Gamepad-Unterstützung mitbringt; ist die Nutzung über die Dockingstation und ein separates Gamepad ohne entsprechende Mods nicht sinnvoll. Der Chat hingegen kann in der Theorie über die Bildschirmtastatur genutzt werden.