programming - JAX https://jax.de/tag/programming/ Java, Architecture & Software Innovation Wed, 02 Oct 2024 13:37:21 +0000 de-DE hourly 1 https://wordpress.org/?v=6.5.2 Verdächtige Gerüche https://jax.de/blog/verdaechtige-gerueche-durch-code-smells-zum-besseren-code/ Fri, 03 Jun 2022 11:52:41 +0000 https://jax.de/?p=86883 Will man bestehenden Code verbessern, ist es nicht immer leicht, zu entscheiden, wann man mit dem Refactoring [1] anfängt und wann man es beendet. Die Idee der „Code Smells“ hilft dabei. Der Begriff beschreibt Strukturen im Code, die überarbeitet werden sollten. In diesem Artikel betrachten wir eine Sammlung wichtiger Code Smells und führen an einem Beispiel Schritt für Schritt ein Refactoring durch, durch das wir eine übelriechende Codestruktur verbessern.

The post Verdächtige Gerüche appeared first on JAX.

]]>
Die Idee der Code Smells ist über zwanzig Jahre alt, aber immer noch aktuell. Der Begriff wurde von Kent Beck geprägt und erlangte durch das Buch „Refactoring“ [2] von Martin Fowler eine weite Verbreitung. Fowler und Beck haben 22 Smells identifiziert und für deren Beseitigung einen umfangreichen Refactoring-Katalog erstellt. Laut der Software Craftsmanship Community gehört dieses Buch zu den Klassikern, die jeder Entwickler gelesen haben sollte, denn es verändere die Art und Weise, wie wir Software entwickeln [3]. Auch in dem Standardwerk „Clean Code“ von Robert C. Martin sind Code Smells ein fester Bestandteil. „Uncle Bob“ listet in seinem Buch nicht nur die Smells von Fowler und Beck auf, sondern ergänzt sie um seine eigenen Erfahrungen.

Fünf Kategorien

Die klassischen 22 Smells von Fowler und Beck werden ohne eine Kategorisierung oder Beziehung zueinander vorgestellt. Da einige Code Smells eng miteinander verwandt sind und die gleichen Symptome zeigen, haben Lassenius und Mäntylä in einer wissenschaftlichen Ausarbeitung vorgeschlagen, eine Kategorisierung einzuführen [4]. Das Ergebnis waren folgende fünf Kategorien: Bloaters, Change Preventers, Dispensables, Couplers und Object Orientation Abuser. Diese Kategorisierung ist mittlerweile sehr verbreitet und Code Smells lassen sich damit sehr einfach erklären. Abbildung 1 zeigt die fünf Kategorien mit der Einordnung der Code Smells, die wir in diesem Artikel näher betrachten werden.

Das Ziel ist es, eine Übersicht über die große Anzahl von Code Smells zu bekommen und erste Hinweise mitzunehmen, anhand welcher Kriterien wir sie jeweils wiedererkennen.

Abb. 1: Fünf Kategorien von Code Smells

Bloaters

Die Kategorie Bloaters umfasst Smells, die den Code aufblähen. Es handelt sich dabei um Strukturen, durch die der Code sehr groß und unbeweglich wird. Solche Strukturen entstehen nicht zwingend von Anfang an, sondern über eine gewisse Zeit, gerade, wenn technische Schulden nicht abgebaut werden. Ab einer bestimmten Größe des Codes wird es für Entwickler sehr schwer, Erweiterungen oder Modifikationen daran durchzuführen. Für sehr umfangreiche Codestrukturen ohne Modularität ist es schwierig bis unmöglich, sie unter Test zu bringen. Die Smells dieser Kategorie lassen sich aufgrund ihrer großen Präsenz sehr einfach erkennen.

Eine davon ist die „lange Methode“. Es ist deutlich einfacher, die Logik einer kurzen Methode als die einer langen im Kopf zu behalten. Eine Methode sollte keine 100 Zeilen und bestenfalls keine 20 Zeilen groß sein, hier waren sich Kent Beck und Robert C. Martin bereits vor zwanzig Jahren einig [5]. Ihre Empfehlung liegt bei ca. fünf Zeilen pro Methode. Fakt ist: Je kleiner die Methoden sind, desto einfacher sind sie zu verstehen und desto einfacher lassen sie sich erweitern. Dies hat sich in vielen Projekten bewährt. In der Praxis, gerade in Legacy-Systemen, kommen leider schon mal Methoden mit 1 000 Zeilen vor. Hierbei handelt es sich dann um einen sehr übelriechenden Code, der nach Verbesserung geradezu schreit.

Durch lange Methoden entstehen indirekt auch lange Parameterlisten, da die benötigten Werte auf irgendeinem Weg in die Methode gelangen müssen. Keine wirkliche Alternative ist die Verwendung globaler Daten, denn auch hier muss der Entwickler die Herkunft der Daten interpretieren und verstehen. Je länger die Parameterliste, desto höher die Komplexität. Aus Testsicht sind lange Parameterlisten sehr schwierig, denn es muss jede mögliche Konstellation mit Tests abgedeckt werden. Hier wird empfohlen, ab drei Parametern darüber nachzudenken, ob man zum Beispiel ein Parameterobjekt einführt [5].

 

 

Die „umfangreiche Klasse“, auch als „Gottobjekt“ bekannt, bezeichnet eine Klasse, die zu viel weiß oder zu viel tut. Da in so einem Konstrukt die Übersicht schnell verloren geht, schleichen sich auch gern Redundanzen ein. Ein bekanntes Prinzip, um diesem Code Smell entgegenzuwirken, ist das Single Responsibility Principle (SRP). Jede Klasse sollte nur eine Verantwortlichkeit haben.

Bei „Datenklumpen“ handelt es sich um verschiedene Datenelemente, die häufig zusammen auftreten, in Feldern, aber auch in Parameterlisten. Das Konstrukt ist meistens durch gleiche Präfixe oder Suffixe erkennbar (Abb. 2). Ein sehr einfacher Test, um Datenklumpen zu identifizieren, ist es, ein Feld aus dem Klumpen zu löschen. Wenn die Felder nicht für sich alleine stehen können, dann handelt es sich um einen Datenklumpen. Dieser Code Smell würde auch in die Kategorie „Object Orientation Abuser“ passen, denn aus jedem Datenklumpen wird eine eigene Klasse extrahiert.

Abb. 2: Datenklumpen extrahieren

Change Preventers

Code Smells dieser Kategorie verhindern bzw. verlangsamen jegliche Erweiterung von Software. Bei einer Änderung an einer Stelle ist zwingend auch eine Änderung an einer anderen Stelle notwendig. Damit erhöht sich der Aufwand von Anpassungen drastisch. Diese Kategorie enthält zwei Code Smells, die zwar eng miteinander verwandt, aber im Grunde das genaue Gegenteil voneinander sind.

Abb. 3: Change Preventers

Wenn eine Klasse oder Methode aus verschiedenen Gründen und auf verschiedene Art und Weise immer wieder geändert wird, dann handelt es sich dabei um den Code Smell „divergierende Änderungen“. Eine Klasse wird zum Beispiel an drei Stellen geändert, um eine Datenbankänderung durchzuführen. Die gleiche Klasse muss aber auch an vier ganz anderen Stellen geändert werden, um einen neuen Datentyp einzuführen. Bei diesem Code wird das Single Responsibility Principle (SRP) verletzt. Die Klasse enthält mehrere Zuständigkeiten und sollte aufgeteilt werden.

Genau das Gegenteil von divergierenden Änderungen ist die sogenannte „Chirurgie mit der Schrotflinte“ (engl. Shotgun Surgery). Hierbei müssen bei jeder Änderung zwingend auch Anpassungen an weiteren Stellen durchgeführt werden. Die Modifikationen für die Änderung sind weit verstreut, dadurch kann es äußerst aufwendig sein, alle relevanten Stellen zu identifizieren. Um diese komplexe Struktur aufzulösen, sollte man zuallerst die schlecht voneinander getrennte Programmlogik bündeln und im Anschluss in eigene Strukturen extrahieren. Denn das oberste Ziel sollte sein, eine Änderung immer nur an einer einzigen Stelle durchführen zu müssen.

Dispensables

Die Kategorie „Dispensables“ repräsentiert entbehrlichen und sinnlosen Code. Hierzu gehören Code Smells wie „redundanter Code“, „träges Element“, „spekulative Generalisierung“ und „Kommentare“. Das Fehlen dieser Konstrukte würde den Code deutlich sauberer und leichter verständlich machen.

Redundanter Code kann in verschiedenen Ausprägungen vorkommen. Wenn es sich bei der Redundanz um komplett identischen Code handelt, dann lässt sich dieser sehr einfach identifizieren und beseitigen. Mittlerweile kann auch die IDE sehr gut unterstützen, um identischen Code über das gesamte Projekt zu finden. Handelt es sich bei den Redundanzen jedoch um ähnlichen, aber nicht vollständig identischen Code, dann kann das Identifizieren und Beseitigen schon aufwendiger werden. Eines der wichtigsten Prinzipien für sauberen Code ist das DRY-Prinzip (Don’t Repeat Yourself). Die Kernaussage ist, redundanten Code zu vermeiden.

Wenn eine Klasse zu wenig tut, sollte ihre Daseinsberechtigung hinterfragt werden. Es könnte sein, dass sie in der Vergangenheit mehr Verantwortung hatte und durch eine Überarbeitung zu einem „trägen Element“ (engl. lazy class) geworden ist. Hierbei muss man konsequent sein und das Ziel haben, unnötige Komplexität zu entfernen.

Die „spekulative Generalisierung“ entsteht, wenn Entwickler mit der Prämisse „Früher oder später brauchen wir die Funktionalität“ programmieren. Es wird Funktionalität umgesetzt, die nicht verwendet wird. Dies erhöht die Komplexität des Gesamtsystems unnötig und sollte vermieden werden. Hier gelten die zwei Prinzipien: KISS – Keep it Simple, Stupid und YAGNI – You ain’t gonna need it.

