EPUB aus einem mdBook erstellen

Das Community-Buch über Rust (deutsche Version), wird als mdBook gepflegt. Damit ist es möglich dieses als Webseite auszugeben. Allerdings war ich auf der Suche nach einer Möglichkeit unkompliziert ein EPUB aus einem mdBook zu generieren. Fündig geworden bin ich bei einem Backend, welches diese Funktionalität zur Verfügung stellt. Dieses muss im ersten Schritt über den Rust-Paketmanager Crates installiert werden:

cargo install mdbook-epub

Anschließend kann in das gewünschte mdBook-Projekt gewechselt werden und dort das E-Book erzeugt werden:

mdbook-epub --standalone true .

Dieses wird im im Ordner book bereitgestellt und kann dort weitergenutzt 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.

Workflows im Entwicklungsalltag

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

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

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

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

Anforderungen

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

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

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

Ein entsprechendes Board

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

Versionskontrollsysteme

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

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

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

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

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

Unterschiedliche Branches in einem Repository bilden eine Baumstruktur

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

Branchen und Mergen

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

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

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

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

Branches, Branches, Branches

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

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

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

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

Namensgebung

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

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

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

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

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

Pull Requests

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

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

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

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

Ein Pull Request unter GitHub

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

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

Review und Test?

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

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

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

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

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

Workflows

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

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

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

One branch to rule them all

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

Alle Commits landen direkt im main-Branch

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

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

Eine Aufgabe: ein Branch

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

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

Jede Entwicklung findet in einem separaten Branch statt

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

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

Entwicklungs- und stabile Linien

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

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

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

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

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

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

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

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

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

Releases aus dem main-Branch

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

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

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

Neue Releases werden aus dem main-Branch abgeleitet

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

Von Release zu Release

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

Release-Branches werden nach oben gemergt

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

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

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

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

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

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

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

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

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

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

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

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

Git Flow

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

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

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

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

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

Bei Git Flow existieren eine Reihe von Branchtypen

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

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

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

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

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

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

GitHub Flow

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

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

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

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

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

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

Trunk Flows

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

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

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

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

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

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

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

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

Wenn der Test fehlschlägt

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

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

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

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

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

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

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

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

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

Merge-Hölle?

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

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

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

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

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

Fazit

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

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

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

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

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

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

Erste Schritte mit Rust

Vor ein paar Tagen wollte ich die Programmiersprache Rust ausprobieren. Bei Rust handelt es sich um eine Sprache, welche syntaktisch stark an C angelehnt ist und besonderes Augenmerk auf Sicherheit legt. Die erste Frage, die sich mir stellte, ist, ob es für Rust, eine vernünftige IDE-Unterstützung existiert. Fündig geworden bin ich bei IntelliJ IDEA, welches nach Installation des Rust-Plugins zur Programmierung in der Sprache genutzt werden kann. Debugging von Rust-Programmen wird aus technischen Gründen nur in der JetBrains IDE CLion unterstützt, sodass hier einige Abstriche gemacht werden müssen. Neben der Rust-Integration für IntelliJ IDEA gibt es ebenfalls ein Plugin für Visual Studio Code, welches hier allerdings nicht weiter behandelt werden soll.

Mit dem passenden Plugin beherrscht IntelliJ IDEA Rust.

Neben der eigentlichen IDE wird Rust benötigt. Dieses kann über die offizielle Seite der Sprache bezogen werden. Nach der Installation kann in der IDE ein erstes Projekt angelegt werden. Anschließend findet sich in einem Ordner mit dem Namen src eine Datei mit dem Namen main.rs:

fn main() {
    println!("Hello, world!");
}

In dieser Datei findet sich ein minimales Hello world-Programm. Das Ausrufezeichen hinter dem println zeigt unter Rust an, das es sich um ein Makro handelt. Damit können Methoden und Makros einfach auseinander gehalten werden. Da ich Hello world-Programme immer etwas sinnfrei finde was das Lernen einer neuen Programmiersprache angeht, wollte ich für den ersten Versuch das Spiel Zahlenraten programmieren. Ziel des Spieles ist es eine Zahl zwischen 0 und 1000 zu erraten, welche der Rechner sich ausgedacht hat. Dazu muss im ersten Schritt eine Variable definiert werden, in welcher die zufällige Zahl gespeichert wird. Grundsätzlich sieht eine Variablendefinition und Deklaration unter Rust wie folgt aus:

