Java Magazin   7.2015 - Web Components

Erhältlich ab:  Juni 2015

Autoren / Autorinnen: 
Sebastian Meyen ,  
Michael Müller ,  
Michael Müller ,  
Kypriani SinarisHartmut SchlosserMarc TeufelMoritz Hoffmann ,  
Hartmut SchlosserComan Hamilton ,  
Brian Goetz ,  
Stefan Glase ,  
Manuel Mauky ,  
Anton Epple ,  
Lars RöwekampArne Limburg ,  
Michael SchäferAchim Müller ,  
Sven Kölpin ,  
Patrick Hillert ,  
Patrick HillertChristian Meder ,  
Dirk Dorsch ,  
Thorben JanssenAnatole Tresch ,  
Peter Roßbach ,  
Daniel Takai ,  
Joachim ArraszChristian Mennerich ,  
Karsten VoigtDavid Broßeit ,  
Tam Hanna ,  
Lars RöwekampArne Limburg ,  

Zurück von der JAX, zu der wir Mitte April wieder 2 000 Gäste begrüßen durften, sind wir noch immer dabei, all die Eindrücke zu verarbeiten und unsere Lehren zu ziehen. Besonders beeindruckt hat uns im Redaktionsteam der große Anklang, den die vielen Vorträge zum Thema „Infrastruktur“ fanden; schon erstaunlich, wenn man bedenkt, dass die Konferenz vor Jahren rein auf das Thema Softwareentwicklung ausgerichtet war …

Moderne Infrastruktur – stets mit dem Gedanken einer radikalen Automatisierung möglichst vieler Abläufe einhergehend – ist heute ohne Zweifel einer der wichtigsten Schlüssel für eine schlagkräftige IT. Unter dem Stichwort „DevOps“ wird dies auch schon lange thematisiert, aber noch immer zu sehr unter den Aspekten „Konfigurationsautomatisierung“ und „Redet doch mal miteinander“.

Die Konfigurationsautomatisierung, typischerweise mit Werkzeugen wie Chef oder Puppet umgesetzt, fokussiert noch sehr auf der Automatisierung „klassischer“ Konfigurationen, was gewiss kein schlechter Ansatz ist, aber möglicherweise zu wenig in die Zukunft der IT weist. Es scheint vielmehr so, dass moderne Infrastrukturautomatisierung nicht ohne die Cloud gedacht werden kann, und dass Konzepte wie Microservices oder Container (typischerweise Docker) eine zentrale Rolle spielen sollten.

Beim Ansatz „Redet doch mal miteinander“ wird DevOps insofern zu kurz gedacht, als dass immer nur die „herkömmlichen“ Operations-Leute und die „herkömmlichen“ Entwickler im Fokus stehen. Es zeichnet sich aber ab, dass sich beide Rollen bald stark verändern müssen und ein Blick aufs „Große Ganze“ für alle Akteure immer wichtiger wird!

Man könnte sagen, dass klassische Java-Experten einerseits auf diese Entwicklung gut vorbereitet sind, da sie stärker als die meisten anderen Entwickler in architektureller Denkweise geschult sind. Andererseits muss man feststellen, dass die klassische Java-Schule über Jahre dahin tendierte, möglichst viele Infrastrukturaspekte (Hardware, Betriebssysteme, Datenspeicher) vor dem Entwickler zu verbergen, in dem Glauben, dass dieser sich dann besser auf die Ausgestaltung der Geschäftslogik konzentrieren könne.

Jedenfalls ist es gut, dass sich die Java-Gemeinde aufmacht und über den größeren Infrastrukturkontext nachdenkt. Wir werden euch auf diesem Weg unterstützen und über interessante Technologien und spannende Entwicklungen informieren.

In diesem Sinne: Viel Spaß bei der Lektüre des neuen Java Magazins!

meyen_sebastian_sw.tif_fmt1.jpgSebastian Meyen, Chefredakteur

Website Twitter Google Xing

In agilen Zeiten wird die Rolle des Architekten manchmal verneint. Dennoch, auch wenn es in einigen Vorgehensweisen keine explizite Architektenrolle gibt, so ist in agilen Projekten, wie in jedem anderen Softwareprojekt, Architekturarbeit erforderlich. Sie wird vom Team, häufiger jedoch von wenigen besonders erfahrenen Personen, durchgeführt. Und so mag sich die Arbeitsweise von der eines klassischen Architekten in bestimmten Aspekten unterscheiden, insbesondere darin, dass solche Architekten dies nicht als Vollzeitjob, sondern neben ihrer Entwicklerarbeit erledigen.

Und so beschreibt Stefan Toth auch nicht die Softwarearchitektur als großes Ganzes, sondern teilt sie in einzelne Praktiken ein, die je nach Intensität der Architekturarbeit einzeln oder kombiniert eingesetzt werden können. Damit spricht das Buch sowohl Teilzeit- als auch Vollzeitsoftwarearchitekten an.

Softwarearchitektur – was ist das?

Der Autor behauptet, wenn man diese Frage zehn Softwareentwicklern stelle, erhalte man zehn unterschiedliche Antworten. Und so definiert er, was er im Buch darunter versteht, wobei er auf eine Definition von Martin Fowler zurückgreift. Zudem erläutert er sein Vorhaben, typische Vorgehensweisen einzeln in Form von Architekturmustern zu beschreiben. Dies ist nicht mit den bekannten Software-Design-Mustern zu verwechseln, die wiederkehrende Problemstellungen lösen. Und auch nicht mit Architekturmustern, die sich mit Softwaremodellierung auf einem höheren Abstraktionsniveau beschäftigen.

In diesem Buch geht es vielmehr darum, wie Architekturarbeit durchzuführen ist, wie man erkennt, wann grundsätzliche Entscheidungen zu treffen sind, wie die Zusammenarbeit im bzw. mit dem Team und anderen Stakeholdern zu gestalten ist und mehr. Die Muster, die Stefan Toth in seinem Buch vorstellt, sind alle relativ kurz gehalten und decken teilweise überlappende Aspekte ab. Diese Kürze erlaubt es dem Leser, sich genau die Ratschläge und Vorgehensweisen herauszupicken, die für die eigene Arbeit gerade sinnvoll sind, und diese je nach Bedarf zu kombinieren.

Aufteilung in Muster

Nach Einleitung und Erläuterung seines Vorhabens gruppiert der Autor seine Muster in vier Bereiche, die durch jeweils eigene Kapitel abgedeckt werden. Zunächst geht es um die Basis der Arbeit, z. B. das Erarbeiten und Pflegen von Anforderungen und deren Abbildung in Szenarien. Weiter wird der Frage nachgegangen, ob Architekturanforderungen immer sofort entschieden werden müssen, oder ob sie als technische Schulden in die Zukunft geschoben werden sollten. Das Kapitel „Richtig entscheiden“ befasst sich mit der Trennung der Architekturarbeit vom Rest – ohne einen Elfenbeinturm zu schaffen – und den Fragen, wann Entscheidungen zu treffen sind, und wie viel im Vorfeld oder im Laufe des Projekts zu entscheiden ist. Dazu gehören auch die vernünftige Abwägung von Risiken oder Ad-hoc-Architektursitzungen. Auch ob eine Zusammenarbeit mit Stakeholdern oder im Team vorzuziehen ist, wird abgewogen, sowie die Frage, ob und wann eher eine einsame oder gemeinsame Entscheidung gefällt werden sollte. Und schließlich der Abgleich mit der Realität: Hier stellt Toth einige Muster vor, beispielsweise dazu, wie Qualität erreicht wird und wie Architekturziele laufend mit dem Zustand der Software abgeglichen werden können.

Fazit

Es handelt sich also nicht um ein klassisches Buch, das Architekturarbeit oder mehr oder weniger sinnvolle Softwarearchitekturen en bloc beschreibt. Durch die Aufteilung in Muster erhält der Leser eher leichtgewichtige Häppchen, die er je nach Bedarf kombinieren kann. Und jedes dieser Muster leitet der Autor mit einem Dialog der Protagonisten – Mitarbeiter eines fiktiven Unternehmens – ein. Dies zeigt den Praxiskontext auf und hilft dem Leser, die Muster entsprechend einzuordnen. Sodann wird die Problemstellung aufgeführt und das empfohlene Vorgehen beschrieben. Durchgängig gibt es außerdem Tipps zum Scheitern, nach dem Motto: So lieber nicht.

Insgesamt ergibt sich so ein Buch, das vom Leser auch als Nachschlagewerk für bestimmte Situationen genutzt werden kann, als Anregung zum Gestalten guter Architektur­arbeit.

hanser_vorgehensmuster.tif_fmt1.jpg

Stefan Toth

Vorgehensmuster für ­Softwarearchitektur

Kombinierbare Praktiken in Zeiten von Agile und Lean

249 Seiten, 34,99 EuroCarl Hanser Verlag, 2013ISBN 978-3-446-43615-2

Die Nutzer von heute stellen hohe Erwartungen an die Benutzeroberflächen ihrer Programme. Ein sofortiges Feedback auf Eingaben und stets aktuelle Informationen werden erwartet, auch ohne dazu ständig auf irgendwelche Aktualisieren- oder Übernehmen-Knöpfe drücken zu müssen. Reactive Programming ist ein Programmierparadigma, das wie geschaffen für die moderne GUI-Entwicklung ist. Im Folgenden soll das Konzept vorgestellt und der Einsatz mit JavaFX gezeigt werden.

Video: Produktives Hacking mit JavaFX

Die Anforderungen an moderne Programmoberflächen stellen GUI-Entwickler vor nicht zu unterschätzende Herausforderungen, für die klassische Ansätze kaum noch geeignet sind oder zumindest einige Probleme bereiten. Das Problem ist, dass unsere Programmiersprachen und -paradigmen im Wesentlichen darauf ausgelegt sind, den Programmablauf zu kontrollieren und zu steuern: zuerst Daten einlesen, verarbeiten und dann ausgeben. Bei interaktiven Programmen kann der Programmablauf aber nicht mehr ausschließlich durch den Entwickler vorgegeben werden, da stattdessen Ereignisse wie Benutzereingaben oder Änderungen in externen Datenquellen über die zeitliche Abfolge des Programms bestimmen, über die der Entwickler keine Kontrolle besitzt.

Die klassische Antwort auf diese Situation heißt Observer, Listener oder Callback. Es wird ein Stück Code registriert, das ausgeführt wird, sobald ein bestimmtes Ereignis, zum Beispiel eine Nutzereingabe, eintritt. Die Nachteile dieses Vorgehens können jedoch beträchtlich sein: Bei nicht trivialen Programmen ist der Programmfluss oft nur noch schwer zu erahnen, vor allem wenn Callbacks verschachtelt sind. Wann und in welcher Reihenfolge welcher Code ausgeführt wird, ist dann nicht mehr ohne Weiteres ersichtlich. Fehler und Bugs sind folglich nur schwer aufzuspüren.

Ein weiterer Aspekt, der Callbacks schwierig handhabbar macht, ist die Koordinierung des Programmzustands: Callbacks sind keine „puren“ Funktionen im Sinne der funktionalen Programmierung, da sie keinen Rückgabetyp besitzen, sondern ausschließlich über Seiteneffekte wirken, also z. B. den Zustand von (aus Sicht des Callbacks) globalen Variablen verändern und auch lesend auf den globalen Programmzustand zugreifen. Dies macht oft umständliche Prüfungen bezüglich des gegenwärtigen Programmzustands notwendig. Unter JavaScript-Entwicklern (und nicht nur bei diesen) ist diese Situation als „Callback-Hölle“ bekannt.

Listing 1

int a = 5;
int b = 3;
 
int c = a + b;
System.out.println(c) // 8
 
a = 4;
System.out.println(c) // ?

Das Paradigma „Reactive Programming“ stellt eine vielversprechende Alternative dar, mit der GUI-Programme effizienter entwickelt werden können. Seit einiger Zeit erlebt der Begriff einen gewissen Hype, tatsächlich ist das Paradigma aber schon älter. Ein häufiges Missverständnis in diesem Zusammenhang soll aber noch ausgeräumt werden, bevor wir uns Reactive Programming im Detail anschauen:

Obwohl der Begriff „Reactive Programming“ oft synonym zu „Functional Reactive Programming“ benutzt wird, ist das in dieser Form nicht ganz korrekt. Reactive Programming lässt sich z. B. sehr wohl auch mit objekt­orientierten Mitteln umsetzen, wie wir in diesem Artikel sehen werden. Richtig ist aber, dass die Entwicklung von reaktiven Programmiersprachen vor allem auf Basis von funktionalen Sprachen stattfand und dort eine große Bedeutung erfährt.

Um Reactive Programming zu verstehen, lohnt sich der Blick auf nicht reaktive Programme. Schauen wir uns dazu das Listing 1 an. Das Beispiel ist für jeden Programmierer trivial, trotzdem wollen wir einmal genau hinschauen, was hier passiert. In einer imperativen Programmiersprache hat dieser Code folgende Semantik: Die beiden Variablen a und b repräsentieren jeweils einen bestimmten Speicherbereich, in den per Zuweisungsoperator die Werte 5 und 3 geschrieben werden. Anschließend werden die aktuellen Werte der beiden Variablen addiert und das Ergebnis wiederum in der Variable c abgelegt. Das erste Sysout gibt folglich 8 aus. Anschließend wird wieder per Zuweisungsoperator der Wert der Variable a verändert. Diese Zeile hat aber nur Auswirkungen auf a, c wird davon nicht beeinflusst, daher gibt auch das zweite Sysout wieder 8 aus. Um das Ergebnis der Addition mit dem neuen Wert von a zu bekommen, müssten wir die Additionsoperation erneut ausführen und den Wert von c im Speicher aktualisieren.

Doch was wäre, wenn wir eine Programmiersprache hätten, in der dieser Code eine andere Semantik besitzen würde? Wir würden die Anweisung int c = a + b so interpretieren, dass c stets die Addition der Variablen a und b enthält, auch dann, wenn sich a oder b ändern. Statt einer Zuweisung stellen wir eine ständige Relation zwischen den Variablen her. Das zweite Sysout würde dann also ohne weiteres Zutun 7 ausgeben. Vereinfacht gesagt stellen reaktive Programmiersprachen genau diese Semantik bereit. Zwei Faktoren sind dazu notwendig: Erstens können Abhängigkeiten zwischen Variablen definiert werden und zweitens werden bei Änderungen einer oder mehrerer Variablen alle abhängigen Variablen automatisch aktualisiert und notwendige Berechnungen erneut ausgeführt. Darum kümmert sich jedoch die Programmierumgebung, nicht der Entwickler.

Java stellt eine solche Semantik freilich nicht direkt zur Verfügung. Allerdings wurden mit der Einführung des GUI-Frameworks JavaFX so genannte Properties und Data Bindings mitgeliefert, mit denen genau diese Art der Programmierung möglich wird. Listing 2 zeigt das obige Beispiel in der Binding-Variante mit JavaFX. Zugegebenermaßen ist der Code aber nicht mehr so kompakt, was u. a. an der fehlenden Unterstützung für Operator-Überladung bei Java liegt.

Listing 2

IntegerProperty a = new SimpleIntegerProperty(5);
IntegerProperty b = new SimpleIntegerProperty(3);
 
NumberBinding c = a.add(b);
System.out.println(c.getValue()) // 8
 
a.setValue(4);
System.out.println(c.getValue()) // 7

a und b sind nun vom Typ IntegerProperty. Sie repräsentieren jeweils einen Integer-Wert, der vom Entwickler gesetzt und gelesen werden kann. c dagegen ist vom Typ NumberBinding. Es kann nur gelesen, jedoch nicht gesetzt werden, da es das Ergebnis einer Binding-Operation repräsentiert. Es kann aber selbst wiederum als Quelle weiterer Binding-Operationen dienen. ­JavaFX bietet von Haus aus eine ganze Reihe von Operationen zum Konstruieren von Bindings an. Neben den Grundrechenarten sind beispielsweise auch Methoden zum Konstruieren von booleschen Bindings vorhanden. Die Anweisung c.isEqualTo(10) erzeugt beispielsweise ein BooleanBinding, das immer dann den Wert true annimmt, wenn c den Wert 10 besitzt.

Der interne Aufbau der JavaFX-Properties ist im Prinzip leicht erklärt. Jede Property wrappt eine Variable des entsprechenden Typs und hat damit die volle Kontrolle über Wertänderungen. Im ersten Schritt bieten Properties nun die Möglichkeit, Observer zu registrieren, um über Wertänderungen benachrichtigt zu werden. Beim Data Binding wird intern genau das getan. Mit der Anweisung a.add(b) wird unter der Haube eine Binding-Instanz angelegt, die Observer bei a und b registriert, sich über Wertänderungen benachrichtigen lässt und in diesem Fall die Addition erneut ausführt. Der Vorteil liegt nun aber darin, dass der Entwickler sich nicht mehr selbst um die Steuerung der Observer kümmern muss.

Wirklich spannend wird das Ganze aber erst, wenn man die hier definierten Properties und Bindings mit einer grafischen Oberfläche verknüpft. Da bei JavaFX alle Controls sämtliche ihrer Eigenschaften ebenfalls als Properties zur Verfügung stellen, könnten wir zum Beispiel zwei Textfelder anlegen, dessen aktuelle Werte wir per Data Binding mit unseren Properties a und b verknüpfen. Tippt der Nutzer Zahlen in die Textfelder ein, sind diese sofort auch in unseren Properties präsent, wodurch auch das Ergebnis-Binding aktualisiert wird. Dieses könnten wir nun mit einem Label verknüpfen, sodass es sofort sichtbar wird. Eine solche Anwendung braucht keinen Berechnen-Button mehr, stattdessen reagiert die Anwendung selbstständig auf die Eingaben des Nutzers. Das hier angegebene Programm ist natürlich nur ein extrem vereinfachtes Beispiel, das aber sicherlich das Potenzial des Paradigmas erkennen lässt.

JavaFX bietet eine solide Basis für reaktives Programmieren. Möchte man aber komplexere Dinge ausdrücken, bieten sich diverse zusätzliche Bibliotheken an. So kann man zwar mit Standardmitteln Bindings für die Grundrechenarten erzeugen, darüber hinaus gehende Operationen fehlen allerdings. Eine mögliche Erweiterung stellt die Open-Source-Bibliothek Advanced-Bindings [1] dar, die vom Autor dieses Artikels entwickelt wurde. Sie enthält unter anderem Data-Binding-Methoden für sämtliche in java.lang.Math enthaltene Operationen, zum Beispiel für die Quadratwurzel oder Sinus und Cosinus.

Ein anderes Beispiel ist eine Binding-Methode für reguläre Ausdrücke. Sie erwartet als Argumente zum einen das RegEx-Pattern und zum anderen den zu prüfenden Text und gibt ein BooleanBinding zurück, das angibt, ob der Eingabetext zum Pattern passt. Selbstverständlich können beide Eingabewerte auch als StringProperties übergeben werden, sodass das Ergebnis stets aktualisiert wird, wenn sich einer der beiden Eingabewerte ändert. Dies kann für einfache Validierungsaufgaben benutzt werden (Listing 3). Dort wird es mit einem SwitchBinding kombiniert: Je nachdem, welchen Wert die StringProperty language enthält, wird ein anderes RegEx-Pattern für die Validierung einer Telefonnummer benutzt (über die Korrektheit der Patterns ließe sich natürlich trefflich streiten). Sobald der eingegebene Wert nicht mehr zum Pattern passt, wird eine Fehlermeldung sichtbar geschaltet, was sowohl durch Änderung des Eingabewerts als auch Änderung der Sprache geschehen kann.

