Reif für die Hängematte Von der Schokoladenseite

Eine Branchingstrategie sie zu knechten

Published on Tuesday, April 8, 2014 4:30:00 AM UTC in Philosophical & Programming & Tools

Mit dem Aufkommen und dem Erfolg verteilter Versionsverwaltungssysteme und den damit verbundenen technischen Änderungen und Verbesserungen wurden und werden nicht selten altgewohnte Vorgehensweisen in Frage gestellt. Git etwa steht im Ruf, beim Zusammenführen (Merge) von Branches mit potenziellen Konflikten besonders gut umgehen zu können, und allgemein wird im Gegensatz zu vielen zentralen Systemen Branching als "billige" Aktion betrachtet (im Sinne des technischen Aufwands). Daher hört man "branch often" als eines der immer wiederkehrenden Schlagworte insbesondere wenn es um Git geht. Aber wie sieht eine gute, robuste Branchingstrategie wirklich aus?

Vorüberlegungen

Oft schaudert mich beim Lesen von Artikeln über das Thema, wenn der Autor über die ideale Strategie schreibt. Das impliziert, dass es tatsächlich die eierlegende Wollmilchsau gibt, an die sich jeder halten sollte, um optimale Ergebnisse zu erhalten. Tatsächlich ist eine Entscheidung und das Abwägen von Für und Wider meiner Meinung nach höchst kontextabhängig und muss immer auf das Team und ggfs. auch auf das konkrete Projekt zugeschnitten sein. Ein Beispiel: benötige ich tatsächlich eine ausgefeilte Strategie zur parallelen Verwaltung von Releaseständen, beispielsweise um einfach Hotfixes erstellen, testen und ausrollen zu können? Als Produktentwickler stellt sich diese Frage vermutlich gar nicht. Wenn ich aber etwa eine In-House-Anwendung entwickle, eventuell sogar als SaaS, von der immer nur ein definierter Stand in Produktion ist, dann kommen schnell die berüchtigten Kanonen und Spatzen ins Spiel – dann reicht vermutlich auch ein einzelner Release-Branch und eventuell das Vergeben von Tags, um etwa im Notfall noch auf ältere Versionen zurückgreifen zu können.

Eine zweite Problematik, die ich immer wieder beobachte, ist die Bestimmtheit, mit der verschiedenste Autoren immer wieder ihre Lösung als die einzig sinnvolle in den Raum stellen, erhaben über jede Diskussion. Ein Vorteil gerade der verteilten Systeme ist ja, dass man Flexibilität gewinnt und die Benutzer letztlich in ihren lokalen Repositories ganz erheblichen Spielraum haben, wie sie in der Entwicklung vorgehen, bis der Abgleich mit dem Rest des Teams passiert. In meinen Augen wird die lokale Vorgehensweise bei verteilten Systemen oft pauschal mit der Teamintegration über einen Kamm geschert und keine saubere Unterscheidung mehr durchgeführt, so dass man am Ende die schöne, flexible Lösung wieder in einen starren, unflexiblen Rahmen gepresst hat.

Wie machen es andere?

Der Blick über den eigenen Tellerrand hinaus erlaubt es nicht nur, Anregungen für die eigene Vorgehensweise zu bekommen, sondern – viel wichtiger – verdeutlicht auch, dass es zu jeder einzelnen Spielart sowohl Erfolgsgeschichten wie auch epische Fehlschläge gibt (wobei freilich letztere eher selten an die große Glocke gehängt werden).

Selbstverständlich gibt es Teams, die Branching als tägliches Hilfsmittel verstehen und so tief in ihre Abläufe integriert haben, dass es ein ganz natürlicher Teil des Arbeitsablaufs geworden ist. Nicht selten erstellen einzelne Entwickler in solchen Teams sogar mehrmals täglich neue Branches, um kleinste Features oder Bugs abzuarbeiten.

Heißt das, dass man – ggfs. mit Disziplin – an solch einen Punkt hinarbeiten muss? Mitnichten. Am anderen Ende der Skala stehen nämlich Konzepte wie Trunk-based Development, bei denen alle Entwickler direkt auf dem Entwicklungszweig arbeiten und Branches lediglich noch für Release-Stände oder allenfalls für die Arbeit an neuen Major-Versionen (Lebensdauer viele Jahre) erstellt werden. Auch das funktioniert offensichtlich, wie viele größere Firmen wie Google oder Facebook beweisen. Im Office-Team von Microsoft arbeiten anscheinend etwa dreitausend Entwickler auf demselben Trunk. In solchen Größenordnungen müssen natürlich diverse prozessliche und technische Hilfsmittel etabliert werden, angefangen von einem vehementen Code-Review-Prozess über Anpassungen der verwendeten Werkzeuge bis hin zu "Cloud"-Lösungen für die Integrations- und Buildprozesse. Das Entscheidende ist, dass beide Extreme erfolgreich praktiziert werden können, und dass jede Variante, egal wie die eigene Wahl ausfällt, Vor- und Nachteile mit sich bringt.

