Continuous Delivery mit dem FeatureToggle Pattern

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. Mit dem FeatureToggle Pattern lassen sich die daraus resultierenden Probleme im Handumdrehen beseitigen.

In Pocket speichern vorlesen Druckansicht 4 Kommentare lesen
Lesezeit: 16 Min.
Von
  • Niko Köbler
Inhaltsverzeichnis

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.

Da es sich bei den FeatureToggles ja "nur" um if-then-else Statements handelt, lassen sie sich selbst mit allen benötigten Funktionen im Code implementieren. Auf die Idee sind auch schon andere gekommen, und es gibt einfach zu bedienende Frameworks dafür, die einem die Programmierarbeit abnehmen. Im Java-Umfeld gibt es mit Togglz eine Bibliothek, die die genannten Funktionen hinsichtlich der Feature-Schalter implementiert und zur Verfügung stellt. Aber auch das .NET-Umfeld stellt diverse Bibliotheken für FeatureToggles bereit, die Google-Suche hilft hier für einen Überblick weiter. Im Grunde ist aber die Funktionsweise auf allen Programmierplattformen größtenteils identisch.

Das eigene Projekt ist schnell für die Verwendung von Togglz konfiguriert. Lediglich die gewünschten und benötigten Abhängigkeiten sind hinzuzufügen. Nutzt man Maven, ist es besonders einfach, da sich alle Ressourcen über das zentrale Repository beziehen lassen. Togglz ist in Module aufgeteilt, die je nach Anwendungsfall die benötigten Ressourcen enthalten. In folgenden Listing sind beispielsweise die benötigten Abhängigkeiten für eine JSF-Webanwendung konfiguriert.

<!-- Togglz für Webanwendungen -->
<dependency>
<groupId>org.togglz</groupId>
<artifactId>togglz-servlet</artifactId>
<version>1.1.0.Final</version>
</dependency>

<!-- CDI-Integration (optional) -->
<dependency>
<groupId>org.togglz</groupId>
<artifactId>togglz-cdi</artifactId>
<version>1.1.0.Final</version>
</dependency>

<!-- JSF-Integration (optional) -->
<dependency>
<groupId>org.togglz</groupId>
<artifactId>togglz-jsf</artifactId>
<version>1.1.0.Final</version>
</dependency>

Außer den Servlet-, CDI- und JSF-Integrationen stehen Module für das Spring Framework und Authentifizierungen mit Spring Security, Seam Security, Apache DeltaSpike und Apache Shiro bereit. Zusätzlich gibt es noch ein Modul mit einer Webkonsole, die die Administration über eine in die eigene Webanwendung integrierte Oberfläche anbietet.

Die weitere Konfiguration geschieht nicht per XML, sondern komplett typsicher in Java. Die Definition der benötigten Features erfolgt in einer Feature-Enum-Klasse, die das Interface Feature implementiert. Hierbei wird jedes Feature als Konstante in der Enum-Klasse repräsentiert:

public enum MyFeatures implements Feature {

@EnabledByDefault
@Label("Smart First Feature")
FIRST_FEATURE,

@Label("Cool Second Feature")
SECOND_FEATURE;

@Override
public boolean isActive() {
return FeatureContext.getFeatureManager().isActive(this);
}
}

Das Verwenden einer Enumeration als Feature-Definition hat unter anderem den Vorteil, dass die Funktionen im Code typsicher zu referenzieren sind, sich Autovervollständigung in der IDE der Wahl verwenden lässt und Tippfehler vermieden werden. Beim Ausbauen der Features wird somit durch entsprechende Compiler-Fehler schnell ersichtlich, wo der zu entfernende Schalter zum Einsatz kommt.

Im Normalfall ist ein definierter Schalter erst mal inaktiv und schützt so automatisch davor, dass der Code innerhalb des Feature-Schalters ausgeführt wird. Der Mechanismus kann so manches unerwünschte Fehlverhalten vermeiden. Soll der entsprechende Code ausgeführt werden, ist der Schalter manuell zu aktivieren. Im seltenen Fall, dass er initial aktiviert sein soll, lässt sich die Annotation @EnabledByDefault verwenden. Die Nutzung weiterer Annotationen zur Anreicherung der Features mit Metadaten ist ebenfalls möglich, Details hierzu findet man auf der Togglz-Website.

Nach der Feature-Definition ist Togglz noch zu konfigurieren. Das geschieht ebenfalls über die Implementierung eines Interface, in diesem Fall TogglzConfig:

public class MyTogglzConfiguration implements TogglzConfig {

@Override
public Class<? extends Feature> getFeatureClass() {
return MyFeatures.class;
}

@Override
public StateRepository getStateRepository() {
return new FileBasedStateRepository(
new File("/path/to/features.properties")
);
}

@Override
public UserProvider getUserProvider() {
return new NoOpUserProvider();
}
}

In der neuen Klasse sind nun drei Methoden enthalten, jede für eine einzelne Aufgabe der Konfiguration. Eine der wichtigsten Methoden ist getFeatureClass(), in der einfach nur anzugeben beziehungsweise zurückzugeben ist, welche Feature Enum das soeben definierte Feature enthält.