Listing 3

import eu.lestard.advanced_bindings.api.StringBindings;
import eu.lestard.advanced_bindings.api.SwitchBindings;
...
 
StringProperty language = new SimpleStringProperty("DE");
 
final ObservableValue<String> phonePattern =
        SwitchBindings.switchBinding(language, String.class)
            .bindCase("DE", lang -> "\\+?[0-9\\s]{3,20}")
            .bindCase("US", lang -> "^[2-9]\\d{2}-\\d{3}-\\d{4}$")
            .bindDefault(() -> "[0-9 ]{3,20}")
            .build();
 
TextField phoneNumberInput = new TextField();
Label errorMessage = new Label("Falsches Format");
 
final BooleanBinding valid = 
  StringBindings.matches(phoneNumberInput.textProperty(), phonePattern);
 
errorMessage.visibleProperty().bind(valid.not());

Eine weitere nützliche Bibliothek mit ähnlichem Ziel, die hier jedoch nur genannt und nicht im Detail vorgestellt werden soll, ist EasyBind [2]. Genauer betrachten wollen wir nun aber die Bibliothek ReactFX [3], die vom gleichen Autor wie EasyBind stammt. Der Name lässt bereits auf den Anwendungsbereich des Reactive Programmings schließen, der Fokus liegt hier jedoch auf einer anderen Art von Reactive Programming als die bisher gezeigte. Bisher haben wir vor allem den Programmzustand mittels zeitlich variierender Werte abgebildet. Hier stellt JavaFX sehr gute Möglichkeiten zur Kombination und Komposition bereit. Applikationen müssen aber auch mit Ereignissen wie Mausklicks umgehen können. Hierfür stellt JavaFX lediglich die bekannten Observer bereit, eine Möglichkeit zur Komposition fehlt allerdings.

Diese Lücke füllt ReactFX. Dafür stellt es so genannte EventStreams bereit. Sie senden (emittieren) Events, auf die man sich „subscriben“ kann. Das Besondere ist die Möglichkeit zur Komposition: Der Entwickler kann Filter anwenden, mehrere Streams zusammenführen oder einen Stream auf einen anderen Stream abbilden (map).

Listing 4

Pane root = new Pane();
 
final EventStream<MouseEvent> clicks = 
  EventStreams.eventsOf(root, MouseEvent.MOUSE_CLICKED);
 
clicks.filter(click -> click.getButton() == MouseButton.PRIMARY)
            .mapToBi(click -> Tuples.t(click.getX(), click.getY()))
            .map((x,y) -> new Circle(x, y, 10, Color.RED))
            .subscribe(circle -> root.getChildren().add(circle));

In Listing 4 wird zum Beispiel zunächst ein Stream von Mausklicks angelegt. Dieser wird zunächst gefiltert, sodass lediglich Rechtsklicks übrigbleiben. Dieser Stream von Rechtsklicks wird nun auf einen Stream von Koordinaten des jeweiligen Klicks abgebildet. Der Koordinatenstream wird wiederum auf einen Stream von zu zeichnenden Kreisen an der entsprechenden Position abgebildet. Im letzten Schritt werden die Kreise der Anzeige hinzugefügt. Das Programm zeichnet also bei jedem Rechtsklick einen Kreis an die entsprechende Stelle.

Hier werden auch die funktionalen Konzepte deutlich, die im Reactive Programming ihr Potenzial ausspielen können: Für jeden Filter und Abbildungsschritt können tatsächlich „pure functions“ benutzt werden, also Funktionen, deren Rückgabewert ausschließlich von ihrem Eingabewert abhängt und die keine Seiteneffekte besitzen. Ein Seiteneffekt in dem Sinne findet nur ganz am Schluss statt, wenn der resultierende Stream tatsächlich im GUI erscheint.

Das Benutzen von Filtern, Abbildungen usw. im Zusammenhang mit Streams kommt dem interessierten Java-Entwickler aber sicherlich noch aus einer anderen Richtung her bekannt vor: Genau diese funktionalen Konzepte haben mit dem neuen Stream API auch Einzug in Java 8 gehalten. Es gibt jedoch einen wesentlichen Unterschied: Die Java-8-Streams sind nicht reaktiv! Man erzeugt einen Stream (z. B. aus einer Col­lection), führt entsprechende Filter und Abbildungen durch und reduziert den Stream auf das gewünschte Ergebnis. Dieser Reduktionsschritt ist terminal, anschließend kann die Stream-Instanz nicht mehr weiterverwendet werden.

Anders bei den EventStreams von ReactFX. Diese bleiben die ganze Zeit über aktiv. Treffen neue Events auf dem Stream ein, werden die definierten Verarbeitungsschritte erneut durchgeführt. Das Verhalten der Java-8-Streams ist aber kein Fehler bzw. eine Schwäche, im Gegenteil, es handelt sich lediglich um ein anderes Werkzeug für andere Problemstellungen. Gleich ist das prinzipielle Denken: Mengen werden mittels Operationen verarbeitet und auf andere Mengen abgebildet – eine typische Denkweise der funktionalen Programmierung.

Konzeptionell sehr viel näher ist dagegen die Bibliothek RxJava [4], die zusammen mit ihren Pendants für andere Programmiersprachen aus der ReactiveX-Familie vor einiger Zeit den gegenwärtigen Hype für Reactive Programming mit ausgelöst hat. Auch hier dreht sich letztlich alles um Event-Streams, die entsprechend der Anforderungen komponiert und zusammengesetzt werden. Und auch hier sind die bekannten Operationen für Filter, Abbildung, Kombination und Aggregation vorhanden. Der wesentliche Unterschied ist, dass RxJava ihren Fokus auf die asynchrone Verarbeitung der Streams setzt. Damit ist sie zum Beispiel hervorragend für verteilte Applikationen und Netzwerkanwendungen geeignet.

ReactFX setzt dagegen auf synchrone Event-Propagation und Single-Threaded-Verarbeitung [5], was es besonders für die GUI-Entwicklung prädestiniert. Darüber hinaus ist das API direkt für die Integration in JavaFX-Anwendungen optimiert.

RxJava besitzt mit dem Subprojekt RxJavaFX [6] zwar ebenfalls erste Ansätze in dieser Richtung, aktuell ist die Integration aber noch nicht sehr weit fortgeschritten. Mit etwas Handarbeit lässt sich aber auch RxJava mit JavaFX kombinieren, beispielsweise um einen Service einzubinden, der als RxJava-basiertes API vorliegt.

Listing 5 zeigt ein einfaches Beispiel. Die Methode temperatureService liefert einen Stream von sekündlich aktualisierten Temperaturwerten, der von einem Thermometer oder einem Internetdienst stammen könnte. Diese Werte wollen wir in einem Line-Chart anzeigen. Dazu erzeugen wir zusätzlich einen Stream von aufsteigenden Zahlen. Mit der zip-Funktion kombinieren wir anschließend beide Streams und erzeugen einen Stream von Chartdaten, der zum Schluss mit dem JavaFX-Chart verknüpft wird. Das Chart wird dadurch automatisch immer aktualisiert, sobald ein neuer Wert vom Temperaturservice geliefert wird.

Listing 5

final Observable<Long> ascendingNumbers = 
       Observable.interval(1, TimeUnit.SECONDS);
final Observable<Integer> temperature = temperatureService();
 
final Observable<XYChart.Data<Number, Number>> dataObservable =
        Observable.zip(ascendingNumbers, temperature, XYChart.Data::new);
 
 
final LineChart<Number, Number> lineChart =
        new LineChart<>(new NumberAxis(), new NumberAxis());
XYChart.Series<Number, Number> series = new XYChart.Series<>();
lineChart.getData().add(series);
 
dataObservable.subscribe(data -> series.getData().add(data));

Fazit

Wir haben gesehen, dass Reactive Programming ein interessantes Thema ist, ganz besonders auch für GUI-Entwickler. Es ermöglicht die Vermeidung von komplizierten und fehlerträchtigen Callback-Gerüsten und setzt stattdessen den Fokus auf die Beschreibung von Abhängigkeiten zwischen Werten und ihrer automatischen Aktualisierung. Vor allem bietet Reactive Programming einen sehr interessanten Ansatz, um die objektorientierte und funktionale Programmierung zu kombinieren bzw. das Beste aus beiden Welten zu vereinen.

Auch wenn die Reactive-Programming-Community aktuell ihren Blick sicherlich klar auf Event-Stream-basierte Ansätze richtet, ist auch der klassische Ansatz interessant. Dass dazu nicht zwingend eine funktionale Programmiersprache notwendig ist, sondern auch geeignete Abstraktionen über objektorientierte Design Patterns ausreichen, ist eine interessante Erkenntnis.

JavaFX bietet dem Entwickler bereits gute Grundlagen, um reaktive Anwendungen zu programmieren. Darüber hinaus stehen einige Bibliotheken bereit, die dem Entwickler die Arbeit erleichtern. Ein Aspekt, der im Artikel nicht betrachtet wurde, ist die Frage nach einer geeigneten Frontend-Architektur. Hier steht zum Beispiel mit dem Model View ViewModel Design Pattern [7] ein Architekturmuster bereit, das selbst Elemente von Reactive Programming beinhaltet und sehr gut für die Entwicklung von reaktiven GUI-Programmen geeignet ist.

Der vollständige Quellcode für alle Beispiele kann auf GitHub [8] angeschaut und ausprobiert werden.

mauky_manuel_sw.tif_fmt1.jpgManuel Mauky arbeitet seit 2010 als Softwareentwickler bei der Saxonia Systems AG in Görlitz. Er beschäftigt sich dort vor allem mit Java Enterprise und der UI-Entwicklung mit JavaFX. Aktuell arbeitet er an der Open-Source-Bibliothek mvvmFX zur Umsetzung von Model View ViewModel mit JavaFX.

Web

Durch Projektionen lassen sich passgenau die Informationen bereitstellen, die in einer Enterprise-Anwendung benötigt werden. Das Flugplanungssystem einer Fluggesellschaft etwa ist eine typische Enterprise-Anwendung. So benötigen Mitarbeiter einer Fluggesellschaft, abhängig vom Anwendungsfall, ganz unterschiedliche Informationen, seien es detaillierte Angaben über eine Flugverbindung oder die zu einer Flugverbindung gehörenden Flüge. Wie können wir diese unterschiedlichen Projektionen bereitstellen? Im Folgenden zeigen wir, welche methodischen und technischen Probleme das aufwirft und wie sie sich lösen lassen.

Enterprise-Anwendungen sind Unternehmensanwendungen, die die Mitarbeiter einer Organisation bei der Ausführung ihrer täglichen Aufgaben unterstützen. Mit ihnen werden die Geschäftsprozesse in den Unternehmen optimiert. Die Anforderungen an Unternehmensanwendungen, insbesondere an die Usability, werden immer höher. Daher verwenden moderne Unternehmensanwendungen JavaScript-MVC-Frameworks wie AngularJS, Backbone.js, oder CompontentJS [1] in Kombination mit HTML und CSS. Wir sprechen in diesem Fall von einer Rich-Client-Architektur [2]. Der Rich-Client besitzt im Gegensatz zum Thin-Client Teile der Präsentationslogik und Ressourcen. So kann er autonom Oberflächenelemente erzeugen und Dialogabläufe steuern. Die Usability der Anwendung erhöht sich entscheidend. Und der Server wird dann zu einem schlanken, einem Thin-Server, der im Wesentlichen für die Bereitstellung von Ressourcen verantwortlich ist. Er stellt die Ressourcen über einen CRUD-Service [3] bereit, der über ein REST-API angesprochen wird. Bei der Ausführung des REST-API beschränken wir uns auf die REST-Prinzipien nach dem Maturity Model – Level 2 von Richardson [4]. Das heißt, Hypermedia kommt nicht zum Einsatz sondern nur URIs, die über HTTP aufgerufen werden. In dem beschriebenen Anwendungskontext bringt Hypermedia für Unternehmensanwendungen keinen entscheidenden Mehrwert. Der Aufwand wäre nicht gerechtfertigt. Das haben Arbeiten wie [5] und die Praxis gezeigt.

Starten wir das Gedankenexperiment. Eine Fluggesellschaft will wissen, welche Ziele sie von München aus anfliegt. Mit einem URI-Aufruf auf einem CRUD-Service ließen sich alle Flugverbindungen vom Server auf den Client übertragen. Schränken wir die Sicht dabei nicht ein, erhalten wir alle Informationen der Flugverbindungen einschließlich der bereits ausgeführten und geplanten Flüge. Im schlimmsten Fall kommen noch weitere mit der Flugverbindung verlinkte Ressourcen wie Personal, Besatzung oder Flugzeuge hinzu. Das wäre ein sehr einfaches REST-API, allerdings nicht sehr effizient. Der Anwender würde lange auf den Bildschirm starren, bis alle Ressourcen geladen sind. Keine gute Idee! Wie lassen sich also nur die Daten einer Ressource vom Server laden und übertragen, die auch wirklich benötigt werden? Wir brauchen eine definierte Sicht auf die Ressourcen auf dem Server. Die Lösung heißt: Projektionen.

Projektion in REST

Eine Projektion stellt eine andere Sicht auf eine bestehende Ressource dar. Sie enthält nur einen Teil der bestehenden Informationen.

Sucht der Anwender nach den Routen, die montags in München starten, bekommt er mit dem Aufruf eines URI und einer dafür definierten Projektion genau das, was er sucht. Die Bereitstellung von Projektionen wirft zwei Fragen auf: Wie lassen sich Projektionen sinnvoll auf Ressourcen abbilden? Und wie setzt man das technisch um?

Projektionen im DDD

Klar ist: Projektionen auf Ressourcen ergeben sich in erster Linie aus dem Anwendungsfall und der Fachlichkeit. Für die Modellierung der Projektionen liefert das Domain-driven Design (DDD) von Eric Evans [6] einen methodischen Rahmen. Wer mehr zu DDD wissen will, dem empfehlen wir „Domain-Driven Design Quickly“ [7], eine kurze Zusammenfassung des Standardwerks von Eric Evans.

Warum haben wir uns für DDD entschieden? Zunächst ist DDD eine fundierte Softwaredesignmethode, die gerade in den letzten Jahren wieder an Bedeutung gewonnen hat. Außerdem lassen sich Projektionen mit den Domain-Pattern Entities, Aggregate, Aggregate Root und Repositories sehr gut definieren.

schaefer_mueller_1.tif_fmt1.jpgAbb. 1: Entities, Aggregates und Repositories

Abbildung 1 zeigt den Zusammenhang zwischen Entities, Aggregates und Repositories. Entities sind alle Subjekte, die unsere Beispielanwendung erfasst, z. B. die Route, den Flug oder den Mitarbeiter. Sie bilden die kleinste Einheit. Ein Aggregate bündelt inhaltlich zusammenhängende Entities, etwa Route und Flug. Ein konkreter Flug kann ohne die Route mit Flugnummer und Start- bzw. Zielflughafen nicht existieren. Die Entity Route nennt man Aggregate Root Entity, da von ihr alle anderen Entities abhängig sind. Über Repositories können Aggregate Root Entities erstellt, gelesen, verändert oder gelöscht werden.

Kommen wir nun zu der Methodik und definieren Regeln für die Abbildung von Projektionen in einem existierenden Domain-Model, wie in Abbildung 2 dargestellt.

schaefer_mueller_2.tif_fmt1.jpgAbb. 2: Projektionen in einem Aggregat

Ressourcen in DDD

Gedanklich entspricht eine Ressource in REST einer Aggregate Root Entity in DDD. Eine Projektion entspricht somit einem Teil der verfügbaren Informationen einer Aggregate Root Entity.

Wir sehen zwei Aggregate A1 und A2. Die gestrichelten Linien definieren die Grenzen der Projektionen p1, p2, p3 und p4. Schauen wir uns zunächst die Projektion p1 aus dem Aggregate A1 an. Es handelt sich dabei um eine Projektion auf die Entity e1 der Aggregate Root Entity. Wir erhalten mit der Projektion p1 eine Sicht auf ausgewählte Attribute aus e1. Die Projektion p2 erweitert die Projektion p1 um eine Sicht auf die Entity e2. Wir haben mit p2 somit eine Projektion auf e1 und e2. Bei beiden Projektionen handelt es sich um eine Root Projection, da sie eine Sicht auf die Aggregate Root Entity bereitstellen. Die dritte Projektion p3 in Aggregate A1 liefert uns eine Sicht auf die Entity e3. Diese Projektion steht nur innerhalb des Aggregates zur Verfügung; wir sprechen von einer Anonym Projection. In Aggregate 2 haben wir nur eine Root Projection p4 definiert. Es ist erlaubt, dass eine Root Projection auf eine andere Root Projection in einem anderen Aggregate verweist. Es ist auch erlaubt, dass eine Anonym Projection auf eine Root Projection verweist.

Regeln für die Definition von Projektionen in DDD

  • Eine Projektion ist eine definierte Menge von Entities innerhalb eines Aggregate.

  • Es können beliebig viele Projektionen in einem Aggregate definiert werden.

  • Enthält die Projektion die Aggregate Root Entity, sprechen wir von einer Root Projection.

  • Ausschließlich Root Projections können über das Repository nach außen bereitgestellt werden.

  • Eine Root Projection darf eine Root Projection aus einem anderen Aggregate referenzieren.

Die erste Frage, wie sich Projektionen sinnvoll auf Ressourcen abbilden lassen, ist damit geklärt. Unter Einhaltung der fünf Projektionsregeln haben wir ein strukturiertes Vorgehen auf Grundlage von DDD, mit dem wir Projektionen auf Aggregate Root Entities bzw. Ressourcen definieren können.

DTOs vs. Spring Data REST Projections

Projektionen werden in Enterprise-Anwendungen oft über das Data Transfer Object (DTO) Pattern abgebildet, bekannt aus dem Buch „Enterprise Pattern“ von Martin Fowler [8]. Ein DTO kapselt die Projektion in einer DTO-Klasse. Für die Konstruktion der DTO-Klasse benötigt er einen DTO-Assembler, der die Instanziierung und Befüllung der DTO-Klasse übernimmt. Das hat einen entscheidenden Nachteil. Für jede Projektion sind eine DTO-Klasse und ein DTO-Assembler zu implementieren. Das führt bei steigender Anzahl von Projektionen schnell zu einem enormen Wartungsaufwand und einer großen Menge an Boilerplate-Code. Spring Data REST (SDR) Projections hingegen werden, wie in Listing 1 zu sehen, über ein Interface definiert.

Listing 1: Spring Projections

@Projection(name = "flightplan", types = {Route.class})
public interface FlightplanProjection {
  String getFlightNumber();
}

Das Interface wird mit der Annotation @Projection deklariert und damit zur Projektion. Die Daten werden über die Definition von Getter-Methoden innerhalb des Interface bereitgestellt. Die SDR-Projection ersetzt die DTO-Klasse. Die Implementierung einer DTO-Klasse entfällt. Und die Instanziierung und Befüllung der SDR-Projection erfolgt automatisch durch einen Proxy. Somit entfällt die Implementierung eines DTO-Assemblers. Wie können wir nun die SDR-Projections in unserer Enterprise-Anwendung verwenden?

