Java Magazin   1.2021 - JavaFX & GraalVM

Preis: 9,80 €

Erhältlich ab:  Dezember 2020

Umfang:  100

Autoren / Autorinnen: 
Dominik Mohilo ,  
Tam Hanna ,  
Thomas Maas ,  
Karine VardanyanStephan Rauh ,  
Michael Simons ,  
Axel Rengstorf ,  
Niko Köbler ,  
Wolfgang WeigendJosé Pereda Llamas ,  
Johan Vos ,  
Anzela Minosi ,  
Renke Grunwald ,  
Arnold Franke ,  
Malte Brunnlieb ,  
Henning Schwentner ,  
Elena BochkorDr. Veikko Krypczyk ,  
Manfred Steyer ,  
Tam Hanna ,  

Wo sich früher große, reichhaltige Clientanwendungen auf den Maschinen breitmachten, setzt man heute lieber auf weniger schwergewichtige Cross-Platform-Apps. Gleichwohl ist die Entwicklergemeinde damit juristisch nicht zum omnimodo facturus geworden, denn diese Verschiebung der Prioritäten ist die Folge der Umstände: Immer mehr findet online und in der Cloud statt.

Die essentialia negotii, also die wesentlichen Vertragspunkte dafür, sind unter anderem oben erwähnte Leichtgewichtigkeit und Kompatibilität mit unterschiedlichsten Plattformen. Hand aufs Herz: Wer denkt heute in Zeiten von JavaScript, HTML5 und CSS bei diesen Begriffen denn wirklich an Java und unseren lieben Duke? Eben. Normalerweise könnte die Diskussion damit beendet sein, denn minima non curat praetor: Das Gesetz kümmert sich nicht um Kleinigkeiten. Wäre da nicht JavaFX.

Bis etwa 2014 galt das GUI-Toolkit Swing als absoluter Platzhirsch, heutzutage ist JavaFX populärer und bietet auch deutlich mehr Möglichkeiten. Dennoch konnte sich die Technologie keineswegs gegen die moderneren Stacks aus dem Webbereich durchsetzen. Erwartet hatte man das auch nicht wirklich, denn nultra posse nemo obligatur: keiner wird über sein Können hinaus verpflichtet. JavaFX hin oder her, Java bleibt Java und daher im Vergleich mit auf Geschwindigkeit und Leichtgewichtigkeit getrimmten Technologien immer ein wenig benachteiligt. Das könnte sich aber nun dank der GraalVM ändern: Die neue virtuelle Maschine verleiht nicht nur polyglotte Fähigkeiten für die Programmierung, sondern könnte durch den versprochenen inhärenten Performanceboost JavaFX wieder interessanter für grafische Exkursionen machen.

Doch hatte Oracle nicht jüngst das Projekt aus dem Oracle JDK entfernen lassen? Ganz recht, laut Oracle die ultima ratio, um dem Projekt wieder frischen Wind zu geben. Und Unrecht hatten sie damit nicht: JavaFX wird heute liebevoll von der Community in Zusammenarbeit mit Oracle (pacta sunt servanda, sie hatten immerhin versprochen, sich weiter zu beteiligen und Verträge sind einzuhalten, wie der Jurist weiß) verwaltet und blüht.

In dieser Ausgabe des Java Magazins beschäftigen wir uns ausführlich mit JavaFX: Es gibt eine Übersicht, wie die GraalVM JavaFX beflügelt, wir sprachen mit Johan Vos über die aktuellen OpenJFX-Versionen, und im Praxisartikel geht es darum, wie man JavaFX-Anwendungen für mehrere unterschiedliche Bildschirmgrößen optimiert.

Sie sehen, liebe Leserinnen und Leser, JavaFX hat durchaus die Chance auf Resozialisierung verdient und es außerdem gilt ohnehin: In dubio pro Dukeo!

Gegen das Urteil ist das Rechtsmittel der Berufung nicht zugelassen, damit schließe ich die Verhandlung und wünsche gute Unterhaltung.

mohilo_dominik_sw.tif_fmt1.jpgDominik Mohilo | Redakteur

Mail Website Twitter Xing

Was ist GraalVM? Welche Möglichkeiten bietet die Laufzeitumgebung? Wie kann die Anwendungsleistung durch GraalVM verbessert werden? In dieser Artikelserie beschäftigen wir uns mit verschiedenen Aspekten und Anwendungsfällen der GraalVM.

Über die Java Virtual Machine (JVM) mit dem Namen GraalVM hat bereits einer von uns (Stephan) gebloggt. Seit damals hat sich viel getan, die GraalVM hat die Java-Landschaft deutlich verändert. Twitter verwendet sie schon seit Jahren für seine Scala Microservices. Die GraalVM fasst immer mehr Fuß im Bereich der Cloud-nativen Anwendungen. Grund genug also, sich die GraalVM noch einmal genau und ausführlich anzusehen und die Frage zu beantworten, ob es sich lohnt, die gute alte JVM durch etwas Neues zu ersetzen.

Eine ausführliche Einführung in GraalVM

In dieser Serie zur GraalVM erfährst du, welche Performanceeigenschaften und Möglichkeiten die GraalVM bietet.

Starten wir zunächst einmal mit der Definition, was die GraalVM eigentlich ist. Karine hat es vor einiger Zeit so auf den Punkt gebracht: „Die GraalVM ist eine universelle virtuelle Maschine für das Betreiben von Anwendungen, die in Sprachen wie JavaScript, Python, Ruby und R geschrieben sind. Zudem unterstützt sie Sprachen wie C und C++. Überdies kann sie sämtliche Sprachen ausführen, die einen LLVM Compiler haben, dazu gehört sogar FORTRAN. Und natürlich kommt sie wunderbar mit dem gesamten Java-Universum klar, inklusive Scala, Kotlin und (Überraschung!) Java selbst. Das Versprechen der GraalVM ist die absolute Interoperabilität zwischen unterschiedlichsten Programmiersprachen. Vielleicht werden wir also bald polyglotte Anwendungen sehen, die sich eine gemeinsame Instanz einer virtuellen Maschine teilen. Doch die GraalVM kann noch mehr: Sie lässt sich im ‚eingebetteten Modus‘ nutzen, etwa in Verbindung mit der Oracle Database und MySQL.“

Wow, das ist schon ein gewaltiger Brocken. Die GraalVM verspricht eine Menge zu bieten. Wir werden sie uns gemeinsam Stück für Stück ansehen – das wird ein Weilchen dauern. Dieser Artikel ist die Einführung zu einer neunteiligen Serie, in deren Verlauf wir die großen Versprechen kritisch beleuchten, das Potenzial der GraalVM ausloten und viel über die Technologie erfahren werden. Heute beleuchten wir kurz, worum es bei GraalVM eigentlich geht.

Was ist GraalVM?

GraalVM ist ursprünglich ein Forschungsprojekt der Oracle Labs. Seit 2012 hat das Entwicklungsteam Dutzende wissenschaftlicher Abhandlungen über die GraalVM verfasst. Die erste und einfachste Antwort auf die Frage ist also: GraalVM ist ein bemerkenswert lang andauerndes Forschungsprojekt. Und ein erfolgreiches noch dazu. Das Ergebnis der Forschungen ist ein kommerzielles Produkt mit einem Open-Source-Ableger.

Vieles dabei dreht sich um Futamura-Projektionen, mit deren Hilfe Truffle implementiert wurde. Truffle ist der polyglotte Teil der GraalVM, also die Welt von JavaScript, Ruby, R und allen LLVM-Sprachen. Vereinfacht ausgedrückt ist LLVM eine virtuelle Maschine für Sprachen wie C/C++, FORTRAN und viele andere. Der LLVM Compiler konvertiert C-Code in LLVM-Bitcode, der ohne Probleme auf der GraalVM läuft (Abb. 1).

Eine Übersicht der GraalVM

Abb. 1: Eine Übersicht der GraalVM

Mit anderen Worten: Hätten wir den Quellcode von DOOM, könnten wir das Spiel auf der Java VM spielen. Schade, dass es aus Copyrightgründen so schwer ist, an den Code heranzukommen. Es wäre jedenfalls ein faszinierendes Projekt. Vielleicht habt ihr ja auch Daniel Kurkas legendäre JavaScript-Präsentationen gesehen, die seinerzeit einen JavaScript-Port von DOOM zeigten. Damals nutzte sein Team Emscripten, um den C-Code in JavaScript zu übersetzen. Wir fänden es spannend, das mit der GraalVM noch einmal durchzuführen, damit man die beiden Ansätze miteinander vergleichen kann. Unsere Vermutung: mit GraalVM und LLVM ist die Portierung leichter als mit Emscripten.

