Suche
Features gekonnt definieren und implementieren

Jede Änderung ein Feature

Hendrik Ebbers

Um kontinuierlich Software zu deployen, die den Kunden und Endanwender zufriedenstellt, ist es im Bereich der Entwicklung wichtig, neue Workflows und Technologien einzusetzen. Ziel sollte es sein, möglichst flexibel auf Änderungen, Kundenwünsche und Probleme reagieren zu können. Ein Verfahren, das sich hier bewährt hat, ist das „Feature-driven Development“.

Wenn es heute um die Modernisierung und Verbesserung von Software und deren Entwicklungszyklus geht, dann ist einer der am meisten genannten Punkte die Verkürzung von Releasezyklen. Durch CD/CI und Tools wie Jenkins ist es möglich, das Releasen von Software deutlich zu vereinfachen und zu automatisieren. Setzt man die hier benötigten Prozesse ideal um, so sind Releases theoretisch jederzeit auf Knopfdruck möglich. Dies führt dazu, dass man die Zeit zwischen zwei Releases deutlich verkürzen kann. Die Codedifferenz zwischen zwei Auslieferungen verringert sich somit deutlich. Neben der Automatisierung des Deployments mit Tests, Build und automatischer Verteilung der Artefakte gibt es aber noch mehr Punkte, die ein Entwicklerteam beachten sollte. Um so flexibel wie möglich auf Änderungen und Probleme der Software eingehen zu können, hat sich das Feature-driven Development etabliert. Hier wird jede Änderung als Feature betrachtet. Durch Nutzung verschiedener Git-Workflows lassen sich die so definierten Features isoliert entwickeln und können gezielt dem nächsten Release hinzugefügt werden. Parallel können Entwickler Feature-Toggles nutzen, um besser auf Probleme im Produktivbetrieb reagieren zu können. Außerdem können so neue Funktionen gezielt an bestimmten Benutzergruppen getestet werden. Im Folgenden sollen diese verschiedenen Aspekte des Feature-driven-Development-Ansatzes genauer betrachtet werden.

Was ist ein Feature?

Bevor man mit Feature-driven Development beginnen kann, sollte man wissen, wie ein Feature definiert ist. Es gibt verschiedene Typen: Features, die ein Endanwender direkt nutzen kann und interne Features, die z. B. die Performance einer Anwendung verbessern. Im Serverumfeld könnte ein Feature zum Beispiel ein neuer Serviceendpunkt sein. Auch die Nutzung eines neuen API könnte in diesem Fall ein Feature sein. Ein anderes Beispiel ist eine Shuffle-Funktion für einen Musik-Player. Hier handelt es sich um ein Feature, das der Endanwender aktiv sehen und nutzen kann.

Grundsätzlich kann man festhalten, dass jedes Feature ein Issue ist, der in einem Tool wie Jira festgehalten und verwaltet werden sollte. Hierbei sollte ein Feature aber nicht unbedingt als eine große „User Story“ angesehen werden, denn es sollte deutlich kleiner sein. In der Planung kann eine User Story oder ein Use Case gut in mehrere kleine Features unterteilt werden

Im Folgenden werden verschiedene Workflows aufgezeigt, die durch Nutzung von Git die Realisierung von Features erlauben. Dabei werden weder bevorstehende Releases gefährdet noch endlose Merge-Vorgänge verursacht.

Git-Workflows zur isolierten Entwicklung von Features

Um die Entwicklung von Features in der täglichen Arbeit effektiv umsetzen zu können, müssen verschiedene Workflows definiert und eingehalten werden. Man sollte dabei jederzeit releasefähig sein, ohne mit langlebigen Braches oder Merge-Orgien kämpfen zu müssen. Ein Workflow, in dem die Entwickler immer in einen Trunk bzw. Master Branch committen, ist hier definitiv nicht geeignet. Dies führt viel mehr dazu, dass die Software zu keiner Zeit releasefähig ist, da Entwickler immer wieder unvollständige Implementierungen von Features einchecken werden (Abb. 1).

ebbers_1

Abb. 1: Durch Nutzung eines einzelnen Entwicklungszweigs entsteht nie ein stabiler Stand

