Fluent Interfaces unter Java

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

Herkunft

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

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

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

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

Ideen des Fluent Interfaces

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

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

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

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

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

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

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

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

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

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

Method Chaining

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

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

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

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

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

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

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

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

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

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

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

Method Nesting

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

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

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

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

Statische Imports

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

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

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

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

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

Object Scoping

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

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

Anwendung

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

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

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

Fluent Interfaces in der Java-Welt

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

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

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

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

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

Fluent Interfaces im Builder-Pattern

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

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

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

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

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

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

Builder-Pattern im Beispiel

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

public class User {

    private String name;

    private String forename;

    private String username;

    public User() {
    }
}

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

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

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

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

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

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

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

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

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

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

public String getName() {
    return name;
}

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

public String getForename() {
    return forename;
}

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

public String getUsername() {
    return username;
}

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

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

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

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

User user = new User();

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

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

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

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

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

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

    private Builder() {
    }

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

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

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

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

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

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

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

Fluent Interfaces zur Definition einer DSL nutzen

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Automatische Generation von sprechenden Schnittstellen

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

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

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

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

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

Fazit

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

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

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

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

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

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

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

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

Workflows im Entwicklungsalltag

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

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

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

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

Anforderungen

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

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

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

Ein entsprechendes Board

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

Versionskontrollsysteme

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

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

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

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

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

Unterschiedliche Branches in einem Repository bilden eine Baumstruktur

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

Branchen und Mergen

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

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

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

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

Branches, Branches, Branches

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

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

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

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

Namensgebung

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

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

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

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

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

Pull Requests

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

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

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

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

Ein Pull Request unter GitHub

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

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

Review und Test?

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

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

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

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

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

Workflows

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

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

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

One branch to rule them all

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

Alle Commits landen direkt im main-Branch

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

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

Eine Aufgabe: ein Branch

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

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

Jede Entwicklung findet in einem separaten Branch statt

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

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

Entwicklungs- und stabile Linien

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

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

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

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

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

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

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

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

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

Releases aus dem main-Branch

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

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

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

Neue Releases werden aus dem main-Branch abgeleitet

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

Von Release zu Release

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

Release-Branches werden nach oben gemergt

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

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

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

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

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

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

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

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

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

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

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

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

Git Flow

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

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

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

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

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

Bei Git Flow existieren eine Reihe von Branchtypen

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

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

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

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

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

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

GitHub Flow

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

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

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

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

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

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

Trunk Flows

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

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

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

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

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

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

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

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

Wenn der Test fehlschlägt

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

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

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

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

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

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

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

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

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

Merge-Hölle?

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

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

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

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

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

Fazit

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

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

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

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

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

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