Habt ihr bemerkt, dass unsere Liste der von Truffle unterstützten Sprachen eine gar nicht auflistet, nämlich Java? Das ist kein Versehen. Truffle ist in Java geschrieben, es war aber nie dazu gedacht, Java auszuführen. Das ist unpraktisch, weil es bedeutet, dass Truffle keinen Java-Code aufrufen kann. Java ist in der polyglotten Welt von Truffle ein Außenseiter.

Das könnte sich ändern. Es gibt ein aufregendes Projekt im GraalVM-Universum, das sich damit beschäftigt: Espresso. Wir sind uns allerdings nicht sicher, ob es noch aktiv vorangetrieben wird. Die Blogosphäre ist auffällig still, was Espresso betrifft. Die Idee dahinter ist, Java zu einem First-Class-Citizen im Truffle-Universum zu machen. Espresso ist das Versprechen einer wirklich polyglotten Entwicklerwelt. Das Projekt macht es möglich, Java aus JavaScript, Ruby, R und all den anderen Sprachen aufrufen zu können, die von Truffle unterstützt werden.

GraalVM als Plug-in-Ersatz für die JVM

Es gibt noch einen zweiten Teil des GraalVM-Projekts, der aktuell deutlich mehr Beachtung findet. Das Herzstück der GraalVM ist ein neuer C2-Compiler. Einfacher ausgedrückt: GraalVM implementiert einen Teil der virtuellen Maschine von Java neu.

Seit 2019 ist die GraalVM auf Linux bereits „production ready“. Seit Version 20.1.0 wird auch Windows vollständig unterstützt. Ob das alles wirklich so einwandfrei funktioniert, damit befassen wir uns im dritten Teil unserer Serie. Um es vorwegzunehmen: Eure Java-Anwendung läuft auf der GraalVM. Alles andere würde uns sehr überraschen. Spannender ist die Frage, ob sie besser läuft als auf der traditionellen JVM.

Das Hauptargument für die GraalVM ist das Versprechen besserer Performance. Die Implementierung von Ruby auf der GraalVM soll bis zu 30-mal schneller laufen als die ursprüngliche Implementierung, sofern man einigen Videos auf YouTube glauben kann. Die Präsentationen sind überzeugend, doch haben wir schon oft die Erfahrung gemacht, dass die Praxis der Anwendungsentwicklung anders aussieht. Wir haben daher verschiedene Szenarien mit JavaScript, Ruby und Java getestet.

Ruby hat sich dabei tatsächlich als angenehme Überraschung herausgestellt. Die GraalVM führt den künstlichen Ruby Benchmark rund 30 Prozent schneller aus als Ruby 2.7.0 selbst. Natürlich gibt es zwei weitere Ruby-Implementierungen, die wir bislang nicht getestet haben – wir sind keine Ruby-Experten. Dennoch sind 30 Prozent mehr Performance ein vielversprechender Start. Außerdem glauben wir fest daran, dass es noch Spielraum nach oben gibt.

Aus Sicht von Java-Entwicklern ist der Boden der Tatsachen näher. Grundsätzlich kann man sagen, dass die GraalVM in etwa gleich schnell wie Oracles HotSpot Compiler ist. Das ist nicht schlecht – aus technologischer Sicht sogar äußerst beeindruckend – lockt im Businessumfeld aber auch keinen Hund hinter dem Ofen hervor. Zu dem Zeitpunkt, da dieser Artikel veröffentlicht wird, könnte sich das bereits wieder geändert haben: Dann steht die nächste Version 21.0.0 in den Startlöchern. Wir haben unsere Messungen mit GraalVM 20.1.0 durchgeführt. Langfristig spricht einiges dafür, dass die GraalVM die traditionelle JVM abhängen kann. Kurzfristig wird das wohl eher nicht passieren. Java ist bis dato eine der am heftigsten optimierten Programmiersprachen und es wird nach wie vor fleißig daran gearbeitet. Es dürfte also sehr schwer werden, sie zu schlagen. Wir halten es längerfristig aber für möglich, denn die GraalVM ist ein frischer und neuer Ansatz, der eben nicht mit der Last von über 20 Jahren Optimierungsarbeiten ins Rennen geht. Derzeit ist der Quellcode der GraalVM sehr geradlinig, verglichen mit dem C++-Code der JVM. Wir müssen uns dabei auf die Aussagen von Experten verlassen: Wir selbst hatten bisher nur sehr flüchtig Gelegenheit, den Quelltext der GraalVM zu lesen.

Bei JavaScript sieht es ähnlich aus. Wenn man den vielen Talks und Vorträgen auf Konferenzen Glauben schenken kann, führt die GraalVM JavaScript-Programme in etwa genauso schnell aus wie Googles V8-Compiler. Das ist bemerkenswert, aber nichts, was einem IT-Manager das Wasser im Munde zusammenlaufen lässt. Wie sich bei unseren Tests herausstellte, wird es auch schwer, die JavaScript-Entwickler zu begeistern. Bei den Präsentationen wird normalerweise die Peak Performance herausgestellt. Diese Geschwindigkeit erreicht die GraalVM erst nach einiger Zeit. Ein einfacher Benchmark besteht nur aus wenigen Zeilen. Der JIT-Compiler der GraalVM hat wenig Mühe, ihn zu optimieren. Unsere Tests zeigen das sehr deutlich. Die meisten Anwendungen sind sehr viel größer, brauchen daher länger, um in Fahrt zu kommen und erreichen im Endeffekt manchmal nur 10 Prozent der Performance. Wir waren zum Beispiel sehr frustriert, als wir eine Angular-Anwendung mit dem Angular CLI kompilieren wollten. Mit GraalVM 20.1.0 ist das eine Geduldsprobe.

Wobei – ist das überhaupt die richtige Fragestellung? GraalVM ist angetreten, um Java zu verbessern und die polyglotte Programmierung zu revolutionieren. Ein Teil davon ist die Unterstützung von JavaScript und Node.js. Aber im Kern reden wir immer noch über eine virtuelle Maschine für Java. Und jetzt beschweren wir uns darüber, dass diese virtuelle Maschine sich mit dem Angular CLI schwertut?

Ehrlich gesagt waren wir sehr überrascht, dass das Angular CLI überhaupt auf der GraalVM läuft. Das CLI benötigt immerhin das gesamte Node.js-Ökosystem. Die GraalVM hat funktionierende Kopien von Node. js und npm an Bord, darüber hinaus ist sie mit ECMAScript 2019 kompatibel. Selbst wenn die GraalVM heute noch etwas langsam ist, ist das an sich schon ein großer Erfolg. Schneller als Rhino und Nashorn ist sie allemal. Das Projektteam hat zudem ein sehr optimistisches Ziel formuliert: Es will die gleiche Performance erreichen, die Node.js bereitstellt. Hoffen wir, dass mehr gemeint ist als nur die Peak Performance – wobei auch das ein ambitioniertes Ziel wäre.

Fassen wir zusammen: GraalVM erleichtert die polyglotte Programmierung enorm, kann mit einiger Wahrscheinlichkeit ohne Weiteres als Ersatz für eure herkömmliche JVM verwendet werden und bringt dann ungefähr die gleiche Performance für Java und JavaScript, die ihr vorher auch hattet. Aus der Perspektive der Techniker ist das äußerst beeindruckend. Und dennoch: All diese technologische Exzellenz wird trotzdem euren CEO nicht davon überzeugen, die Cash-Cow-Anwendung auf der GraalVM laufen zu lassen. Die läuft nun mal mit Java, und da bietet die GraalVM keine wesentlichen Verbesserungen. Wenn die GraalVM eine valide Alternative werden soll, muss sie ihren Wert erst unter Beweis stellen. Unsere Suche geht also weiter. Was ist das Killerfeature, das für so viel Aufmerksamkeit in den Medien gesorgt hat?

GraalVM als Treiber des Erfolgs: Performance und Stabilität