Um dies zu verhindern, sollten während der Entwicklung Branches genutzt werden. Hierbei eignet sich der GitFlow besonders gut als Workflow [1]. Dieser soll hier aber nicht in aller Tiefe vorgestellt werden. Für unser Thema ist es wichtig, dass jedes Feature in einem eigenen Branch entsteht und erst dann in den Developer Branch gemerged wird, sobald es fertig entwickelt und getestet ist. Durch dieses Vorgehen ist es möglich, dass ein Entwicklerteam parallel an Features arbeitet. Da sich im Dev Branch nie unvollständige Features befinden, kann die Anwendung theoretisch zu jeder Zeit releast werden (Abb. 2).

ebbers_2

Abb. 2: Nutzung von Feature Branches

 

Für einen produktiven und schnellen Workflow müssen allerdings noch ein paar weitere Aspekte beachtet werden. Die Branches sollten so kurzlebig wie möglich sein. Jeder Branch bedeutet Integrationsaufwand durch einen Merge. Auch wenn dieser in Git mittlerweile sehr gut machbar ist, können sich durch zu große Differenzen immer wieder Fehler in den Developer Branch einschleichen, die im schlimmsten Fall dann auch im Release landen. Aus diesem Grund sollten einzelne Features im Idealfall so klein wie möglich definiert werden. Nutzt man Scrum, so könnte man z. B. einen Maximalwert an Story Points pro Feature definieren. Theoretisch ist hier aber jedes Mittel denkbar. Am Ende ist wichtig, dass man ein Gefühl dafür bekommt, wie groß ein Feature maximal sein sollte, damit es ohne Mehraufwand innerhalb weniger Tage entwickelt, getestet und in den Developer Branch übernommen werden kann. Es gibt ein weiteres Mittel, um Probleme beim Merge des Features in den Hauptzweig zu umgehen: ein Rebase bzw. ein Merge des Dev Branches. Durch diese Mittel kann man Änderungen des Dev Branches, die nach der Erstellung des Feature Branches eingeflossen sind, in den Feature Branch übernehmen. Diese Technik führt dazu, dass man auf der einen Seite keine Konflikte beim Merge des Feature Branches in den Dev Branch gibt. Auf der anderen Seite kann bereits vor diesem Merge die Integration getestet werden, da der Feature Branch nach einem Rebase bereits die Zusammenführung von Feature Branch und Dev Branch enthält (Abb. 3). Bei einem Rebase kann man auch mit einem Fast Forward Merge arbeiten, um so die Graphstruktur des Repositories einfach zu halten.

ebbers_3

Abb. 3: Durch Merge des Dev Branches in den Feature Branch kann man die Integration testen und Konflikte vermeiden

Aufteilung eines Issues in Features

Damit der Aufwand für die Entwickler möglichst gering gehalten wird, sollten Features so klein wie möglich definiert werden. Hierbei muss die einzelne Einheit natürlich trotzdem Sinn ergeben. Das genaue Maß findet sich am besten durch Ausprobieren und Sammeln eigener Erfahrungen. Als Beispiel betrachten wir das Bewertungssystem eines Onlineshops. Um dies zu implementieren, benötigt man in der Regel drei verschiedene Bestandteile:

  • das Frontend/UI, damit der Benutzer Bewertungen abgeben und betrachten kann
  • eine Erweiterung der Persistenzschicht, um die Bewertung zu speichern
  • Businesslogik, um z. B. Durchschnittsbewertungen auszurechnen.

Eine mögliche Umsetzung ist es, aus dem definierten Use Case nun ein einzelnes Feature zu erstellen. Hierbei hat man aber unter Umständen das Problem, dass die Implementierung des Features lange dauern kann, da möglicherweise verschiedene Entwickler beteiligt sein müssen:

  • ein UI-Experte definiert Rating Component und CSS
  • ein DB-Experte definiert Tabellenschema
  • JPA-Klassen werden erweitert
  • Businesslogik wird erstellt
  • Komponente wird in Page-Layout integriert

Im schlimmsten Fall benötigt man also drei bis fünf Personen zur Abarbeitung des Features. Steht nur einer der Entwickler nicht zur Verfügung, so kann die Realisierung des Features mehrere Wochen in Anspruch nehmen. Dieses Problem führt dazu, dass der Merge komplizierter werden kann und unbemerkte Bugs in die Software gelangen. Eine weitere Möglichkeit bietet die Aufspaltung in mehrere Features. Hierbei gibt es verschiedene Ansätze zur Aufteilung. Eine Möglichkeit wäre folgende Unterteilung:

  • UI-Template für Rating Component und CSS erstellen
  • Persistenz für Rating Component
  • Businesslogik und Integration der Rating Component

 