Nachteile von Trunk-based Development

Einen Nachteil habe ich im letzten Abschnitt schon angedeutet: wenn alle Entwickler auf demselben Branch arbeiten, muss auf jeden Fall gewährleistet sein, dass dieser immer "grün" ist – ein Commit von semantischen oder gar Compiler-Fehlern wirkt sich potenziell schnell und sehr nachteilig auf das gesamte Team aus. Daher sind entweder die Anforderungen an die Disziplin im Team sehr hoch, oder man muss über Werkzeuge wie Code Reviews oder Gated Check-Ins dafür sorgen, dass solche Situationen nicht oder äußerst selten auftreten.

Die gemeinsame Arbeit und der regelmäßige Commit in denselben Branch bedeuten auch, dass die Versionsverwaltung nicht mehr für das sogenannte "Cherry Picking" verwendet werden kann. Die Arbeit aller Entwickler befindet sich immer im Projekt bzw. Produkt, und es ist keine bequeme Selektion über das Werkzeug mehr möglich, um etwa ein Feature doch aus dem nächsten Release herauszulassen und noch einen Zyklus zu verschieben – man muss hierfür nun andere Wege beschreiten.

Dieser Umstand hat auch zur Folge, dass die Kosten für ein "Unmerge" hoch sind. Fanden Änderungen ihren Weg in die Versionsverwaltung, die man rückgängig machen möchte, sind die Aufwände dafür nicht zu unterschätzen. Ein striktes Modell mit Feature-Branches und Cherry Picking würde hier idealerweise ein Feature einfach nicht zurück in den Hauptzweig führen, womit der Aufwand in diesem Fall gleich null wäre. Potenzielle Lösungen für solche Probleme beschreibe ich unten.

Nachteile von Feature Branching

Der offensichtlichste Nachteil von exzessivem Branching liegt auf der Hand: das Mergen. Für viele Entwickler ist das Zusammenführen von Branches noch immer ein Schreckgespenst, das regelrecht Angst verbreiten kann. Insbesondere bei größeren Umbauten an sensiblen Teilen eines Systems (Framework-Arbeiten und Refactorings), wenn viele Teile einer Software und vielleicht sogar abhängige Komponenten betroffen sind, steigt die Wahrscheinlichkeit für Konflikte mit den parallelen Änderungen anderer Entwickler enorm. Tatsächlich hat sich Git zwar in dieser Hinsicht als robuster und "intelligenter" als frühere Systeme herausgestellt, aber die potenziellen Gefahren sind nicht wegzudiskutieren. Immer wieder kommt es zu versehentlich vergessenen Teilen oder subtilen Fehlern, weil beim Merge nicht alle Einzelheiten berücksichtigt wurden oder werden konnten. In diesem Zusammenhang sind natürlich semantische Fragestellungen deutlich kniffliger als rein technische Fehler, auf die man schon vom Compiler mit der Nase gestoßen wird.

Der Wechsel auf ein neues Werkzeug wie Git trägt zu solchen negativen Erfahrungen leider auch seinen Teil bei. Wer bisher nur Erfahrung mit CVS, Subversion oder TFS sammeln konnte, ist mit Git nicht selten anfangs überfordert oder unterschätzt subtile Probleme, die sich in der Fülle der technischen Möglichkeiten verstecken. Dadurch kommt es gerade in der Anfangsphase einer solchen Umstellung, wenn man mit Begeisterung "zu viel" vom neuen Werkzeug auf einmal nutzen und gleichzeitig noch seine Branching-Strategie umstellen möchte, besonders oft zu frustrierenden Erlebnissen.

Als Folge davon kann man immer wieder beobachten, dass die Angst vorm Merge zu übermäßiger Vorsicht führt. Umbauten oder Refactorings werden dann vielleicht nicht oder zumindest nicht in dem Umfang vorgenommen, der eigentlich angebracht wäre. Schon der leise Verdacht, es könnten Konflikte mit der Arbeit eines Kollegen auftauchen, wird zum regelrechten Hemmnis. Im Extremfall leidet die Qualtät eines Projekts im Laufe der Zeit erheblich.

Schließlich gibt es auch diverse technische Fragestellungen und Bedenken, die, abhängig vom verwendeten System, beantwortet und ausgeräumt werden müssen. Continuous Builds beispielsweise sind heutzutage schon eher die Regel – beim Commit wird automatisch serverseitig gebaut, Tests ausgeführt und eventuell sogar ein Deployment zu Testsystemen vorgenommen. Funktioniert das noch mit Feature Branches? Müssen wir die Werkzeugkette erweitern oder unsere Prozesse ändern? Nicht selten tauchen im Nachgang Situationen auf, die anfangs so gar nicht bedacht wurden.

Potenzielle Lösungen