In unserer Branche ist Performance oft der Schlüssel zum Erfolg. Wir hatten schon erwähnt, dass die Bilanz eher durchwachsen ist – zumindest bei Java und JavaScript – aber hier sind Fortschritte zu erwarten. Das GraalVM-Projekt ist Open Source und die virtuelle Maschine ist in einer populären Programmiersprache geschrieben. Heutzutage kann nun wirklich fast jeder in Java programmieren, es ist also sehr einfach, sich am GraalVM-Projekt zu beteiligen und Verbesserungen einzubringen. Na gut, „sehr einfach“ mag ein wenig übertrieben sein, aber es ist sicher einfacher, als Verbesserungsvorschläge für den HotSpot Compiler zu machen. Man muss bedenken, dass der HotSpot Compiler bereits zwei Jahrzehnte alt und zu allem Überfluss eben in C/C++ geschrieben ist, einer Sprache, die zwar noch beliebt ist – allerdings nicht unter Java-Entwicklern.

Den JVM Compiler in Java umzuschreiben, eröffnet ganz neue Möglichkeiten. Jede Wette, dass etliche Java-Entwickler wenigstens einige Zeit mit der GraalVM herumexperimentieren werden, dabei Schwächen finden und dann Bugfixes und Verbesserungen vorschlagen oder sogar als Pull Request umsetzen. Die relative Aktualität der GraalVM begünstigt dies deutlich – anders als bei der V8 Engine und dem HotSpot Compiler, die beide Jahrzehnte voller Optimierungen auf dem Buckel haben. Erfahrungsgemäß wird es dadurch schwer, etwas zu verbessern, ohne etwas Anderes kaputtzumachen. Wer schon einmal an einer großen Enterprise-Anwendung gearbeitet hat, kennt das Problem. In der Businesswelt entstanden aus dieser Not die Microservices, in der JVM-Welt ist es die GraalVM. Basierend auf neuen Ideen ist es ein frischer Lösungsansatz für alte Probleme. Außerdem wurde die GraalVM bereits mit dem Gedanken an Optimierung und Erweiterung entworfen.

Das ist vermutlich einer der Gründe, warum Twitter auf die GraalVM setzt. Der Produktionscode des sozialen Netzwerks läuft auf Scala, einer Sprache, die zu JVM-Bytecode kompiliert wird. Natürlich nutzt Twitter nach wie vor den gleichen Bytecode, aber sie haben die GraalVM mit einigen Scala-spezifischen Optimierungen ausgestattet.

Deutlicher wird das alles mit Blick auf Ruby und R. Diese Sprachen sind keine JVM-Sprachen. Natürlich gibt es das JRuby-Projekt, mit dem Ruby zu Java-Bytecode kompiliert werden kann. JRuby ist sehr erfolgreich, aber die Ruby-Implementierung von Graal geht das Problem von einem anderen Winkel an. Die eigentliche Zielsprache von GraalRuby ist Maschinencode – über den Umweg des in Java geschriebenen Interpreters und der Futamara-Projektion. Auf die Besonderheiten des Java-Bytecodes braucht der Interpreter keine Rücksicht zu nehmen. Im (theoretischen) Idealfall leisten der JIT-Compiler und der Optimizer so gute Arbeit, dass man dem Maschinencode nicht mehr ansieht, dass er ursprünglich aus Bytecode generiert wurde.

Truffle: Die Futamura-Projektion wird Wirklichkeit

Auf die Futamura-Projektion werden wir in einem späteren Teil der Artikelserie genauer eingehen. Die Kurzfassung sieht so aus: Der einfachste Weg, eine Programmiersprache zu implementieren, ist, einen Interpreter zu schreiben. Leider sind Interpreter viel langsamer als Compiler. Einen guten Compiler zu schreiben, ist eine Kunst für sich. An den Universitäten dauern die Vorlesungen über Compilerbau üblicherweise mehrere Semester.

Der Geistesblitz von Professor Futamura war es, sich zu überlegen, was passiert, wenn der Interpreter in einem JIT-Compiler läuft. Der JIT-Compiler übersetzt den Interpreter in Maschinencode – aber eben nicht nur den Interpreter, sondern alles, auch das Programm, das auf dem Interpreter läuft. Wenn der JIT-Compiler noch einen Optimizer enthält, wird der Interpreter im Idealfall wegoptimiert. Übrig bleibt ein Programm, das genauso schnell läuft wie mit einem traditionellen Compiler. Die Idee ist nicht neu. Yoshihiko Futamura hat seine Projektion in den frühen 1970er Jahren erfunden. Er scheint seiner Zeit voraus gewesen zu sein. Wir haben bei unserer Recherche nur wenige Implementationen der Futamura-Projektion gefunden. Erst jetzt, rund 50 Jahre nach seiner Grundlagenforschung, materialisiert sich seine Idee nun in der Businesswelt.

Übrigens nicht ganz unwichtig: die reine Performance ist das eine. Darauf haben wir vor allem geschaut, als Computer noch sehr langsam waren. Jetzt tun sich neue Hürden auf: Aus ökonomischer Sicht kostet Energieverbrauch Geld. Globaler betrachtet wird dadurch auch der CO2-Fußabdruck des Unternehmens größer. Als Entwickler habt ihr euch darüber vermutlich bisher wenig Gedanken gemacht. Für eure Marketingabteilung könnte das schon eher spannend sein. Klimafreundliche Produkte lassen sich leichter verkaufen als Klimakiller.

GraalVM. Schreib deine eigene Programmiersprache!

Für die akademische Welt ist GraalVM spannend, weil es durch Truffle so einfach wird, neue Programmiersprachen zu designen. Wir sind gespannt, wo das noch hinführen wird. In den letzten zehn Jahren haben wir eine Vielzahl neuer Programmiersprachen gesehen, die neue Konzepte ausprobierten. Wer in der eher seriösen Welt der Tech-Industrie sein Geld verdient, hat dies vielleicht wegen der dort anhaltenden Dominanz von Java, JavaScript und C/C++ nicht mitbekommen. Aber viele der aktuellen Features populärer Programmiersprachen haben ihren Ursprung in den Nischensprachen.

Nehmen wir das dynamische Typisieren zum Beispiel, dessen Wurzeln in Ruby, Groovy und JavaScript liegen. Streams und funktionale Programmierung sind erst mit Groovy und Scala so richtig ernst genommen worden. Später haben Kotlin und Ceylon die Idee aufgegriffen, bevor sie schließlich in Java 8 auch in der „Muttersprache“ landete. Auch die Null-Safety zum Zeitpunkt der Kompilierung kam via Kotlin und Ceylon ins Java-Universum (und wartet noch darauf, von Java aufgegriffen zu werden). Scala demonstrierte eindrucksvoll, wie man das Konzept der Immutability ohne Schmerz nutzen kann, was es dann wiederum Akka ermöglichte, reaktive Programmierung und die Macht von Multi-Core-Prozessoren nutzbar zu machen.

Die genauen historischen Diskussionen um Enums, Value Types und Autoboxing haben wir leider nicht mitbekommen. Nichtsdestotrotz wird das Muster erkennbar: Bevor man einen echten Game Changer in eine Programmiersprache packt, die von Millionen Usern verwendet wird, ist es sehr sinnvoll, diesen in einer weniger populären Programmiersprache auszuprobieren und damit herumzuspielen. Entwickler sind im Hinblick auf Breaking Changes sehr viel toleranter, wenn sie an der neuesten Technologie arbeiten. Es ist zudem schon ein tolles Gefühl, wenn man seine Ideen und Vorschläge in die Entwicklung einer neuen Sprache einfließen lassen kann.

Mainstreamsprachen können sich keine Breaking Changes erlauben. Jedes Feature, das einmal Eingang in die Sprache gefunden hat, bleibt auch dort. Versucht einmal in Java die Type Erasure loszuwerden. Wir vermuten, dass das schlicht unmöglich ist. Type Erasure war zu seiner Zeit eine großartige Idee und hat eine Menge Probleme gelöst. Das Problem ist, dass es vor allem die Kopfschmerzen der Sprachdesigner gemildert hat. Nach der Veröffentlichung des Features hat es allerdings in der freien Wildbahn einige Unannehmlichkeiten verursacht. Als Anwendungsentwickler seid ihr dem Problem vermutlich noch nie begegnet, aber für Framework-Designer ist Type Erasure eine echte Qual. Im Rückblick wäre es besser gewesen, diese Idee in einer „kleineren“ Sprache auszuprobieren, Gson oder Jackson mit ins Spiel zu bringen und so die Nachteile aufzudecken. Nach unserem Eindruck machen die Designer von Java das heutzutage auch so.