SDR-Projections in Enterprise-Anwendungen

Projections sind ein Feature von Spring Data REST (SDR), das Mitte 2014 mit dem Release 2.1 eingeführt wurde. SDR stellt ein REST-API für einen einfachen Zugriff auf JPA, MongoDB, Gemfire, und Neo4j Repositories unter strenger Einhaltung aller REST-Prinzipien wie HTTP, URIs, Hypermedia über HAL und Caching. Mit SDR sind somit Hypermedia-Services nach dem Maturity Model – Level 3 von Richardson [4] – möglich.

Da wir bei Enterprise-Anwendungen aber kein Hypermedia verwenden, stellt sich die Frage: Wie können wir Hypermedia in SDR abschalten? Das Abschalten von Hypermedia ist von SDR explizit nicht gewollt [9], da SDR ausschließlich für REST-Architekturen im Sinne von Roy Fielding ausgelegt ist. Wie also können wir das Feature dennoch verwenden? SDR stellt ein Common-Paket bereit, das von anderen Spring-Projekten verwendet werden kann. Listing 2 zeigt, wie einfach über einen Spring-MVC-Controller eine Projektion bereitgestellt werden kann.

Listing 2: Projections in Spring MVC

@RestController
@RequestMapping("/routes")
public class RouteController {
  @Autowired private RouteRepository rr;
  @Autowired private ProjectionFactory pf;
  
  @RequestMapping("{id}/connection")
  public ResponseEntity<CP> getConnection(@PathVariable(value="id") Long id) {
    Route routes = routeRepository.findOne(id);
    CP cp =  pf.createProjection(CP.class, routes);
    return new ResponseEntity<ConnectionProjection>( cp,HttpStatus.OK);
  }

Die SDR-Projection CP muss dafür über eine Factory-Klasse manuell erstellt werden. Dazu ist eine ProjectionFactory notwendig, die über eine Bean Definition mit SpelAwareProxyProjectionFactory instanziiert und über @Autowired eingebunden wird. Die Projektion wird über die Methode createProjection() erzeugt.

Ran an den Code: Routenplanung für unsere Fluggesellschaft

Kommen wir zur konkreten Umsetzung. Für unsere Fluggesellschaft entwickeln wir ein Flugplanungssystem. Die Fluggesellschaft pflegt darin ihre Flugzeuge (Aircraft), Mitarbeiter (Employee) und Flugverbindungen (Route) ein. Sie kann dann auf den Routen konkrete Flüge (Flight) durchführen, denen sie bestimmte Mitarbeiter zuweist. Eine Route wird dabei durch die Flugnummer identifiziert. Sie besteht aus dem maximal dreistelligen Airline-Code, z. B. „LH = Lufthansa“ und einer bis zu vierstelligen Nummer, z. B. „LH7902“. Die Flugnummer ist außerdem für eine bestimmte Strecke zu einer bestimmten Uhrzeit unabhängig vom Wochentag immer gleich. Sie wird im Flugplan mit den Verkehrstagen aufgeführt, an denen die Route bedient wird. Wird eine Verbindung an einem Tag zweimal geflogen, dann wird eine zweite Flugnummer vergeben, und es handelt sich im Sinne unserer Definition um eine zweite Route. Die Verbindung aus Flugnummer und Datum stellt einen konkreten Flug auf der Route dar. So finden sich etwa im Flugplan der Lufthansa [10] mit Abflughafen München die in Abbildung 3 zu sehenden Einträge.

schaefer_mueller_3.tif_fmt1.jpgAbb. 3: Ausschnitt aus dem Flugplan der Lufthansa

Die Route „München – Houston“ wird täglich um 9:30 Uhr unter der Flugnummer „LH7902“ mit einem Flugzeug vom Typ B764 bedient, während die Route „München – Ibiza“ samstags (Tag 6) unter der Flugnummer „LH1602“ mit einem A320 geflogen wird. Da wir auch noch die Besatzung des Flugs planen wollen, enthält unser Flugplanungssystem in Abbildung 4 als konkrete Ausprägungen der Mitarbeiterrolle (Role) zusätzlich Piloten (Pilot) und Kabinenpersonal (CabinAttendant).

schaefer_mueller_4.tif_fmt1.jpgAbb. 4: Das Datenmodell als Graph

Auf Basis des Domain-Model wünscht sich die Fluggesellschaft nun drei User Stories:

  • Übersicht aller Routen

  • Erzeugung eines Flugplans wie in Abbildung 3

  • Mitarbeitereinsatzplanung mit der Detailansicht eines Flugs und der Crew

Der Sourcecode unseres Beispiels ist auf GitHub [11] zu finden. Der Anwendungsrahmen wird denkbar einfach durch Spring Boot vorgegeben. Die Entities aus unserem Domain-Model werden mit JPA-Annotationen versehen und nach Aggregates getrennt in verschiedenen Java-Packages abgelegt. So befinden sich im Employee-Package die Entities Employee, CabinAttendant, Pilot und Role, im Aircraft-Package nur die Entität Aircraft und im Route-Package die Entitäten Flight und Route. Für jedes Aggregate Root aus jedem Aggregate gibt es nun ein Repository AircraftRepository, EmployeeRepository und RouteRepository, welches sich um das Persistieren aller Entities im Aggregate kümmert.

User Story 1 : Übersicht aller Routen

Die Definition eines Repositorys ist mit Spring Data (SD) trivial und besteht nur aus einer Zeile. Wir leiten ein neues Interface RouteRepository vom SD-Interface CrudRepository ab, das auf die Aggregate Root Entity Route mit dem entsprechenden Schlüssel typisiert wird. Alle unsere Entities haben eine gemeinsame Basisklasse AbstractEntity, die einen technischen Schlüssel id vom Typ Long definiert:

public interface RouteRepository extends CrudRepository<Route, Long> {
}

Der Zugriff auf das Repository erfolgt über einen CRUD-Service, der über ein REST-API exponiert wird. Das REST-API wird, wie in Listing 3 gezeigt, über Spring MVC Controller realisiert.

Listing 3: CRUD-Service mit Spring MVC

@RestController
@RequestMapping("/routes")
public class RouteController {
  @Autowired
  private RouteRepositoryrouteRepository;
  