Zu Kommentaren haben Fowler und Beck eine klare Meinung: Es handele sich um Deodorant für Code Smells und sei damit selbst ein solcher [2]. Wenn Entwickler übelriechenden Code schreiben und ihnen das bewusst ist, sprühen sie etwas Deodorant drüber, indem ein Kommentar dazu verfasst wird. Daher sollten Kommentare immer kritisch betrachtet werden: Was will man damit verbergen?

Stay tuned

Regelmäßig News zur Konferenz und der Java-Community erhalten

 

Couplers

Die Code Smells dieser Kategorie führen zu einer übermäßigen Kopplung zwischen Klassen. Für die leichte Wandelbarkeit einer Software strebt man jedoch eine möglichst lose Kopplung zwischen Bausteinen an sowie eine hohe Kohäsion (starke Bindung) innerhalb eines Bausteins [6]. Enge Kopplung führt dazu, dass Änderungen in einer Klasse automatisch auch Änderungen in einer anderen Klasse verursachen.

Zu dieser Kategorie gehören Smells wie „Featureneid“, Insiderhandel, Mitteilungsketten und Vermittler. Die letzten beiden entstehen, wenn die enge Kopplung durch eine übermäßige Delegation ersetzt wird.

Abb. 4: Featureneid und Insiderhandel

Wenn eine Methode mehr an den Daten einer anderen Klasse interessiert ist als an den Daten der eigenen Klasse, dann sprechen wir von Featureneid (Abb. 4). Ein typisches Szenario ist beispielsweise, wenn eine Methode eine Berechnung durchführen will und dafür ein Dutzend Getter-Methoden einer anderen Klasse aufruft, um sich die Daten für die Berechnung zu holen. Bei so viel Neid sollte hinterfragt werden, ob die Methode in der richtigen Klasse ist.

Beim Insiderhandel tuscheln zwei Klassen hinter dem Rücken der offiziellen Schnittstelle. Diese Kopplung ist nicht auf Anhieb ersichtlich und nur schwer zu entdecken, da die Absprachen der Klassen heimlich erfolgen. Bei der Vererbung ist dieses Konstrukt oft zu beobachten, wenn die Unterklasse mehr über die Basisklasse weiß als notwendig. Daraufhin verschafft sich die Unterklasse einen Vorteil, in dem sie mit den Daten der Basisklasse arbeitet. In diesem Fall sollte die Vererbungshierarchie hinterfragt werden.

Abb. 5: Mitteilungsketten und Vermittler

Eine Mitteilungskette (Abb. 5) entsteht, wenn ein Objekt A die Daten von Objekt D benötigt und dies nur durch unnötige Navigation durch die Objekte B und C erreicht. Objekt A fragt nach Objekt B, danach das Objekt C und anschließend Objekt D mit den relevanten Daten.

A.getB().getC().getD().getTheNeededData()

Jede Änderung an den Beziehungen der beteiligten Objekte zwingt den Aufrufer ebenfalls zu einer Änderung. Durch die Refactoring-Maßnahme „Delegation verbergen“ kann die Mitteilungskette verkürzt werden. Bei Delegation verbergen muss der Aufrufer (Objekt A) nicht zwingend alle Objekte kennen, wenn zum Beispiel Objekt B die Verbindung zu Objekt C und D verbirgt.

A.getB().getTheNeededDataThroughCandD()

Somit muss bei Änderungen nicht immer der Aufrufer (Objekt A) geändert werden. Durch solch eine Maßnahme könnte aber auch der Code Smell „Vermittler“ entstehen. Wenn eine Klasse hauptsächlich aus einfachen Delegationen besteht und sonst keinen eigenen Mehrwert bietet, dann sprechen wir von einem Vermittler. Der Vermittler ist an der Stelle eine unnötige Komplexität, wir sollten versuchen, darauf zu verzichten.

Object-oriented Abuser

Diese Kategorie enthält übelriechenden Code, bei dem die objektorientierten Prinzipien falsch oder unvollständig umgesetzt wurden. Vererbung und Polymorphie sind die grundlegenden Konzepte der Objektorientierung. Wenn diese Konzepte falsch angewendet werden, so leiden darunter hauptsächlich die Wartbarkeit und Wiederverwendbarkeit. Object-oriented Abuser enthält Code Smells wie wiederholte Switch-Anweisung, alternative Klassen mit unterschiedlichen Schnittstellen und temporäres Feld.

Wenn sich Switch-Anweisungen mit identischer Steuerungslogik wiederholen, dann ist dies ein Anzeichen für das Fehlen von objektorientiertem Design. Die gleiche Fallunterscheidung anhand eines Typs ist an verschiedenen Stellen im Code verteilt. Möchte man einen weiteren Typ für die Fallunterscheidung hinzufügen, so muss man zwingend alle Switch-Anweisungen finden und dort die Erweiterung durchführen. Das ist sehr mühsam, unübersichtlich und fehleranfällig, doch das Problem lässt sich sehr elegant durch Polymorphie lösen. Wir schauen uns im nächsten Abschnitt diesen Code Smell anhand eines Beispiels etwas genauer an.

Beim ausgeschlagenen Erbe (engl. Refused Bequest) übernimmt eine Unterklasse nicht alle Daten oder Funktionen der Oberklasse, sondern unterstützt nur bestimmte Aspekte. Dazu kann es kommen, wenn die Vererbung verwendet wird, um bestimmte Aspekte der Oberklasse wiederzuverwenden, ohne dass die zwei Objekte etwas miteinander zu tun haben. Bei diesem Code Smell sollte die Vererbungshierarchie hinterfragt werden.

MEHR PERFORMANCE GEFÄLLIG?

Performance & Security-Track entdecken

 

 

Klassen untereinander austauschen zu können, ist ein großer Vorteil der objektorientierten Programmierung. Dies wird durch die Verwendung von Schnittstellen erreicht. Um Klassen untereinander auszutauschen, sollte man aber auch die gleichen Schnittstellen verwenden. Der Code Smell „alternative Klassen mit unterschiedlichen Schnittstellen“ kann entstehen, wenn beim Erstellen einer Klasse eine äquivalente Klasse übersehen wird. Dann wird eine ähnliche Klasse mit einer anderen Schnittstelle erzeugt. Die Schnittstellen müssen in so einem Fall vereinheitlicht werden.

Ein nur temporär genutztes Feld in einer Klasse kann zu großer Verwirrung führen. Der Entwickler muss die Fragen „Wo wird es verwendet?“ und „Warum wird es nur da verwendet?“ klären. Das erhöht unnötig die Komplexität, denn die Erwartungshaltung ist, dass ein Objekt alle seine Felder benötigt und benutzt und das nicht nur unter bestimmten Konstellationen. Ein temporäres Feld sollte am besten in eine eigene Klasse extrahiert werden und sämtlicher Code, der mit dem Feld arbeitet, sollte auch umgezogen werden.

Der Weg zu besserem Code

Das Erkennen von übelriechendem Code ist erst die halbe Miete. Uns ist dadurch zwar bewusst, welche Konstrukte überarbeitet werden sollten, doch nun stellt sich die Frage, wie wir übelriechenden Code beseitigen, ohne die Funktionalität der Software zu gefährden.

Ein Refactoring sollte immer in kleinen Schritten durchgeführt werden. Ein großes Big Bang Refactoring geht selten gut. Bei der Überarbeitung ist eine entsprechende Testabdeckung sehr wichtig. Dies kann jedoch eine Herausforderung sein, speziell im Umfeld von Legacy Code. Aber auch für Legacy-Systeme gibt es Ansätze, um die Testabdeckung aufzubauen, z. B. kann man mit der umgedrehten Testpyramide beginnen und diese mit der Zeit drehen.

Fowler und Beck haben für jeden identifizierten Code Smell eine Anleitung verfasst, welche Schritte notwendig sind, um ihn aufzulösen. Wir schauen uns weiter unten in Listing 1 exemplarisch so ein Vorgehen an. Da die Ausprägungen von Code Smells sehr unterschiedlich sein können, ist nicht immer eine Anleitung notwendig, gerade wenn es sich um einfache Smells handelt. Erfahrungsgemäß lassen sich diese bereits durch folgende toolgestützte Refactoring-Maßnahmen beseitigen:

  • Methode/Variable verschieben
  • Methode/Variable umbenennen
  • Methode/Variable extrahieren
  • Methode/Variable inline platzieren

Diese Maßnahmen werden von der Entwicklungsumgebung (IDE) unterstützt. Es lohnt sich, diese Unterstützung der IDE im Detail zu kennen und zu verwenden, denn das gibt dem Entwickler eine zusätzliche Sicherheit beim Refactoring.

Anhand des Beispiels in Listing 1 schauen wir uns den Code Smell „wiederholte Switch-Anweisungen“ etwas genauer an. Wir analysieren zum einen die übelriechenden Stellen im Code und schauen, wie wir ihn mit Hilfe der Anleitung aus dem Refactoring-Katalog [2] verbessern können. Bei dem Beispiel handelt es sich um das Parrot-Refactoring Kata [7], das in verschiedenen Programmiersprachen existiert.

Die Klasse Parrot (engl. für Papagei) enthält zwei Methoden mit den redundanten Switch-Anweisungen. Die Methode getSpeed bestimmt die Geschwindigkeit des Papageis und die Methode getPlumage den Zustand des Federkleides. Es gibt die drei Papageientypen „European“, „African“ und „Norwegian Blue“, die unterschiedliche Ausprägungen für Geschwindigkeit und Gefieder haben. Würde ein neuer Papageientyp hinzukommen, so müsste man alle Fallunterscheidungen anpassen. Dies widerspricht dem Open Closed Principle (OCP) und lässt sich mit Polymorphie sehr gut lösen.

public class Parrot {
// ...
double getSpeed(ParrotTypeEnum type) {
  switch (type) {
    case EUROPEAN:
      return getBaseSpeed();
    case AFRICAN:
      return getBaseSpeed() - getLoadFactor() * numberOfCoconuts;
    case NORWEGIAN_BLUE:
      return (isNailed) ? 0 : getBaseSpeed(voltage);
  }
  throw new RuntimeException("Should be unreachable");
}
 
String getPlumage(ParrotTypeEnum type) {
  switch (type) {
    case EUROPEAN:
      return "average";
    case AFRICAN:
      return numberOfCoconuts > 2 ? "tired" : "average";
    case NORWEGIAN_BLUE:
      return voltage > 100 ? "scorched" : "beautiful";
  }
  throw new RuntimeException("Should be unreachable");
}
// ...
}