Wir glauben, dass Truffle die Zeit für das Schreiben einer Programmiersprache locker um die Hälfte reduziert. Das öffnet Spielraum für Experimente. Als Bonus kommt noch hinzu, dass die Futamura-Projektion im Zusammenspiel mit dem optimierenden GraalVM-JIT-Compiler gleich von Anfang an für eine ordentliche Performance sorgt.

Cloud Computing und GraalVM: Der große Schubfaktor

Kommen wir endlich zum Killerfeature der GraalVM: dem Ahead-of-Time-(AOT-)Compiler. Nach über zwei Jahrzehnten ist es uns endlich möglich, Java zu nativem Maschinencode zu kompilieren. Wer schon länger dabei ist, mag sich noch an AOT-Compiler für Java erinnern. Bis 2017 gab es den GNU-Compiler. IBM Websphere brachte einen AOT-Compiler mit. Die Fortschritte der JIT-Compiler-Technologie machte die AOT-Compiler obsolet. Die modernen JIT-Compiler bieten bei einer typischen Serveranwendung mehr Leistung als ein AOT-Compiler.

Bei AWS-Lambda-Funktionen (und deren Gegenstücke bei den anderen Cloud-Providern) ist das anders. Das Tolle bei Lambda-Funktionen ist, dass man in der Cloud ein Pay-per-use-Modell hat. Man bezahlt für die Cloud-Kapazitäten nur dann, wenn der Code auch ausgeführt wird – ruft keiner den entsprechenden Service auf, fallen keine Kosten an. Damit der Cloud-Anbieter dabei dann nicht auf Kosten sitzen bleibt, muss er die virtuelle Maschine des Kunden herunterfahren, wenn niemand sie nutzt. Das bedeutet, dass die Nutzer, wenn sie den Service dann doch aufrufen, auf den Kaltstart warten müssen. Bei einer typischen JakartaEE- oder Spring-Boot-Anwendung reden wir hier meist über einen Delay von einigen Sekunden. Das ist viel zu viel in einer Welt, in der die wichtigsten Suchmaschinen langsame Webseiten abstrafen. Große Webshops berichten von einem signifikanten Effekt auf die Absatzzahlen, wenn der Shop auch nur um eine Zehntelsekunde langsamer wird. Was heißt das dann wohl erst für einen Spring Boot Service, der einen Kaltstart durchführen muss? Oder für eine Microservices-Anwendung, die aus Dutzenden von Lambda-Funktionen zusammengesetzt ist, jede mit ihrem eigenen Kaltstart?

Der AOT-Compiler löst dieses Problem. Traditionell startet Java eher gemächlich, da es mehrere Tausend Klassen laden muss. Eine herkömmliche CRUD-Anwendung nutzt allerdings nur einen Bruchteil dieser Klassen. Sie braucht einfach nicht die volle Power von Spring Boot, um auf eine GET-Abfrage zu antworten. Drei Zeilen Code reichen vollkommen aus, wie Frameworks wie Helidon, Micronaut oder Express.js (aus der JavaScript-Welt) zeigen. Der AOT-Compiler reduziert den Code auf das, was wirklich nötig ist, und gibt vorkompilierten Maschinencode aus. Wenn man Glück hat, sind das am Ende lediglich drei Zeilen Helidon-Code plus die Infrastruktur, um diese Zeilen auszuführen. Alle anderen Klassen sind nicht Bestandteil der Binärdatei.

Besser noch: Der Code ist sofort nutzbar, ohne dass man eine umfangreiche Infrastruktur laden und initialisieren müsste. Fügt man noch ein Cloud-natives Framework wie Quarkus oder Helidon in die Gleichung ein, kann man Lambda-Funktionen mit einer Antwortzeit von rund 0.005 Sekunden erreichen, wie einige Talks gezeigt haben. Das werden wir im letzten Teil dieser Artikelserie selbst ausprobieren.

GraalVM eröffnet neue Möglichkeiten

Im Laufe der vergangenen Monate haben einige Entwickler damit begonnen, sich intensiv mit GraalVM auseinanderzusetzen. Michiel Borkent hat etwa babashka erstellt, einen Interpreter für Clojure, durch die GraalVM kompiliert als natives Image. Es lässt sich nahtlos in eine Linux- oder macOS-Shell integrieren und erlaubt, Bash-Befehle durch Clojure-Code zu ersetzen. Der Popularität des Projekts auf GitHub zufolge (1500 Sterne in 14 Monaten) hat er damit den Nagel auf den Kopf getroffen.

Auch in der JavaFX-Gemeinschaft scheint es eine große Sache zu sein, zu nativen Executables kompilieren zu können, wie das Interview mit Johan Vos zeigt [1]. Man muss so weder Java noch JavaFX installieren, um eine nativ kompilierte JavaFX-Anwendung auszuführen. Das erleichtert es natürlich, solche Anwendungen auf Tausenden von Computern im Unternehmen zu installieren.

Zu guter Letzt treibt die GraalVM auch eine schiere Welle neuer Microservices Frameworks an. Quarkus ist ein gutes Beispiel hierfür. Wir haben Quarkus einen Artikel auf meinem (Stephans) Blog gewidmet, fassen uns hier also kurz: Quarkus ist ein Set aus Frameworks, die man als Industriestandard bezeichnen kann, in Verbindung mit Erweiterungen, die diesen Frameworks erlaubt, den AOT-Compiler zu verwenden und native Binaries zu erstellen.

Was ist GraalVM? Zusammenfassung

Die großen Versprechungen, die die GraalVM macht, werden wir uns im Laufe der Serie genau ansehen. Sie ist ein frischer, unverbrauchter Ansatz für den jahrzehntealten Wunsch, die JVM zu optimieren. Zudem macht sie polyglotte Programmierung mit einer deutlich verbesserten Performance möglich, wie ein späterer Artikel zeigen wird – Nashorn und Rhino kommen da nicht mit.

GraalVM wird unserer Meinung nach einen großen Einfluss auf die Cloud-native Entwicklung und auf die Welt solcher Programmiersprachen wie R, Python und Ruby haben. Wir sind auch fasziniert von der Möglichkeit, C/C++-Code in Java- und JavaScript-Anwendungen zu integrieren. Die GraalVM ist dahingehend eine gute Gelegenheit, um von der Erfahrung vieler Entwicklercommunitys zu profitieren, die bislang vom Java-Universum abgeschnitten waren. Im zweiten Teil der Serie werden wir uns näher mit Bytecode, Interpretern und Compilern beschäftigen, also dem „Low-Level-Zeug“.

Links & Literatur

[1] Vos, Johan: „Man sollte die Cross-Plattform-Kapazitäten von JavaFX nicht unterschätzen“. Interview mit Johan Vos zum JavaFX-Release, Java Magazin 1.2021

Unit-Tests gelten als das Mittel der Wahl, um sicherzustellen, dass eine Anwendung wie erwartet funktioniert. Fast keine Anwendung kommt heutzutage ohne Unit-Tests aus. Wer würde also auf die verrückte Idee kommen, Unit-Tests zu löschen? Vielleicht reicht es ja erst einmal aus, zu überlegen, wann Unit-Tests sinnvoll sind, und vor allem gute von schlechten Unit-Tests zu unterscheiden.

Wenn es um das Testen einer Anwendung geht, kommen die meisten Entwickler zuallererst mit Unit-Tests in Berührung. Aus gutem Grund: Unit-Test sind ein gutes Mittel, um auf einfachem Wege die korrekte Funktionsweise einer Anwendung sicherzustellen. Gerade deswegen sind Unit-Tests bei Entwicklern durchaus beliebt und werden inzwischen in vielen Projekten als selbstverständlicher Teil der Anwendung angesehen. Umso größer war die Empörung, als ein geschätzter Kollege bei einer internen Konferenz im Hause folgende kontroverse Aussage tätigte: „Löscht eure Unit-Tests!“

Natürlich war das keine pauschale Aussage, die ohne weitere Diskussion stehenbleiben konnte. In einer differenzierten Analyse wurde das Für und Wider von Unit-Tests diskutiert und viele Kollegen konnten ihre eigenen positiven, aber auch negativen Erfahrungen mit Unit-Tests schildern. Ein klarer Konsens, ob Unit-Tests das Allheilmittel sind oder eben nicht, kam leider nicht zustande. Daher hier ein Versuch, meine Meinung und auch die in Projekten gesammelten Erfahrungen zum Thema Unit-Tests darzulegen, ohne direkt vorzuschlagen, alle Unit-Tests zu löschen.