Bei diesem Vorgehen sind die beiden ersten Features komplett losgelöst und können einzeln entwickelt werden. Da sie keinerlei Auswirkung auf die Anwendung haben, können sie auch direkt nach der Fertigstellung in ein Release übernommen werden. Der Endnutzer wird diese Erweiterungen nicht bemerken, da es lediglich neue ungenutzte Datenbanktabellen, Codeblöcke und CSS-Regeln gibt. Nur das dritte Feature ist nicht mehr unabhängig, da es die beiden ersten als Voraussetzung benötigt und die „losen Enden“ zusammenführt. Hierbei muss man nun beachten, dass diese Abhängigkeiten korrekt definiert bzw. die einzelnen Features sinnvollen Sprints zuordnet werden.

Auch andere Aufteilungen sind hier denkbar. So könnte man z. B. die Businessschicht als einzelnes Feature definieren. Hierbei könnte man dann losgelöst von einer vorgegebenen Persistenzschicht die Logik definieren und erst in einem Folgefeature die zur Logik passende Persistenz implementieren. Dieser Ansatz eignet sich sehr gut zum Test-driven Development, da hier die Funktionalität der Businessschicht nur über Unit Tests überprüft werden kann.

Ebenso kann man aber auch mit dem Frontend beginnen und den Button zu Beginn auf visible=false schalten. Erst wenn alle anderen Features implementiert sind und somit die Funktionalität des Buttons komplett gegeben ist, wird er auf visible=true geschaltet. Solange dies noch nicht der Fall ist, ist der Button für den Endanwender nicht sichtbar, obwohl die ersten Features zu diesem Use Case bereits in produktiven Releases vorhanden sind.

Das hier genannte Beispiel wurde lediglich zur Verdeutlichung des Vorgehens gewählt. Mit einer guten Basisarchitektur und den richtigen APIs bzw. Workflows implementiert ein Entwickler das genannte Feature möglicherweise innerhalb von ein bis zwei Tagen. In diesem Fall ist eine weitere Aufteilung nicht sinnvoll.

Das hier beschriebene Vorgehen bringt allerdings auch einige Probleme mit sich: Es gibt Features/Issues, die stark voneinander abhängig sind. Man darf nicht vergessen, dass bestimmte Komponenten im Frontend aktuell ausgeblendet sind. Werden diese Komponenten zu früh oder gar nicht aktiviert, könnte ein Release mit einem ungewollten Verhalten auf einem Kundensystem landen. Dass die einzelnen Bestandteile (Frontend, Logik und Persistenz) erst am Ende „zusammengestrickt“ werden und deshalb deren Zusammenspiel möglicherweise nicht ausreichend getestet wird, stellt ein Problem dar. Vielleicht führt genau dies zu einem kritischen Bug im Kundensystem, wodurch man das ganze Release wieder zurückfahren muss. Genau an diesem Punkt kommen Feature-Toggles ins Spiel. Sie erlauben es, einzelne Features zur Laufzeit der Anwendung zu aktivieren bzw. zu deaktivieren. Hierdurch erhalten Entwickler deutlich mehr Kontrolle über das Verhalten einer ausgerollten Anwendung und können im Idealfall ohne eine neue Auslieferung auf Probleme reagieren.

Nutzung von Feature-Toggles

Features-Toggles wurden unter anderem von Martin Fowler definiert [2]. Große Unternehmen wie Flickr berichten über den Einsatz von Feature-Toggles und deren Vorteile [3]. In der Basis ist ein Feature-Toggle lediglich ein Schalter, mit dem man ein Feature zur Laufzeit aktivieren bzw. deaktivieren kann. In der einfachsten Form ist dieses Toggle eine boolesche Variable, die in einer if-Abfrage ausgewertet wird. Listing 1 zeigt ein Beispiel, in dem ein als neues Feature hinzugefügter Button nur dann sichtbar ist, wenn dessen definiertes Feature-Toggle aktiv ist.

 

Listing 1

public class MyController {

private boolean shuffleButtonFeature;

public void init() {

// create UI . . .

if(shuffleButtonFeature) {

add(new ShuffleButton());

}

}

public void setShuffleButtonFeature(boolean active) {

this.shuffleButtonFeature = active;

}

}
 