Wie so häufig bietet sich in den meisten Fällen auch beim Thema Branching ein Mittelweg mit gesunden Kompromissen an, wenn nicht technische, historische oder politische Gründe dagegensprechen. Grundsätzlich ist meine persönliche Empfehlung, so wenige starre Vorgaben wie möglich zu machen und insbesondere im Einzelfall das Team in Entscheidungen einzubinden - was gemeinsam entschieden wird, wird auch gemeinsam getragen.

Anmerkung: die folgenden Eckpunkte beziehen sich allesamt auf die zentrale Versionsverwaltung, also etwa bei Git das geteilte Repository, gegen das gemeinsam synchronisiert wird. Die lokale Vorgehensweise sollte in meinen Augen den Entwicklern selbst überlassen werden. Viele adaptieren mit Freuden häufiges Branching und Merging oder Rebasing, um den eigenen Arbeitsfluss zu optimieren und auch um Sicherheit zu gewinnen (Stichwort einfaches Rollback). Andere können sich mit dieser Denkweise nur schwer anfreunden; erzwungene Vorgaben sind da erfahrungsgemäß kontraproduktiv.

Minimales Branching, das ich selbst immer empfehle, ist:

  • (Mindestens) ein separater Zweig für Releases, der den aktuellen Stand des Produktivsystems enthält. Das ist insbesondere auch deshalb wichtig, um Fehler korrekt nachstellen zu können und die Möglichkeit zu haben, isolierte Hotfixes (auch diese häufig per temporären Branches) zu erstellen und auszurollen.
  • Separate Branches sind erforderlich für Machbarkeitsstudien/Spikes, Prototypen und ähnliche Situationen, in denen nicht klar ist, ob eine Implementierung tatsächlich Eingang ins eigentliche Projekt findet. Da die Kosten für ein "Undo" wie oben beschrieben ohne separate Branches hoch sind, ist dies schon aus rein technischen Gründen in diesen Fällen sinnvoll.

Darüber hinaus kann es dem Team selbst überlassen werden, ob weitere Empfehlungen zum Branching gegeben werden oder alle Entwickler direkt auf dem Trunk arbeiten sollen. Solche Details können auch z.B. im Planning fallabhängig kurz gemeinsam diskutiert werden, ohne sich pauschal festzulegen.

Cherry Picking

Ein häufig als Argument für exzessives Feature-Branching angeführtes Detail ist die Isolation einzelner Implementierungen gegeneinander, insbesondere mit der Absicht, später ggfs. Cherry Picking durchführen und damit einzelne Features für ein Release einfach selektieren zu können. Mein Lieblingszitat hierzu stammt von Dan Worthington-Bodart:

Feature Branching is a poor man's modular architecture.

Hinter diesem etwas provokanten Statement steckt die Anregung, sich mit alternativen Möglichkeiten fürs Cherry Picking und zur Isolierung von Implementierungen und Änderungen zu beschäftigen. Ein simples, gerne angeführtes Beispiel ist es, neue Features etwa durch Weglassen aus der Benutzeroberfläche so lange zu verstecken, bis die Implementierung abgeschlossen ist und eine Freischaltung erfolgen soll (siehe Feature Toggle). Technische Hilfsmittel dafür reichen von hart-kodierten Schaltern über Konfigurationseinstellungen bis hin zu Compiler-Konstanten und ähnlichem. Wichtig ist in diesem Fall, diese Fragmente wieder zu entfernen, wenn ein Feature freigeschaltet ist.

Viel besser ist es natürlich, die Architektur des Projekts auf flexiblere Änderungen und Erweiterungen auszulegen. Diese Technik ist landläufig unter dem etwas irreführenden Begriff "Branch by Abstraction" bekannt (irreführend deshalb, weil kein "echtes" Branching im Spiel ist). Die Vorgehensweise sieht beispielsweise bei Umbauten vor, dass die zu ändernden Teile zunächst abstrahiert werden, falls dies nicht schon in der Vergangenheit geschehen ist. Über bekannte Techniken wie Dependency Injection/IoC kann dann die alte Implementierung einfach gegen eine überarbeitete Variante getauscht werden. Dies kann auch punktuell bzw. Schritt für Schritt geschehen, bis eine neue Implementierung so stabil und vollständig ist, dass die alte Variante vollständig entfernt werden kann.

Auch dieses Modell wird natürlich teilweise heftig diskutiert. Häufig ist ein seitenfreies Implementieren neuer Features oder auf die obige Art beschriebene Änderungen eben nur schwerlich möglich und erfordert dadurch zusätzliche Disziplin oder Testabdeckung. Ich persönlich bevorzuge Branch by Abstraction aber dennoch, da es insgesamt und implizit zu einer besseren Architektur führt und zum genaueren Nachdenken insbesondere über anstehende Änderungen und Refactorings anspornt.

Tags: Branchingstrategie · Eierlegende Wollmilchsau · Git