Schlechte und gute Unit-Tests

Da ich mit besagtem Kollegen zufälligerweise auch im selben Projekt tätig bin, war sein gewagter Aufruf für mich natürlich nicht neu. Wir haben uns in unserem Projekt bereits frühzeitig entschieden, weniger auf – ich nenne sie mal klassische – Unit-Tests zu setzen. Die von uns entwickelten Unit-Tests halfen uns zu selten, Bugs zu bemerken oder gar zu vermeiden. Das Vertrauen in die Tests schwand im Laufe der Zeit immer mehr. Gleichzeitig erschwerten die Unit-Tests notwendige Refactorings, da diese zu häufig fehlschlugen, ohne dabei wirklich auf einen Fehler hinzuweisen. Wir stellten uns daher nach einiger Zeit die Frage, warum manche der Unit-Tests uns das Leben eher schwerer als einfacher machten. Die Antwort war schnell gefunden: Besagte Tests mocken zu viele oder gar all ihre Abhängigkeiten. Wie robust ein Test letztlich war, hing zu oft davon ab, ob die Mocks so konfiguriert wurden, dass sie auch das echte Verhalten der gemockten Abhängigkeit abbilden. Das ist tatsächlich oft schwieriger als erwartet. Die Tests, in denen weniger oder gar nicht gemockt wurde, sorgten hingegen seltener für Probleme. Hier konnte deutlich weniger falsch gemacht werden als bei Tests, in denen gemockt wurde.

Wo findet man schlechte Unit-Tests?

Nachdem wir verstanden hatten, warum manche unserer Unit-Tests schlechter als andere sind, lohnte sich auch ein Blick darauf, wo genau dieses Tests zu finden waren – oder anders gesagt, welche Art von Unit häufig Probleme macht. Ein Großteil der schlechten Unit-Tests überprüfte das Verhalten von Services in der Anwendungsschicht. Also oft da, wo über Schichtgrenzen hinaus viele Abhängigkeiten aufeinandertreffen. Genau dort, wo die durchaus komplexe fachliche Logik der Anwendungsfälle durch verschiedene Aufrufe an Repositories und Services der Domänenschicht umgesetzt wurde. In den zugehörigen Unit-Tests mussten wir natürlich all diese Abhängigkeiten mocken. Die guten Unit-Tests hingegen ließen sich häufig innerhalb einer Schicht finden, vorrangig der Domänenschicht. Insbesondere Unit-Tests für in sich abgeschlossene Domänen-Services oder auch Entities und Value-Objekte machten selten Probleme. Hier gab es keine – oder zumindest weniger – Abhängigkeiten, die gemockt werden mussten. Uns wurde langsam, aber sicher bewusst, dass der Einsatz von Unit-Tests an besagten problematischen Stellen überdacht werden müsste.

Unit – eine Frage der Definition

Bevor wir die vermeintlich schlechten Unit-Tests löschen konnten, rief uns glücklicherweise ein neu dazugestoßener Kollege einen oft vergessenen Fakt ins Gedächtnis: Eine Unit in einem Unit-Test muss nicht notwendigerweise eine einzelne Klasse oder Funktion sein. Vielmehr kann eine Unit auch eine oder mehrere Klassen umfassen und somit deutlich größer sein. Eine größere Unit bedeutet oft auch, dass wir weniger Abhängigkeiten mocken müssen und somit genau das vermeiden, was wir zuvor als Problem erkannt haben. Eine geringere Anzahl von Mocks führt dazu, dass wir die Unit weniger als eine Whitebox und als mehr als eine Blackbox testen. Wir müssen weniger über die Innereien der getesteten Unit wissen. Nur das von außen beobachtbare Verhalten ist für uns relevant. Refactorings, die das Verhalten nach außen nicht verändern, können somit problemlos durchgeführt werden, ohne dass dabei der Test fehlschlägt. Zudem kommt es seltener zu Problemen durch falsche Annahmen beim Mocken von Abhängigkeiten. Durch die Wahl einer größeren Unit konnten wir unsere Unit-Tests deutlich robuster und zuverlässiger gestalten.

Trotz der Wahl größerer Units kam es in Produktion immer wieder zu Problemen, die während der Entwicklung nicht entdeckt wurden. Genau die Abhängigkeiten, die wir weiterhin in unseren Tests mocken mussten, sorgten immer noch für Probleme. Allen voran der von uns verwendete Message-Broker, die SQL-Datenbank, aber auch die von uns entwickelten HTTP-Schnittstellen seien hier genannt. Die Integration mit externen Komponenten führte also häufig zu Problemen. Der nächste logische Schritt war demnach der Einsatz der deutlich weniger beliebten Integrationstests. Statt die externen Komponenten zu mocken, fahren wir bei Integrationstests eine vollwertige Datenbank oder einen Message-Broker hoch. Lediglich externe Systeme, die wir nicht kontrollieren können, werden weiterhin auch in den Integrationstests gemockt. Durch Integrationstests konnten wir erneut die Anzahl der gemockten Abhängigkeiten reduzieren und somit unsere Tests robuster gestalten. Durch einen Einsatz externer Komponenten, wie etwa einer SQL-Datenbank, konnten wir in unseren Tests das Verhalten von Produktion auch während der Entwicklung deutlich besser nachstellen.

Man muss stets Kompromisse eingehen

Integrationstests bringen viele Vorteile mit sich, aber sie haben gegenüber Unit-Tests natürlich auch gewisse Nachteile. Unit-Tests sind üblicherweise deutlich schneller in ihrer Ausführungszeit. Integrationtests hingegen sind deutlich langsamer, da externe Komponenten gestartet und aufgerufen werden müssen. Sie können mitunter nichtdeterministisch sein, schlagen also durchaus mal fehl (z. B. blockiert die Firewall den Aufruf der Datenbank), obwohl der dahinterliegende Code einwandfrei ist. Von Unit-Tests erwartet man indessen Determinismus. Wenn sie einmal funktionieren, funktionieren sie immer, solange der Code unverändert bleibt.

Einige dieser Nachteile lassen sich durchaus minimieren, wenn man etwa statt einer vollwertigen Datenbank auf eine In-Memory-Datenbank setzt, um sowohl die Ausführungszeit des Tests zu minimieren als auch andere Fehlerquellen – wie etwa die TCP-Verbindung zur Datenbank – zu vermeiden. Oft führen aber gerade subtile Unterschiede zwischen In-Memory-Datenbanken und vollwertiger Datenbank dazu, dass Fehler, die in Produktion auftauchen, nicht frühzeitig gefunden werden. Hier muss daher der Kompromiss zwischen schnelleren, deterministischen Tests und gewünschter Produktionsnähe abgewogen werden. Generell gilt aber: Je produktionsnäher der Test, desto einfach lassen sich Fehler in Produktion reproduzieren und hoffentlich vermeiden.

Wollten wir über die Integrationstests noch einen Schritt hinaus gehen, kämen jetzt logischerweise die bei vielen verhassten End-to-End-Tests (e2e-Tests) zum Einsatz. Diese versuchen die Anwendung aus der Sicht des Benutzers und damit produktionsnäher zu testen. Da e2e-Tests zwar sinnvoll, aber durchaus mit anderen Herausforderungen und Problemen daherkommen, werden wir diese hier nicht weiter betrachten.

Unit-Tests sind trotzdem hilfreich

Heißt das nun, dass wir nur noch Integrationstests schreiben sollten? Die Antwort ist natürlich „nein“. Genau die Funktionalitäten, die ohne eine große Anzahl von Mocks getestet werden können, sind oft auch gute Kandidaten für Unit-Tests. Es gibt durchaus Stellen in einer Anwendung, die bewusst isoliert von anderen Teilen der Anwendung getestet werden sollten. Oft muss lediglich die Größe der Unit so gewählt werden, dass die Anzahl der gemockten Abhängigkeiten auf ein Minimum reduziert werden kann. Muss keine Abhängigkeit gemockt werden, hat man die perfekte Unit für einen Test gefunden. Das ist beispielsweise in der Domänenschicht häufig der Fall: Domänenobjekte sind perfekt geeignet, um durch Unit-Tests getestet zu werden, da diese selten Abhängigkeiten haben. Das ist auch gut so, denn gerade die Domainschicht soll oft bewusst isoliert von konkreten Anwendungsfällen entwickelt und getestet werden, auch wenn diese durch Integrationstests bereits implizit mitgetestet wird. Die Größe der getesteten Unit in der Domainschicht entspricht glücklicherweise auch häufig genau einer Klasse oder Funktion.