Bedingung durch Polymorphie ersetzen

Im Folgenden schauen wir uns in Kürze die Anleitung aus dem Refactoring-Katalog von Fowler und Beck an. Es handelt sich dabei um ein bewährtes Vorgehen zum Auflösen unseres Code Smells „wiederholte Switch-Anweisungen“.

  1. Klassen für die Polymorphie erstellen
  2. Fabrikfunktionen zur Rückgabe der richtigen Instanzen erstellen
  3. Im aufrufenden Code die Fabrikmethoden verwenden
  4. Methoden in der Unterklasse überschreiben
  5. Verschieben der bedingten Funktionen aus der Basisklasse in die Unterklassen

Das ganze Refactoring ist auf sehr kleine Schritte aufgeteilt. Nach jedem Schritt wird geprüft, ob der Code kompilierbar ist und die automatisierten Tests noch funktionieren.

Im ersten Schritt werden die Klassen für die Polymorphie erstellt. Das Ergebnis sind die Klassen EuropeanParrot, AfricanParrot und NorwegianParrot, die von der Oberklasse Parrot erben. Im zweiten Schritt werden die entsprechenden Fabrikfunktionen erstellt und im nächsten Schritt in die aufrufenden Stellen eingebunden. Die ersten drei Schritte haben nur die neue objektorientierte Klassenstruktur erzeugt. Die gesamte Logik ist weiterhin in der Oberklasse Parrot und wird von den Unterklassen verwendet. Dies hat den großen Vorteil, dass eine neue Klassenstruktur in bestehenden Code eingeführt werden kann, ohne die Funktionalität zu beeinträchtigen.

Im vierten Schritt überschreiben die Unterklassen die Methoden getSpeed und getPlumage und rufen vorerst über super (Listing 2) die Methoden der Oberklasse auf, in der noch immer die Switch-Anweisungen ausgeführt werden.

public class AfricanParrot extends Parrot {
// ...
@Override
public double getSpeed() {
  return super.getSpeed();
}
@Override
public double getPlumage() {
  return super.getPlumage();
}
}

Erst im fünften Schritt wird die Logik aus den Switch-Anweisungen in die entsprechenden Unterklassen verschoben, und das Refactoring ist fast vollbracht. Zum Schluss haben die Methoden getSpeed und getPlumage in der Parrot-Klasse keine Funktionalität mehr und können damit abstract werden. Das Ergebnis aller Schritte inklusive der Bereinigung zum Schluss zeigt Listing 3.

public abstract class Parrot {
 
  public static Parrot createEuropeanParrot() {
    return new EuropeanParrot();
  }
  public static Parrot createAfricanParrot( int numberOfCoconuts) {
    return new AfricanParrot(numberOfCoconuts);
  }
 
  public static Parrot createNorwegianBlueParrot(double voltage, 
boolean isNailed) {
    return new NorwegianBlueParrot(voltage, isNailed);
  }
 
  public abstract double getSpeed();
  public abstract double getPlumage();
  // ...
}
 
public class AfricanParrot extends Parrot {
  // ...
  @Override
  public double getSpeed() {
    return Math.max(0, getBaseSpeed() - getLoadFactor() * numberOfCoconuts);
  }
 
  @Override
  public String getPlumage() {
    return numberOfCoconuts > 2 ? "tired" : "average";
  }
  // ...
}
 
public class EuropeanParrot extends Parrot{
  // ...
  @Override
  public double getSpeed() {
    return getBaseSpeed();
  }
 
  @Override
  public String getPlumage() {
    return "average";
  }
  // ...
}
 
public class NorwegianBlueParrot extends Parrot{
  // ...
  @Override
  public double getSpeed() {
    return (isNailed) ? 0 : getBaseSpeed(voltage);
  }
 
  @Override
  public String getPlumage() {
    return voltage > 100 ? "scorched" : "beautiful";
  }
  // ...
}

Dieses Vorgehen zeigt uns, wie iterativ eine komplexe Strukturänderung im Code erfolgen kann. Auch wenn die Schritte teilweise zu kleinteilig wirken, sind sie in der Praxis, wenn der Code deutlich umfangreicher ist, genau richtig. Nur durch solche kleinen Schritte und die ständige Ausführung der Tests kann ein komplexes Refactoring ohne Seiteneffekte erfolgreich durchgeführt werden.

Fazit

In diesem Artikel haben wir anhand von fünf Kategorien 18 Code Smells betrachtet. Um die schlechten Gerüche im Code wieder loszuwerden, sollte als Erstes die Testabdeckung sichergestellt werden. Weitere Sicherheit bieten toolgestützte Refactorings, mit denen bereits gute Ergebnisse erzielt werden. Bei komplexen Code Smells lohnt es sich, auf bewährtes Vorgehen zurückzugreifen. Ein Beispiel hierfür haben wir uns im Detail angeschaut.

Mit der Metapher Code Smells steht uns seit mehr als zwanzig Jahren ein Werkzeug zur Verfügung, mit dem wir schlechten Code identifizieren und bereinigen können. Wir sollten von diesen Erfahrungen profitieren und nicht alle Fehler selbst machen. Sich mit den Code Smells auseinanderzusetzen, hat zwei wesentliche Vorteile. Zum einen erkennen wir leichter die Strukturen, die überarbeitet werden sollten, und zum anderen entwickeln wir ein besseres Bewusstsein dafür, welche Fehler wir vermeiden sollten, wenn wir neuen Code schreiben. Beide Aspekte führen zu dem Ergebnis, dass die Qualität der Software besser wird.

Stay tuned

Regelmäßig News zur Konferenz und der Java-Community erhalten

 

 

Links & Literatur

[1] https://de.wikipedia.org/wiki/Refactoring

[2] Fowler, Martin: „Refactoring: Improving the Design of Existing Code“; Addison-Wesley, 2018

[3] Mancuso, Sandro: „The Software Craftsman: Professionalism, Pragmatism, Pride“, Pearson Education, 2014

[4] Mäntylä, M. V. and Lassenius, C.: „Subjective Evaluation of Software Evolvability Using Code Smells: An Empirical Study“, Journal of Empirical Software Engineering, vol. 11, no. 3, 2006, pp. 395–431.

[5] Martin, Robert C.: „Clean Code: A Handbook of Agile Software Craftsmanship“, Prentice Hall, 2008

[6] Starke, Gernot: „Effektive Softwarearchitekturen: Ein praktischer Leitfaden“, Hanser Fachbuch, 2002

[7] https://github.com/emilybache/Parrot-Refactoring-Kata.git

The post Verdächtige Gerüche appeared first on JAX.

]]>
Alles im Blick https://jax.de/blog/alles-im-blick/ Fri, 11 Mar 2022 13:59:28 +0000 https://jax.de/?p=85794 Wie funktionieren APM Agents in der Java Virtual Machine im Detail? Was ist bei der Instrumentierung zu beachten und welche Besonderheiten von APM-Agenten muss man berücksichtigen? Und warum werden oftmals nur die bekanntesten Java-Frameworks unterstützt? Diesen Fragen gehen wir in diesem Beitrag auf den Grund.

The post Alles im Blick appeared first on JAX.

]]>
APM steht für Application Performance Management und erlaubt es als Teil der Observability, die eigene Anwendung genauer zu durchleuchten. Wie lange dauern bestimmte SQL-Abfragen? Welche Microservices oder Datenbanken werden innerhalb eines HTTP Requests abgefragt? Welcher Teil eines HTTP Requests ist der eigentliche Flaschenhals und weist die längste Antwortzeit auf? Um diese Fragen zu beantworten, reicht es nicht, Logs oder Metriken zu betrachten, sondern man muss sich die Laufzeit einzelner Methoden oder Aufrufe innerhalb der eigenen Anwendung ansehen. Hier kommt APM ins Spiel.

APM als Säule der Observability

APM gehört zum Tracing, das neben Logs und Metriken als eine der drei Säulen der Observability gilt. Dabei geht es allerdings nicht um die Daten selbst oder die Art der Datengenerierung, sondern allein um die Möglichkeit, aus vielen Signalen (Logs, Metriken, Traces, Monitoring, Health Checks) diejenigen herauszufiltern, die auf eine mögliche Einschränkung eines eigenen oder fremden Service wie Antwortzeit oder Verfügbarkeit hindeuten. Einzelne Bereiche der Observability sollten nicht isoliert betrachtet und idealerweise auch nicht mit unterschiedlichen Tools bearbeitet werden, damit man nicht nachts um drei Uhr mit mehreren Browsertabs und manueller Korrelation bei einem Ausfall eingreifen muss. Eine Kerneigenschaft des APM ist die Darstellung der Laufzeit von Komponenten der eigenen Anwendung. Die zwei wichtigsten Begriffe sind hier Transaction und Span. Eine Transaction ist eine systemübergreifende Zusammenfassung einzelner Spans, welche die Laufzeit konkreter Methoden oder Aktionen innerhalb eines Systems zusammenfassen. Eine Transaction kann sich über mehrere Systeme ziehen und beginnt bei einer Webanwendung im besten Fall im Browser des Users (Abb. 1).

Abb. 1: Transaction über mehrere Systemgrenzen (farbig gekennzeichnet)

Instrumentierung innerhalb der JVM

Bei APM-Agenten liegt der Fall etwas anders als bei Logging und Metriken, da sie in die Anwendung hineinschauen müssen und diese unter Umständen auch verändern. Bugs in diesen Agenten sind gefährlich und können sich auf die Anwendung auswirken – unabhängig von der Programmiersprache. Java hat im Gegensatz zu vielen anderen Sprachen eine standardisierte Schnittstelle zur Instrumentierung. Der durch Kompilierung erstellte Bytecode kann verändert werden und die Anwendung kann mit diesen Änderungen weiterlaufen. Um diese Veränderung so einfach wie möglich zu implementieren, gibt es Bibliotheken wie ASM oder Byte Buddy, mit denen Methodenaufrufe abgefangen werden können, um beispielsweise die Laufzeit zu messen. Wenn ich als Entwickler eines Agenten also alle Aufrufe des in der JVM eingebauten HTTP-Clients abfange und die Laufzeit sowie den Endpunkt als Teil eines Spans logge, kann ich danach einfach im APM UI sehen, wie viel Zeit diese Anfrage benötigt und ob lokales Request Caching beim Einhalten möglicher SLAs hilft. Durch Aktivieren des Agenten darf kein oder nur geringer Einfluss auf die Geschwindigkeit der Anwendung genommen werden (Overhead). Das Gleiche gilt für die Garbage Collection. Beides lässt sich nicht völlig verhindern, jedoch stark reduzieren.

