Continuous Delivery mit dem FeatureToggle Pattern

Werkzeuge  –  4 Kommentare

Wer Continuous Delivery umsetzen und betreiben will, hat mit Features zu kämpfen, deren Entwicklung noch nicht abgeschlossen ist und die noch nicht oder nur für eine bestimmte Benutzergruppe in Produktion gehen sollen, aber bereits entwickelt sind. Dass kein Feature-, sondern nur der Master-Branch existiert, aus dem auch regelmäßig verteilt wird, könnte zur Herausforderung werden. Mit dem FeatureToggle Pattern ist es einfach, all das im Handumdrehen zu realisieren.

Für die sich in Produktion befindliche Software soll ein neues Feature entwickelt werden, bestenfalls nur für Kunde A. Kunde B dagegen nutzt ein anderes Modul der Software und hat einen nun zu behebenden Bug gefunden. Die Entwicklung des Features für Kunde A dauert aber länger als bis zum nächsten geplanten Releasezyklus, und die Korrektur des Fehlers ist als Hotfix an Kunde B auszuliefern. Also erstellen die Entwickler vom Master-Branch einen neuen Feature-Branch und zusätzlich noch einen für den Hotfix. Die Software im Hauptzweig des Repository wird derweil planmäßig weiterentwickelt. Nach einiger Zeit sind die so entstandenen, gewachsenen und gewucherten Branches, nicht selten deutlich mehr als die angesprochenen zwei, zusammenzuführen, da irgendwann die Software wieder alle Features und Bugfixes im Master-Branch vereinen soll. Wer möchte jetzt gerne seine Freizeit opfern und die Merges manuell durchführen, da parallel an gleichen Codefragmenten in den Branches gearbeitet wurde? Hier gilt es, neben den strukturellen die semantischen Konflikte aufzulösen und die Applikation nach dem Zusammenführen weiterhin lauffähig zu halten. Wohl dem, der auf ausreichende und funktionsfähige Unit-Tests zurückgreifen kann.

Einer der Grundsätze des Continuous Integration besagt, dass es lediglich einen Master-Branch und keine weiteren Feature-Branches geben soll, in den der Code eingecheckt und damit eben diese Merge-Hölle vermieden wird. Weiterhin soll sich für Continuous Delivery aus dem Master-Branch jederzeit ein neues lauffähiges Release erzeugen lassen. Wie schaffen es Softwareprojekte nun, die genannten Anforderungen unter einen Hut zu bringen? Jederzeit ein lauffähiges Release erzeugen zu können, keine Feature-Branches zu pflegen und dennoch neue Features zu implementieren. Diesen Code oft (und gegebenenfalls noch nicht fertig entwickelt) ins Repository zu "committen", ohne dass dieser die allgemeine Entwicklung, womöglich an identischen Modulen beziehungsweise Codefragmenten, stört.

Im Grunde ist die Lösung recht einfach mit if-then-else-Statements zu implementieren. Die Bedingung für den if-Zweig sollte allerdings nicht hart im Code implementiert sein, sondern sich von außen in irgendeiner Art und Weise beeinflussen oder ändern lassen. Der bekannte Softwarearchitekt Martin Fowler spricht hier vom FeatureToggle Pattern. In einer Konfigurationsdatei wird eine Menge an Schaltern (Toggle) für verschiedene Features definiert. Die Software nutzt dann die Schalter, um das Feature anzuzeigen und zu verarbeiten, oder auch nicht:

if (schalter.ist_an)
zeige_neues_Feature_an
end

Somit hat ein Schalter grundsätzlich zwei Zustände: AN und AUS. Solange sich das Feature noch in der Entwicklung befindet, ist der Schalter in allen Umgebungen deaktiviert: Lediglich der Entwickler, der damit arbeitet (oder die Gruppe derer) schaltet das Feature aktiv und kann somit darüber verfügen, die Entwicklung vorantreiben und testen. Alle anderen Umgebungen (oder Entwickler) beachten das neue Feature einfach noch nicht. Nach Finalisierung der neuen Funktionen lässt es sich in allen Umgebungen aktivieren und damit in der Software zur Verfügung stellen.

Features nicht nur nach ihrem Fertigstellungsgrad, sondern auch benutzer- und/oder umgebungsabhängig zu verwenden und anzuzeigen ist ein weiteres Anwendungsbeispiel für die Verwendung von Schaltern. Zum Beispiel lässt sich eine neue Funktion vorerst nur für eine Gruppe von Power-Usern anzeigen und später, nach erfolgreicher Erprobung, für alle Benutzer freischalten ("Dark Testing"). Oder das neue Feature wird zunächst nur für Anwender auf einem bestimmten Server bereitgestellt. Läuft es dort fehlerfrei, lassen sich die Schalter für alle weiteren Server umlegen. Im Fehlerfall wird der Schalter einfach wieder zurückgelegt, und die Software läuft wieder ohne die neue Funktion weiter, als wenn sie gar nicht da wäre.