Integrationstests sind auch nur Unit-Tests

Ein Integrationstest ist in erster Linie ein Unit-Test, bei dem wir auch externe Komponenten möglichst produktionsnah einbeziehen. Die Unit, die integrativ getestet wird, muss aber nicht zwangsläufig maximal groß sein. Das ist zwar häufig sinnvoll, wenn die Funktionsweise eines gesamten Anwendungsfalls getestet werden soll, es gibt aber durchaus Stellen im Projekt, bei denen bewusst der Integrationsaspekt im Fokus steht. Ein gutes Beispiel ist etwa ein Message Consumer, der eine Nachricht via Kafka empfangen soll und damit die Ausführung eines Anwendungsfalls anstößt. Da eine Anbindung an einen Message Broker nicht unbedingt trivial ist, ist es durchaus sinnvoll, nur genau diesen Aspekt durch einen Integrationstest abzudecken, ohne dass man dabei auch den eigentlichen Anwendungsfall testet. Lediglich der korrekte Aufruf muss hier sichergestellt werden. Die eigentliche fachliche Logik, wird in diesem Fall gemockt. Ein weiteres Beispiel ist ein JPA Repository, das komplexe JPQL Queries enthält. Auch hier möchte man das Repository integrativ testen, ohne dabei die Logik des aufrufenden Anwendungsfalls zu testen.

Es gibt mehr als nur Integrations- und Unit-Tests

Anders als bei Unit-Tests kann es bei Integrationstests schwierig sein, von außen zu erkennen, welche Funktionalität genau in einem Test überprüft wird. Gibt es im Projekt viele Integrationstests, die sich konkret auf einen bestimmten Aspekt beziehen, ist es daher ratsam, Tests in Kategorien aufzuteilen, anhand derer man direkt erkennen kann, was genau mit diesem Test überprüft werden soll. Frameworks wie Spring Boot bringen etwa von Haus aus Annotationen mit, die neben einer besseren Erkennbarkeit auch direkt das entsprechende Set-up mit hochfahren. So lassen sich mit @DataJpaTest hervorragend JPA Repositories und mit @WebMvcTest HTTP Controller in Isolation testen. Es bietet sich zudem an, eigene, auf das Projekt abgestimmte Kategorien zu erfinden – etwa durch eigene Annotationen oder eine Namenskonvention. Für unser Message-Consumer-Beispiel könnte es also eine Annotation @MessageConsumerTest geben, die automatisch den gewünschten Message-Broker hochfährt. Bei einem Message-Consumer-Test ist auf den ersten Blick klar, dass hier überprüft wird, ob das Empfangen einer Nachricht vom Message Broker funktioniert, ohne dass dabei die Details des aufgerufenen Anwendungsfalls im Fokus stehen. Durch Testkategorien kann man also auf einfache Weise ein einheitliches Testvokabular für die Entwickler schaffen. Folgt das Projekt einer klaren Architektur, ergeben sich diese Testkategorien häufig bereits aus den architekturellen Bausteinen. So kann etwa als Architekturregel festgelegt werden, dass ein Service in der Anwendungsschicht stets durch mindestens einen Integrationstest überprüft wird und zusätzlich eine hohe Testabdeckung durch Unit-Tests erreicht werden muss.

Brauchen wir überhaupt noch Mocks?

Wir haben gezeigt, dass Unit-Tests immer noch ihre Daseinsberechtigung haben. Insbesondere dann, wenn wir durch die Wahl einer großen Unit weitestgehend auf Mocks verzichten können. Es gibt aber Situationen, wo Mocks durchaus sinnvoll sind. Insbesondere bei der Entwicklung eines neuen Features spielen Mocks ihre Stärken aus. Eine große Unit ist oft zu Beginn der Entwicklung eines Features wenig sinnvoll, da die benötigten Abhängigkeiten eventuell noch gar nicht existieren. Vielmehr möchte man sich zunächst genau auf einzelne Teilfunktionalitäten konzentrieren, ohne allzu sehr über andere Abhängigkeiten nachdenken zu müssen. Durch die Verwendung von Mocks kann man bewusst die konkrete Implementierung der Abhängigkeiten ausblenden und stattdessen den Fokus auf ihre Schnittstelle legen. Dieser Ansatz führt häufig zu einem deutlich saubereren Design des Codes. Genauso ergibt ein Integrationstest, der eine Datenbank einbezieht, wenig Sinn, wenn die SQL-Tabellen noch gar nicht entworfen wurden.

Ein häufiges Problem ist auch, dass man gerne einen Test ohne Mocks hätte, dies aber mit zu viel Aufwand verbunden wäre. Um einen sinnvollen Test zu schreiben, der keine Mocks verwendet, braucht es Zeit – vor allem, um das Zusammenspiel der involvierten Abhängigkeiten überhaupt erst zu verstehen. Hierfür wird neben technischem Verständnis auch ein enormes fachliches Wissen benötigt, um eine sinnvolle Überprüfung des Verhaltens zu gewähren. Selbst wenn dieses Wissen vorhanden ist, mangelt es oftmals auch an geeigneten Testdaten (wie etwa eine prall gefüllte SQL-Datenbank mit realistischen Daten). Genauso entscheidend ist auch die Wichtigkeit der getesteten Funktionalität. Ist diese aus Unternehmenssicht lediglich Beiwerk und trägt kaum zum finanziellen Erfolg des Projekts bei, ist die benötigte Zeit zur Entwicklung des Integrationstests oft nicht gerechtfertigt. In diesem Fall muss eine Reihe von Unit-Tests mit Mocks häufig als Alternative ausreichen.

Löschen wir also doch keine Unit-Tests?

Nein, tun wir nicht. Nicht pauschal. Der Aufruf „Löscht eure Unit-Tests!“ sollte lediglich ein Denkanstoß sein, um öfter mal zu hinterfragen, ob die Unit-Tests, die wir tagtäglich entwickeln, auch tatsächlich ihr Geld wert sind und uns bei unserer Arbeit unterstützen. Es gibt einige Stellschrauben, wie etwa die Größe der getesteten Unit oder das Einbeziehen von Datenbank und Co., die oft schon aus einem mittelmäßigen einen robusten und zuverlässigen Test machen können. Ein Test, auf den man sich auch langfristig verlassen kann, und einen, der beim Refactoring nicht im Weg steht, sondern wirklich dabei hilft, Fehler zu erkennen und zu vermeiden – und das vor allem bevor diese in Produktion auftreten.

Also, beim nächsten Test einfach auf den einen oder anderen Mock verzichten und vielleicht sogar eine Datenbank mit ausführen – die Anwendung in Produktion wird es euch danken!

grunwald_renke_sw.tif_fmt1.jpgRenke Grunwald ist Enterprise-Entwickler bei der OPEN KNOWLEDGE GmbH in Oldenburg. Zu seinen Schwerpunkten gehören moderne Frontend-Technologien (insbesondere im Kontext von SPAs), neuartige Architekturansätze wie Microservices sowie das Thema Continuous Integration/Deployment. Die Mission seiner Arbeit ist immer, den Prozess der Softwareentwicklung zu verbessern und zu vereinfachen.

Microservices versprechen eine Vielzahl von Verbesserungen in der Softwareentwicklung. Diese können nur umgesetzt werden, wenn eine gut geschnittene Architektur als Grundlage vorhanden ist. DDD hilft uns, diese Grundlage in der Fachlichkeit zu finden.

Microservices sind in aller Munde. Mit diesem Architekturstil werden uns bessere Wartbarkeit, kürzere Time to Market, einfachere Skalierbarkeit und alle sonst noch denkbaren Verbesserungen für die Softwareentwicklung versprochen. Zunächst wurden Microservices vor allem als technisches Thema verstanden. Um ein modernes verteiltes System zu bauen, braucht es schließlich die Beherrschung verschiedenster Technologien. Schnell wurde allerdings klar, dass für eine tragfähige Microservices-Architektur ein guter Schnitt nötig ist und dass ein guter Schnitt nur einer sein kann, der auf der Fachlichkeit basiert. Dadurch wurde ein Thema ins Rampenlicht geholt, das vorher (zu Unrecht) nur einer kleinen Gruppe von Experten bekannt war – Domain-driven Design oder kurz DDD.