Stay tuned

Regelmäßig News zur Konferenz und der Java-Community erhalten

 

Der Zeitpunkt der Instrumentierung ist unterschiedlich. Die bekannteste und beliebteste Variante ist das Setzen des Agents als Parameter beim Starten der JVM:

java -javaagent:/path/to/apm-agent.jar -jar my-application.jar

Dieser Aufruf führt die Instrumentierung aus, bevor der eigentliche Code geladen wird. Alternativ kann die Instrumentierung auch bei einer bereits laufenden JVM stattfinden.

Doch was genau bedeutet Instrumentierung eigentlich? Die Laufzeit von Methoden kann nur gemessen werden, wenn diese Methoden abgefangen werden und Code des Agenten um den eigentlichen Code herum ausgeführt wird – zum Beispiel das Anlegen eines Spans oder einer Transaction, wenn ein HTTP Request abgeschickt wird. Dieser Ansatz ist aus der aspektorientierten Programmierung bekannt. Eine Voraussetzung muss gegeben sein: Der jeweilige APM-Agent muss die Methode inklusive Signatur kennen, die instrumentiert wird. Für Java Servlets ist zum Beispiel die Methode service(HttpServletRequest req, HttpServletResponse resp) im Interface HttpServlet die wichtigste Methode, um jeden HTTP-Aufruf zu überwachen, unabhängig vom Pfad oder der HTTP-Methode. Das bedeutet gleichzeitig, dass bei einer Änderung der Methodendeklaration der Agent ebenfalls angepasst werden muss. Und hier liegt eines der größten Probleme mit dieser Form der Überwachung: Es muss sichergestellt sein, dass sowohl möglichst viele Frameworks (Spring Boot, JAX-RS, Grails, WildFly, Jetty etc.) und deren Methoden instrumentiert werden, als auch ständig überprüft wird, ob die Instrumentierung bei einem neuen Release noch funktioniert, besonders bei Major Releases. Das ist eine der großen Maintenance-Aufgaben bei der Entwicklung von APM-Agenten. Während es einige Standards im Java-Bereich gibt, die einfach zu instrumentieren sind, wie Servlets, JDBC oder JAX-RS, gibt es ebenfalls eine Menge Frameworks, die keinen solchen Standards folgen, wie beispielsweise Netty. Das heißt auch, dass jeder Agent, dessen Instrumentierung eines Frameworks nicht funktioniert hat, weil eventuell Methoden aus einer anderen Major-Version mit anderer Signatur existieren, sicherstellen muss, dass dies kein Problem darstellt.

Bisher haben wir nur APM Agents im Kontext von Java Agents erwähnt; es gibt aber noch weitere Anwendungsfälle. Ganz aktuell hat das AWS-Corretto-Team um Volker Simonis einen Agent zum Patchen der Log4Shell-Sicherheitslücke entwickelt [1]. Der Agent verhindert das Sicherheitsproblem der Remote Code Execution bei einer bereits laufenden JVM. Ein anderer Use Case sind Agents, die Sicherheitsfeatures wie das automatische Setzen von HTTP Headern oder WAF-Funktionalität bereitstellen.

 

Agents und ihre Features

Der bekannteste Standard in der Observability-Welt ist OpenTelemetry [2]. Die Idee von OpenTelemetry ist, den einen, herstellerneutralen, universellen Standard zur Verfügung zu stellen, den alle verwenden, unabhängig von der Programmiersprache. Mit dem OpenTelemetry Agent gibt es einen JVM Agent, den viele Observability-Anbieter als eigene Distribution veröffentlichen, wie zum Beispiel Lightstep oder Honeycomb. Dieser kommt bereits mit einigen Instrumentierungen für bekannte Frameworks [3].

Eine weitere Standardisierung in Zeiten von Microservices und der Möglichkeit, einen Request vom Laden der Webseite im Browser bis zur SQL-Query zu identifizieren, ist das Distributed Tracing. Um Distributed Tracing zwischen verschiedenen Programmiersprachen und Umgebungen zu implementieren, existiert die OpenTracing-Spezifikation und -Implementierung. Viele APM Agents folgen dieser Spezifikation, um Kompatibilität sicherzustellen, unter anderem Lightstep, Instana, Elastic APM, Apache Skywalking und Datadog.

Vor OpenTelemetry gab es einige wenige Agents, die aus JVM-Sicht eigentlich gar keine waren, weil sie keinerlei Instrumentierung vorgenommen, sondern lediglich Interfaces in bestimmten Frameworks implementiert haben, um Monitoringdaten auszulesen. Ich gehe davon aus, dass es über kurz oder lang nur noch Agents geben wird, die auf dem OpenTelemetry Agent basieren, und dass die Alleinstellungsmerkmale nicht im Sammeln der Daten, sondern ausschließlich in der Auswertung liegen.

Elastic APM Agent

Wenn es einen OpenTelemetry Agent gibt, wieso gibt es dann zum Beispiel auch einen Elastic-APM-spezifischen Agent? Zum einen gibt es eben doch mehr Features als einen universellen Standard. Beispiel: das Erfassen interner JVM-Metriken (Speicherverbrauch, Garbage-Collection-Statistiken) oder auch das Auffinden langsam ausführender Methoden ohne das Wissen um konkrete Methoden oder Code mit Hilfe des async-profilers [4] – eine Technologie, die bei Datadog, Elastic APM oder Pyroscope [5] verwendet wird. Des Weiteren existieren eine Menge Agenten bereits länger als die OpenTelemetry-Implementierung und bringen mehr Unterstützung für bestimmte Frameworks mit, die erst in den OpenTelemetry-Agenten portiert werden müssen.

Der Elastic Agent bietet zudem ein weiteres sehr interessantes Feature, und zwar das programmatische Konfigurieren des Agent anstatt der Verwendung des JVM-Agent-Mechanismus als Parameter beim Starten der JVM. Das bedeutet, man bindet den Agent als Dependency in den Code ein, und versucht die folgende Zeile Code beim Start der Anwendung so früh wie möglich auszuführen:

ElasticApmAttacher.attach();

Jetzt geschieht prinzipiell dasselbe wie bei der Agent-spezifischen Konfiguration: Der Agent attacht sich selbst an den laufenden Code. Diese Art der Einrichtung hat einen großen Vorteil: Die Dependency ist bereits Teil des Deployments und muss nicht als Teil des Build-Prozesses oder der Container-Image-Erstellung heruntergeladen werden. Gleichzeitig ist der Entwickler für das fortlaufende Aktualisieren verantwortlich.

Bisher ungeklärt ist die Frage, was ein APM Agent mit den erhobenen Daten eigentlich machen soll. Im Fall von Elastic APM werden diese an einen APM-Server geschickt, der sie wiederum im nächsten Schritt in einem Elasticsearch Cluster speichert. Der APM-Server kommuniziert nicht nur mit den anderen Elastic APM Agents (Node, Ruby, PHP, Go, iOS, .NET, Python), sondern kann auch Daten puffern und als Middleware für Source Mapping bei JavaScript-Anwendungen agieren.

Wie bereits erwähnt, liegt der Mehrwert weniger im Sammeln als im Auswerten von Daten. Im Fall von Elastic APM ist das unter anderem die Integration mit Machine Learning, genauer der Time Series Anomaly Detection, um automatisiert Laufzeiten von Transaktionen zu erkennen, die im Vergleich zu vorher gemessenen Ergebnissen überdurchschnittlich lange brauchen, dem automatischen Annotieren von Deployments im APM UI oder auch der Korrelation von plötzlich auftretenden Transaktionslatenzen und Fehlerraten in allen von der Anwendung generierten Logs. So wird sichergestellt, dass die Grenzen zwischen den anfangs erwähnten Observability-Säulen nicht existieren.

Programmatische Spans und Transactions

Nicht jeder Entwickler möchte ein eigenes APM-Agent-Plug-in schreiben, damit die eigene Java-Anwendung Spans und Transactions innerhalb der eigenen Geschäftslogik verwendet. Ein alle 30 Sekunden laufender Job im Hintergrund sollte als eigene Transaktion und jede der darin aufgerufenen Methoden als eigener Span konfiguriert werden. Hier gibt es zwei Möglichkeiten der Konfiguration. Entweder werden die Methodennamen über die Agentenkonfiguration angegeben oder man wählt die programmatische Möglichkeit. Ein Beispiel innerhalb von Spring Boot zeigt Listing 1.

@Component
public class MyTask {
 
  @CaptureTransaction
  @Scheduled(fixedDelay = 30000)
  public void check() {
    runFirst();
    runSecond();
  }
 
  @CaptureSpan
  public void runFirst() {
  }
 
  @CaptureSpan
  public void runSecond() {
  }
}

Die Annotation @CaptureTransaction legt eine neue Transaktion an und innerhalb dieser Transaktion werden die beiden Spans für die Methoden via @CaptureSpan angelegt. Sowohl Transaktion als auch Spans können mit einem eigenen Namen konfiguriert werden, der im UI einfacher identifiziert werden kann. Unter [6] gibt es ein GitHub Repository, das sowohl die Instrumentierung des Java-HTTP-Clients zeigt als auch das Verwenden von programmatischen Transaktionen und Spans im eigenen Java-Code.

 

Elastic APM Log Correlation