let name: Typ = Wert;

Variablen in Rust sind immer Konstanten, wenn sie nicht explizit als veränderlich angegeben werden. Möglich ist dies mit dem Schlüsselwort mut:

let mut name: Typ = Wert;

Die Benennung von Variablen und anderen Elementen folgt in Rust einem bestimmten Schema. So werden Variablen snake_case benannt. Neben der Definition der Variable muss eine zufällige Zahl zwischen 0 und 1000 generiert werden. Dazu dient ein Zufallsgenerator, welcher über ein use eingebunden werden muss:

use rand::Rng;

Anschließend kann die Variable definiert und deklariert werden:

let number: u32 = rand::thread_rng().gen_range(0, 1000);

In diesem Fall wird ein vorzeichenloser Integer mit 32 Bit Breite als Datentyp definiert. Wird nun versucht das Rust-Programm zu kompilieren, so wird die Fehlermeldung:

error[E0432]: unresolved import `rand`
 --> src\main.rs:1:5
  |
1 | use rand::Rng;
  |     ^^^^ use of undeclared type or module `rand`

error[E0433]: failed to resolve: use of undeclared type or module `rand`
 --> src\main.rs:4:23
  |
4 |     let number: u32 = rand::thread_rng().gen_range(0, 1000);
  |                       ^^^^ use of undeclared type or module `rand`

error: aborting due to 2 previous errors

auftauchen. Grund hierfür ist, dass das genutzte Paket über den Paketmanager von Rust bezogen werden muss. Pakete werden über den Paketmanager Cargo bezogen. Die entsprechenden Pakete des offiziellen Repositorys sind unter crates.io zu finden. In der Datei Cargo.toml müssen die entsprechenden Abhängigkeiten eingebunden werden:

[dependencies]
rand = "0.7.3"
text_io = "0.1.8"

Neben dem Pseudozufallszahlengenerator, wurde gleich noch das Paket text_io eingebunden, welches später für die Eingabe von Text benötigt wird. Dieses Paket stellt hierbei das Makro read zur Verfügung, mit dessen Hilfe eine Eingabe realisiert werden kann:

let user_number: u32 = read!();

Damit sind die Grundlagen für das Spiel Zahlenraten gelegt und der Rest des Quellcodes ergibt praktisch sich von selbst:

use rand::Rng;
use text_io::read;

fn main() {

    let number: u32 = rand::thread_rng().gen_range(0, 1000);

    let mut running = true;

    println!("Ich habe mir eine Zahl zwischen 0 und 1000 ausgedacht.");

    while running {

        println!("Dein Vorschlag: ");
        let user_number: u32 = read!();

        if number > user_number {
            println!("Meine Zahl ist größer.");
        } else if number < user_number {
            println!("Meine Zahl ist kleiner.");
        } else {
            running = false;
        }
    }

    println!("Du hast die Zahl erraten. Es war die {}.", number);
}

Hervorzuheben ist, das unter Rust in if-Statements und Schleifen keine Klammern benötigt werden. Werden diese trotzdem genutzt, so warnt der Compiler entsprechend und bittet die überflüssigen Klammern zu entfernen.

Neben der entsprechenden Dokumentation auf der offiziellen Seite empfiehlt sich das Rust Cookbook als Quelle für die ersten Schritte unter Rust. Wer Rust einfach mal im Browser ausprobieren möchte, kann hierfür den Rust Playground nutzen.

Generator für Melodien

Einige Melodien, vor allem solche die in Songs genutzt werden, sind durch das Urheberrecht geschützt. Auch wurden und werden viele Prozesse geführt, weil ein Künstler die Melodie eines anderen Liedes genutzt hat. Allerdings kann man nur Melodien schützen, wenn sie neu sind.

allthemusic.info

Damien Riehl und Noah Rubin haben sich diesem Problem angenommen. Mithilfe einer Software, welche auf GitHub bezogen werden kann, haben sie alle Melodien, der häufigsten in der Musik vorkommenden Kombinationen generiert und in einer Datenbank gespeichert. Das Werkzeug kann auch genutzt werden, um eigene Melodien anhand bestimmter Parameter zu generieren. Die offizielle Seite des Projektes ist unter allthemusic.info zu finden.