„Fokussiere Dich auf die Fachlichkeit deiner Software, verstehe zunächst das Problem, bevor du mit einer Lösung durch die Tür kommst.“ Das ist die grundlegende Lehre von DDD. Um die Fachlichkeit zu lernen, brauchen wir Kommunikation mit den Fachexperten. Schauen wir uns an einem Beispiel an, was das konkret bedeutet.

Beispieldomäne

Unser Beispiel liegt in der Domäne „Bankwesen“: Ein Kunde geht zur Bank und eröffnet ein Girokonto. Dann zahlt er einen Betrag von 100 € ein und überweist diesen auf das Konto eines anderen Kunden. Am Ende des Jahres berechnet ein Bankmitarbeiter den Jahreszins für das Konto und zahlt ihn auf das Konto ein.

Die Geschichte der Domäne stellt man gerne in einer sogenannten Domain Story (Kasten: „Domain Storytelling“) dar. Hierbei wird das, was in der Domäne passiert, in einem Diagramm dargestellt. Um die Domain Story zu lesen, gilt es, den Sequenznummern zu folgen (Abb. 1).

schwentner_ddd_1.tif_fmt1.jpgAbb. 1: Unsere Domain Story

Domain Storytelling

Domain Stories sind eine einfache grafische Notation und stammen aus dem Domain Storytelling [1], [2]. Es gibt zwei Arten von Icons und eine Art von Pfeil:

  • Actor – die handelnden Personen

  • Work Object – die Arbeitsgegenstände, mit denen etwas getan wird

  • Activity – die Handlungen, die die Akteure an den Arbeitsgegenständen vornehmen

Domain Stories entstehen, indem wir uns von den Fachexperten erzählen lassen, was in der Domäne geschieht. Während der Erzählung wird die Geschichte so, wie wir sie verstehen, aufgezeichnet. So geben wir direkte Rückmeldung über das Verständnis, und mögliche Missverständnisse werden schnell aufgeklärt.

Lösungsversuch

Jeder von uns kennt diese Geschichte, aber wie baut man eine Software dafür? Wie würde das typische Entwicklungsteam an diese Aufgabe herangehen? Das Team aus Technikern beantwortet zuerst die (scheinbar) wichtigsten Fragen: Welche Programmiersprache? Wie schaffen wir es, die neuesten Frameworks/Versionen zu verwenden? Können wir statt einer klassischen relationalen DB modernes NoSQL einsetzen?

Moment mal! Haben diese Fragen irgendetwas mit der Bank und Konten zu tun? Hat uns ihre Beantwortung näher an unser Ziel gebracht? Fühlt sich nicht so an. Was ist denn überhaupt unser Ziel?

Leider ist das Ziel nicht technische Schönheit. Das Ziel ist es, den Anwenderinnen und Anwendern in ihrer Domäne zu helfen; sie bei ihrer Arbeit zu unterstützen; diese Arbeit leichter, schneller, effizienter zu machen. Wenn wir das mit Software schaffen – super! Deshalb sagt Domain-driven Design: Baue die Software so, dass sie in der Domäne tief verwurzelt ist. Baue sie als Reflektion der Domäne. Damit wir die Domäne in Software reflektieren können, müssen wir sie verstehen. Mit diesem Verständnis entwickeln wir ein Modell und implementieren es – das sogenannte Domänenmodell.

Ubiquitous Language

Damit die Entwickler das Domänenmodell bauen können, müssen sie mit den Fachexperten über die Domäne sprechen können – deshalb brauchen wir eine gemeinsame Sprache für Entwickler und Fachexperten. Die Entwickler haben eine technische Sprache. Sie reden über Klassen, Methoden und Interfaces. Auch für sie spielen Konten eine Rolle, aber wenn wir sagen: „Ich logge mich in mein Konto ein“, geht es uns nicht um das Girokonto aus der Bankgeschichte. Diese technische Sprache ist wichtig, allerdings nicht besonders gut geeignet, um über die Domäne zu kommunizieren. Die Domänenexperten haben ihre eigene Fachsprache. In der Bank sprechen wir z. B. über Girokonto, Geldbetrag, Währung, einzahlen, überweisen usw.

DDD sagt, dass wir eine gemeinsame Sprache für Fachexperten und Entwickler haben wollen, um Sprachverwirrung zu vermeiden. Sie soll auf der Fachsprache basieren und überall verwendet werden. Überall bedeutet im Gesprochenen, im Geschriebenen, in Diagrammen und eben auch im Code. Diese Sprache wird deshalb Ubiquitous Language (also allgegenwärtige Sprache) genannt. Wenn wir die Domain Story aus unserem Beispiel anschauen, sehen wir ausschließlich Fachwörter und keine technischen Wörter. Das ist gut so, wir haben uns also wirklich auf die Fachlichkeit fokussiert. Alle diese Wörter sind Kandidaten dafür, auch Wörter der Ubiquitous Language zu werden.

DDD im Kleinen

DDD gibt uns Konstruktionsanleitungen für Domänenmodelle und zwar im Großen und Kleinen. Im Kleinen betrachten wir, welche Arbeitsgegenstände unsere Fachexperten verwenden und, noch wichtiger, was sie damit tun, wie sie damit umgehen. Auch hier lassen wir uns also von der Domäne treiben. Aus den Gegenständen werden Klassen. Aus den Umgangsformen werden Methoden.

Was heißt das in unserem Beispiel? Ein zentraler Gegenstand im Bankwesen ist das Girokonto. Also implementieren wir eine entsprechende Klasse Girokonto in unser Domänenmodell. Was wird mit einem Girokonto getan? Beispielsweise wird ein Betrag eingezahlt und Jahreszinsen werden berechnet. Deshalb bekommt die Klasse Girokonto entsprechende Methoden. In Abbildung 2 sieht man, wie aus einer Domain Story das Domänenmodell abgeleitet werden kann. Als zusätzliche Klasse kommt noch Betrag dazu. In Java würde daraus folgender Code entstehen:

public class Girokonto {
  public void zahleEin(Betrag betrag)
  //...
}
schwentner_ddd_2.tif_fmt1.jpgAbb. 2: Aus der Domain Story wird das Domänenmodell abgeleitet

Darin steckt eine ganze Menge Ubiquitous Language: Die Wörter „Girokonto“, „einzahlen“, „Betrag“ stammen alle aus der Fachsprache. Es ist ein ausdrucksstarkes Domänenmodell mit reichem fachlichem Verhalten („domain model with rich behaviour“). Nicht nur die Klasse, auch die Methoden haben einen fachlichen Namen.

In der Praxis trifft man oft auf das Gegenteil, das sogenannte Anemic Domain Model. Dieses hat zwar fachliche Klassen, aber diese haben kein fachliches Verhalten. Bei dem Girokonto würde man aus technischer Sicht z. B. denken, dass alles, was wir damit machen können, am Ende ja immer den Kontostand ändert. Also ist eine mögliche anämische Implementierung die folgende:

public class Girokonto {
  public void setKontostand(Betrag kontostand)
  //...
}

Man erkennt anämische Domänenmodelle daran, dass ihre fachlichen Klassen hauptsächlich aus Gettern und Settern bestehen. Hier ist jeder fachliche Umgang verloren. Das Modell ist nicht mehr so ausdrucksmächtig. Außerdem kann so nicht mehr sichergestellt werden, dass nur fachlich sinnvolle und valide Operationen ausgeführt werden. „Kontostand setzen“ ist nicht das, was man mit einem Girokonto machen kann – Konten bekommen nicht auf einmal einen beliebigen Kontostand. Das wird noch klarer sichtbar, wenn wir uns das Abheben anschauen. Das ist in der Fachlichkeit nur möglich, wenn das Konto gedeckt ist. Im anämischen Modell wird die Deckung nicht geprüft und das Konto kann illegal überzogen werden. Im sauberen Domänenmodell wird auch sauber geprüft und Überziehen so unmöglich gemacht (Listing 1).

Listing 1

public class Girokonto {
  public void zahleEin(Betrag betrag)
  // ...
  public boolean isGedecktMit(Betrag betrag)
  // ...
  public void hebeAb(Betrag betrag) {
    if (!isGedecktMit(betrag)) {
      throw new Exception("Vorbedingung verletzt");
    }
    // ...
  }
}

So können die Klassen des Domänenmodells sicherstellen, dass ihre Instanzen immer einen validen Zustand haben. Das Entwerfen des Domänenmodells nennt DDD auch taktisches Design. Weitere Informationen dazu findet man z. B. unter [3].