Wie erwähnt, ist es sinnvoll, Logs, Metriken und Traces miteinander zu verbinden. Wie aber kann eine bestimmte Logzeile mit einer bestimmten Transaktion verbunden werden? In Elastic APM heißt dieses Feature Log Correlation. Der erste (optionale) Schritt ist, Logdateien ins JSON-Format zu überführen. Das macht es wesentlich einfacher, weitere Felder zu den Logdaten hinzuzufügen. Eben diese Felder werden für die Korrelation benötigt. Wenn man im Agent die Option log_ecs_reformatting verwendet, werden im sogenannten MDC der jeweiligen Logger-Implementierung die Felder transaction.id, trace.id und error.id hinzugefügt, nach denen dann sowohl in Transactions und Spans als auch in einzelnen Lognachrichten gesucht werden kann. So können Lognachrichten unterschiedlichster Services miteinander korreliert und durchsucht werden; Logmeldungen eines Service sind einer konkreten eingehenden HTTP-Anfrage zuzuordnen.

Automatische Instrumentierung mit K8s

Will man Container mit Java-Anwendungen unter Kubernetes instrumentieren, kann man jederzeit die verwendeten Images/Pod-Konfigurationen anpassen und innerhalb dieser den Agent konfigurieren sowie APM-Konfigurationen einstellen, zum Beispiel APM-Endpunkte, API-Token (zum Beispiel via HashiCorp Vault [7]) oder die erwähnte Log Correlation. Es gibt eine weitere Möglichkeit, und zwar die Verwendung eines Init-Containers, der vor den eigentlichen Anwendungscontainern in einem Pod ausgeführt wird [8]. Dieser Container konfiguriert Umgebungsvariablen, die dann beim Starten des regulären Containers ausgelesen werden und somit zusätzlich den passenden JVM-Agenten starten [9]. Dieser Ansatz kann sinnvoll sein, wenn man keine Kontrolle über die erstellten Container hat oder sicherstellen möchte, dass ein Agent in einer bestimmten Version für alle Java-Anwendungen läuft.

MEHR PERFORMANCE GEFÄLLIG?

Performance & Security-Track entdecken

 

Distributed Tracing mit RUM

In Zeiten von Microservices und APIs ist es in vielen Systemarchitekturen wahrscheinlich, dass ein einzelner Aufruf eines Anwenders sich zu mehreren Aufrufen in der internen Architektur multiplext und mehrere Services abgefragt werden. Hier ist es besonders wichtig, verfolgen zu können, wie ein initialer Request durch die unterschiedlichen Services weitergereicht und verändert wird. Eine Transaktion kann mehrere Spans haben, die in unterschiedlichen Systemen auftreten, unter Umständen auch gleichzeitig. Hier kommt Distributed Tracing mit Hilfe von Trace IDs ins Spiel, die durch alle Requests hindurch – zum Beispiel mit Hilfe von HTTP-Headern – an die jeweiligen Spans angehängt werden und somit durch den Lebenszyklus des initialen Request rückverfolgbar sind. Ein weiterer Vorteil von Distributed Tracing ist die Möglichkeit, aus diesen Daten eine Service Map zu erstellen, da man weiß, welche Services miteinander kommunizieren (Abb. 2).

Abb. 2: Service Map, um Kommunikationsflüsse einzelner Komponenten zu visualisieren

Bei der Entwicklung von Webanwendungen ist es ebenfalls nicht ausreichend, erst an den eigenen Systemgrenzen mit dem Anlegen von Transactions und Spans zu beginnen, da man sonst keinen Überblick über die komplette Performance der eigenen Anwendung hat. Wie lange dauert das Aufbauen der Verbindung im Browser zum Webserver? Ist die Latenz hier eventuell so hoch, dass es irrelevant ist, 50 ms bei einer komplexen SQL-Query zu sparen? Um dieses Problem anzugehen, gibt es das Real User Monitoring, kurz RUM. Zum einen können Transaktionen an der richtigen Stelle begonnen werden, zum anderen werden auch Browserereignisse geloggt, um festzustellen, wie lange das initiale Rendern der Seite braucht, sodass der Anwender mit der Anwendung interagieren kann (Abb. 3).

Abb. 3: RUM-Dashboard mit Ladezeiten und Browserstatistiken

APM in der Zukunft

Das Bedürfnis, für Anwendungen eine Art Röntgengerät zu bekommen, wird in Zukunft noch zunehmen – vielleicht werden sich die Methoden etwas ändern. Zeit für einen kleinen Ausblick. In den vergangenen Jahren ist eine neue Art von Agents auf den Markt gekommen, die eine neue, sprachunabhängige Technologie verwenden: eBPF. Mit Hilfe von eBPF kann man Programme im Kernelspace laufen lassen, ohne den Kernel zu verändern oder ein Linux-Kernel-Modul laden zu müssen. Alle eBPF-Programme laufen innerhalb einer Sandbox, sodass das Betriebssystem Stabilität und Geschwindigkeit garantiert. Da eBPF Syscalls überwachen kann, ist es ein idealer Einstiegspunkt für jegliche Observability-Software. Der weitaus wichtigere Teil ist allerdings die Möglichkeit, diese Syscalls auf Methodenaufrufe in die jeweilige Programmiersprache des überwachten Programms zu übersetzen. eBPF-basierte Profiler haben generell einen geringen Overhead, da sie sehr tief im System verankert sind. Des Weiteren müssen keine Deployments angepasst werden, da diese Profiler auch innerhalb eines Kubernetes-Clusters für alle Pods konfiguriert werden können. Beispiele für diese Art von Profiler sind prodfiler [10] von Elastic, Pixie [11], Parca [12] oder Cilium Hubble [13].

Ein weiteres neues Themenfeld ist das Überwachen auf Serverless-Plattformen. Hier braucht man etwas andere Lösungen, da nicht garantiert ist, dass nach dem Verarbeiten einer Anfrage noch Rechenkapazität zur Verfügung gestellt wird. Methoden wie etwa Spans und Traces als Batch zu sammeln und periodisch an den APM-Server zu verschicken, funktionieren hier also nicht. Für AWS Lambda steht mit opentelemetry-lambda [14] ein eigenes GitHub-Projekt zur Verfügung. Die grundlegende Idee ist ein sogenannter Lambda-Layer, der diese Observability-Aufgaben übernimmt. Wenn man also in diese Art von Plattformen eintaucht, sollte man sicherstellen, dass die eigene Observability-Plattform diese Technologien unterstützt.

Ein weiterer wichtiger Baustein abseits vom Sammeln und Auswerten der Livedaten ist der Trend zu Shift Left – nicht nur in der Security. Hier bietet JfrUnit [15] von Gunnar Morning einen interessanten Ansatz aus dem Umfeld des Java Flight Recorders. Als Teil des Unit Testings werden JFR Events herangezogen, um bestimmte Constraints wie Garbage Collection, erhöhte Memory Allocation oder I/O bereits in Tests festzustellen und vor dem eigentlichen Deployment zu korrigieren.

Stay tuned

Regelmäßig News zur Konferenz und der Java-Community erhalten

 

Schlusswort

Wie überall, so ist auch in der Welt der JVM Agents für APM nicht alles rosig. Einige Agents unterstützen zum Beispiel nur die bekanntesten Web-Frameworks wie Spring oder Spring Boot bzw. auch innerhalb eines Frameworks nur synchrones Request Processing. Es gilt daher, anfangs in Ruhe mögliche Agents zu testen. Fast alle Agents sind Open Source, sodass man im Fall der Fälle auch ein eigenes Plug-in schreiben kann. Je nach Sicherheitseinstellungen der Plattform, auf der Services betrieben werden, ist es eventuell nicht erlaubt, einen Agent programmatisch an den Java-Prozess anzuhängen – zum Beispiel ist mir das bei der Digital-Ocean-Apps-Plattform, einem PaaS, nicht gelungen. Der -javaagent-Parameter innerhalb des Docker Image hat hingegen einwandfrei funktioniert.

Ein weiteres Thema, das man in den aktuellen Java-Trends wahrscheinlich schon entdeckt hat, ist GraalVM. Falls mit Hilfe der GraalVM die Anwendungen in native Binaries umgewandelt werden, existiert der Mechanismus zum Anhängen von Java Agents nicht. Das heißt nicht, dass keinerlei Instrumentierung möglich ist. Die programmatische Erstellung von Spans und Traces könnte allerdings bei einigen APM-Lösungen noch funktionieren, die nicht auf reines Bytecode Enhancement setzen. Da viele bekanntere Frameworks wie Spring und Quarkus inzwischen native Extensions und Module haben, um möglichst einfache Binaries zu erstellen, erwarte ich in den nächsten Monaten, dass auch die APM-Plattformen nachziehen werden. Quarkus hat bereits Support für OpenTracing und DataDog im native Mode.

Um es noch einmal abschließend zu wiederholen: Observability ersetzt kein Monitoring und APM ersetzt kein effizientes Entwickeln performanter Software. Viele Probleme können durch Testing, Reviews oder Pair Programming sehr viel früher im Lebenszyklus der Software gefunden werden und sind dann weitaus ökonomischer zu fixen. Wesentlich komplizierter ist das bereits bei Distributed Tracing und dessen Intersystemkommunikation, um mögliche Bottlenecks vor dem Produktionsbetrieb zu identifizieren. Nichtsdestoweniger ist ein so tiefer Einblick in die selbstgeschriebene Software, wie APM ihn bietet, von Vorteil und sollte auch genutzt werden, wenn der zusätzliche Aufwand der initialen Einrichtung einmal erledigt ist.

 

Links & Literatur

[1] https://github.com/corretto/hotpatch-for-apache-log4j2

[2] https://opentelemetry.io

[3] https://github.com/open-telemetry/opentelemetry-java-instrumentation/blob/main/docs/supported-libraries.md#libraries–frameworks

[4] https://github.com/jvm-profiling-tools/async-profiler

[5] https://pyroscope.io

[6] https://github.com/spinscale/observability-java-samples

[7] https://www.vaultproject.io/

[8] https://kubernetes.io/docs/concepts/workloads/pods/init-containers/

[9] https://www.elastic.co/blog/using-elastic-apm-java-agent-on-kubernetes-k8s

[10] https://prodfiler.com

[11] https://px.dev

[12] https://www.parca.dev

[13] https://github.com/cilium/hubble

[14] https://github.com/open-telemetry/opentelemetry-lambda