Kann man den booleschen Wert zum Beispiel über eine Administrationsoberfläche setzen, so entsteht die Möglichkeit, das Verhalten der Anwendung zur Laufzeit zu ändern. Sollte man merken, dass dieser neue Button im Produktivsystem beim Klicken den gesamten Server lahmlegt, so kann man den Button, ohne eine neue Auslieferung oder einen Neustart des Systems entfernen.

Ein weiterer Vorteil entsteht, wenn beispielsweise eine Anwendung bei verschiedenen Kunden zum Einsatz kommt. In einem konkreten Fall gab es zum Beispiel Kunden, die jedes neue Feature gerne aufgenommen und genutzt haben. Ein anderer Kunde wollte grundsätzlich keine neuen Features, die im UI sichtbar waren, um so die Mitarbeiter nicht ständig mit neuen Oberflächen und Funktionen zu konfrontieren. In diesem Fall wurden diese Features über Feature-Toggles realisiert. Jedes halbe Jahr wurden dann die vorhandenen Feature-Toggles aktiviert. Die Mitarbeiter wurden in einer Schulung mit den neuen Funktionen und geänderten Oberflächen vertraut gemacht. Durch diesen Workflow konnte das System des Kunden jederzeit auf dem neuesten Stand gehalten werden: Neue Releases wurden ausgerollt, allerdings wurden bei einigen Kunden nur die Features „im Hintergrund“ (Änderungen und Neuerungen auf der Serverseite etc.) aktiviert. Hierdurch konnten gemeldete Bugs behoben werden, neue Features parallel entwickelt und alle Kundensysteme auf den aktuellen Stand gebracht werden.

Nutzung des Togglz-API

Will man ein Java-Projekt um Feature-Toggles erweitern, so empfiehlt sich die Nutzung des Togglz API [4]. Dieses Open-Source-API bietet alles, was man zur Integration von Feature-Toggles in ein Java-Projekt benötigt. Positiv anzumerken ist, dass das API auf Java SE basiert und optionalen Support für Java EE oder Spring anbietet. Hierdurch kann das API in wirklich jeder beliebigen Java-Anwendung genutzt werden. In Togglz werden Features typischerweise über ein Enum definiert. Die einzelnen Features kann man über Annotationen mit Metainformationen wie einer Beschreibung oder einem Link zum Jira Issue versehen. Zusätzlich kann man über die @EnabledByDefault-Annotation das Default-Verhalten der Features definieren. Listing 2 zeigt die Definition zweier verschiedener Features.

 

Listing 2

public enum MyFeatures implements Feature {

@EnabledByDefault

@Label("First Feature")

FEATURE_ONE,

@Label("Second Feature")

FEATURE_TWO;

public boolean isActive() {

return FeatureContext.getFeatureManager().isActive(this);

}

}
 

Durch die Nutzung der isActive()-Methode kann man nun den Status des Features jederzeit im Code abfragen. Listing 3 verdeutlicht, wie man so z. B. zwischen einer alten und einer neuen Serviceimplementierung wechseln kann.

 

Listing 3

if( MyFeatures.NEW_PERSISTENCE.isActive() ) {

return jpaCall();

} else {

return jdbcCall();

}

 

Abgesehen von der Definition von Features hat Togglz aber noch einiges mehr zu bieten. Neben CDI, Servlet und Spring-Support kann man ein standardisiertes Administrations-UI einbinden. Dieses basiert auf Servlets und sollte somit in jeder Webanwendung problemlos nutzbar sein. In der Oberfläche ist eine Übersicht zum Status aller Features enthalten. So kann man Features jederzeit schnell aktivieren oder deaktivieren. Für alle angesprochenen Erweiterungen benötigt man nur die jeweils passende Maven Dependency.

Neben den vorgestellten Erweiterungen lässt sich Togglz in vielen Bereichen konfigurieren. So kann man State Repositories definieren, die die Persistenz von Featurezuständen verwalten. Es ist somit z. B. kein Problem, den initialen Zustand aus einer Properties-Datei auszulesen und Änderungen in eine Datenbank zu persistieren. Auch Activation Strategies können definiert werden. Hierdurch kann man einzelne Features zum Beispiel nur für bestimmte Benutzer aktivieren, indem man eine Client-IP-Range oder Benutzergruppen angibt. Dies wird auch oft von großen Onlineplattformen, wie beispielsweise Facebook, genutzt. Hier werden Neuerungen im UI oft erst einmal an einer kleinen Gruppe von Benutzern getestet, bevor das neue Layout allen Benutzern angezeigt wird. Da die gesamte Konfiguration von Togglz über Interfaces definiert ist (Abb. 4), kann man das gewünschte Verhalten leicht selbst implementieren und so ein maßgeschneidertes Feature-Toggle-API für die eigene Anwendung erstellen.

 