  @RequestMapping("{id}")
  public ResponseEntity<Route> get(@PathVariable(value="id") Long id) {
    Route route = routeRepository.findOne( id);
    return new ResponseEntity<Route>(route,HttpStatus.OK);
  }

So erhalten wir unter http://localhost:8080/routes/{id} noch ohne Projection bereits eine Route. Inbegriffen sind alle Attribute einer Route. Die zugehörigen Flüge auf der Route usw. sind darin eingebettet. Neben der Tatsache, dass wir die Flüge in unserer ersten User Story gar nicht benötigen, stört auch die Darstellung der Uhrzeit als eigenständiges Objekt. Für die geforderte Übersicht aller Routen kommt nun unsere erste SDR-Projection zum Einsatz. Wir definieren ein neues Interface ConnectionProjection und annotieren es mit @Projection. Mit dem Attributwert name= connection legen wir den Namen der Projection fest und mit dem Attributwert types=Route.class die Entity, auf welche die Projektion angewandt werden soll.

Doch wie definieren wir nun, welche Felder in unserer Projektion angezeigt werden sollen? Wir listen einfach die gewünschten Getter-Methoden aus der Entity-Klasse auf: Flugnummer, Abflughafen, Zielflughafen und Uhrzeit. Die Schreibweise muss dabei genauestens eingehalten werden. Was uns in der Ausgabe noch stört, ist die unhandliche Darstellung der Uhrzeit als Objekt. Hier werden sich die Clientprogrammierer zu Recht beklagen, zumal Implementierungsdetails des Servers unnötigerweise veröffentlicht werden. Hier lässt sich Abhilfe schaffen, indem wir den entsprechenden Getter getDepartureTime() mit einer @Value-Annotation versehen. Der übergebene Wert benutzt die Spring Expression Language (SpEL). Mit dem Aufruf #{target.departureTime.toString()} formatieren wir die Uhrzeit in das Standardformat hh:mm. Die @Value-Annotation ist damit ein wertvolles Hilfsmittel, um Werte für die Projektion zur Laufzeit zu berechnen. Die fertige Projektion zeigt Listing 4.

Listing 4: Die Connection-Projection

@Projection(name = "connection", types = Route.class)
public interface ConnectionProjection {
  String getFlightNumber();
  String getDeparture();
  String getDestination();
  @Value("#{target.departureTime.toString()}")
  String getDepartureTime();
}

Der RouteController in Listing 5 ist entsprechend Listing 4 zu erweitern. Die Projection Connection Projection ist im Code für eine bessere Lesbarkeit mit CP abgekürzt.

Listing 5: RouteController mit Projektion

@RequestMapping("{id}/connection")
public ResponseEntity<CP> getConnection(@PathVariable(value="id") Long id) {
  Route routes = routeRepository.findOne( id);
  CP connectionProjection = pf.createProjection(CP.class, routes);
  return new ResponseEntity< CP >(connectionProjection,HttpStatus.OK);
}

Die Projektion können wir nun aufrufen, indem wir den URI um den Path-Parameter connection ergänzen: http://localhost:8080/routes/{id}/connection (Listing 6).

Listing 6: Die Ausgabe der Connection-Projection (Auszug)

{
  "flightNumber" : "LH7902",
  "departure" : "MUC",
  "destination" : "IAH",
  "departureTime" : "09:30",
}

User Story 2: Erzeugung eines Flugplans

Kommen wir zu unserem zweiten Anwendungsfall: dem Flugplan. Wir benötigen als zusätzliche Felder die Flugtage der Verbindung und den Flugzeugtyp (Abb. 3). Die Flugtage werden am Beginn der Zeile durch eine Auflistung von Zahlen (1 = Montag etc.) oder ein X für tägliche Verbindungen angezeigt. Da die Darstellung Sache des Clients ist, beschränken wir uns im Server auf die Übergabe der Rohdaten: die einzelnen Wochentage, so wie wir die Informationen auch gespeichert haben. Erhält der Client ein Set mit allen Wochentagen, dann entscheidet er selbstständig über die Ausgabe des X. Um den Flugzeugtyp zu ermitteln, müssen wir die Aggregate Root Entity Aircraft mit in die Projektion aufnehmen.

Dazu definieren wir zunächst im Aircraft Aggregate eine Root Projection AircraftProjection nach dem gleichen Muster wie in Listing 4 und geben in der Projektion über die Getter-Methode getType() den Typ zurück. Diese Projektion verwenden wir nun innerhalb der Root Projection FlightPlanProjection (Listing 7).

Listing 7: Die Flightplan Projection

@Projection(name = "flightplan", types = Route.class)
public interface FlightplanProjection extends ConnectionProjection {
  Set<DayOfWeek> getScheduledWeekdays();
  @Value("#{@routeService.formatTime(target.arrivalTime)}")
  String getArrivalTime();
  AircraftProjection getAircraft() ;
}

Die Projektion FlightplanProjection erweitert über extends die Projektion ConnectionProjection. Es sind somit nicht mehr alle Getter-Methoden aus ConnectionProjection neu zu implementieren. Der Aufruf http://localhost:8080/routes/flightplan führt zur Ausgabe (Listing 8).

Listing 8: Ausgabe der Flightplan ­Projection (Auszug)

routes" : [ {
  "flightNumber" : "LH7902",
  "departure" : "MUC",
  "destination" : "IAH",
  "departureTime" : "09:30",
  "scheduledWeekdays" : 
  ["MONDAY", "TUESDAY", "WEDNESDAY", 
    "THURSDAY", "FRIDAY", "SATURDAY", "SUNDAY" ],
  "arrivalTime" : "14:00",
  "aircraft" : {
    "type" : "B764"
  },

Die Flightplan Projection zeigt noch ein weiteres interessantes Feature: Die Spring Expression Language erlaubt auch das Aufrufen einer Servicemethode, um den Wert für ein Feld in der Projection zu berechnen. Wenn wir mit der Standardformatierung für LocalTime nicht einverstanden sind, können wir die Formatierung mit dem Ausdruck ("#{@routeService.formatTime(target.departureTime)} auch in eine Servicemethode auslagern. Dort könnten wir auch Berechnungen durchführen und so völlig neue Felder in die Projection einfügen, z. B. die Anzahl der Flüge auf einer Route etc.

Aber was machen wir, wenn wir den Flugplan nur für einen bestimmten Abflughafen ausgeben wollen? Hier kommt uns Spring Data zu Hilfe, und wir können unser Repository einfach durch Hinzufügen einer findBy-Methode erweitern, in diesem Falle findByDeparture (Listing 9).

Listing 9: Erweiterung des Repositorys um eine „findBy“-Methode

public interface RouteRepository extends CrudRepository<Route, Long> {
  @EntityGraph("byDepature")
  @Query("select r from Route r where r.departure = :departure")
  public Iterable<Route> findByDeparture(@Param("departure") String eparture);
}

Damit das funktioniert, müssen wir unseren RouteController so erweitern, dass er den Suchparameter departure als Query-Parameter entgegennimmt und an das Repository übergibt. Der URI könnte dann wie folgt lauten: http://localhost:8080/routes/flightplan?departure=MUC.

Als Ergebnis erhalten wir alle Verbindungen für den Flughafen München. Listing 9 zeigt noch einen weiteren interessanten Aspekt. Mit der Annotation @EntityGraph aus JPA 2.1 steuern wir, dass nur die Daten aus der Datenbank gelesen werden, die wir für die Projektion wirklich benötigen.

User Story 3: Mitarbeitereinsatzplanung

Kommen wir zu unserem letzten Beispiel, der Mitarbeitereinsatzplanung. Hier schauen wir uns das Thema Vererbung näher an. Unsere Projection crew soll die Flüge auf einer Route inklusive der Mitarbeiter mit Namen und Rolle anzeigen. Listing 10 zeigt die Root Projection EmployeeProjection, die in die CrewProjection eingebunden wird.

Listing 10: Die Employee Projection

@Projection(name = "employeeRole", types = Route.class)
public interface EmployeeProjection {
  @Value("#{target.firstName} #{target.lastName}")
  String getName();
  Role getRole();
  // Funktioniert nicht 
  // @Projection(types = { Role.class,//  CabinAttendant.class, Pilot.class})
  // public interface RoleProjection {
    // String getCertificateNumber();
  //}
}

Beim Aufruf der Projektion crew fällt auf, dass die Rolle getRole() mit den Attributen der Basisklasse und der konkreten Ausprägung der Kindklasse gerendert wird. Leider ist es nicht möglich, mithilfe einer weiteren Projection nur einzelne Attribute einer konkreten Kindklasse in die Ausgabe der Perspektive zu packen, z. B. die Certificate­Number des Piloten, wie in Listing 10 auskommentiert. Das ist zwar grundsätzlich möglich, funktioniert aber nur bei homogenen Collections, d. h., alle Elemente sind Exemplare derselben Klasse. Listing 11 zeigt die Ausgabe der crew Projection mit einer inhomogenen Collection.

Listing 11: Ergebnis der „crew“ Projection

{
  "flightNumber" : "LH7902",
  "departure" : "MUC",
  "departureTime" : "09:30",
  "flights" : [ {
    "date" : "2015-09-23",
    "employees" : [ {
      "name" : "Fred Flieger",
      "role" : {
        "roleName" : "Pilot",
        "certificateNumber" : "RF775566734",
        "allowedAircrafts" : [ "A320", "B764" ]
      }
    }, {
      "name" : "Tom Purser",
      "role" : {
        "roleName" : "CabinAttendant",
        "rank" : 1
      }
    } ]
  },

Fazit

Mit der neuen Spring-IO-Plattform ist in Spring eine Fokussierung in Richtung Cloud, Microservices und Big Data zu beobachten. Diese Technologien haben sich im beschriebenen Enterprise-Umfeld bisher nicht durchgesetzt. Daher können aktuell viele Features nicht genutzt werden. Das zeigt sich auch bei Spring Data REST (SDR) Projections. In Enterprise-Anwendungen bringt Hypermedia und das HAL-Rendering keinen Mehrwert. Beide können in SDR aber nicht abgeschaltet werden. Die alternative Nutzung der SDR-Projections über Spring MVC Controller führt dazu, dass wichtige Features wie die Integration der SDR-Projections in URI-Query usw. nicht genutzt werden können.

Unser Beispiel zeigt allerdings das Potenzial, das SDR-Projections auch für Enterprise-Anwendung haben. Sie ermöglichen uns eine einfache Implementierung der Projektionen mithilfe des SDR-Common-Pakets und Spring MVC Controllern. Der Boilerplate-Code, den wir mit DTOs hätten, entfällt. So können wir Projections auch in Enterprise-Anwendungen ohne Hypermedia und HAL-Rendering verwenden.

Im Rahmen von Domain-driven Design (DDD) haben wir Regeln gefunden, SDR-Projections in einem Aggregate zu definieren. Damit haben wir ein strukturiertes Vorgehen, um SDR-Projections in Enterprise-Projekten zu realisieren.

schaefer_michael_sw.tif_fmt1.jpgMichael Schäfer arbeitet als Lead IT Consultant im Bereich Applied Technology Research bei der msg-systems AG in München. Sein Schwerpunkt liegt auf der Spring-IO-Plattform. In diesem Bereich ist er in Projekten, als Trainer und Autor unterwegs.

mueller_achim_sw.tif_fmt1.jpgAchim Müller ist als Lead IT Consultant im Bereich Applied Technology Research bei der msg-systems AG in München tätig. Er besitzt langjährige Erfahrung in der Entwicklung komplexer und hochwertiger Softwaresysteme, insbesondere mit Java EE und Spring-Architekturen. Aktuell interessiert ihn besonders das Design von REST-Schnittstellen für HTML5-Rich-Clients.

Die wartbare Integration von JavaScript in serverseitig getriebene Webanwendungen stellt eine große Herausforderung dar. Dazu existiert bisher kein standardisierter Weg. Bislang greifen viele auf Inline-Scripting zurück, was oft zu Nachteilen führt. Dieser ­Artikel zeigt, wie JavaScript und JSF sauber zusammengeführt werden können.

Video: Oberflächen mit Tiefgang

Die Verwendung von JavaScript ist bei der Entwicklung von Webanwendungen nicht mehr wegzudenken. Um die Wartbarkeit einer Webapplikation auch noch bei einem hohen JavaScript-Anteil zu garantieren, ist eine strikte Codeorganisation im Frontend von großer Wichtigkeit. Die Erfahrung zeigt jedoch, dass dies bei JSF nur schwierig zu erreichen ist. Der Grund dafür liegt im deklarativen und serverseitigen Ansatz des Frameworks, bei dem die Kapselung clientseitiger Logik in XML-Tags einer direkten Verwendung von JavaScript vorgezogen wird (vgl. Client Behaviors).

Es existiert demzufolge bisher kein standardisierter Weg für die unmittelbare Integration von eigenem JavaScript-Code in JSF-Komponenten. Ganz im Gegenteil: Schaut man in die aktuelle JSF-Spezifikation, so scheint diese die Möglichkeit der Interaktion zwischen JSF und JavaScript, mit Ausnahme der für die AJAX-Funktionalität benötigten eigenen JavaScript-Bibliothek, vollständig zu ignorieren. Deshalb wird bei der Verbindung von JavaScript mit JSF in den meisten Fällen einfach Inline-Scripting, also das direkte Einbetten von JavaScript-Code im HTML, verwendet. Eine häufige Folge dieses Ansatzes ist schwierig zu wartender „Spaghetticode“, der an die früheren JSP-Zeiten erinnert, mit dem Unterschied, dass nun JavaScript anstatt Java mit HTML vermischt wird.

Separation of structure and behavior

Unter dem Oberbegriff „unobtrusive JavaScript“ [1] werden seit einigen Jahren Paradigmen für den Einsatz von JavaScript in Webanwendungen zusammengefasst. Eines der Hauptprinzipien dieser Sammlung betrifft die strikte Trennung von JavaScript und HTML („Separa­tion of structure and behavior“).

Das Trennen von Markup und Scripting hat viele Vorteile. Aus dem Blickwinkel der Arbeitsorganisation betrachtet, bietet sich der Nutzen einer klaren Aufgabentrennung. Webdesigner erstellen das Markup und Design, während Webentwickler die nötige GUI-Logik in eigenen JavaScript-Dateien implementieren können.

Der größte Vorteil der Trennung ergibt sich aber in erster Linie aus der erzwungenen Codeorganisation: Inline-Scripting macht Projekte auf Dauer unwartbar, weil es keine eindeutigen Anlaufpunkte für clientseitige Logik und Module gibt. Vor allem mit wachsender Codebasis führt Inline-Scripting häufig zu einer Verletzung vieler Grundprinzipien der modernen Softwareentwicklung (z. B. Wiederverwendbarkeit (DRY) oder Separation of Concerns). Des Weiteren erweist sich die Fehlersuche mit den im Browser vorhandenen Tools bei einer Vermischung von JavaScript mit HTML oft nicht als trivial.

Die Auslagerung der Skriptlogik in externe Dateien hingegen zwingt zur Modularisierung und führt somit implizit zu einer besseren Organisation und zu einer höheren Wiederverwendbarkeit des clientseitigen Codes.

Status quo und Herausforderungen

Leider birgt die größte Stärke von JavaScript, nämlich die Dynamik der Sprache, bei fehlender Erfahrung große Gefahren – es gibt eine enorme Vielzahl an Wegen, die zum gleichen Ziel führen (Listing 1). Aufgrund dessen ist man in einem Enterprise-Projekt förmlich dazu gezwungen, feste Grundstrukturen für die Entwicklung von JavaScript-Komponenten vorzugeben, um so einheitlichen und damit wartbaren JavaScript-Code zu garantieren. Das kann entweder explizit über vorher definierte Entwicklungsrichtlinien oder implizit mithilfe von clientseitigen Bibliotheken erreicht werden.

Listing 1: JavaScripts Dynamik: Tausend Wege führen nach Rom

var SomeNameSpace = SomeNameSpace || {}; 
//constructor function
SomeNameSpace.DatePicker = function () { 
  this.init = function () { 
  }; 
}; 
//module pattern
SomeNameSpace.DatePicker = (function () { 
  var init = function () { 
  }; 
  return { 
      init: init 
  }; 
})(); 
//js-object
SomeNameSpace.DatePicker = { 
  init: function () { 
  } 
}; 
//es6 class
class DatePicker { 
  init() { 
  }
};

Eine zweite Herausforderung liegt darin, Daten aus JSF in JavaScript verfügbar zu machen. Bei der Erstellung wiederverwendbarer JSF-Komponenten ist beispielsweise die Übergabe des jeweiligen ID-Präfixes einer Komponente an ein JavaScript-Modul essenziell. Die Kenntnis der Client-ID ist nämlich unabdingbar für die Manipulation des in einer JSF-Komponente enthaltenen HTML-Codes durch JavaScript (Listing 2, Zeile 5 ff.). Weil JSF und JavaScript aber heterogene Laufzeitumgebungen haben (Server vs. Client) und externe JavaScript-Dateien ohne Weiteres vom serverseitigen Rendering-Prozess ausgeschlossen sind, muss die Datenübergabe bereits beim Rendern der JSF-Seite auf dem Server geschehen. Listing 3 zeigt einen gängigen Ansatz hierfür.

Neben der Übergabe der Client-ID sind außerdem die Informationen wichtig, die von außen deklarativ über Composite-Attribute an eine JSF-Komponente gereicht werden. Die meisten dieser Parameter haben häufig auch Relevanz für die in JavaScript implementierte clientseitige Logik. Als Beispiel sei hier das Format eines Datumsfelds genannt, das dynamisch an eine JSF-Komponente übergeben und dann im JavaScript-Code verwendet werden soll (Listing 2, Zeile 7 und Listing 3, Zeile 14).

Listing 2: Nutzung der Client-ID im ­JavaScript-Modul

1  var SomeNameSpace = SomeNameSpace || {}; 
2
3  SomeNameSpace.DatePicker = (function () { 
4    var init = function (settings) { 
5      var dateInputField = document 
6        .getElementById(settings.clientId + ":dateInput"); 
7      var dateFormat = settings.dateFormat; 
8      //create datepicker...
9    }; 
1    return { 
1      init: init 
1    }; 
1  })();

Listing 3: Konfiguration der Komponente mit Inline-Scripting

1  <?xml version="1.0" encoding="UTF-8"?> 
2  <!DOCTYPE html> 
3  <html 
4        xmlns:composite="http://xmlns.jcp.org/jsf/composite" 
5        xmlns:jsf="http://xmlns.jcp.org/jsf"> 
6    <composite:interface> 
7      <composite:attribute name="format" type="java.lang.String"/> 
8    </composite:interface> 
9    <composite:implementation> 
10      <input type="text" jsf:id="dateInput"/> 
11      <script> 
12        SomeNameSpace.DatePicker.init({ 
13          clientId: "#{cc.clientId}", 
14          dateFormat: "#{cc.attrs.format}" 
15        }); 
16      </script> 
17    </composite:implementation> 
18  </html>

Optimierungspotenzial

Die aktuellen Ansätze für die Integration von JavaScript in JSF-Komponenten haben den Nachteil, dass für die Initialisierung der jeweiligen JavaScript-Module noch immer Inline-Scripting verwendet werden muss (Listing 3). Für eine ideale Frontend-Architektur nach dem Prinzip der „Separation of structure and behavior“ sind solche Medienbrüche allerdings nicht optimal. Vielmehr sollten die Module, wie man es in JSF gewohnt ist, über XML-Tags konfiguriert und die eigentliche Implementierung der clientseitigen Logik ausschließlich in ausgelagerten JavaScript-Dateien vorgenommen werden. Zusätzlich müssen aus den eingangs erwähnten Gründen klare Grundstrukturen für die jeweiligen JavaScript-Module zur Verfügung stehen, um einen Wildwuchs an Patterns von vorneherein zu vermeiden.

Konkreter Lösungsansatz

Listing 4 und Listing 5 zeigen die Konfiguration und Implementierung der vorher gezeigten DatePicker-Komponente, ohne dass Inline-Scripting verwendet werden muss. Für die Umsetzung dieses Lösungsansatzes wurde die Bibliothek „nicole“ entwickelt. Listing 4 zeigt die Konfiguration einer Komponente mit nicole und Listing 5 ein JavaScript-Modul mit nicole und jQuery. Diese Bibliothek ermöglicht es, externe JavaScript-Module in JSF über XML-Tags zu konfigurieren und gleichzeitig eine einheitliche Grundstruktur für die clientseitige Script-Logik zu schaffen. nicoles Sourcecode und die Dokumentation sind öffentlich auf GitHub [2] hinterlegt.

Die Erstellung von JavaScript-Komponenten mit nicole erfordert zwei Schritte. Zuerst muss ein nicole-Modul im HTML konfiguriert werden. Dazu ist die in Listing 4 gezeigte DatePicker-Komponente in Zeile 12 ff. um ein zusätzliches XML-Tag <nicole:module/> erweitert. Dieses signalisiert, dass zu der JSF-Date­Picker-Komponente ein JavaScript-Modul mit dem Namen DatePicker gehört. In Zeile 13 wird zusätzlich das Datumsformat per <nicole:jsparameter/>-Tag an das nicole-Modul weitergereicht. In HTML befindet sich, im Gegensatz zu den vorher gezeigten Ansätzen, keine einzige Zeile JavaScript-Code.

Als Zweites muss zu einer in HTML deklarierten nicole-Komponente stets ein entsprechendes JavaScript-Modul mit dem gleichen Namen existieren (Listing 5). Es wird über Nicole.module("modulName", function() {}) initialisiert (Zeile 1). In der als zweiten Parameter übergebenen Funktion (Zeile 1–5) wird dann die eigentliche Clientlogik der Komponente implementiert.

Listing 4: Konfiguration einer ­Komponente mit nicole

1  <?xml version="1.0" encoding="UTF-8"?> 
2  <!DOCTYPE html> 
3  <html 
4    xmlns:jsf="http://xmlns.jcp.org/jsf" 
5    xmlns:nicole="http://openknowledge.de/nicole"
6    xmlns:composite="http://xmlns.jcp.org/jsf/composite">
7    <composite:interface> 
8      <composite:attribute name="format" type="java.lang.String"/>
9    </composite:interface> 
10    <composite:implementation> 
11      <input type="text" jsf:id="dateInput"/> 
12      <nicole:module modulename="DatePicker"> 
13        <nicole:jsparameter name="format" value="#{cc.attrs.format}"/>
14      </nicole:module> 
15    </composite:implementation> 
16  </html>

Listing 5: JavaScript-Modul mit nicole und jQuery

1  Nicole.module("DatePicker", function () { 
2    this.$elm("datePicker").datepicker({ 
3      dateFormat: this.parameter("format") 
4    }); 
5  });

nicole bietet außerdem Hilfsfunktionen, die die Entwicklung von JavaScript-Komponenten erheblich erleichtern. Beispielsweise ermöglichen folgende zwei Funktionen den Zugriff auf die DOM-Elemente einer Komponente:

  • this.elm("id"): Über diese Methode bekommt man das zu der übergebenen ID gehörige DOM-Element aus der Komponente (z. B. this.elm("datePicker") liefert das Input-Feld datePicker aus der Komponente).

  • this.$elm("id"): Wie this.elm, allerdings wird das jQuery-Element zu der ID geliefert. Diese Funktion erfordert, dass jQuery eingebunden wurde.

Auch für den Zugriff auf von außen an die Komponente übergebene Parameter existiert eine Hilfsfunktion: this.parameter("parameter"). Mit dieser Funktion kann auf die in der JSF-Komponente übergebenen Parameter zugegriffen werden (z. B. this.parameter("format") liefert das in der JSF-Komponente deklarierte Datumsformat). Weitere Hilfsfunktionen können der Dokumentation auf GitHub [2] entnommen werden.

Unter der Haube

Der in nicole implementierte Ansatz kommt ohne die Verwendung von Inline-Scripting aus. Relevante Parameter aus JSF werden während des Renderns auf dem Server unsichtbar in das zu übertragene HTML eingefügt und so an den Browser übermittelt.

Für die versteckte Datenübertragung werden Hidden-Input-Felder verwendet, die über die seit HTML5 standardisierten data-*Attribute [3] um Informationen aus JSF erweitert werden (Listing 6). Alle zu einer jeweiligen JSF-Komponente wichtigen Daten sind so im Browser für die entsprechenden JavaScript-Module in einem einzigen DOM-Element verfügbar. Der große Vorteil dieses Prinzips: Es existiert keine einzige Zeile JavaScript im HTML-Code, und HTML wird auf legalem Weg als Vermittler zwischen JSF und JavaScript genutzt.

Die <nicole:module/>-Komponente übernimmt das Rendern des in Listing 6 gezeigten Hidden-Input-Felds. Die <nicole:jsparameter/>-Komponenten werden in die data-*Attribute des Felds übersetzt. nicoles mitgeliefertes JavaScript-API findet, sobald der Browser den DOM-Baum fertig geladen hat, automatisch die entsprechenden Hidden-Input-Felder und initialisiert die dazugehören JavaScript-Module.

Listing 6: nicoles Geheimnis

1  <html> 
2    <input id="contentForm:datePickerCC:datePicker" 
3      name="contentForm:datePickerCC:datePicker" type="text"/> 
4    <input id="contentForm:datePickerCC:nicoleCC:nicole" 
5      type="hidden" 
6      data-modulename="DatePicker" 
7      data-format="dd-mm-yy"
8      data-clientid="contentForm:datePickerCC"/> 
9  </html>

Fazit

Der gezeigte Ansatz bringt Vorteile für die Strukturierung von Code bei der Entwicklung von JavaScript-lastigen JSF-Anwendungen mit sich. Anders als bisher wird eine strikte Trennung von JSF und JavaScript-Code ermöglicht. Dadurch entsteht ein klares Pattern für die Entwicklung von JSF-Komponenten, und die Wartbarkeit von Projekten wird gesteigert. Zusätzlich werden weitere Nachteile, die durch die Verwendung von Inline-Scripts entstehen, beseitigt. Beispielsweise können, vor allem bei älteren Browsern, viele Inline-Script-Tags die Ladegeschwindigkeit einer Seite negativ beeinflussen.

Eine wartbare Integration von JavaScript in serverseitig getriebene Webframeworks ist auch außerhalb von JSF eine Herausforderung. Deshalb ist der hier gezeigte Lösungsansatz in seinen Grundsätzen auch in anderen Welten, zum Beispiel in dem mit Java EE 8 kommenden MVC-1.0-Framework, denkbar.

koelpin_sven_sw.tif_fmt1.jpgSven Kölpin ist Enterprise Developer bei der open knowledge GmbH in Oldenburg. Sein Schwerpunkt liegt auf der Entwicklung webbasierter Enterprise-Lösungen mittels Java EE.

Konfiguration ist ein wichtiger Bestandteil moderner Anwendungen und erlaubt es, das Verhalten ohne Neubauen zu verändern. Wie vielfältig und unterschiedlich die dabei auftretenden Anforderungen sind, haben wir im vorherigen Teil dieser Reihe betrachtet. Diesmal wollen wir uns einige bestehende Lösungen ansehen.

Wie nicht anders zu erwarten, sind die bestehenden Konfigurationslösungen genauso vielfältig wie die an sie gestellten Anforderungen. Eine vollständige Betrachtung aller Lösungen ist aufgrund ihrer großen Anzahl leider nicht möglich. Daher haben wir eine Reihe verbreiteter Lösungen ausgewählt, um einen möglichst breiten Überblick über die unterschiedlichen Designansätze geben zu können. Dies sind:

  • Java Properties

  • Apache Commons Configuration

  • Apache DeltaSpike

  • Spring

  • ConfigBuilder

  • Owner

  • Java EE

Aus Platzgründen nicht berücksichtigt haben wir u. a. Netflix Archaia [1] und Akka Configuration [2], obwohl auch sie einige interessante Konzepte aufweisen. Mit jeder dieser Lösungen wollen wir versuchen, die config.properties-Datei aus Listing 1 einzulesen, die wir im Wurzelverzeichnis des Klassenpfads ablegen.

Listing 1

db.port: 5432
db.host: 127.0.0.1
db.user: admin
db.pass: geheim

Wenn das verwendete Framework die Kombination verschiedener Konfigurationsquellen unterstützt, wird der Konfigurationsparameter db.pass mithilfe einer System-Property mit dem Wert my$ecret überschrieben.

Java-Properties

Mit der Properties-Klasse [3] bietet Java eine einfache Möglichkeit, um Konfigurationsdaten aus einer Properties-Datei zu lesen und auf diese in Form von Schlüssel-/Wert-Paaren zuzugreifen.

Listing 2

Properties prop = new Properties();
prop.load(new FileReader("config.properties"));
 
int dbPort = Integer.parseInt(prop.getProperty("db.port", "5432"));
String dbHost = prop.getProperty("db.host", "127.0.0.1");
String dbUser = prop.getProperty("db.user");
String dbPass = prop.getProperty("db.pass");

Wie Listing 2 zeigt, erfolgt das Lesen der Properties-Datei und der Zugriff auf die Konfigurationsparameter ausschließlich programmatisch. Annotationen können nicht verwendet werden.

Ein weiterer Nachteil dieses Ansatzes ist die geringe Anzahl unterstützter Konfigurationsquellen. Das API ist auf das Einlesen von genau einer Properties-Datei ausgelegt, und auch die Kombination verschiedener Konfigurationsquellen zu einer Konfiguration muss vom Entwickler selbst implementiert werden. Darauf haben wir in diesem Beispiel verzichtet.

Der Zugriff auf die Parameter erfolgt nicht typisiert. Die Methode getProperty(String key) gibt ein Objekt vom Typ String zurück, das anschließend, wie beim Parameter db.port zu sehen, programmatisch in den Zieltyp umgewandelt werden muss. Wie bei den Parametern db.port und db.host zu sehen, besteht die Möglichkeit, einen Standardwert anzugeben. Dieser wird verwendet, wenn die Parameter nicht in der Konfigurationsdatei enthalten sind.

Auch wenn durch das Format der Properties-Dateien genau genommen keine hierarchische Struktur modelliert wird, wäre in vielen praktischen Anwendungen ein Zugriff auf alle Parameter mit demselben Namenspräfix wünschenswert, z. B. db. Dies wird jedoch nicht unterstützt.

Zusammenfassend lässt sich feststellen, dass die Verwendung von Java-Properties eine einfache und aufgrund ihrer Zugehörigkeit zum JDK naheliegende Möglichkeit zum Zugriff auf Konfigurationsdaten in jeder Laufzeitumgebung darstellt. Der Funktionsumfang ist jedoch stark begrenzt, und bereits einfache Anforderungen müssen vollständig selbst entwickelt werden. Besonders der ungetypte Zugriff auf Konfigurationsparameter und die fehlende Unterstützung mehrerer Konfigurationsquellen schmerzen dabei in der Praxis sehr.

Apache Commons Configuration

Wie alle Apache-Commons-Projekte erfreut sich auch Commons Configuration [4] einer großen Beliebtheit in der Entwicklergemeinde. Im Rahmen dieses Artikels betrachten wir die Version 1.10, da sich die überarbeitete Version 2.0 noch im Alphastadium befindet.

Im Vergleich zum vorher betrachteten Zugriff mittels Java-Properties verfügt Commons Configuration über einen deutlich größeren Funktionsumfang, über den wir hier nur einen Überblick geben können. Dazu zählen unter anderem die Unterstützung verschiedener Konfigurationsquellen, wie Properties-Dateien, XML-Dokumente, System-Properties und JNDI sowie deren Kombination zu einer gemeinsamen Konfiguration. Eine vollständige Liste der unterstützten Konfigurationsquellen und -formate kann [5] entnommen werden. Alle Konfigurationsquellen implementieren das Configura­tion-Interface [6], mit dem ein einheitlicher Zugriff auf die Konfigurationsdaten möglich ist. Dies nutzen wir in Listing 3, um den Konfigurationsparameter db.port aus der config.properties-Datei mit einem Wert aus den System-Properties zu überschreiben. Dabei werden die zuerst geladenen Parameter vorrangig behandelt. Ein Zugriff auf die Konfigurationsdaten ist auch hier ausschließlich programmatisch möglich.

Listing 3

CompositeConfiguration config = new CompositeConfiguration();
config.addConfiguration(new SystemConfiguration());
config.addConfiguration(new PropertiesConfiguration("config.properties"));
int dbPort = config.getInt("db.port");
String dbHost = config.getString("db.host");
String dbUser = config.getString("db.user");
String dbPass = config.getString("db.pass");

Wie in Listing 3 dargestellt, unterstützt Commons Configuration auch den typisierten Zugriff auf die Konfigurationsparameter. Dabei werden u. a. int, float, ­double, boolean und String unterstützt. Eine vollständige Liste kann [7] entnommen werden.

Für den hierarchischen Zugriff auf Konfigurationsdaten bietet das API unterschiedliche Möglichkeiten. Zum einen können mithilfe der Methode subset(String prefix) des Configuration-Interface alle Konfigurationsparameter mit einem bestimmten Präfix ausgewählt werden. Wir könnten also mittels subset("db") die Parameter db.port, db.host, db.user und db.pass auswählen. Zum anderen stehen für die Verwendung echter hierarchischer Konfigurationsquellen, wie z. B. XML-Dateien, verschiedene spezialisierte Implementierungen des Configuration-Interface zur Verfügung, die zusätzliche Funktionen anbieten. Dadurch besteht die Möglichkeit, große Konfigurationen zu strukturieren und innerhalb der Anwendung einfach auszuwerten.

Neben den genannten Funktionalitäten bietet Commons Configuration auch ein Eventsystem [8], um zur Laufzeit auf Konfigurationsänderungen zu reagieren. Des Weiteren wird die Konfiguration von Mehrmandantensystemen [9] unterstützt. Damit können auch die im ersten Teil der Serie genannten Anforderungen komplexerer Systeme umgesetzt werden. Eine genauere Beschreibung dieser Funktionalitäten würde allerdings den Umfang dieses Artikels überschreiten.

Insgesamt bietet Commons Configuration einen deutlich größeren Funktionsumfang als die vorher betrachteten Java-Properties. Dabei stellen vor allem die Kombination verschiedener Konfigurationsquellen und der typisierte Zugriff auf die Konfigurationsparameter eine deutliche Erleichterung dar. Durch das Eventsystem und die Unterstützung von Systemumgebungen mit mehreren Mandanten können auch komplexere Anwendungsszenarien realisiert werden.

Apache DeltaSpike

DeltaSpike [10] ist ein weiteres sehr verbreitetes Projekt der Apache Software Foundation. Hierbei liegt der Fokus allerdings nicht auf der Verarbeitung von Konfigurationsdaten, sondern auf der Bereitstellung verschiedener portabler CDI Extensions. Dabei handelt es sich um den Erweiterungsmechanismus der CDI-Spezifikation, mit dem zusätzliche Funktionalität zum Container hinzugefügt werden kann. DeltaSpike nutzt dies, um u. a. einen Konfigurationsmechanismus bereitzustellen, den wir uns im Folgenden genauer ansehen wollen.

Es werden verschiedene Konfigurationsquellen unterstützt, die mit unterschiedlicher Priorität verarbeitet werden. Dies bietet die Möglichkeit, Konfigurationsdaten aus verschiedenen Quellen miteinander zu kombinieren und einzelne Parameter gezielt zu überschreiben. Dazu werden die Konfigurationsquellen [11] System Properties, Environment Properties, JNDI und Properties-Dateien unterstützt. Zusätzliche Konfigurationsquellen können über den ClassLoader hinzugefügt oder selbst implementiert werden, um z. B. Konfigurationsdaten aus einer Datenbank zu lesen [12]. Die Reihenfolge, in der die Konfigurationsquellen ausgewertet werden, wird durch den Wert deltaspike_ordinal bestimmt, der von jeder ConfigSource abgefragt werden kann. Jede Properties-Datei wird mit einer eigenen PropertiesConfigSource eingelesen und kann mit einem individuellen deltaspike-ordinal-Wert konfiguriert werden. Dies ermöglicht es, beliebige Konfigurationsdateien miteinander zu kombinieren.

Der Zugriff auf die Konfigurationsdaten kann entweder programmatisch oder mithilfe von Dependency Injection erfolgen. Der programmatische Zugriff ist unabhängig von CDI und kann dadurch z. B. auch für die interne Konfiguration von DeltaSpike selbst verwendet werden. Dependency Injection (oder besser Configuration Injection) erzeugt aber weitaus weniger Codeabhängigkeiten und vermeidet vor allem auch unnötigen Code und ist somit häufig die bessere Alternative. Daher betrachten wir diese hier näher.

DeltaSpike und CDI erlauben die typsichere Injection von Konfigurationsdaten in eine Bean. Dazu müssen die jeweiligen Properties der Bean mit @Inject und @ ConfigProperty annotiert werden (Listing 4). Zur Ausführungszeit der Anwendung werden die Konfigurationsdaten dann mithilfe von CDI-Producer-Methoden bereitgestellt. DeltaSpike selbst stellt leider nur Producer für den Datentyp String zur Verfügung. Dieser Mechanismus kann durch die Implementierung eigener Qualifier- und Producer-Methoden erweitert werden, sodass auch die Unterstützung anderer Datentypen möglich ist.

Listing 4

@ApplicationScoped
public class MyConfiguredBean {
    @Inject
    @ConfigProperty(name = "db.port")
    private Integer dbPort;
 
    @Inject
    @ConfigProperty(name = "db.host")
    private String dbHost;
 
    @Inject
    @ConfigProperty(name = "db.user")
    private String dbUser;
 
    @Inject
    @ConfigProperty(name = "db.pass")
    private String dbPassword;
 
    ...
 }

Wie wir gesehen haben, überzeugt Apache Delta­Spike vor allem durch die Verwendung von Injection und die einfache Erweiterbarkeit. Der Funktionsumfang ist nicht so hoch wie im zuvor betrachteten Commons-Configuration-Projekt, allerdings durch die mögliche Kombination verschiedener Konfigurationsquellen und den typsicheren Zugriff auf die Konfigurationsdaten für viele praktische Anwendungen ausreichend.

Spring

Auch das Spring Framework [13] stellt einen Konfigurationsmechanismus zur Verfügung, der es erlaubt, Konfiguration aus beliebigen Datenquellen zu beziehen. Die Basis bildet dabei der PropertyResourceConfigurer, der einen dynamischen Auflösungsmechanismus von Schlüssel-/Wert-Paaren zur Verfügung stellt und für den Spring verschiedene Implementierungen anbietet. Die Klasse PropertySourcesPlaceholderConfigurer erlaubt es, mehrere PropertySource-Instanzen in geordneter Weise einzubinden. Unser Beispiel könnte also in Spring wie in Listing 5 dargestellt aussehen.

Listing 5

<bean id="applicationProperties"
    class="org.springframework.beans.factory.config.
           PropertyPlaceholderConfigurer">
  <property name="locations">
    <list>
      <value>classpath:config.properties</value>
    </list>
  </property>
  <property name="systemPropertiesMode" 
            value="SYSTEM_PROPERTIES_MODE_OVERRIDE"/>
</bean>

Der konfigurierte PropertyPlaceholderConfigurer wird dann vom Spring-Konfigurationsmechanismus verwendet, um entsprechende Ersetzungen in der Spring-Konfiguration zu ermöglichen (Listing 6).

Listing 6

<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource">
    <property name="driverClassName" value="${db.driver}"/>
    <property name="url" value="${db.url}"/>
    <property name="username" value="${db.user}"/>
    <property name="password" value="${db.pass}"/>
</bean>

Neben den genannten Mechanismen bietet der Spring-Container auch Injection-Mechanismen an, die ganz ohne XML auskommen (ein Beispiel zeigt Listing 7). Basis ist dabei ein so genannter BeanPostProcessor, der bei Bedarf auch sehr einfach selbst implementiert, erweitert oder angepasst werden kann. Somit bietet Spring ein flexibles, erweiterbares und uniform anwendbares Konfigurationssystem, das sich in der Praxis bewährt hat.

Listing 7

@Configuration
@SystemPropertiesValueSource
@PropertiesValueSource("classpath:config.properties")
public class DbConfig {
    @ExternalValue("db.driver") String dbDriver;
    @ExternalValue("db.url") String dbDriver;
    @ExternalValue("db.user") String dbUser;
    @ExternalValue("db.pass") String dbPassword;
}

ConfigBuilder

Auf [14] findet sich mit ConfigBuilder ein weiteres Java-SE-Framework, das Annotationen verwendet, um auf Konfigurationswerte zuzugreifen.

Um die Konfiguration aus unserer config.properties-Datei zu verwenden, wird eine einfache Klasse mit je einer entsprechend typisierten Eigenschaft für jeden Konfigurationsparameter verwendet (Listing 8). Mithilfe der Annotation @PropertiesFiles wird die Konfigurationsdatei benannt. Dabei können auch mehrere Dateien referenziert werden. Zusätzlich werden Kommandozeilenparameter, Environment und System-Properties als Konfigurationsquellen unterstützt.

Die Parameter der Klasse werden mit @PropertyValue annotiert, um die Zuordnung zu den Parametern aus der Properties-Datei herzustellen. Um einen Parameter mit einer System-Property zu überschreiben, wird zusätzlich die Annotation @SystemPropertyValue verwendet. Standardwerte können mitfilfe von @DefaultValue definiert werden. Des Weiteren wird eine Validierung der Konfigurationsparameter auf Basis der Bean-Validation-Spezifikation unterstützt.

Listing 8

@PropertiesFiles("config.properties")
public class MyConfiguredBean {
    @PropertyValue("db.port")
    @DefaultValue("5432")
    private int dbPort;
 
    @PropertyValue("db.host")
    private String dbHost;
 
    @PropertyValue("db.user")
    private String dbUser;
 
    @PropertyValue("db.pass")
    @SystemPropertyValue("db.pass") 
    private String dbPassword;
 
    ...
 }

Der Zugriff auf die Konfiguration erfolgt dann über eine Factory-Methode (Listing 9) und den entsprechenden typsicheren getter-Methoden.

Listing 9

Config myConfig = ConfigBuilder.on(Config.class).build();
int dbPort = myConfig.getDbPort();

Wie auch die anderen auf Annotationen basierenden Konfigurationslösungen, überzeugt ConfigBuilder durch die einfache Verwendung. Der Funktionsumfang scheint uns aber etwas reduziert im Vergleich zu anderen, bereits hier betrachteten Lösungen.

Owner

Auch das Framework Owner [15] arbeitet mit deklarativen Annotationen. Allerdings wird hier nicht eine Klasse, sondern ein Interface als Template verwendet, um die Konfiguration zu modellieren (Listing 10). Das Interface muss dabei das Markerinterface Config erweitern.

Ohne weitere Annotationen sucht Owner im Package des Interface nach einer Properties-Datei mit demselben Namen, im Beispiel also MyConfig.properties. Wir wollen allerdings die Datei config.properties laden und müssen dazu den Namen und Pfad mithilfe der Annotation @Sources definieren. Dabei können auch mehrere Properties-Dateien kombiniert werden.

Die in den Dateien definierten Konfigurationsparameter werden anhand ihres Namens mit dem Template verknüpft oder mit @Key annotiert. Auch Standardwerte und System-Properties werden mittels @DefaultValue und @SystemPropertyValue an den jeweiligen Properties vorgegeben.

Listing 10

@Sources("classpath:config.properties")
public interface MyConfig extends Config {
    @Key("db.port")
    @DefaultValue("5432")
    int dbPort();
 
    @Key("db.host")
    String dbHost();
 
    @Key("db.user")
    String dbUser();
 
    @Key("db.pass")
    @SystemPropertyValue("db.pass") 
    String dbPassword();
 
}

Ähnlich wie beim zuvor betrachteten ConfigBuilder wird mithilfe der ConfigFactory ein entsprechendes Konfigurationsobjekt erzeugt:

MyConfig cfg = ConfigFactory.create(MyConfig.class, System.getProperties());

Owner ist allerdings eher schwierig zu erweitern oder anzupassen. Die Verwendung und die Möglichkeit, mehrere Properties-Dateien und System-Properties zu einer Konfiguration zu kombinieren, sind jedoch für viele Anwendungen ausreichend.

Java EE

Java-EE-Applikationen werden mithilfe von diversen XML-Konfigurationen konfiguriert. Alle hier aufzulisten, würde den Rahmen des Artikels sprengen, deshalb geben wir nur eine kurze Übersicht über die wichtigsten Erweiterungspunkte:

  • JSR 339 JAX RS: Ein javax.ws.rs.ext.RuntimeDelegate kann registriert werden.

  • JSR 352 Batch: META-INF/batch.xml, META-INF/batch-jobs/*.xml. Hierbei ist es möglich, mit Placeholdern zu arbeiten, die z. B. durch System-Properties ersetzt werden.

  • JSR 338 JPA: Die PersistenceUnits werden in META-INF/persistence.xml konfiguriert, zusätzlich können auch ein PersistenceProvider oder PersistenceProviderResolver registriert werden.

  • JSR 342 EE7 definiert META-INF/application.xml, META-INF/MANIFEST.mf, META-INF/application-client.xml.

  • JSR 345 EJB 3.2: Definiert META-INF/ejb-jar.xml; es ist möglich, eine eigene javax.jms.QueueConnectionFactory zu registrieren.

  • JSR 346 CDI: META-INF/beans.xml, CDI Extensions lassen sich unter META-INF/services/javax.enterprise.inject.spi.Extension registrieren.

  • JSR 349 Bean Validation: META-INF/validation.xml; zusätzlich lassen sich verschiedene SPIs registrieren (ConstraintValidatorFactory, MessageInterpolator, ParameterNameProvider, TraversableResolver).

  • JSR 315 Servlets: web.xml.

  • JSR 372 JSF: Es kann eine Klasse vom Typ javax.faces.application.ApplicationConfigurationPopulator registriert werden, die Zugriff auf das JSR-Konfigurationsdokument ermöglicht, bevor JSF die Konfiguration auswertet.

Zusammenfassend kann festgehalten werden, dass die Vielfalt an Konfigurationsformaten und -lokationen sehr groß ist. Hinzu kommt, dass die Konfiguration zum Deployment-Zeitpunkt einer Java-EE-Applikation gelesen wird und dass es praktisch keine standardisierten Möglichkeiten gibt, diese auf einfache Weise von außen beizusteuern. Eine Ausnahme bildet hier lediglich Java Batch, das Placeholder in sehr eingeschränktem Rahmen zulässt. JPA bietet zwar ein umfangreiches Metamodell an, dieses ist aber leider nur lesbar. Dasselbe gilt auch für das Java-EE-Management-API. CDI und Bean Validation sind positiv hervorzuheben, da sie sehr flexible SPIs mitbringen, die eine Konfigurationsanbindung ohne technische Kniffe ermöglichen. Trotzdem muss eine Anbindung entsprechend selbst programmiert werden.

Zusammenfassend muss die applikatorische EE-Konfiguration bereits zum Bauzeitpunkt einer Applikation weitgehend bekannt sein. Daran ändert auch der so genannte altdd-Mechanismus nicht viel, der es erlaubt, mittels einer System-Property auf alternative Deployment-Deskriptoren zuzugreifen, da dieser Mechanismus auf den Klassenpfad beschränkt ist [16].

Damit eine EE-Applikation lauffähig ist, muss aber auch der Container entsprechend konfiguriert werden (administrative Ressourcen), damit die Applikation die gewünschten Ressourcen vorfindet. Dieser Bereich wird von den Spezifikationen überhaupt nicht abgedeckt und ist somit produktspezifisch.

Fazit

Es existiert eine Reihe von Konfigurationslösungen, die sich in Umfang und Komfort teilweise erheblich unterscheiden. In Java EE hat sich über die Jahre ordentlich Staub angesammelt, und die bestehenden Möglichkeiten scheinen uns im Zeitalter von Cloud- und Microservices-Architekturen doch eher bescheiden zu sein. Im Bereich Java SE bringen praktisch alle Lösungen interessante Konzepte mit, die sich wie folgt umschreiben lassen:

  • Typsicherheit: Die textuellen Schlüssel-/Werte-Paare können bei Bedarf mit Konvertermechanismen in die verlangten Zieltypen konvertiert werden.

  • Default-Werte: Fehlende Werte können mit Standardwerten ergänzt werden.

  • Unterstützung verschiedener Konfigurationsquellen (und -formate)

  • Kombinationen und Überschreiben: Konfigurationen können zu neuen Konfigurationen kombiniert werden und erlauben so das kontrollierte Überschreiben von Werten. Das Kombinieren von verschiedenen Konfigurationsquellen geschieht dabei grundsätzlich in zwei Varianten: 1. einem Builder-/Factory-Ansatz, bei dem die finale Konfiguration explizit programmatisch definiert wird; 2. mit Registrierung und Ordnung von Konfigurationsquellen mittels Ordinalwerten oder Konfiguration, wo das Framework eine flexible und erweiterbare Logik zur Verfügung stellt.

  • Der Zugriff auf die konfigurierten Werte erfolgt ebenfalls nach zwei Mustern: 1. Configuration Injection oder 2. explizite Abfrage der Werte von einem Configuration-Objekt (entweder definiert durch das Framework-API oder als benutzerdefiniertes Tem­plate).

  • Laufzeitänderungen an der Konfiguration werden nicht durchgängig unterstützt. Wenn diese möglich sind, werden sie meist als Events an die registrierten Observer weitergegeben.

Die meisten hier vorgestellten Lösungen bieten jedoch nur wenige oder keine höherwertigen Dienste, z. B. für kontextabhängige Konfiguration, dynamische Auflösung innerhalb der Konfiguration, Filter und Staging-Mechanismen etc. Bereits bei vielen relativ einfachen Einsatzszenarien muss oft selbst Hand angelegt werden. Oftmals ist auch ein Einsatz in Java EE nicht im Grunddesign berücksichtigt, und viele der diskutierten Lösungen scheinen uns nicht immer einfach erweiter- und anpassbar.

Apache Tamaya kombiniert nun genau diese Aspekte und Defizite zu einem flexiblen und modularen Konfigurationsframework. Dabei stellen einige Funktionen absolut essenzielle Aspekte dar, andere wiederum lassen sich problemlos auf der Basis eines minimalen, funktionalen Fundaments als Erweiterungsmodule realisieren. Auch Java-EE-Applikationen können, wenn die entsprechenden Konfigurationspunkte ausgenutzt werden, zumindest teilweise effizient und flexibel konfiguriert werden. Wie das genau funktioniert, werden wir uns in den folgenden Teilen dieser Serie ansehen.

janssen_thorben_sw.tif_fmt1.jpgThorben Janssen arbeitet als Senior-Entwickler und Architekt für die Qualitype GmbH und entwickelt seit mehr als zehn Jahren Anwendungen auf Basis von Java EE. Er ist Mitglied der JSR 365 Expert Group und bloggt über Themen rund um Java Enterprise.

Web Twitter

tresch_anatole_sw.tif_fmt1.jpgAnatole Tresch studierte Wirtschaftsinformatik und war anschließend mehrere Jahre lang als Managing Partner und Berater aktiv. Heute arbeitet Anatole Tresch als technischer Koordinator und Architekt bei der Credit Suisse, ist Specification Lead des JSR 354 (Java Money & Currency) und PPMC Member von Apache Tamaya.

Twitter

Schnelle und schöne Websites sind heute die Regel. Auf den Schultern der Riesen moderner Frontend-Technologien lassen sich höchst ergonomische und hübsch anzusehende Oberflächen herstellen. Das Fach hat sich daran gewöhnt: Wird ein neues System bestellt, so soll es selbstverständlich flott, sicher und attraktiv sein, zusätzlich auch auf dem Tablet oder dem Smartphone, das erst kommenden Monat in den Handel kommt.

Dass schnelle, sichere, geräteunabhängige und schöne Websysteme aber nicht einfach zu entwickeln sind, sondern Investitionen benötigen, ist dabei nicht jedem klar. Der Aufwand, der betrieben werden muss, lässt sich hinter der adretten Oberfläche nur erahnen. Tatsächlich zeigt sich, dass die gewünschten Eigenschaften aber Kostentreiber in der Entwicklung und im Betrieb sind: Möchte man ein schnelles System, so muss gegebenenfalls ein Content Delivery Network her, damit der Content auch weltweit schnell geliefert werden kann. Ein sicheres System sollte auditiert und im Labor angegriffen werden, um Schwachstellen zu finden. Ein schönes System braucht ein gut gemachtes Design in Kombination mit durchdachter Bedienbarkeit, und hier sind Design- und Usability-Experten nötig. Diese Liste ließe sich endlos fortführen, und die Kosten summieren sich, sodass es geraten erscheint, nur auf die Qualitätsmerkmale Wert zu legen, die für das vorliegende System wichtig sind.

Aber welche Merkmale gibt es überhaupt, und welche davon sind für mich wichtig? Und wie kann ich dem Auftraggeber den Zusammenhang zwischen Qualität und den Kosten am besten erklären? Diese Wechselwirkung und die griffige Beschreibung von Qualität ist das Thema dieser neuen Artikelserie.

Qualitätsmerkmale

Ein Qualitätsmerkmal ist eine Eigenschaft eines Softwaresystems. Merkmale werden auch nicht funktionale Anforderungen genannt, um sie von den funktionalen Anforderungen zu unterscheiden. Funktionale Anforderungen beschreiben, was ein System tun soll; ein Qualitätsmerkmal hingegen beschreibt eine gewünschte Eigenschaft des Systems, die messbar und/oder testbar sein sollte (wie schnell, wie sicher etc.). Es gibt verschiedene Qualitätsmodelle, welche die Merkmale gliedern und hierarchisch anordnen, d. h., Merkmale können Submerkmale haben. Ein paar Beispiele für Qualitätsmerkmale sind:

Die Wartbarkeit beschreibt die Anpassbarkeit einer Software über ihren Lebenszyklus. Sie beeinflusst die Kosten von Änderungen am System. Websysteme sollten sich leicht ändern lassen, da sie oft von Marketing­anforderungen getrieben werden, die wechselhaft sind, da sich Märkte, bzw. der Wunsch nach ihrer Bewirtschaftung, beständig wandeln. Deswegen ist eine hohe Wartbarkeit für Websysteme von entscheidender Bedeutung, denn wir gehen davon aus, dass sich ein solches System innerhalb einer nützlichen Frist und zu vertretbaren Kosten anpassen lassen muss, ansonsten wird es ersetzt. Die Wartbarkeit lässt sich beispielsweise analysieren nach Prüfbarkeit, Änderbarkeit, Analysierbarkeit, Testbarkeit oder auch Flexibilität.

Die Performance beschreibt die Sparsamkeit in Bezug auf Rechenzeit und Speicherplatz einer Software (Effizienz), deren Dynamik (ob sich die Performance im Laufe der Zeit verändern soll), sowie das Antwortverhalten des Systems (das insbesondere bei Webanwendungen aufgrund der geografischen Verteilung wichtig ist). Im Allgemeinen ist die Performance neben der Verfügbarkeit für viele Stakeholder das wichtigste Thema, auch weil sie diese im Browser selbst sehr gut beurteilen können. Lädt eine Webseite zu langsam, so sinken Besucherzahlen und der Page Rank bei Google. Stellt man hingegen zu viel Kapazität zur Verfügung, so sind die Kosten im Betrieb unverantwortlich. Eine korrekte Erfassung der Performance ist also wichtig. Die Performance lässt sich nach Kapazität, Latenz oder Skalierbarkeit analysieren.

Die Sicherheit eines Systems beschreibt, wie es die legitimen Interessen Dritter vertreten soll. Speichert ein System personenbezogene Daten, so haben diese Personen ein Interesse daran, dass diese Daten nicht missbraucht werden, und wir müssen sie entsprechend schützen. Die Sicherheit eines Systems zu beschreiben, bedeutet also, sich darüber klar zu werden, welche Arten von Daten verarbeitet werden, wer auf diese Daten Zugriff erhält und wie sie manipuliert werden dürfen. Im Allgemeinen wird Sicherheit nach den drei Faktoren Vertraulichkeit, Verfügbarkeit und Integrität analysiert.

Die Portierbarkeit beschreibt die möglichen Umgebungen, auf denen die Software betrieben werden soll. In der Webentwicklung wird die Portierbarkeit gerne übersehen. Man meint, dass die Software nur auf einem Webserver laufen muss. Dies ist aber nicht der Fall, denn in den meisten Fällen ist die Entwicklungsumgebung verschieden von der Test- und Produktionsumgebung, sodass die Software in Wahrheit auf drei verschiedene Umgebungen portiert werden muss. Ebenfalls steht nicht immer fest, wer das System später installieren und welche Kompetenzen diese Personen haben soll. Hinzu kommt ein Zoo an verschiedensten Browsern auf unterschiedlichsten Endgeräten, von denen aus das System bedienbar sein muss. Zur Portierbarkeit gehören also neben der Geräteunabhängigkeit auch die Installierbarkeit.

Unter Transparenz fasse ich die Anforderungen an die Sichtbarkeit eines Systems zusammen. Da Software unsichtbar ist und Werkzeuge zur Beobachtung manchmal selbst hergestellt werden müssen, lauern hier versteckte Kosten, die es im Vorfeld zu klären gibt. So möchte der Betrieb die Last auf den Maschinen beobachten können: Es soll rechtzeitig Alarm geschlagen werden, bevor die Platte voll läuft. Oft sind hier schon Werkzeuge wie Nagios im Einsatz, die aber auch integriert werden möchten. Im Marketing und der Redaktion sind hingegen andere Monitore nötig. Hier möchte man den Erfolg einer Kampagne messen oder sehen, wie viele Benutzer gerade einen Artikel lesen. Auch das Marketing hat eventuell schon Werkzeuge hierfür bereit, die integriert werden möchten. Neben der Wartbarkeit stößt vor allem die Transparenz bei den Stakeholdern auf großes Interesse, da sie aus dieser große Vorteile ableiten können. Es ist sehr gut, wenn hierfür benötigte Etats im Vorfeld besprochen werden können.

Architektur

Qualitätsmerkmale haben Einfluss auf das Systemdesign und damit auch auf das notwendige Budget für die Systementwicklung und den Betrieb. Ein einleuchtendes Beispiel ist die Verfügbarkeit. Eine hohe Systemverfügbarkeit bedeutet den Betrieb von vielen Servern oder Serverinstanzen (wenn das System in einer Cloud deployt wird), vielleicht sogar an mehreren Standorten; eine niedrige Verfügbarkeit den Betrieb von wenigen Servern oder Instanzen. Natürlich kostet der Betrieb von mehr Servern an mehr Standorten auch mehr Geld.

Der Grad des Einflusses auf das Systemdesign ist unterschiedlich. Einige Qualitätsmerkmale bedeuten nur kleine Änderungen oder können sogar organisatorisch umgesetzt werden. Andere Merkmale haben jedoch weitreichende Konsequenzen, die alle Systemteile berühren und massive Kosten auslösen können. Dies zu beurteilen obliegt dem Architekten, der entscheiden muss, wie ein gewünschtes Merkmal erreicht werden kann. Dabei kommt ihm eine schwierige Rolle zu: Er muss es nicht nur schaffen, das Merkmal griffig fassbar zu machen, um mit dem Fach darüber sprechen zu können; er muss auch die Auswirkungen auf die Architektur und die damit verbundenen Kosten transparent und nachvollziehbar machen.

Am Ende muss der Architekt dann die Entscheidungen auf Basis seiner Abstimmungen fällen. Hat man sich dann z. B. für AngularJS entschieden, wird das System auch bis an sein Lebensende mit diesem Framework auskommen müssen, da ein Umbau voraussichtlich enorme Kosten gemessen am Geschäftsnutzen auslösen würde. Dabei sind die Kosten nicht nur finanzieller Natur: Ein Umbau benötigt eben auch Ressourcen, die in dieser Zeit keine anderen Aufgaben wahrnehmen können, und somit kann die Zeitplanung durcheinander geraten. Ein weiteres Kostenkriterium ist auf der fachpolitischen Ebene auch das Renommee des Architekten: Man wird ihn daran messen wollen, ob seine Entscheidungen zu Projektbeginn richtig und vorausblickend genug waren.

Zusätzlich zu den Qualitätsmerkmalen werden auch Informationen zur gewünschten Funktionalität benötigt. Hier werden Sie möglicherweise bombardiert: 350 Seiten Grobkonzept, dazu 2 500 Seiten Feinkonzept, die die fachlichen Anforderungen aus dem 800 Zeilen langen Anforderungs-Excel abbilden. Von den 800 Anforderungen sind circa 700 als kritisch eingestuft, der Rest ist dem Fach lediglich wichtig. Schon auf den ersten Blick fallen Ihnen Widersprüche auf. Aus diesem Wust an Informationen haben Sie nun die Aufgabe, die wesentlichen Informationen zu extrahieren, die Ihnen erlauben, einen Architekturvorschlag zu konstruieren. Geben Sie also die Dokumente sofort zurück! Verlangen Sie vom Anforderungsingenieur eine Zusammenfassung der Geschäftsziele, die mit dem System erreicht werden sollen und lassen Sie Epics erstellen, die die Anforderungen auf einer hohen Abstraktionsebene bündeln. So haben Sie eine Chance zu verstehen, was mit dem System erreicht werden soll und kommen in der Architektur weiter. Meiden Sie Detaildiskussionen zu funktionalen Anforderungen, denn diese sind in der Regel nicht relevant für die Architektur [1]. Sehr schöne Diskussionen zu architekturrelevanten Anforderungen und zum Vorgehen finden Sie übrigens in [2] und [3].

Einen weiteren Einflussfaktor stellen die Rahmenbedingungen dar. Darunter verstehen wir allfällige Vorgaben, die wir bei unseren Entscheidungen in der Architektur berücksichtigen müssen. Beispiele für Rahmenbedingungen sind das verfügbare Budget, die Ausbildung der Projektmitarbeiter oder Compliance-Regulierungen, denen das Geschäft des Kunden unterliegt. Rahmenbedingungen sind in der Regel nicht durch uns veränderbar, aber sie müssen nicht notwendigerweise statisch sein. So kann es sein, dass sich eine Compliance-Regulierung im Laufe des Projekts verändert. Eine disziplinierte Dokumentation der Rahmenbedingungen kann wichtig sein, um Entscheidungen später verstehen zu können.

takai_websystemqualitaet_1.tif_fmt1.jpgAbb. 1: Einflussfaktoren und Ergebnisse der Architektur

Die Erhebung von Qualitätsmerkmalen ist nicht nur für korrekte Architekturentscheidungen bedeutend, sondern auch ein wichtiger Faktor im Erwartungs- und Risikomanagement. Wie eingangs erwähnt, erwartet der Kunde im Regelfall ein schnelles, sicheres und schönes System, das sich gut warten lässt. Erwartet hingegen der Kunde keine hohe Wartbarkeit, so ist er später nicht überrascht, wenn sie zusätzliches Geld kostet. Wenn Sie also die gewünschten Qualitäten im Vorfeld besprechen, gibt es am Ende für alle Beteiligten keine Überraschungen mehr. Denken Sie immer daran: Die stillschweigende Erwartung des Fachs ist, dass das System schnell wie Google, sicher wie Fort Knox und schön wie die Mona Lisa wird. Wenn Ihr Entwurf auf Basis des verfügbaren Budgets diese Erwartungen nicht erfüllen kann und Sie dies kommunizieren, dann wissen das alle.

Gegebenenfalls identifizieren Sie dabei aber ein echtes Problem, beispielsweise wenn die Bedienbarkeit aufgrund der Kosten nicht erreicht werden kann; oder, was häufig vorkommt, die Anforderungen an die Wartbarkeit nicht mit der Releaseplanung vereinbar sind. Oft wünscht der Kunde einen Go-Live in drei Monaten, aber um die Wartbarkeit herzustellen, werden sechs Monate benötigt. Es lohnt sich an dieser Stelle, die Qualitätsmerkmale mit Projektrisiken in Beziehung zu setzen und an die Projektsteuerung zur Lösung zu übergeben. Eine strukturierte Ausweisung der Projektrisiken über die gewünschten Systemqualitäten, die zu einer rationalen Diskussion mit den Sponsoren führt, zeugt von einer hohen Maturität im Architekturprozess.

Nehmen wir nun einmal an, ein Kunde wünscht sich ein System, das nur eine bestimmte Aufgabe in einem bestimmten Zeitraum erfüllen soll, beispielsweise ein Christmas-Special: Der Auftragnehmer soll es so bauen, dass möglichst wenig Kosten bei der Entwicklung entstehen. Er gibt an, dass Änderungen an der Software nicht notwendig sein werden, und dementsprechend auf Code-Reviews oder Lesbarkeit der Quelltexte verzichtet werden kann. Auch automatische Tests sollen nicht entwickelt werden, weil die Software nur ein einziges Mal getestet werden muss. Dies kann der Auftragnehmer leisten und das Ergebnis ist funktional in Ordnung.

Entscheidet sich der Kunde jedoch später doch noch für Änderungen, so sind diese ungleich teurer, weil der Quelltext nicht lesbar ist und keine Tests existieren. Die Anwendung ist nicht wartbar, weil der Kunde dies nicht finanzieren wollte. Wurde dies vorgängig dokumentiert, so sind spätere Mehrkosten leicht zu begründen. Andernfalls kann dies negativ auf den Auftragnehmer zurückfallen, weil der Kunde stillschweigend davon ausgeht, dass die Wartbarkeit gegeben ist. Idealerweise hatte der Projektleiter die fehlende Wartbarkeit als Risiko geführt, sodass es auch im Steuerungsausschuss ein präsentes Thema war.

Websysteme

In dieser Artikelserie werden Qualitätsmerkmale anhand von Websystemen erläutert. Man nennt ein solches System auch ein sozio-technisches Feedbacksystem: Es handelt sich um ein von Menschen genutztes Informationssystem, das sich über die Zeit durch Feedback der Stakeholder verändert. Solche Systeme sind genau dann erfolgreich, wenn sie die Anforderungen der Stakeholder erfüllen, die das System verwenden. Dabei ändern sich die Anforderungen der Stakeholder stetig, da sich die Welt, insbesondere im Marketing, sehr schnell dreht. Websysteme haben also zumeist eine hohe Änderungsrate, und deswegen spielt die Wartbarkeit bei ihnen eine wichtige Rolle. Ein Websystem besteht typischerweise aus vier Schichten, wie Abbildung 2 zeigt. Ich werde im Verlauf dieser Serie immer wieder auf diese allgemeine Architektur zurückgreifen, um Qualitätsmerkmale zu diskutieren.

takai_websystemqualitaet_2.tif_fmt1.jpgAbb. 2: Generische Architektur eines Websystems

Die Benutzerschnittstelle heißt heute Frontend und lebt im Browser. Oft wird gewünscht, dass das Frontend auf allen Browsern laufen soll, was aber nicht möglich ist, da wir nicht alle Browser kontrollieren können (es ist eine Rahmenbedingung, dass sich Browser und Endgeräte stetig ändern). Wir können es aber selbstverständlich auf verschiedenen Browsern und Endgeräten testen, mit linearem Aufwand.

Das Frontend wird vom Backend geladen und bezieht auch seine Daten von diesem. Das Backend ist nur selten eine einzige Komponente, sondern besteht aus mehreren Einzelteilen mit sehr unterschiedlichen Aufgaben wie Authentifizierung, Caching oder Load Balancing. Wichtig ist dabei, dass es sich hierbei um unsere eigentliche Webanwendung handelt, die sich im Laufe der Zeit verändert.

Die Integration modelliere ich als dedizierte Schicht, die sich um die Anbindung von Daten- und Geschäftsdiensten kümmert. Dabei kann die Integrationsschicht auch selbst Teile der Geschäftslogik implementieren, ihre Hauptaufgabe ist aber die Entkopplung der Dienste, was Auswirkungen auf Wartbarkeit, Verfügbarkeit, Sicherheit usw. haben kann. Die Dienste selbst sind zumeist REST- oder SOAP-Services und gehören in der Regel nicht zu unserem System, sondern bestehen bereits und können konsumiert werden. Die Qualität der zu integrierenden Dienste hat einen erheblichen Einfluss auf die Qualität unseres Systems, zum Beispiel die Performance. Es ist heute eher selten, die Integrationsschicht als eigene Systemkomponente vorzufinden, sie ist zumeist ein Teil des Backends (siehe auch Point-2-Point als Anti-Pattern).

Nicht dargestellt ist die Entwicklungsinfrastruktur, die aber aufgrund der hohen Anforderungen an die Wartbarkeit ein wichtiger Bestandteil der Architektur ist. Eine ausführliche Diskussion wird es hierzu in einem Folgeartikel über Änderbarkeit geben.

Qualitätsszenarien

Wie bei Anforderungen üblich, ist die Messbarkeit ein wichtiger Faktor, damit objektiv festgestellt werden kann, ob ein System die gewünschten Qualitätsmerkmale tatsächlich erfüllt. Die Messkriterien von Qualitätsmerkmalen verlangen ein hohes Maß an technischer Kompetenz (Antwortzeiten, Last und Kapazität, Usability-Heuristiken usw.). Da dieses Verständnis auf der Fachseite nicht vorausgesetzt werden kann, braucht es also eine Brücke zwischen den notwendigen technischen Entscheidungsgrundlagen und dem fachlichen Verständnis auf der anderen Seite. Hierfür eignen sich Qualitätsszenarien als Kommunikationsmittel. Diese Szenarien beschreiben als hypothetische Aufeinanderfolge von Ereignissen das Verhalten des Systems in einer Art und Weise, die für das Fach verständlich ist und bilden damit eine gute Diskussionsgrundlage.

Die Erhebung der Szenarien ist schwierig. Oft müssen diese zu einem Zeitpunkt erhoben werden, an denen die Stakeholder noch gar kein klares Bild von der zukünftigen Lösung haben. Sie sind zudem sehr abstrakt, sodass sich auch ein erfahrener Entwickler oft nichts Konkretes unter ihnen vorstellen kann. Es bedarf also einer guten Kommunikationskompetenz, um die Merkmale verständlich beschreiben zu können, sowie einer sorgfältigen Dokumentation, um die Ergebnisse der Erhebung so zu formulieren, dass sie auch tatsächlich bei Architektur­entscheidungen hilfreich sind. Der Architekt sorgt dafür, dass die Szenarien möglichst „spitz“ formuliert sind und sich stets auf nur ein einziges Qualitätsmerkmal beziehen. Eine Menge von Qualitätsszenarien beschreibt dann das Qualitätsmerkmal aus unterschiedlichen Perspektiven und erlaubt so eine ausreichende Definition derselben.

Ein Qualitätsszenario [1] beschreibt also eine bestimmte Systemqualität an einem fachlichen Beispiel, welches für den Stakeholder verständlich und beurteilbar ist. Ein solches Szenario entspricht dem Schema in Abbildung 3.

takai_websystemqualitaet_3.tif_fmt1.jpgAbb. 3: Schema eines Qualitätsszenarios

Die Quelle des Stimulus gibt an, woher der Reiz kommt (Benutzer, Administrator, externe Schnittstelle). Der Stimulus beschreibt eine spezifische Zusammenarbeit der Quelle mit dem System. Die Umgebung beschreibt den Zustand des Systems zu einem bestimmten Zeitpunkt. Das Artefakt ist in der Regel ein Funktionsblock oder ein anderer Baustein. Die Antwort wird über das Messkriterium überprüfbar gemacht.

Die folgenden Beispiele sind nach diesem Schema geschrieben und verständlich. Sie können sich als Übung Gedanken über mögliche Lösungsoptionen und damit verbundene Kosten in der Entwicklung machen:

  • Ein entfernter Benutzer sendet bei Hochauslastung des Systems eine Suchanfrage und erhält innerhalb von 5 Sekunden die erste Seite des Suchergebnisses.

  • Ein Entwickler kann während der Wartungsphase automatische Testfälle erstellen, die auch das Buchungssystem betreffen.

  • Ein Servicemitarbeiter kann auf die persönlichen Daten eines Kunden während des regulären Systembetriebs nicht zugreifen, sofern er nicht auch der Rolle „Kundendienst“ angehört.

  • Ein neuer Entwickler kann während der System­evolution innerhalb von zwei Tagen Änderungen am System selbstständig durchführen.

  • Ein Administrator kann zu Spitzenzeiten die Kapazität des Systems selbstständig anpassen, ohne dass der Betrieb unterbrochen werden muss.

Nicht alle Eigenschaften können per Szenario beschrieben werden. Ein Rechte- und Rollenkonzept wird meistens als Matrix zwischen Berechtigungen und Ressourcen geführt. Dieses in Form von Qualitätsszenarien zu dokumentieren, ist unübersichtlich und wird deswegen nicht empfohlen. Auf diese Besonderheiten komme ich bei der Diskussion der einzelnen Qualitätsmerkmale zurück.

Rechtliches

Werden Qualitätsszenarien erhoben, so könnte man aus deren Messbarkeit ableiten, dass die gewünschten Werte immer erreicht werden können. Leider ist dies nicht der Fall. Es ist also für alle Beteiligten wichtig zu verstehen, dass Qualitätsmerkmale keine Verträge darstellen, sondern eine schwer messbare Beschreibung der Produkteigenschaften sind, von denen nicht immer klar ist, ob sie überhaupt eingehalten werden können.

Für eine Performanceanforderung wurde eine maximale Antwortzeit für einen gewissen Prozentsatz an Anfragen an das unter Last stehende System vorgegeben. Eine Messung würde voraussetzen, dass man ein zweites System, das mit der Produktionsumgebung identisch ist, zur Verfügung hat und dieses ausschließlich für die Tests zur Verfügung stellt, da Performancetests nicht auf einem Produktionssystem durchgeführt werden können. Es müsste zudem ein Eingabemuster entwickelt werden, das alle möglichen Kombinationen von Anfragen enthält. Beides ist technisch theoretisch möglich, aber nur schwierig finanzierbar.

Viele Qualitätsmerkmale sind auch methodische Absichtserklärungen und dienen der Vollständigkeit in Design und Planung des Systems oder um Erwartungen auf der politischen Ebene zu kanalisieren. Nehmen Sie das Beispiel von oben, bei dem sich ein Entwickler innerhalb von zwei Tagen in das System einarbeiten können muss. Da müssten Sie dann viele Abgrenzungen einfließen lassen, bevor sie das als Vertrag unterschreiben möchten.

Vertraglich relevant sind Qualitätsmerkmale hingegen insofern, als dass ihre Umsetzung aufwändig sein kann. Eine Diskussion dieser Merkmale lenkt also auch die Aufmerksamkeit des Kunden weg von der reinen Funktionalität hin zu Themen, die in vielen Fällen für den Erfolg des Systems relevanter sind, als eine große Menge von Funktionen in der Software.

Schlussendlich kann über den Erfüllungsgrad der Anforderungen die Qualität der Architektur in Bezug auf die fachlichen Wünsche aber durchaus gemessen werden. Hiervon geht beispielsweise die ATAM-Methode (Architecture Tradeoff Analysis Method) aus. In den wenigsten Fällen basieren diese Messungen jedoch auf harten Daten, sondern werden per Gutachten oder Heuristik interpretiert. Es ist üblich, an dieser Stelle auch auf weiche Indikatoren zu setzen. Stefan Toth hat in seinem Buch über Vorgehensmuster in der Architektur hierfür ein paar gute Beispiele [4].

Fazit

Die Erhebung der Qualitätsmerkmale ist essenziell, wenn man ein passendes System entwerfen möchte, mit dem der Auftraggeber am Ende zufrieden ist. Ein System kann funktional fehlerfrei sein und doch den Kunden maßlos enttäuschen, weil seine stillschweigenden Erwartungen nicht erfüllt wurden. Dem Architekten kommt die schwierige Rolle zu, diese Erwartungen im Rahmen des vorhandenen Budgets über die Qualitätsmerkmale zu verhandeln. Dafür benötigt er neben Kommunikationskompetenz und Weitsicht auch ein gutes Stück Gelassenheit. Belohnt wird diese offene und transparente Arbeit durch ein nützliches System, mit dem der Kunde glücklich sein kann.

takai_daniel_sw.tif_fmt1.jpgDaniel Takai ist Technologiemanager bei der Unic AG in Bern. Er ist dort für die Entwicklungsprozesse und Softwarearchitekturen verantwortlich.

Twitter

Bisher haben wir NoSQL-Datenbanken und ihre junge Evolutionsgeschichte betrachtet. Wir haben Parallelen zur Entwicklung der relationalen Systeme aufgezeigt und NoSQL-Systeme gemäß der üblichen Klassifikation diskutiert. Dieser dritte und letzte Teil unserer kleinen Reise greift einige Punkte noch einmal auf, um aktuelle Trends zu beleuchten. Wir schauen insbesondere auf Multi-Modell-Datenbanken und NewSQL-Systeme. Abschließend wagen wir ein paar Thesen über mögliche Entwicklungen in der nahen und mittelfernen Zukunft.

In den ersten beiden Teilen unserer kleinen Serie haben wir zunächst die relationalen Datenbanksysteme mit ihren Vor- und Nachteilen im Kontext ihrer Entstehungsgeschichte betrachtet. Zu Recht haben die relationalen Datenbanksysteme ihren Ruf als Universalsysteme eingebüßt.

Wir haben die relationalen Systeme bisher immer als ein Gesamtkonzept betrachtet: Dieses schließt das relationale Datenmodell und ACID-Transaktionalität genauso mit ein wie die Mittel zur technischen Implementierung. Die Techniken stammen allerdings zumeist aus den 1970er Jahren und sind heute oft nicht mehr zeitgemäß. Das Bild eines behäbigen alten Elefanten für diese Systeme erscheint hier also durchaus als passend.

NoSQL-Datenbanken versuchen nun in einigen Bereichen, die Mankos der gemütlichen Riesen auszugleichen. Durch Flexibilität und horizontale Skalierbarkeit unter Verwendung von Standardhardware machen sie diesen den Markt streitig. Egal, ob man der NoSQL-Bewegung positiv oder negativ gegenübersteht, eines ist auf jeden Fall erreicht worden: Die Diskussion um die Wahl der richtigen Datenbank mit dem richtigen Datenmodell für das vorliegende Problem ist wieder erlaubt und gewünscht.

Ein bekennender Kritiker sowohl der alten relationalen Datenbanken aber auch der NoSQL-Bewegung ist Michael Stonebraker, der uns auf unserer kleinen Reise bereits mehrfach begegnet ist. Stonebraker plädiert seit jeher für die richtige Wahl des Datenspeichers, ist jedoch auch ein Befürworter von ACID-Transaktionen [1]. Seine Hauptkritik an relationalen Datenbanksystemen ist nicht das relationale Datenmodell an sich, sondern die nicht zeitgemäße technische Implementierung der Systeme. Diese beruhen sämtlich auf Ideen aus System R und orientieren sich an in den 1970er Jahren vorherrschenden Techniken [2]. Gemäß dem Motto „Stand on the shoulders of those who came before you, not on their toes.“ [1] finden die Konzepte der relationalen Welt in VoltDB eine technisch aktuelle Umsetzung.

Anwendungsfelder von Datenbanken

In den ersten beiden Teilen unserer Serie haben wir im Wesentlichen zwischen SQL- und NoSQL-Systemen unterschieden und Datenbanksysteme gemäß des unterliegenden Datenmodells in fünf Kategorien eingeteilt: in relationale Systeme und die üblichen vier NoSQL-Kategorien Dokumenten-, Key-Value und Wide-Column Stores sowie Graphdatenbanken.

An dieser Stelle schauen wir auf die Unterscheidung nach Anwendungsbereichen. Die wohl gewichtigsten Anwendungsbereiche sind

  • Online Transaction Processing (OLTP)

  • Data Warehouses

  • Textmanagement

  • Stream Processing

Viele NoSQL-Datenbanken lassen sich den oben genannten Bereichen zuordnen. So sind Elasticsearch oder Solr der Textindizierung zuzuordnen. Anwendungen im Data Warehousing können mit Column Stores wie ­HBase oder Cassandra umgesetzt werden. Auf dem Hadoop-Stack basierende Lösungen wie Storm oder Kafka sind für Stream­verarbeitung verschiedenster Art optimiert.

In diesen drei Bereichen gibt es also nicht relationale Lösungen, die relationalen Lösungen um Größenordnungen den Rang ablaufen. Der Bereich des OLTP ist die originäre und nun letzte große Domäne der relationalen Datenbanksysteme, und dieser wird ihnen im Moment von NoSQL streitig gemacht.

Wir schauen uns zwei Arten von Datenbanksystemen im Bereich OLTP genauer an: zum einen Multi-Modell-Datenbanken, die verschiedene Datenmodelle im Sinne der NoSQL-Klassifikation vereinen. Und zum anderen NewSQL-Systeme, die zumindest teilweise das relationale Datenmodell beibehalten, die Mechanismen zur Erfüllung der gewünschten Eigenschaften aber zeitgemäß implementieren.

Multi-Modell-Datenbanken

Multi-Modell-Datenbanken sind NoSQL-Datenbanken, die unterschiedliche Datenmodelle vereinen, beispielsweise Eigenschaften von dokumentenorientierten Datenbanken und Graphdatenbanken. Denn gelegentlich lassen sich die Entitäten der Anwendungsdomäne gut mit Dokumenten beschreiben, allerdings bestehen zwischen diesen ja auch Beziehungen.

Mitarbeiterdaten, Abteilungen und Projekte können gut als Dokument erfasst werden. Den Ähnlichkeiten und kleinen Unterschieden der Daten (wie mehrere Adress- oder Telefonnummernarten) kann so gut Rechnung getragen werden. Die Beziehungen der Mitarbeiter zu Abteilungen und Projekten kann über Datenredundanz modelliert werden, wenn Daten über Mitarbeiter und Projekte atomar zur Verfügung gestellt werden sollen. Wird normalisiert modelliert, so sind für das Zugreifen auf Mitarbeiter- und Abteilungsdaten mitunter mehrere Anfragen notwendig.

Multi-Modell-Datenbanken erlauben nun unterschiedliche Sichtweisen auf die gespeicherten Daten und vereinen in ihren Anfragesprachen Elemente aus verschiedenen der vier NoSQL-Kategorien. Die Beziehungen zwischen Mitarbeitern, Abteilungen und Projekten werden dann als ein Graph beschrieben, dessen Knoten die Dokumente sind. Die Datenbank unterstützt nativ sowohl Anfragen und Auswertungen über die Dokumente als auch Fragen nach Beziehungen und Pfaden. Die Anzahl der Multi-Modell-Datenbanken im NoSQL-Archiv ist überschaubar [4], die bekanntesten Vertreter sind wohl ArangoDB, OrientDB und FoundationDB. Generell ist bei Verwendung einer Multi-Modell-Datenbank darauf zu achten, nicht versehentlich einen unpassenden monolithischen Ansatz zu wählen.

Die ArangoDB ist eine „Multi-Purpose-Datenbank”, deren Basismodell Dokumente sind [5]. Sie bietet aber von Beginn an auch die Vorzüge einer Graphdatenbank und unterstützt die ACID-Eigenschaften sowie das atomare „Joinen“ über mehrere Datensätze hinweg. Beziehungen zwischen Daten können als Graph modelliert werden. Da Dokumente die Basiseinheit sind, sind auch Beziehungen Dokumente und können selbst wieder als Knoten interpretiert werden, was das Abbilden komplizierter Szenarien erlaubt.

Die ArangoDB Query Language AQL ist die Schnittstelle zum System. Das Beispiel von der ArangoDB-Homepage [5] zeigt in Listing 1, wie in einer einzigen Anfrage Gebrauch von den Konzepten Dokument, Graph und Transaktionalität gemacht werden kann.

Listing 1

// Persons und Cities sind Dokumente
FOR p IN Persons
  // Filtern von Dokumenten
  FILTER myfunctions::mustPayTax(p)
  LET distance = LENGTH(
    // Graphalgorithmus kürzester Weg
    SHORTEST_PATH(Persons, Friends, p, 'Persons/godfather', 'any'))
FOR c IN Cities
  FILTER p.zip == c.zip
  // atomare Aktualisierung mehrerer Personendaten
  UPDATE p WITH {taxPaid: p.taxPaid * c.discount * distance}
    IN Persons

Das Verwenden mehrerer Datenmodelle innerhalb einer Anwendung und Anfrage ist aber nur einer der Vorteile, die Multi-Modell-Datenbanken anpreisen. Schauen wir noch einmal zurück auf unser Beispiel eines Webshops aus dem zweiten Artikel unserer Serie (Abb. 1). Wir haben für die verschiedenen Teile des Webshops unterschiedliche Persistierungstechnologien, zugeschnitten auf die Verwendung, identifiziert und im Sinne einer polyglotten Persistenzstruktur verschiedene Stores vorgeschlagen. Dies lässt sich nun auch mit verschiedenen, unabhängigen Instanzen einer Multi-Modell-Datenbank realisieren, wobei jede Instanz nur ein Datenmodell (also nur Dokument oder nur Graph) nutzt. Der Vorteil: Es handelt sich um nur eine Technologie; Expertenwissen und Support lassen sich also bei einem Anbieter anfragen.

mennerich_arrasz_1.tif_fmt1.jpgAbb. 1: Der Webshop aus [6]; mit einer Multi-Modell-Datenbank könnten verschiedene Datenmodelle mit derselben Technologie polyglott verknüpft werden

Einen ähnlichen Weg geht die FoundationDB, ein Key-Value Store, der ebenfalls ACID-Transaktionen als Grundbausteine solider Applikationen verwendet. Die FoundationDB bietet Schichten, die sie nach außen hin anderen Kategorien zugehörig erscheinen lassen. Neben einer Graphschicht gibt es auch eine SQL-Schicht, was die FoundationDB schon in die Nähe der NewSQL-Datenbanken rückt.

NewSQL-Datenbanken

NewSQL-Systeme versuchen die Vorzüge von SQL und ACID beizubehalten. Ein Kritikpunkt der NewSQL-Anbieter lautet: In den letzten Jahren sind die sich auf dem Markt befindlichen relationalen Systeme zwar angepasst worden und bieten mit Sharding, Kompression, Bitmap-Indexen, benutzerdefinierten Typen und vielem mehr neue, zeitgemäße Eigenschaften. Kein System ist allerdings von Grund auf neu geschrieben worden [2].

Das Erweitern vorhandener Systeme führt allerdings dazu, dass im Kern Altcode immer noch aktiv ist, der grundlegende Mechanismen verwendet, die für heutige Hardwarearchitekturen, insbesondere die gängige Shared-Nothing-Architektur von Rechnerclustern auf Standardhardware, nicht mehr aktuell ist. Die technologischen Grundideen der meisten großen, auch heute noch im Markt aktiven, relationalen Systeme sind bereits in IBMs System R aus den späten 1970er Jahren vorhanden [2].

Durch die Verbreitung des Webs hat die Maschine-zu-Maschine-Kommunikation zugenommen. Längst ist die Mehrzahl der Nutzer einer Datenbank nicht mehr ausschließlich menschlich. Dies führt zu anderen Bedürfnissen und Anforderungen, als sie eine Konsole für menschliche Endbenutzer fordert.

Wir bewegen uns heutzutage auch in ganz anderen Dimensionen für Haupt- und Persistenzspeicher, deren Preise stark gefallen sind. Der Trend geht weg von vertikal hochskalierten und hochspezialisierten Einmaschinensystemen und hin zu horizontal verteilten Systemen, die auf Standardhardware betrieben werden können.

Stonebraker identifiziert für die gängigen relationalen Systeme vier, auf das Design von System R und die Umstände der Zeit seiner Entwicklung zurückgehende Eigenschaften, die massiven Overhead erzeugen [9], [10]:

  • Führen von Logdateien (Logging)

  • Sperren von Datensätzen (Locking)

  • Sperren von Indexen (Latching)

  • Puffermanagement (Buffer Management)

Die traditionellen Datenbanksysteme sind gezwungen, jeden Datensatz wenigstens zweimal zu schreiben: Einmal in die Datenbank selbst und einmal in eine Logdatei für Backup und Recovery. Die Logdatei muss darüber hinaus auf der Festplatte gespeichert werden, um die Dauerhaftigkeit (das D in ACID) zu gewährleisten.

Bevor eine Transaktion einen Datensatz ändern darf, muss eine Sperre angefordert werden, um exklusiven Zugriff auf den Datensatz zu erhalten. Die Koordination der Sperren geschieht über eine Relation in der Datenbank, deren Verwaltung kostspielig ist.

Der Zugriff auf von mehreren Transaktionen gemeinsam genutzte Strukturen wie B-Bäume oder andere Indexe muss ebenfalls koordiniert werden. Diese Sperren sind in der Regel sehr kurzlebig, ihr Verwaltungsaufwand allerdings ist nicht zu vernachlässigen.

Ausgefeilte Pufferstrategien sorgen dafür, dass möglichst viele der benötigten und auf der Festplatte gespeicherten Daten schnell zugreifbar im Cache der Datenbank vorliegen. Der Austausch von Daten zwischen Puffer und Festplatte erfolgt immer seitenweise. Ein Puffermanager muss zum einen dafür sorgen, dass die benötigten Daten im Hauptspeicher residieren und zum anderen den Datensatz innerhalb der Pufferseite identifizieren.

Den Verwaltungsoverhead, den jeder dieser vier Punkte veranschlagt, liegt bei ca. 25 Prozent [10]. Das heißt, wenn alle vier Punkte eliminiert werden können, kann eine Verdoppelung der Performanz des Systems erreicht werden. Aus diesen Überlegungen ist die NewSQL-Datenbank H-Store entstanden, die mit der VoltDB ihre kommerzielle Variante gefunden hat, siehe hierzu auch den schönen Artikel von Jan Stamer [11].

Die Grundüberlegungen zur Überwindung der oben diskutierten Defizite für OLTP-Systeme können folgendermaßen zusammengefasst werden: Eine verteilte Architektur und Redundanz der Daten machen das Persistieren von Undo-Logs unnötig. Diese können nach dem erfolgreichen Commit oder Rollback einer Transaktion verworfen werden. Redo-Logs werden gar nicht mehr benötigt, die Wiederherstellung nach Datenverlust auf einem Rechnerknoten geschieht über das Netzwerk aus redundanten Daten im Cluster.

Langlaufende Anfragen sind selten und durch gutes Programmdesign oft vermeidbar. Transaktionslaufzeiten im Millisekundenbereich sind durchaus normal. Diese kurzen Transaktionen können in einer Single-Threading-Umgebung linearisiert ausgeführt werden. Durch geschicktes Sharding können Anfragen auch echt parallel auf verschiedenen Knoten des Clusters ausgeführt werden. Das macht knotenübergreifende Sperrverfahren unnötig.

Wenn alle Daten im Hauptspeicher residieren, kann auf Datensätze direkt, ohne Indirektion über den Puffermanager, zugegriffen werden. Das macht diesen obsolet, der Verwaltungsaufwand und eine Indirektionsschicht fallen weg.

Echte langlaufende Anfragen, wie sie für Reporting oder Ad-hoc-Statistiken nötig sind, sollten ohnehin in ein für diese Zwecke bereitgestelltes und optimiertes Data Warehouse verlagert werden, dem gegebenenfalls ein anderes Datenmodell zugrunde liegt. Hauptspeicher ist heutzutage so günstig, dass viele Daten In-Memory gehalten werden können. Die Verteilung der Daten in einem Cluster verändert die Backup- und Recovery-Strategien. Sharding der Daten nach Zugriff ist eine Strategie, um aufwändige Sperrverfahren wie Zwei-Phasen-Commit-Protokolle zu vermeiden. Diese kosten Netzlatenzen von mehreren Millisekunden und sind damit oft länger als die Ausführungsdauer einer Transaktion selbst.

Polyglotte Persistenz: Fluch oder Segen?

NoSQL (die Multi-Modell-Datenbanken eingeschlossen) und NewSQL bieten die Möglichkeit, flexible, polyglotte Systeme zu bauen. Hierbei muss allerdings wiederholt betont werden, dass man die Komplexität durch einen polyglotten Ansatz nicht einfach „wegzaubern“ kann. Die Komplexität, die man durch Abbildung verschiedener Domainteile in einer relationalen Struktur erhält, lässt sich in der Modellierung vielleicht auflösen, beispielsweise durch die Aufteilung in einen Graphen und eine relationale Struktur, die die Domäne natürlicher abbilden. Diese Vereinfachung verschiebt allerdings Aufwand in den Betrieb, die Wartung, die Auswertung sowie das Monitoring des gesamten Systems. Ein Blick auf die Konsequenzen aus dem CAP-Theorem lohnt sich hier (vgl. auch den vorangehenden Teil dieser Serie).

mennerich_arrasz_2.tif_fmt1.jpgAbb. 2: Nebeneinanderstellung eines monolithischen Systemansatzes und einer polyglotten Systemarchitektur

Vergleichen wir einmal die Entwicklung gegen ein verteiltes polyglottes System mit der Entwicklung einer herkömmlichen Datenhaltung in einem einzigen relationalen System (Abb. 2). Wir sehen, dass wir das Wissen über das System breiter streuen müssen. Dies beginnt bei der Konfiguration der Datasources und setzt sich über die genutzten Annotationen innerhalb des Sourcecodes fort bis in die Funktionsweise des Systems selbst. Abläufe und Abhängigkeiten innerhalb des Systems müssen sehr gut verstanden sein, um ein verteiltes polyglottes System zum einen wartbar und am Leben erhalten zu können, zum anderen aber auch die Zugänglichkeit für weitere Entwicklungen zu gewährleisten. Ebenso wächst der Kommunikationsaufwand, wenn Schnittstellen verändert werden sollen, welche nun häufig in mehreren Versionen genutzt werden können. Monitoring gewinnt in solchen Systemen eine noch größere Bedeutung, da auch die Nutzung von Schnittstellen und Services durch andere Teile des Systems protokolliert werden muss, um gegebenenfalls über deren Abschaltbarkeit entscheiden zu können.

Am Beispiel von Spring Data kann man schön sehen, wie versucht wird, zum Umgang mit relationalen Systemen äquivalente Entwicklungsmodelle bereitzustellen, um die Komplexität von polyglotten Systemen zumindest innerhalb der Entwicklung einigermaßen in den Griff zu bekommen. Natürlich könnte man auch ohne Spring Data entwickeln und mit dem nativen API von Neo4j und klassischem JPA mit Hibernate als Provider arbeiten, jedoch ist dann auch die Entwicklungskomplexität ungleich höher. Abbildung 3 zeigt die Ähnlichkeiten zwischen Spring Data für JPA und Neo4j.

mennerich_arrasz_3.tif_fmt1.jpgAbb. 3: Nebeneinanderstellung der Entwicklungsmodelle in Spring Data JPA und Spring Data Neo4j

Wie man im Vergleich in Abbildung 3 schön sehen kann, ist das Entwicklungsmodell sehr ähnlich, ob wir nun gegen eine relationale DB mit JPA oder gegen eine Graphdatenbank wie Neo4j [12] mit der Erweiterung Spring Data Neo4j [13] arbeiten. Auch die Möglichkeiten, speziellere Anfragen innerhalb der Repository-Implementierungen zu nutzen, sind in beiden Fällen sehr ähnlich. Auf der einen Seite wird klassisches SQL, auf der anderen Seite Cypher [14] verwendet.

Durch Hinzufügen von Neo4j in die Systemlandschaft müssen wir jedoch Datenbank-Connections, -Firewalls und -Management für mindestens ein weiteres Datenbanksystem, das sogar aus mehreren Clusterknoten bestehen kann, pflegen, dokumentieren und überwachen.

Wenn man hier nun Systeme der Multi-Modell-Datenbank und der NewSQL-Welt, wie beispielsweise die ArangoDB oder auch VoltDB, mit zum Vergleich heranzieht, sehen wir, dass hier weiteres Spezialwissen zu erarbeiten ist. Die Datenbanktreiber der ArangoDB [15] sind nicht gegen einen Standard wie beispielsweise JPA implementiert, das entstandene API ist proprietär. Das ist nicht notwendigerweise schlecht. Jedoch zeigt uns die Erfahrung der Vergangenheit, dass hier Risiken entstehen können.

Artenvielfalt

Der Datenbankmarkt ist in Bewegung und wird immer vielfältiger. Nach dem Rückgang der relationalen Systeme und den Verwirrungen der NoSQL-Welt komplettieren nun die NewSQL-Systeme die Unübersichtlichkeit bei der Wahl des richtigen Speichersystems. Das richtige System zu wählen, ist jedenfalls weniger denn je eine triviale Aufgabe, und die Gefahr, dass man das für sein Problem optimale System gar nicht kennt, ist bei der gegebenen Vielzahl an erhältlichen Datenbanksystemen natürlich groß.

Polyglotte oder eher monolithische Ansätze lassen sich mit allen Systemen, seien sie OldSQL, NoSQL oder NewSQL, erreichen. Vertreter wie die Multi-Modell-NoSQL-Systeme sind hier sorgsam zu erlernen, da sie einen vielleicht eher dazu verführen, sein System versehentlich monolithischer und enger zu gestalten als beabsichtigt, da unterschiedliche Modelle nebeneinander vereint existieren. Dies allerdings ist ja auch ihr großer Vorteil.

Abzuwarten ist, ob der Druck der Märkte nicht wiederum dazu führt, dass Systeme zu Universallösungen werden wollen. Dies ist zum Scheitern verurteilt und hat das langsame Aussterben der OldSQL-Systeme zumindest eingeleitet. Diese Art der Entwicklung sollte vermieden werden, um die Artenvielfalt zu erhalten. Doch auch Spezialisten müssen sich erst etablieren. Viele neue Systeme werden noch eine Weile brauchen, um sich durchzusetzen. Denn in puncto Tuning und Betriebsstabilisierung sowie generell zur Risikominimierung bei der Wahl eines Systems stehen momentan Stores wie ­MariaDB oder Neo4j noch deutlich besser da als beispielsweise die VoltDB, für die zur Zeit nur schwer Experten zu finden sein dürften.

Mit den neuen Stores müssen sich auch die Programmiermodelle zur Unterstützung der Entwickler ausbilden und wachsen. Dabei kann aus den Erfahrungen der bestehenden Werkzeuge gelernt werden, Spring Data macht es mit der Unterstützung für viele NoSQL-Datenbanken vor.

Fazit

Aus unserer Sicht bleibt es dabei: Nie zuvor war das Angebot an Datenbanken so groß wie heute, und nie war die Wahl so spannend. Alles in allem sollte aber klar sein, dass es die Geschäftsdomäne ist, die letztendlich bestimmt, wie ein System architektonisch zu designen ist und welche Speicherstrategien dazu optimal geeignet sind. Disziplin und Wissen sind hier – wie so oft – der Schlüssel zum Erfolg.

Wer seine Geschäftsdomäne kennt, zusammen mit den Anforderungen und Beschränkungen, die sie stellt, kann maximale Performanz erzielen. Und im Falle weicher Anforderungen, oder wenn die Domäne sich stark ändern darf, sind schemafreie Systeme vorhanden, die Migrationsrisiken klein halten. Welche der Systeme sich letztlich wirklich durchsetzen werden, bleibt nur abzuwarten. Die Wahrscheinlichkeit aber, dass ACID und strengere Programmierparadigmen ihren festen Platz in der Entwicklung behalten, ist anzunehmen. Deutlich sind die Zeichen, die NewSQL-Systeme setzen. Und der Wunsch nach Transaktionen über mehrere Datensätze hinweg steht auch bei einigen NoSQL-Systemen auf der Roadmap, zum Beispiel bei MongoDB [16].

Wir hoffen, dass die Anbieter der Systeme der neuen Welt nicht den Blick zurück scheuen und den Weg der Spezialisierung und Stabilisierung der Systeme weitergehen. So kann spezielles Wissen aus der Domäne optimal unterstützt werden, und Performanz geht nicht verloren, weil das System als Universallösung alles will und nur wenig noch richtig gut kann. Aus der Erfahrung der Vergangenheit kann gelernt werden, denn früher war ja nicht alles schlecht.

mennerich_christian_sw.tif_fmt1.jpgChristian Mennerich hat Diplom-Informatik studiert, einer seiner Schwerpunkte lag auf Theorie und Implementation von Datenbanksystemen. Er arbeitet als Entwickler bei der synyx GmbH & Co. KG in Karlsruhe, wo er sich unter anderem mit NoSQL beschäftigt.

Twitter

arrasz_joachim_sw.tif_fmt1.jpgJoachim Arrasz ist als Software- und Systemarchitekt in Karlsruhe bei der synyx GmbH & Co. KG als Leiter der Code Clinic tätig. Darüber hinaus twittert und bloggt er gerne.

Blog Twitter

Seit der Einführung des Android Studios als offizielle Android-IDE hält sich vehement das Gerücht, dass eine sinnvolle Absicherung der eigenen Sourcen durch Unit Tests nur mit sehr viel Bauchschmerzen und auch nur unter Verwendung der veralteten JUnit-Version 3.x zu realisieren ist. Hatte diese Aussage in der Version 1.0 durchaus noch ihre Berechtigung, gilt spätestens seit der Nachfolgerversion 1.1 das Gegenteil.

Sucht man im Internet nach Quellen zum Thema „Einrichten von Unit Tests im Android Studio“, stößt man unweigerlich auf ausführliche und nicht minder komplizierte Step-by-Step-Anleitungen, die beschreiben, wie Unit Tests im Android Studio trotz angeblich fehlender Unterstützung doch aktiviert werden können. In den meisten Fällen wird man dazu aufgefordert, das build.gradle-Skript um einige Zeilen zu erweitern, diverse Plug-ins zu laden und damit letztendlich die notwendigen Instrumentationstests zu ermöglichen.

Zugegeben, in den frühen Betaversionen des Android Studios war es tatsächlich alles andere als trivial, einen Unit Test zum Laufen zu bekommen. Und auch in der Version 1.0 gab es noch die ein oder andere Hürde, die es zu überwinden galt. Aber spätestens seit der Version 1.1 ist dieses Problem obsolet. Lediglich ein kleines Häkchen unter Enable Unit Test Support in den Gradle Preferences (Experimental) sowie die Verwendung des Gradle-Plug-ins 1.1.0 oder höher und schon können Unit Tests problemlos angelegt und ausgeführt werden. Noch einfacher ist es in der Nachfolgerversion 1.2, in der Unit-Test-Support per Default aktiviert ist.

Wo liegt was?

Android Studio unterscheidet bei Tests zwei Artefaktvarianten, welche gezielt in dem Build-Variantfenster ausgewählt und über eigene Run-Konfigurationen gestartet werden können. Während reine Unit Tests, also Tests, die ohne Unterstützung des Emulators bzw. des Android OS laufen, per Konvention in einem parallel zu src/main liegenden Verzeichnis src/test erwartet werden, sollten Android-Instrumentationstests, also Tests, die direkt oder indirekt auf das Android-Testing-Framework und somit auf den darin enthaltenen Instrumentationsmechanismus zurückgreifen, in dem Verzeichnis src/android­Test abgelegt werden.

JUnit-4-Support

Eine weitere Mär besagt, dass Unit Tests unter Android nur mit der JUnit-Version 3.x möglich sind, was u. a. strikte Namenskonventionen für die Testartefakte, also die Klassen und Methoden, mit sich bringt und die Verwendung von den erst in Version 4 eingeführten JUnit-Annotationen sowie allen weiteren JUnit-4-spezifischen Features verbietet. Auch dies stimmt heute in dieser Form nicht mehr.

Im Rahmen der Android Support Test Library, welche sich im Android Support Repository befindet und innerhalb des Android Studios via SDK Manager heruntergeladen werden kann, stellt Google einen eigenen AndroidJUnitRunner [1] zur Verfügung. Neben der ­JUnit-Version 3 wird so auch die Version 4 im vollen Umfang unterstützt. Auch eine parallele Verwendung beider Versionen in einer Testsuite ist möglich. Lediglich innerhalb eines Tests sollte eine gemeinsame Nutzung beider API-Versionen vermieden werden, da es sonst zu nicht vorhersehbaren Nebeneffekten kommen kann. Zum Einbinden in das eigene Projekt müssen lediglich die folgenden Einträge in dem build.gradle-Skript ergänzt werden, welche übrigens gleichzeitig auch die Verwendung des Espresso-Test-Frameworks ermöglichen:

// inside dependencies
androidTestCompile 'com.android.support.test.espresso:espresso-core:2.0'
androidTestCompile 'com.android.support.test:runner:0.2'
 
// inside android.defaultConfig 
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"

Möchte man JUnit 4 im direkten Zusammenspiel mit dem API des Android-Testing-Frameworks [2] verwenden, um so zum Beispiel einen JUnit-4-basierten Test zu schreiben, der von der Klasse ActivityInstrumentationTestCase2 ableitet, gilt es, zusätzlich zu den oben genannten Schritten einige Regeln zu beachten [3]:

  • Zunächst muss die eigene Testklasse mit @ RunWith(AndroidJUnit4.class) annotiert werden.

  • Zusätzlich muss die setUp()-Methode der Klasse ActivityInstrumentationTestCase2 überschrieben und mit @Before annotiert werden. Innerhalb der Methode muss ein Aufruf von super.setUp() erfolgen.

  • Da durch die Verwendung eines neuen Test Runners keine automatische Einbindung der Android-Instrumation erfolgt, muss dies innerhalb der Setup-Methode durch den Aufruf der Methode injectInstrumentation(InstrumentationRegistry.getInstrumentation()) „manuell“ erfolgen.

  • Tests, die durch den Runner ausgeführt werden sollen, müssen mit @Test annotiert werden. Der Name des Tests spielt dabei, im Gegensatz zu JUnit 3, keine Rolle.

  • Abschließend muss noch die tearDown()-Methode der ActivityInstrumentationTestCase2-Klasse überschrieben und mit @After annotiert werden. Innerhalb der Methode muss ein Aufruf von super.tearDown() erfolgen. Dies ist notwendig, um alle benutzten Testressourcen wieder sauber freizugeben.

Listing 1 zeigt einen von der Klasse ActivityInstrumentationTestCase2 abgeleiteten JUnit-4-Test unter Berücksichtigung der eben beschriebenen Punkte.

Listing 1: JUnit 4 in Aktion

@RunWith(AndroidJUnit4.class)
@LargeTest
public class MyJunit4ActivityInstrumentationTest
extends ActivityInstrumentationTestCase2<MyActivity> {
 
  private MyActivity mActivity;
 
  public MyJunit4ActivityInstrumentationTest() {
    super(MyActivity.class);
  }
 
  @Before
  public void setUp() throws Exception {
    super.setUp();
    injectInstrumentation(InstrumentationRegistry.getInstrumentation());
    mActivity = getActivity();
  }
 
  @Test
  public void checkPreconditions() {
    assertThat(mActivity, notNullValue());
    // Check that Instrumentation was correctly injected in setUp()
    assertThat(getInstrumentation(), notNullValue());
  }
 
  @After
  public void tearDown() throws Exception {
    super.tearDown();
  }
 
}

Fazit

Entgegen anders lautender Gerüchte und veralteter Ressourcen im Internet ist die Umsetzung von Unit Tests auch unter Verwendung des Android Studios problemlos möglich. Voraussetzung ist lediglich die Version 1.1+ der offiziellen Android-IDE sowie die Version 1.1.0+ des zugehörigen Gradle-Plug-ins.

Möchte man seine Tests mithilfe von JUnit 4 schreiben, um so zum Beispiel in den Genuss der Annotationen zu kommen, müssen zusätzlich einige Ressourcen des Android-Test-Kits eingebunden und ein spezieller Test Runner namens An­droidJUnitRunner verwendet werden. Der nächste logische Schritt wäre das Einbinden des Espresso-Test-Frameworks, welches mit der Version 2.1 noch einmal ein paar nützliche Features dazubekommen hat. Das ist aber ein Thema für eine andere Kolumne. In diesem Sinne: Stay tuned ...

roewekamp_lars_sw.tif_fmt1.jpgLars Röwekamp ist Geschäftsführer der open knowledge GmbH und berät seit mehr als zehn Jahren Kunden in internationalen Projekten rund um das Thema Enterprise Computing.

Twitter

limburg_arne_sw.tif_fmt1.jpgArne Limburg ist Softwarearchitekt bei der open knowledge GmbH in Oldenburg. Er verfügt über langjährige Erfahrung als Entwickler, Architekt und Consultant im Java-Umfeld und ist auch seit der ersten Stunde im Android-Umfeld aktiv.

Twitter

Print-Abo

Einmal zahlen, 12x im Jahr das Java Magazin nach Hause geliefert bekommen. Ab 95 €/Jahr

  • 12 gedruckte Exemplare des Java Magazins
  • Lieferung direkt zu Dir nach Hause
Desktop Tablet Mobile
Desktop Tablet Mobile