[15] https://github.com/moditect/jfrunit

The post Alles im Blick appeared first on JAX.

]]>
Die Untiefen reaktiver Programmierung https://jax.de/blog/die-untiefen-reaktiver-programmierung/ Tue, 22 Feb 2022 11:40:38 +0000 https://jax.de/?p=85630 Vor acht Jahren wurde das Reactive Manifesto [1] veröffentlicht, und ganz allmählich beginnt die reaktive Programmierung auch in der Java-Welt Fuß zu fassen. Deswegen lohnt es sich, die Erfahrungen und Best Practices aus einer anderen Community anzuschauen. Angular hat von Anfang an sehr stark auf RxJS gesetzt und sich damit eine ganze Menge Komplexität eingehandelt. Wie kann man sie beherrschbar machen und was können Java-Entwickler daraus lernen?

The post Die Untiefen reaktiver Programmierung appeared first on JAX.

]]>
Haben Sie sich schon einmal gefragt, warum Quarkus oder Netty so schnell sind? Bei Quarkus könnte die Antwort GraalVM lauten, und das wäre sogar richtig. Aber wie sieht es mit Netty aus? Oder Vert.x? Oder warum fühlt sich ein modernes UI mit Angular oder React.js flotter an als die meisten JSF- oder Spring-MVC-Anwendungen? Die Gemeinsamkeit ist die non-blocking IO. Dahinter steckt die Erkenntnis, dass unser Computer die meiste Zeit mit Warten verbringt. Unsere modernen Gigahertz-CPUs können ihr Potenzial gar nicht ausspielen. Kurze Sprints werden abgelöst von schier endlosen Wartezeiten. Irgendwo ist immer ein Stau. Jeder Datenbankzugriff bedeutet mehrere Millisekunden Pause, jeder Zugriff auf ein REST-Backend dauert oft 10 bis 100 Millisekunden, und das summiert sich schnell auf mehrere Sekunden. Die Idee der reaktiven Programmierung ist, die Wartezeiten sinnvoll zu nutzen. Dafür zerschneidet man einen Algorithmus in zwei Teile. Alles, was nach dem REST-Call kommt, wird in eine eigene Funktion ausgelagert. Euer Framework ruft diese Callback-Funktion auf, wenn die Daten angekommen sind. In der Zwischenzeit kann die CPU andere Aufgaben erledigen. Sie kann zum Beispiel einfach mit der nächsten Zeile weitermachen. Beim traditionellen, blockierenden Programmiermodell definiert die nächste Zeile, was wir mit dem Ergebnis der REST-Calls machen wollen. Aber das ist jetzt nicht mehr der Fall. Dieser Teil des Algorithmus wurde ja in die Callback-Funktion verschoben. Es gibt also keinen Grund mehr, mit der Abarbeitung der nächsten Zeile zu warten. Damit können sehr viele Performanceprobleme gelöst werden. Solche Callback-Funktionen sind erst seit Java 8 sinnvoll möglich, als die Lambdafunktionen eingeführt wurden. Vorher konnte man sie mit anonymen inneren Klassen simulieren, aber das war so unattraktiv, dass es kaum jemand gemacht hat. Dadurch erklärt sich, warum reaktive Frameworks wie Quarkus oder Spring Reactor erst seit relativ kurzer Zeit populär werden. Die JavaScript-Welt hat hier einige Jahre Vorsprung, und deswegen soll jetzt RxJS betrachtet werden.

Nichtlineare Programmierung mit RxJS

Auf den nächsten Seiten bewegen wir uns ausschließlich im JavaScript Frontend. Das hat einige Konsequenzen. JavaScript kennt kein Multi-Threading. Das macht aber nichts, es muss ja nur noch ein einziger Anwender bedient werden. Wir reden der Einfachheit halber jetzt auch nur noch über REST-Calls.

Es wird also ein REST-Call zum Backend geschickt. Die Idee ist, die Wartezeit, bis die Antwort eintrudelt, zu nutzen. Das bedeutet, dass sofort mit der nächsten Zeile im Programmcode weitergemacht werden kann. Erfahrene RxJS-Entwickler werden jetzt mit den Achseln zucken und „Ja, und?“ sagen. Für Neueinsteiger ist das aber ein erhebliches Problem. Die Reihenfolge, in der der Programmcode ausgeführt wird, ist nicht mehr linear. In vielen Fällen ist er auch nicht mehr deterministisch. Schauen wir uns ein einfaches Beispiel an (Listing 1).

Stay tuned

Regelmäßig News zur Konferenz und der Java-Community erhalten

 

Alles neu?

Wir sind alle Entwickler und Technologieenthusiasten und daher kommen wir sehr schnell zu der Überzeugung, dass mit Framework X oder Technologie Y alle Probleme schnell gelöst werden und damit auch ganz neuen Herausforderungen und Anforderungen leicht entsprochen werden kann. Einige wollen auch dafür eigene Technologien entwickeln, was gar nicht mal ungewöhnlich ist – man liest und hört nur sehr wenig von diesen Ansätzen. Die meisten Seniorentwickler schreiben die gleiche Art von Anwendung oder lösen die gleiche Art von Problemen Tag für Tag, Jahr um Jahr. Das ist manchmal langweilig, und was gibt es Spannenderes, als ein eigenes Framework zu schreiben? Vielleicht sogar mit der Absicht, es danach als Open-Source-Software zu veröffentlichen? Die Antwort ist: Wahrscheinlich ist es superspannend, aber bringt es unsere Anwendung wirklich weiter? Und viel wichtiger: Lösen wir damit überhaupt die aktuellen Probleme?

Um diese Fragen zu beantworten, müssen die Anwendung und ihre Architektur erst einmal überprüft werden. Bei einer solchen Architekturreview, die idealerweise von Externen durchgeführt wird, hat sich in der Praxis ATAM [1] als Methode bewährt. Dieser Artikel ist zu kurz, um auf ATAM einzugehen, aber ganz kompakt formuliert, definiert man via Szenarien Qualitätsanforderungen an den Sollzustand der Architektur und vergleicht diesen dann mit dem Istzustand des Systems. Das klingt erst einmal furchtbar aufwendig und teuer, daher wird die Investition oft gescheut. Jedoch stehen die Kosten aus unserer Erfahrung in keinem Verhältnis zu den Kosten (über den Lifecycle) einer nicht passenden Architektur oder Technologiewahl. Ähnlich wie bei Fahrzeugen sollte daher ein regelmäßiger TÜV der Architektur zu einer reifen Produktentwicklung dazugehören.

console.log(1);
httpClient.get<BOOK>('https://example.com/books/123').subscribe(
  (book) => console.log(`2 ${book}`));