Facebook wendet ein ähnliches Verfahren an, um neue Features erst einer kleinen Nutzergruppe vorzustellen, bevor diese alle Anwender nutzen können. Außerdem verteilt das soziale Netz täglich mehrmals direkt aus dem Master-Branch, während viele Features und Bugfixes sich noch in Entwicklung befinden. Somit wird der Code zwar mit ausgeliefert, aber aufgrund der entsprechenden Schalterstellungen nicht verwendet und führt so nicht zu Fehlern. Auch Flickr ist ein prominentes Beispiel dafür, wie mit Feature Flags und Flippers (wie diese dort genannt werden) mehrmals am Tag aus nur einem Branch in die Produktion hinein verteilt und die Verwendung von Features gesteuert wird.

Häufig kommen FeatureToggles im Frontend beziehungsweise User Interface vor. Neue Oberflächenteile sollen in der in Produktion befindlichen Software noch nicht sichtbar sein. Um nun hässliche if-Statements in den UI-Templates zu vermeiden, ist es oftmals lohnenswert, eigene Tags dafür zu erstellen, die auf die Schalter reagieren. In einer JSF-Umgebung (JavaServer Faces), in der ein eigener toggle-Tag eingeführt wurde, sähe das wie folgt aus. Korrekt implementiert, kommt der Teil, der zwischen den Tags steht, gar nicht bis in den Komponentenbaum und damit ins Frontend, falls das so gewünscht ist:

<toggle name="meinNeuesFeature">
<p>Hier ist der <a href="newFeature">Link</a> des neuen Features</p>
</toggle>

Wird ein UI-Element einfach durch den Feature-Schalter verborgen, ist meist keine weitere Tätigkeit notwendig. Was nicht vorhanden ist, kann der Anwender nicht verwenden. Im Backend kann die Implementierung des Feature-Schalters die einfache Unterscheidung zwischen der Nutzung verschiedener Algorithmen und Methodenaufrufe bedeuten. Allerdings hat das wieder unschöne if-Statements zur Folge. Eine elegantere Art ist es, in Abhängigkeit des Schalters einfach eine andere Implementierung eines Interfaces in die aufrufende Klasse per Dependency Injection einzuschleusen.

Überwiegend kommen Feature-Schalter zur Laufzeit der Applikation zur Auswertung und zur Anwendung. Vereinzelt werden sie aber auch zum Build-Zeitpunkt verwendet, um einzelne Codebestandteile gar nicht erst in die auszuliefernde Software zu paketieren und neue Features statt altem Code auszuliefern. Das hat allerdings den Nachteil, dass sich nicht auf die alte Version zurückschalten lässt, wenn ein neu ausgeliefertes Feature zur Laufzeit einen Fehler verursacht oder sich doch nicht wie erwartet verhält. Einer Schalterauswertung zur Laufzeit der Software sollte deshalb also der Vorzug gegenüber dem Build-Zeitpunkt gegeben werden.

Das Testen neuer Features mit Unit-Tests darf natürlich nicht vergessen werden. Die Verwendung von FeatureToggles und damit die von "neuem" und "altem" Code parallel im gleichen Entwicklungszweig bringt aber keine erhöhte Komplexität hinsichtlich der Tests mit sich. Es gibt hier keine kombinatorische Explosion von Testfällen, die nun zu schreiben und auszuführen sind. Genauso wie in einer Mehr-Branch-Umgebung ist für die neuen Features dieselbe Anzahl an Testfällen zu implementieren. Ein weiterer Vorteil ist, dass sogar ein Testen beider Schalter-Zustände möglich wird, wenn sich die Schalter durch Test(basis)klassen steuern lassen.

Nachdem die Features nicht mehr neu und in den regulären Bestand und Funktionsumfang der Software eingeflossen sind, sollte man die Feature-Schalter möglichst wieder ausbauen und aufräumen. Dadurch bleibt der Code übersichtlich und ist nicht mit ungenutzten Fragmenten überfrachtet. Kein Entwickler wird sich nach einiger Zeit noch daran erinnern, warum ein altes Fragment noch im Code enthalten ist und es nicht mehr verwendet wird. Auch sorgt das Entfernen der alten Tests für eine weitere Übersichtlichkeit und nötigt einen nicht dazu, auf diese in der weiteren Entwicklung Rücksicht zu nehmen, obwohl sie ja gar nicht mehr gelten. Zusätzlich bewahrt das Ausbauen der Schalter vor dem ungewollten Deaktivieren eines Features, das zu unangenehmen Nebenwirkungen und Fehlverhalten der Software führen kann.