Ein reiches Domänenmodell ist etwas Großartiges. Problematisch ist, dass es immer weiterwächst. In unserem Beispiel kommen für die Klasse Girokonto noch die Methoden berechneJahreszins() und ueberweise() hinzu, und wir haben ja bisher nur ein kleines Stück der Domäne modelliert. Irgendwann wird das Modell so groß sein, dass es nicht mehr im Ganzen zu verstehen und deshalb fehleranfällig ist. DDD im Kleinen mit taktischem Design reicht also nicht aus, wir brauchen auch noch strategisches Design – DDD im Großen.

DDD im Großen – die Domäne aufteilen

Je kleiner ein Modell ist, desto einfacher kann es verstanden werden. Das strategische Design von DDD sagt uns deshalb, dass wir nicht nur ein, sondern mehrere Domänenmodelle bauen sollen. Jedes dieser Modelle existiert in einem eigenen Kontext und ist klar von den anderen Modellen abgegrenzt (bounded). Deshalb spricht man hier von Bounded Context. Das einzelne Modell bildet dann nur diejenigen Eigenschaften des Realweltgegenstands ab, die im jeweiligen Kontext wesentlich sind. Die Domäne wird so in unterschiedliche Bounded Contexts aufgeteilt. Dabei suchen wir nach Bereichen, in denen Unterschiedliches getan wird (Abb. 3).

schwentner_ddd_3.tif_fmt1.jpgAbb. 3: Aufteilung in Bounded Contexts

Es haben sich also die drei Bounded Contexts Kontoeröffnung, Kontoführung und Zinsberechnung herauskristallisiert. Mit diesen Kontexten kann man eine sogenannte Context Map zeichnen. In ihr finden sich die Kontexte und die Beziehungen dazwischen wieder. Jeder Kontext ist dann auch Kandidat für einen Microservice (Abb. 4).

schwentner_ddd_4.tif_fmt1.jpgAbb. 4: Context Map

Ubiquitous Language und Modell hängen eng zusammen. Bei genauem Hinsehen erkennt man, dass jedes Modell (d. h. jeder Bounded Context) sogar seine eigene Ubiquitous Language hat. Oben haben wir gesagt, dass wir mehrere unterschiedliche Domänenmodelle bilden. Das heißt, wir bilden denselben Gegenstand aus der Wirklichkeit mit unterschiedlichen Klassen ab. Unterschiedliche Klassen mit demselben Namen. Daran muss man sich erst einmal gewöhnen! Unsere Beispielmodelle sehen dann wie in Abbildung 5 aus.

schwentner_ddd_5.tif_fmt1.jpgAbb. 5: Beispielmodelle

Bounded Contexts müssen nicht gleich als Microservices implementiert werden. In Java kann man die unterschiedlichen Kontexte auch mit Packages oder (ab Version 9) mit Modules ausdrücken. Hier hätte man dann also zwei Packages oder Module de.bank.kontofuehrung und de.bank.zinsberechnung, die jeweils eine Klasse Girokonto enthalten.

Erste Schritte

Wie führe ich DDD nun in meinem eigenen Projekt ein? Auf der grünen Wiese ist das natürlich leichter als in einem bestehenden Projekt, das vielleicht eine über viele Jahre gewachsene Legacy-Anwendung betreut. Aber auch im zweiten Fall ist es möglich, die Vorteile zu realisieren. In einem neuen Projekt empfiehlt es sich, zunächst grob die Domäne im Ganzen zu modellieren und daraus den Domänenschnitt abzuleiten. Hierzu eignet sich Big Picture Event Storming oder Coarse-grained Domain Storytelling. Als Ergebnis bilden wir eine Context Map.

Als Nächstes schauen wir nun in den jeweiligen Bounded Context hinein und modellieren, was dort im Detail vor sich geht. Hieraus leiten wir das Domänenmodell für diesen Kontext ab. Das Domänenmodell wird in der angemessenen Technologie und Programmiersprache implementiert. Auch hier bieten sich wieder Event Storming und Domain Storytelling an; nun aber in den Varianten Detaillevel bzw. Fine-grained.

Um im Brownfield DDD einzuführen, ist es sinnvoll, in mehreren Schritten vorzugehen. Das System wird nicht von einem Tag auf den anderen durch Wunderheilung zu einem Ideal.

  1. Domänenmodellierung und Domänenschnitt, wie wenn man sich auf der grünen Wiese befände, d. h., Entwicklung der Soll-Architektur

  2. Analyse der Ist-Architektur; das kann händisch oder toolgestützt erfolgen

  3. Übereinanderlegen von Ist- und Soll-Architektur: An welchen Stellen kann das Altsystem in Richtung der gewünschten neuen Struktur bewegt werden?

  4. Herauslösen eines ersten Bounded Context aus dem Big Ball of Mud (d. h. dem komplex verwobenem Softwaremonolithen); beim ersten Mal bietet es sich an, einen weniger wichtigen auszuwählen, also eine Supporting oder Generic Subdomain – hieran wollen wir lernen

  5. Herauslösen der (ersten) Core Domain – hiermit wollen wir jetzt wirklich Nutzen erzielen

Im Text ist dieses Vorgehen schnell beschrieben, im richtigen Leben kann es natürlich komplex werden. Trotzdem lohnt es sich in den meisten Fällen, diesen Weg zu gehen. Das liegt daran, dass Altsysteme, also unsere Big Balls of Mud, typischerweise too big to fail sind. Sie sind so groß, dass ein kompletter Neubau Jahre dauern würde. Diese Zeit ist zu lang, als dass man das Altsystem im gleichen Zustand belassen könnte. Stattdessen entwickelt man es weiter. Das Neusystem kommt also gar nicht hinterher. Deshalb empfiehlt sich das hier beschriebene kleinschrittige Vorgehen, bei dem das Altsystem nicht durch ein Neusystem ersetzt wird, sondern sich das Altsystem in das Neusystem verwandelt und weiterentwickelt.

Das kann auch bei Technologie- und sogar bei Programmiersprachenwechseln funktionieren. So werden auch alte Mainframesysteme, die in COBOL implementiert sind, in eine modernere Programmiersprache migriert und in die Cloud verschoben. Auch dabei wird schrittweise, d. h. Kontext für Kontext, migriert. Wichtig dabei: Das Ziel ist nicht unbedingt die vollständige Migration, sondern das Erhalten (bzw. Wiederherstellen) der Zukunftsfähigkeit. Meist entsteht dann ein Mischsystem, in dem einzelne Bounded Contexts in der neuen Welt, andere in der alten Welt implementiert sind. Evolution kann eben nur schrittweise stattfinden, wobei jeder Schritt eine lebensfähige Version des Systems ist.

Fazit und Ausblick

Domain-driven Design ist eine großartiges Denk- und Entwurfswerkzeugkiste, um Programmiererinnen und alle anderen, die an Softwareentwicklung arbeiten, auf das zu fokussieren, was wirklich wichtig ist: die Fachlichkeit. Wichtige Werkzeuge sind Strategic Design, Ubiquitous Language und Tactical Design. Das hilft uns insbesondere in der modernen Welt, in der unsere Systeme schnell anpassbar, Cloud-fähig und idealerweise als Microservices implementiert sein sollen.

In einem Überblicksartikel wie diesem kann man das Thema nur an der Oberfläche ankratzen. Wer sich tiefer einlesen will, aber nicht viel Zeit hat, dem sei „Domain-Driven Design kompakt“ [4] von Vaughn Vernon empfohlen. Wer sehen will, wie DDD in der Praxis umgesetzt wird, findet mit dem LeasingNinja [5] ein konkretes Beispiel. Anhand der Domäne Leasing wird hier exemplarisch gezeigt, wie man von der Fachlichkeit über Domain Stories, Context Map und taktisches Design bis in die Implementierung in Code kommt.

schwentner_henning_sw.tif_fmt1.jpgHenning Schwentner liebt Programmieren in hoher Qualität. Diese Leidenschaft lebt er als Coder, Coach und Consultant bei der WPS – Workplace Solutions aus. Dort hilft er Teams dabei, ihre gewachsenen Monolithen zu strukturieren oder neue Systeme von Anfang an mit einer tragfähigen Architektur zu errichten. Häufig kommen dann Microservices oder Self-contained Systems heraus.

Twitter
Desktop Tablet Mobile
Desktop Tablet Mobile