httpClient.get<BOOK>('https://example.com/books/234').subscribe(
  (book) => console.log(`3 ${book}`);
console.log(4);

Dieser Quelltext schickt zwei GET Requests an einen fiktiven REST-Server ab, um Informationen über zwei Bücher zu bekommen. Sie werden kurzerhand auf der Entwicklerkonsole ausgegeben, zusammen mit einer durchnummerierten Zahl. Wir drucken also zunächst die Zahl 1, rufen dann das erste Buch ab, dann das zweite, und zum Schluss noch die Zahl 4.

In welcher Reihenfolge werden die Konsolenausgaben erscheinen? Die naive Antwort wäre 1, 2, 3, 4. Tatsächlich ist dieser Programmcode aber nichtdeterministisch. Es kommt darauf an, welcher REST-Call zuerst eine Antwort liefert. Es ist nur klar, dass die Zahl 4 vor der 2 und der 3 kommt.

Das ist auch gut so. Genau das soll erreicht werden. Die Informationen über beide Bücher werden gleichzeitig abgefragt, und als Sahnehäubchen ist die Anwendung auch während des Server-Requests bedienbar. In welcher Reihenfolge der Server die Requests beantwortet, ist nicht klar. Die Requests laufen eben wirklich parallel. Und es kommt noch besser: Egal ob der REST-Call 50 Millisekunden oder 50 Sekunden braucht, die Anwendung ist in der Zwischenzeit für weitere Benutzeraktivitäten verfügbar. Geschickt eingesetzt, fühlt sich die Anwendung dadurch sehr viel flüssiger an als beim traditionellen Ansatz, bei dem die Anwendung während der gesamten Wartezeit einfriert. Reactive Programming ist der natürliche Feind des Wait Cursors.

Nichtsdestotrotz gibt es einen gravierenden Nachteil. Das Programm wird nicht mehr Zeile für Zeile abgearbeitet. Stattdessen gibt es einen nichtlinearen Programmfluss. Die Callbacks stehen normalerweise oberhalb der Zeile, die zuerst abgearbeitet werden. Für Neueinsteiger kann das eine nicht zu unterschätzende Hürde sein. An den nichtlinearen Programmfluss muss man sich erst einmal gewöhnen. Und auch wenn die alten Hasen das selten zugeben: Wenn es komplizierter wird, verliert jeder von uns den Überblick. Manche früher, manche später. Wenn man beruflich programmiert, ist das ein wichtiger Punkt. Es gibt ein Mantra, das viele auswendig mitsprechen können: „Es ist nicht so wichtig, dass ihr euren eigenen Quelltext versteht. Es kommt darauf an, dass jeder eurer Kollegen euren Programmcode versteht“. Der Abstraktionslevel, der im aktuellen Team genau richtig ist, kann das nächste Team überfordern. Man muss sich also ständig nach den jeweiligen Teammitgliedern richten, und sie entweder coachen oder das eigene Abstraktionslevel an das Team anpassen.

Die Kunst des Beobachtens

Gehen wir einen Schritt weiter. Als Nächstes wollen wir erst die Liste aller Bücher holen und dann die Einzelheiten für jedes einzelne Buch. Das Prinzip wurde hier schon beschrieben. Es gibt eine subscribe()-Methode, die aufgerufen wird, wenn der Server Daten liefert. Was wäre also einfacher, als den Algorithmus wie in Listing 2 zu formulieren?

httpClient.get<Array<number>>('https://example.com/books')
    .subscribe((isbns) => {
  ids.forEach((isbns: number) => {
    httpClient.get<Book>(`https://example.com/books/${ isbns }`)
      .subscribe((book) => console.log(`${book}`));
  });
});

Das funktioniert, sollte aber besser so nicht umgesetzt werden. Ich habe bei diesem Minibeispiel mehrere Minuten damit verbracht, die Klammern richtig zu setzen. Und reale Projekte sind komplizierter, da ist die Klammersetzung das kleinste Problem. Es sollte also ein einfacher Quelltext geschrieben werden. RxJS bietet dafür ein reiches Repertoire an Operatoren. Meistens muss man überhaupt keinen Quelltext in die subscribe()-Methode schreiben. Ein Aufruf ohne Callback-Funktion reicht. Um die Idee zu illustrieren, beschränken wir uns zunächst auf den REST-Call, der die Liste der ISBNs (oder IDs) der Bücher liefert (Listing 3).

const promisedIsbns: Observable<Array<number>> =
  httpClient.get<Array<number>>('https://example.com/books');
 
promisedIsbns.pipe(
  tap((isbns: Array<number>) => console.log(isbns))
)
promisedIsbns.subscribe();

Listing 3 macht deutlich, dass http.get() ein Observable zurückliefert. Das ist eine sehr schöne Metapher: Man schickt den REST-Call los, und danach wird beobachtet, was der Server macht. Während des Beobachtens hat man Zeit für andere Dinge. Das ist ähnlich wie im wirklichen Leben, etwa beim Bügeln während des Fernsehens. Wenn es im Fernsehen spannend wird, wird das Bügeleisen kurz zur Seite gelegt, und während der Werbepause macht man mit dem Bügeln weiter. Als Nächstes definieren wir eine Pipeline. Auch das ist eine sehr schöne Metapher. Wenn man reaktiv programmiert, sollte man aufhören, Daten als etwas Statisches zu betrachten. Die Daten müssen als Datenstrom betrachtet werden. Wird die Sache weitergedacht, sind die Daten in der Datenbank so gut wie nie interessant. Sie werden immer nur dann interessant, wenn sie sich verändern. Dieser Gedanke führt dann zu Systemen, wie z. B. Apache Kafka. Das ist ein sehr mächtiger und fruchtbringender Paradigmenwechsel. Aber ich greife vor – noch sind wir ja im Frontend unterwegs.

Die Pipeline besteht aus einer Reihe von Operatoren, die der Reihe nach ausgeführt werden. In unserem Fall ist es der Operator tap, der auch Seiteneffektoperator genannt wird. Er lässt den Datenstrom unverändert passieren, erlaubt es aber, mit den Daten zu arbeiten. In diesem Fall wird die Liste der ISBNs einfach auf der Konsole ausgegeben. Als letztes Kommando kommt noch der Aufruf von subscribe(). RxJS evaluiert Ausdrücke erst dann, wenn sie gebraucht werden – das heißt, wenn sie jemand abonniert. Ohne den Aufruf von subscribe() startet der Algorithmus also nicht.

 

Wenn Beobachter Beobachter beobachten

Zurück zu unserer ursprünglichen Aufgabe. Es sollten die Information über alle Bücher gesammelt werden. Wir können also eine Pipeline mit den Operatoren map() und tap() aufbauen (Listing 4).

const ids: Observable<Array<number>> = httpClient.get<Array<number>>('https://example.com/books');
ids.pipe(
  map(
    (isbns: Array<number>) => ibns.map((isbn) => 
       httpClient.get<Book>(`https://example.com/books/${isbn}`)),
  tap((books: Array<Observable<Book>>) => console.log('Hilfe!'))
  )
);
ids.subscribe();

Tja, das hatten wir uns einfacher vorgestellt. Jetzt gibt es zwei Probleme. Zum einen enthält unsere Pipeline ein Array von ISBNs. Die Pipeline funktioniert aber am besten, wenn das Array in einen Strom von Einzelwerten aufgelöst wird. Das wird am doppelten map() deutlich. Und zum anderen liefert der verschachtelte Aufruf von http.get() ein Observable. Das Interessante ist aber der elementare Datentyp. Die Liste der Bücher wird benötigt, denn mit den Beobachtern der Bücher lässt sich nichts anfangen. Keine Panik, das Internet ist voll mit Ratschlägen, wie das Problem zu lösen ist. Und alle preisen euphorisch, dass die Lösung sehr einfach ist: „Du musst nur den forkJoin()-Operator und den flatMap()-Operator verwenden“. Das stimmt, weckt bei mir aber Zweifel, ob das hier der richtige Weg ist. RxJS hat eine sehr große Zahl von Operatoren – und meine Theorie ist, dass die meisten Operatoren Probleme lösen, die es ohne RxJS nicht gäbe. Die Umstellung auf die reaktive Programmierung stellt sich als verblüffend vertrackt heraus. Wohlgemerkt: Ich bin ein sehr großer Fan von RxJS. Es bietet ein sehr elegantes API für viele Anwendungsfälle. Ich bezweifele aber, dass Observables die richtigen Metaphern für REST-Calls sind. Ein Observable erlaubt es, einen potenziell unendlichen Strom von Daten zu beobachten. Observables sind die perfekte Lösung für WebSockets, Tastatureingaben oder auch für einen Apache-Kafka-Stream. Bei REST-Calls kommt die Metapher an ihre Grenzen. Ein REST-Call liefert maximal ein Ergebnis.

NEUES AUS DER JAVA-ENTERPRISE-WELT

Serverside Java-Track entdecken

 

Ein Internet voller Versprechungen

Den Entwicklern von RxJS ist das auch klar, und sie bieten eine passende Lösung dafür an. Sie hat, völlig zu Unrecht, einen schlechten Ruf in der Angular-Welt. Ihr könnt ein Observable in ein Promise umwandeln. Und wenn man das mit den JavaScript-Schlüsselwörtern async und await verknüpft, wird der Quelltext fast immer dramatisch einfacher (Listing 5).

const isbns: Array<number> = await firstValueFrom(
  httpClient.get<Array<number>>('https://example.com/books'));
const books: Array<Book> = await isbns.map(
  async (isbn) => await firstValueFrom(
    httpClient.get<Book>(`https://example.com/books/${isbn}`)));
books.forEach((book) => console.log(book));

Unser Ziel ist erreicht. Es ist wieder ein linearer Quelltext entstanden, der leicht verständlich ist. Aber da fehlt doch noch etwas. In der map()-Methode steht das Schlüsselwort await. Im Endeffekt gibt es eine for-Schleife, die den Programmfluss blockiert. Aber wir wollten doch eine non-blocking IO haben.

Blockadebrecher

Zum Glück legt JavaScript ohnehin sein Veto ein und bricht mit einer Fehlermeldung ab. Das await vor dem map() kann nicht funktionieren, weil map() kein einzelnes Promise, sondern gleich ein ganzes Array von Promises liefert (Listing 6). Also rufen wir die Funktion Promise.all() zu Hilfe.

const isbns: Array<number> = await firstValueFrom(
  httpClient.get<Array<number>>('https://example.com/books'));
const promisedBooks: Array<Promise<Book>> = 
  isbns.map((isbn) => firstValueFrom(
    httpClient.get<Book>(`https://example.com/books/${isbn}`)));
const books = await Promise.allSettled(promisedBooks);
books.forEach((book) => console.log(book));

Retries, Timeouts und take(1)

In meinem Projekt verwende ich diesen Ansatz sehr erfolgreich und mit großer Begeisterung. Umso erstaunlicher, dass der Rest der Angular-Welt die Möglichkeit, Observables in Promises zu verwandelt, ignoriert oder sogar leidenschaftlich bekämpft. Ein beliebtes Gegenargument ist, dass async/await den Vorteil der reaktiven Programmierung verspielt, indem es alles künstlich wieder linearisiert. Die Gefahr besteht, doch man kann, wie gerade gesehen, leicht gegensteuern. Hierfür packen wir einfach Promise.allSettled() in den Werkzeugkoffer. Auf der Suche nach Gegenargumenten bin ich noch darauf gestoßen, dass firstValueFrom() weder Retries noch Timeouts unterstützt. Das ist aber ein Scheinargument. Auch wenn sie in Promises verwandelt werden, man hat es immer noch mit Observables zu tun. Damit haben wir die gesamte Power von RxJS zur Verfügung (Listing 7).

const isbns: Array<number> = await firstValueFrom(
  httpClient.get<Array<number>>('https://example.com/books')
    .pipe(
      timeout(1000), 
      retry(5))
);

Wem das zu umständlich ist, der kann einfach eine Funktion definieren, die sowohl firstValueFrom() als auch Operatoren timeout() und retry() aufruft. Etwas unangenehmer ist eine Eigenschaft der Methode toPromise(), die in der älteren Version RxJS 6 anstelle von firstValueFrom() verwendet werden muss. toPromise() feuert erst, wenn das Observable das complete Event schickt. Bei REST-Calls ist das kein Problem, aber wenn man das Ergebnis in einem Subject zwischenspeichert, wird das complete Event niemals geschickt. Dieses Pattern ist umständlich, löst aber das Problem:

const isbns: Array<number> = 
      await books$.pipe(take(1)).toPromise();

 

Wenn das alles so einfach ist – warum verwendet es nicht jeder?

Jetzt wird es philosophisch. Observables erzwingen einen funktionalen Programmierstil, und das wiederum hat eine magische Anziehungskraft auf viele Entwickler. Funktional geschriebene Algorithmen bestehen im besten Fall aus einer langen Kette kurzer Zeilen. Jede Zeile kümmert sich um genau eine Aufgabe. Das fühlt sich einfach gut an und hinterlässt den Eindruck, den Algorithmus optimal strukturiert zu haben. Dieser Eindruck verfliegt schnell, wenn man denselben Algorithmus prozedural neu schreibt und ihn danebenlegt. Man könnte den prozeduralen Algorithmus genauso gut in viele kleine Einzeiler aufteilen. Oft genug finde ich den Quelltext wesentlich übersichtlicher. An dieser Stelle wird die Diskussion meistens sehr emotional. Viele Entwickler finden den prozeduralen Programmierstil unübersichtlich und schwer verständlich. Vielleicht macht es sich bemerkbar, dass ich den funktionalen Programmierstil erst sehr spät kennengelernt habe. Die Idee bei JavaScript – und auch bei Java – ist, einfach beides anzubieten. Es kann der gute alte prozedurale Programmierstil verwendet werden, wenn es passt, und wenn das funktionale Paradigma besser passt, verwendet man eben das. Es ist sogar so, dass meine TypeScript- und JavaScript-Programme deutlich mehr funktionale Anteile enthalten als meine Java-Programme, weil ich das funktionale API von JavaScript für wesentlich eleganter halte. Mein Verdacht ist, dass es einfach eine Frage der Übung, der Mode und des Geschmacks ist. Bei meinen Vorträgen, in denen ich das async/await-Pattern propagiere, bekomme ich fast immer Gegenwind. Wenn ich dann zurückfrage, woher der Widerstand kommt, höre ich selten substanzielle Argumente. Diese Argumente gibt es allerdings durchaus. Lest euch nur mal das leidenschaftliche und gut begründete Plädoyer gegen async/await von Daniel Caldas [2] durch. Das sind aber selten die Argumente, die mir spontan genannt werden. Ich befürchte, das Hauptproblem ist, dass alle bei ihrer Einarbeitung in Angular verinnerlicht haben, dass RxJS und Observables das Mittel der Wahl und ein großer Fortschritt sind. Was auch richtig ist, nur setzt sich dadurch im Unterbewusstsein die Überzeugung fest, dass async/await schlecht ist. Der Witz ist nur, dass sich das Angular-Team zu einem denkbar ungünstigen Zeitpunkt für Observables entschieden hat. AngularJS 1.x hatte noch Promises verwendet, allerdings mit der alten umständlichen Syntax. Wenn man diesen Programmierstil mit dem Programmierstil von RxJS vergleicht, ist beides in etwa gleich unhandlich, aber RxJS bietet sehr viel mehr Möglichkeiten als die native Verwendung von Promises. Die Abkehr von Promises war folgerichtig. Rund ein Jahr später wurde das neue Schlüsselwortpaar async/await in JavaScript eingeführt. Ob sich das Angular-Team für RxJS entschieden hätte, wenn async/await schon Mainstream gewesen wäre?

Wie sieht das in Java aus? Dieser Artikel basiert auf einem Vortrag für Angular-Entwickler. Als ich den Vortrag dem Java Magazin angeboten hatte, dachte ich mir, dass es interessant sein könnte, die Erkenntnisse auf Java zu übertragen. Leider war das nicht so fruchtbringend wie erhofft. In erster Linie habe ich beim Recherchieren festgestellt, dass Java eben anders tickt als JavaScript.

Fangen wir mit dem Offensichtlichen an. In Java gibt es kein async/await. Stattdessen gibt es in den Java-Frameworks, die ich mir angeschaut habe, den Operator block(), der aus einem Mono wieder den eigentlichen Wert machen kann. Analog gibt es bei Java den Operator collectList() für ein Flux. Was sich hinter den Begriffen Mono und Flux verbirgt, erzähle ich euch gleich. Die Operatoren block()und collectList() blockieren den Programmfluss. Das klingt schlimmer als es ist: Im Gegensatz zu JavaScript versteht sich Java auf die Kunst des Multi-Threadings. Man blockiert sich also nur selbst, aber nicht gleich den ganzen Server. Es könnte auch sein, dass das Framework gezwungen ist, von einer optimalen Multi-Threading-Strategie auf eine weniger effiziente Strategie umzuschalten. Aber das ist auch alles. Das Rezept, das ich oben vorgestellt habe, funktioniert in Java also nicht. Man muss sich auf die funktionale Programmierung einstellen. Übertreiben sollte man es aber nicht. Faustregel: Wenn man ein if-Statement braucht, extrahiert man den Code einfach in eine Methode. Ich hatte schon kurz Flux und Mono (oder Uni in Quarkus) angesprochen. Das ist ein interessanter Unterschied zu RxJS. Quarkus und Spring Reactive unterscheiden zwischen Datenströmen, die viele Ergebnisse liefern (Flux) und einfachen Requests, die nur ein Ergebnis liefern (Mono bzw. Uni). Als ich das gelesen habe, fand ich das verblüffend. In der Angular-Welt gibt es eine lebhafte Diskussion, ob Promises oder Observables besser sind, und gleichzeitig unterstützen die Java-Frameworks kurzerhand beides gleichzeitig. Uni bzw. Mono entsprechen in etwa dem Promise und Flux entspricht dem Observable.

 

Zustände

Möglicherweise liegt der Hauptunterschied zwischen der reaktiven Programmierung mit Angular und mit Java ganz woanders. Heutzutage entwickeln die meisten Java-Entwickler REST-Services. Diese sind stateless. Alles ist im Fluss. Man muss sich nicht um statische Daten kümmern. Der Datenstrom, den die reaktiven APIs liefern, reicht vollkommen. In Angular hingegen dreht sich am Ende des Tages alles um den State der Anwendung. Das ist ein Aspekt, den ich in diesem Artikel noch nicht gezeigt hatte und der Angular-Entwickler – und vor allem die Neueinsteiger unter ihnen – immer wieder vor Probleme stellt. Es gehört zu den Best Practices der Angular-Welt, möglichst den kompletten State der Anwendung in Observables zu speichern. So ein Observable ist jedoch nur dafür gedacht, Änderungen des Zustands zu kommunizieren, nicht aber, den aktuellen Zustand zu speichern. Wenn man den aktuellen Zustand braucht, man erst einmal schauen, wo man ihn herbekommt. Für Java-Entwickler entfällt diese Notwendigkeit von vorneherein. Das macht das Leben für die Java-Entwickler leichter. Sie müssen viel seltener zwischen dem reaktiven und dem synchronen Code wechseln. Als Gemeinsamkeit gibt es natürlich die Notwendigkeit, Methoden wie flatMap() zu verwenden. Die Quarkus Sandbox [3] von Hantsy Bai enthält ein paar Beispiele dafür. Und was ist mit Annotationen? Ein komplett anderer Weg, reaktive Programmierung zu vereinfachen, ist mir bei Apache Kafka aufgefallen. Schaut euch mal das Tutorial von Baeldung [4] an. Dort wird eine Annotation verwendet, um eine Methode aufzurufen, wenn der Server ein Ergebnis liefert (Listing 8).

@KafkaListener(topics = "topicName", groupId = "foo") 
public void listenGroupFoo(String message) {
   System.out.println("Received Message in group foo: " + message); 
}

Das ist das Reaktive Pattern konsequent zu Ende gedacht. Die Kafka-Topics bieten eine sehr lose Kopplung zwischen Nachrichtenquelle und Nachrichtenempfänger. Das funktioniert natürlich nicht immer. Im ursprünglichen Beispiel, dem Select-Statement oder dem REST-Call, ist das zu unpraktisch. Es würde auch ein falsches Signal aussenden. Eine Angular-Anwendung ist keineswegs lose mit dem Backend gekoppelt. Wenn das Backend nicht verfügbar ist, ist ein Angular-Frontend nutzlos.

Aber lassen wir das mal kurz außer Acht. Dann kann man sehen, dass Annotationen ein schönes Stilmittel sind, um Algorithmen reaktiv zu implementieren. Der Clou ist, dass es hier nicht um Observables, Monos oder Fluxes geht. Die Parameterliste und der Rückgabetyp der Funktion sind die reinen Datentypen. Das vereinfacht die Programmierung enorm. Das Framework übernimmt die komplette Abstraktion. Wir brauchen nicht zu wissen, dass ein @KafkaListener im Grunde genommen auch nichts anderes ist als ein Observable.

Klingt das verrückt? Dann schaut es euch noch einmal genau an. In Angular hättet ihr vermutlich eine Methode listenToKafka(topic, groupId), die ein Observable zurückliefert. Analog hättet ihr in Spring Reactive eine Methode listenToKafka(topic, groupId), die ein Flux zurückliefert. In beiden Fällen kann in der subscribe()-Methode definiert werden, was mit den Daten passieren würde. Und dieser Algorithmus wiederum ist exakt der Inhalt der Methode listenToFoo() aus Listing 7.

Stay tuned

Regelmäßig News zur Konferenz und der Java-Community erhalten

 

Resumée

Das war ein wilder Ritt durch die Untiefen der reaktiven Programmierung. Die Kernbotschaft, die ich vermitteln will, ist aber ziemlich einfach. Reactive Programming ist etwas, mit dem man sich beschäftigen sollte. Die Anwender werden es euch danken. Bessere Performance kommt immer gut an.

Das heißt aber nicht, dass ihr euch blindlings in das Abenteuer stürzen solltet. Oder, doch, das solltet ihr schon. Mut zur Lücke ist immer gut. Irgendwo muss man ja anfangen. Ihr werdet dann aber schnell feststellen, dass reaktive Programmierung ihre Tücken hat. An diesem Punkt angekommen, ist der richtige Zeitpunkt, diesen Artikel (noch einmal) zu lesen. Es gibt Strategien, reaktive Programmierung beherrschbar zu machen. Im Internet wird meistens empfohlen, sich mit Haut und Haaren darauf einzulassen. Das funktioniert für viele Teams ziemlich gut. Man kann aber auch versuchen, die Komplexität zu reduzieren. Bei Angular ist async-await das Mittel der Wahl. Bei Java gibt es diese Möglichkeit nicht. Sie wird auch nicht benötigt. Beim reaktiven Programmieren wird es meistens erst dann schwierig, wenn auf den aktuellen Zustand der Anwendung zugegriffen werden soll. Solange zustandslose REST Services entwickelt werden, habt ihr diese Anforderung nicht. Und falls doch, hat auch Java noch das eine oder andere As im Ärmel. Apache Kafka zeigt, wie mit Annotationen den Abstraktionsgrad der Algorithmen deutlich reduziert werden kann.

 

Links & Literatur

[1] https://www.reactivemanifesto.org

[2] https://goodguydaniel.com/blog/why-reactive-programming

[3] https://hantsy.github.io/quarkus-sandbox/reactive.html

[4] https://www.baeldung.com/spring-kafka

The post Die Untiefen reaktiver Programmierung appeared first on JAX.

]]>