ebbers_4

Abb. 4: Das Administrations-Frontend von Togglz

Spezialisierungen für Frameworks und Toolkits am Beispiel DataFX

Mittlerweile gibt es auch die ersten APIs und Frameworks, die auf Togglz aufbauen. So wird ein spezialisierter Support für Toggles gewährleistet. Für JavaFX-Anwendungen bietet DataFX [5] Feature-Toggle-Support an. Hier können UI-Elemente einfach mit einer Annotation versehen werden, um die Komponenten abhängig von einem Feature anzuzeigen oder zu verbergen. Listing 4 zeigt das Codebeispiel für einen Button. Auch für JSF gibt es bereits Implementierungen, bei denen man direkt im JSF-Tag den Status von Feature-Toggles überprüfen kann.

 

Listing 4

public class MyJavaFXController {

@FXML

@HideByFeature("PLAY_FEATURE")

private Button playButton;

}

Tipps und Tricks

Setzt man die hier vorgestellten Workflows und Pattern um, kann man gezielt einzelne Features entwickeln, bereitstellen und im Problemfall zur Laufzeit deaktivieren. Bei größeren Projekten bemerkt man allerdings schnell, dass noch weitere Organisationschritte benötigt werden, um eine produktive Umsetzung zu gewährleisten. Denken wir uns den Extremfall, in dem für jedes Feature ein Feature-Toggle definiert wird: Selbst wenn man die einzelnen Featuredefinitionen mit Metadaten beschreibt, kann der Vorgang schnell unübersichtlich werden. Der Anwendungscode ist dann nur noch eine Ansammlung von featureabhängigen If-else-Verzweigungen (Listing 5).

 

Listing 5

if(MyFeatures.Feature_256){

if(MyFeatures.Feature_12) {

refreshButton.animate();

}

addButton(refreshButton);

} else {

if(MyFeatures.Feature_1035){

addRefreshToToolbar();

if(MyFeatures.Feature_75) {

initLanguageSupportForToolbarRefresh();

}

} else {

// Do nothing;

}

}

 

Aus diesem Grund ist es ratsam, Feature-Toggles frühzeitig aus dem Code zu entfernen. Den Zeitpunkt zur Entfernung von Features kann man hier frei wählen. Bei einer Anwendung, die bei verschiedenen Kunden im Einsatz ist, hat es sich für mich bewährt, dass ein Feature-Toggle entfernt wird, sobald er einen Releasezyklus lang bei allen Kunden im Einsatz war. In einem konkreten Fall bedeutete dies, dass ein Feature-Toggle nach ca. einem Monat entfernt wurde, da es in diesem Zeitraum bei allen Kunden ausgerollt wurde. Wenn es bei einem der Kunden zu Problemen mit dem Feature kam, wurde es auf dem Kundensystem deaktiviert, und das Feature-Toggle blieb erst einmal bestehen, bis das Problem behoben wurde. Wurden von keinem Kunden Probleme gemeldet, so konnte das Toggle nach kurzer Zeit entfernt werden.

Beim Entfernen eines Feature-Toggles muss man darauf achten, dass auch Code, der durch die Nutzung des neuen Features nicht mehr benötigt wird, komplett entfernt wird. Nur so kann die Komplexität gering gehalten und die Übersichtlichkeit des Codes aufrechterhalten werden. Um hier den Überblick zu bewahren, gibt es einen einfachen Trick: Durch die Nutzung einer speziellen Annotation kann man Codestellen, die nach dem Entfernen eines Feature-Toggles nicht mehr benötigt werden, kennzeichnen. Listing 6 zeigt ein Beispiel eines Service, der durch die Implementierung eines Features erneuert wurde.

 

Listing 6

public class MyService {

public void connect() {

if( MyFeatures.USE_WEBSOCKET_CONNECTION.isActive() ) {

newConnection();

} else {

oldConnection();

}

}

private void oldConnection() {

...

}

private void newConnection() {

...

}

}

 