In der Methode getStateRepository() konfiguriert man, aus welcher Art von Repository die Feature-Zustände gelesen beziehungsweise in welches Repository diese zu persistieren sind. Das einfachste ist sicherlich InMemoryStateRepository, das die Schalterzustände lediglich im Hauptspeicher hält. Bei Beenden der Anwendung gehen auch die Zustände verloren. Möchte man das nicht, sollte man auf das FileBaseStateRepository oder das JDBCStateRepository zurückgreifen, das die Zustände im ersten Fall in einer einfachen Property-Datei und im zweiten Fall per JDBC in einer Datenbank speichert. Eigene StateRepositories lassen sich je nach Bedarf und Belieben erstellen und verwenden.

Die dritte und letzte Methode getUserProvider() der TogglzConfig-Implementierung definiert den zu verwendenden UserProvider, falls Features nur für einzelne Benutzer und Gruppen von Benutzern gelten sollen. Hier lassen sich die genannten Module für verschiedene Security-Implementierungen verwenden, um sich in deren Authentifizierungsmechanismus einzuhängen. Werden andere UserProvider als die mitgelieferten benötigt, lassen sie sich wie die StateRepositories erstellen.

Zu guter Letzt ist der Applikation noch mitzuteilen, dass es Togglz gibt und welche Klasse zur Konfiguration verwendet werden soll. Im Falle einer Enterprise-Java-Anwendung mit CDI (Context and Dependency Injection) ist nichts weiter zu tun. Togglz ist so programmiert, dass es sich im CDI-Container selbst registriert. Im Spring-Kontext ist es ähnlich einfach, hier sind lediglich, je nach Konfiguration, die TogglzConfig Bean in der Spring-XML zu deklarieren beziehungsweise die Klasse mit einer @Component-Annotation zu versehen.

Jetzt lassen sich Togglz und die definierten Feature-Schalter im Code verwenden. Das geschieht mit einer einfachen if-Abfrage:

if (MyFeatures.FIRST_FEATURE.isActive()) {
// ...
}

Dieses Snippet lässt sich an jeder Stelle im Quellcode verwenden, egal welcher Art von Ressource beziehungsweise Klasse. Möchte man im UI Elemente verbergen, wie eingangs im Artikel erwähnt, stellt Togglz für JSF-Anwendungen eine einfache Möglichkeit bereit. Hier wird über das rendered-Attribut einer Komponente gesteuert, ob sie angezeigt werden soll oder nicht:

<h:panelGroup rendered="#{features['FIRST_FEATURE']}">
<!-- Ein neues Bedienelement -->
</h:panelGroup>

Eine Bean mit Namen feature stellt das JSF-Modul von Togglz automatisch zur Verfügung.

Togglz bietet noch eine große Fülle weiterer Möglichkeiten, auf die der Artikel nicht ausführlich eingehen kann. Wie oben angesprochen zählt eine komplette Administrationsoberfläche zum optionalen Ausstattungspaket mit. Sie hängt sich unterhalb der eigenen URL mit dem Pfad /<context-path>/togglz in das Projekt, je nach Servlet-Version automatisch (Version 3.0) oder manuell über die web.xml (bis Version 2.5). Über diese Oberfläche lassen sich die einzelnen Schalterzustände einfach pflegen, ohne dass ein manuelles und fehleranfälliges Bearbeiten der Property-Dateien und Datenbank-Einträge notwendig wird.

Mit der FeatureProxyFactoryBean kann man in einem Spring-Kontext eine komplett andere Implementierung einer Klasse in Abhängigkeit eines Feature-Schalters verwenden:

<bean id="fancyService" 
class="com.example.myapp.services.NewFancyService" />

<bean id="boringService"
class="com.example.myapp.services.BoringService" />

<bean id="myService" class="org.togglz.spring.proxy.FeatureProxyFactoryBean">
<property name="feature" value="FANCY_FEATURE" />
<property name="active" ref="fancyService" />
<property name="inactive" ref="boringService" />
</bean>

Je nach Schalterzustand (aktiv oder inaktiv) stellt die Proxy-Klasse die alte Implementierung oder ihr neues Pendant bereit. Weitere Optionen des Togglz-Frameworks lassen sich auf der Projekt-Website nachlesen.

Continuous Delivery ist bis zum ersten Release gut mit nur einem Branch in der Versionsverwaltung umzusetzen. Sollen danach jedoch weitere Features entwickelt und zeitnah eingecheckt werden, sind Vorkehrungen zu treffen, damit unfertige Features nicht in der Produktionsumgebung sichtbar beziehungsweise bereits fertig entwickelte Funktionen gegebenenfalls erst zu einem bestimmten Zeitpunkt nur für gewisse Benutzer oder Serverumgebungen aktiv werden. Das FeatureToggle Pattern kann hier helfen, auch weiterhin Continuous Delivery nur mit einem Code-Branch durchzuführen und so Schalter für die Features zu setzen.

Mit Togglz steht eine schlanke und leistungsfähige Java-Bibliothek zur Verfügung, die das Pattern mit weiteren Funktionen implementiert und sich in jede beliebige Java-Anwendung integrieren lässt. Für das .NET-Umfeld stehen ebenfalls Implementierungen bereit. Nun sollte es für niemanden mehr ein Problem sein, Continuous Delivery in seinem Projekt umzusetzen.

Niko Köbler
ist freiberuflicher Berater und Softwarearchitekt für Enterprise Java, Integrationen und Webservices. Er hält Workshops und Trainings und führt Architektur-Reviews durch. Zudem ist er Co-Organisator der Java User Group Darmstadt (JUG DA) und regelmäßig als Sprecher auf Fachkonferenzen anzutreffen.
(ane)