Durch ein Feature-Toggle kann zwischen der alten und der neuen Implementierung gewechselt werden. Sollte es nun entfernt werden, muss dies auch bei der alten Methode zur Durchführung des Service geschehen. Dieser Punkt wird allerdings bei der täglichen Arbeit oftmals vergessen. Am Ende enthält die Klasse dann ungenutzten Code. Eine Lösung ist hier eine Annotation, um diesen Code zu kennzeichnen. Für die eigene Anwendung kann man sich solch eine Annotation selbst leicht definieren. Die Annotation sollte einen Wert des Feature-Enum als Value enthalten. Listing 7 zeigt, wie eine solche Annotation aussehen könnte.

 

Listing 7

@Retention(RetentionPolicy.RUNTIME)

@Target({ElementType.TYPE, ElementType.FIELD, ElementType.METHOD,

ElementType.CONSTRUCTOR})

public @interface DeprecatedByFeature {

MyFeatures value();

}

 

Nun kann die veraltete Methode bereits zur Implementierung des Features mit der Annotation versehen werden (Listing 8). Sobald das Feature im Enum entfernt wird, wäre dieser Code nicht mehr kompilierbar, da in der Annotation noch immer auf das Feature referenziert wird. Hierdurch erkennt man mithilfe von Compiler-Errors oder Meldungen in der IDE direkt, welche Codestellen zusammen mit dem Feature entfernt werden müssen.

 

Listing 8

@DeprecatedByFeature(Features.JPA)

public void oldConnection() {

...

} 

Zusätzlich ist es sinnvoll, eine eigene Administrationsoberfläche für Features einzubinden. Im Idealfall wird diese in die Security der Anwendung eingebunden, sodass nur bestimmte Benutzer die Oberfläche zu sehen bekommen. Hier kann man dann eine zur Anwendung passende Ansicht aller Features anzeigen und Aktionen zur Statusänderung von Features definieren. Zu Beginn ist hier die von Togglz mitgelieferte Adminoberfläche ausreichend. Im Produktivbetrieb hat sich aber ein angepasster Administrationsbereich bewährt.

 

ebbers_5

Abb. 5: Zusammenfassung

 

Fazit

Durch Nutzung verschiedener Workflows und APIs bzw. Techniken kann man Feature-driven Development schnell für die Entwicklung einer Anwendung umsetzen. Hierbei bleibt jedem Team überlassen, wie Features genau definiert werden und in welcher Tiefe Feature-Toggles genutzt werden. Oft ist es bereits ein enormer Gewinn, große und kritische Änderungen mit einem Toggle zu versehen oder alle Änderungen im UI durch ein Toggle deaktiviert zu gestalten. Hierdurch kann man schnell und einfach auf Probleme reagieren, ohne dass die komplette Anwendung auf die vorherige Version zurückgefahren werden muss. Für große Plattformen oder Produkte, die zum Beispiel frei im Internet verfügbar sind, eignen sich Feature-Toggles auch sehr gut, um neue Features nur an einer kleinen Untergruppe von Benutzern zu testen. Somit umgeht man die Gefahr, durch einen extremen Bug oder aufgrund einer Fehlentscheidung im Design einen Großteil der Kunden zu verlieren. Weitere Infos und Schaubilder kann man auch in den Präsentationen [6] finden. Eine grafische Zusammenfassung zeigt Abbildung 5.

Aufmacherbild: Different team members working together via Shuttersstock
Urheberrecht: Yabresse

Geschrieben von
Hendrik Ebbers
Hendrik Ebbers
Hendrik Ebbers (@hendrikEbbers) ist Java-Entwickler bei der Canoo Engineering AG. Sein Hauptinteresse liegt hierbei in den Bereichen JavaFX, User Interfaces und Middleware. Hendrik leitet die JUG Dortmund. Auf seiner Webseite www.guigarage.com bloggt er regelmäßig über Architekturansätze im Bereich JavaFX und zu seinen verschiedenen Open-Source-Projekten wie Dolphin Platform oder DataFX. Sein Buch "Mastering JavaFX 8 Controls" ist 2014 bei Oracle Press erschienen. Hendrik ist JavaOne Rockstar, Java Champion und JCP Expert Group Member.
Kommentare

Hinterlasse einen Kommentar

Hinterlasse den ersten Kommentar!

avatar
400
  Subscribe  
Benachrichtige mich zu: