Architecture & Design - JAX https://jax.de/blog/software-architecture-design/ Java, Architecture & Software Innovation Wed, 11 Dec 2024 11:09:35 +0000 de-DE hourly 1 https://wordpress.org/?v=6.5.2 Architektur ist nicht Kubernetes: Diana Montalions Vision für Systemarchitektur auf der W-JAX https://jax.de/blog/moderne-systemarchitektur-systems-thinking-w-jax/ Tue, 19 Nov 2024 15:04:34 +0000 https://jax.de/?p=106834 In ihrer mitreißenden Keynote auf der W-JAX in München stellt Diana Montalion eine neue Sichtweise auf moderne Systemarchitektur vor. Sie zeigt, dass echte Innovation weit über Tools wie Kubernetes hinausgeht. Stattdessen rückt sie Systems Thinking und die kunstvolle, flexible Gestaltung von Beziehungen zwischen Systemkomponenten in den Fokus. Mit ihrer Vision macht sie deutlich, dass moderne Software-Architektur mehr ist als Technik – sie ist eine soziotechnische Disziplin, die technisches Know-how und menschliche Zusammenarbeit nahtlos miteinander vereint.

The post Architektur ist nicht Kubernetes: Diana Montalions Vision für Systemarchitektur auf der W-JAX appeared first on JAX.

]]>

Architektur neu denken!

Eines der kontroversesten Wörter in der heutigen Technologiekultur ist „Architekt“. Was bedeutet Architektur wirklich? Diese Frage sorgt immer wieder für hitzige Diskussionen und Missverständnisse. In ihrer Keynote auf der W-JAX in München wirft die renommierte Expertin Diana Montalion einen frischen Blick auf das Thema. Sie verdeutlicht, dass Architektur weit mehr ist als die Implementierung von Tools wie Kubernetes. Stattdessen geht es um das Entwerfen von Beziehungen zwischen Systemkomponenten und die Fähigkeit, diese Muster flexibel an wechselnde Bedingungen anzupassen. Dies erfordert fundierte mentale Modelle und kollaboratives Arbeiten, um diese weiterzuentwickeln.

Diana Montalion, Autorin des O’Reilly-Buchs Learning Systems Thinking: Essential Nonlinear Skills & Practices for Software Professionals, blickt auf über 18 Jahre Erfahrung in der Software-Entwicklung und -Architektur zurück. Sie hat unter anderem für Organisationen wie Stanford, die Gates Foundation und The Economist gearbeitet und war Principal Systems Architect für die Wikimedia Foundation.

In ihrer Keynote betont Diana, dass moderne Architektur eine soziotechnische Disziplin ist – eine Mischung aus sozialen und technischen Fähigkeiten. Sie beschreibt, wie gutes Architektendenken effektives Systemdenken strukturiert und erklärt die fünf wesentlichen Qualitäten, die einen guten Architekten von einem großartigen unterscheiden.

Die wichtigsten Take-aways

  • Architekturdenken: Systeme sollten nicht isoliert betrachtet werden. Es ist entscheidend, das gesamte Ökosystem zu berücksichtigen und zu verstehen, wie verschiedene Komponenten zusammenarbeiten, um echte Effizienz und Effektivität zu erreichen.
  • Silos aufbrechen: Abteilungsübergreifende Zusammenarbeit ist notwendig, um die Integration verschiedener Technologien und Teams zu verbessern. Teams müssen effektiv kommunizieren und kooperieren können.
  • Kultureller Wandel: Ein grundlegender Kulturwandel in Unternehmen ist erforderlich, um moderne Architekturen erfolgreich zu implementieren. Vertrauen und Autonomie der Teams sowie die Bereitschaft, neue Wege zu gehen, sind hierbei essenziell.
  • Einsatz geeigneter Werkzeuge: Technologien wie Kubernetes können hilfreich sein, dürfen jedoch nicht das zentrale Element des Architekturdenkens werden. Sinnvolle und ganzheitliche Designansätze, die weit über spezifische Tools hinausgehen, sind entscheidend.

The post Architektur ist nicht Kubernetes: Diana Montalions Vision für Systemarchitektur auf der W-JAX appeared first on JAX.

]]>
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.

]]>
Modularisierung und kognitive Psychologie https://jax.de/blog/modularisierung-und-kognitive-psychologie/ Mon, 06 Dec 2021 09:56:02 +0000 https://jax.de/?p=85348 Über Modularisierung wird viel und häufig gesprochen, aber die Gesprächspartner:innen stellen nach einiger Zeit fest, dass sie nicht dasselbe meinen. Die Informatik hat uns in den letzten fünfzig Jahren zwar eine Reihe guter Erklärungen geliefert, was Modularisierung ausmacht – aber reicht das, um wirklich zu den gleichen Schlüssen und Argumenten zu kommen?

The post Modularisierung und kognitive Psychologie appeared first on JAX.

]]>
Die wirkliche Begründung, warum Modularisierung so wichtig ist, habe ich erst bei der Beschäftigung mit der kognitiven Psychologie gefunden. In diesem Artikel werde ich deshalb Modularisierung und kognitive Psychologie zusammenführen und Ihnen die entscheidenden Argumente an die Hand geben, warum Modularisierung uns bei der Softwareentwicklung tatsächlich hilft.

Parnas hat immer noch Recht!

In den letzten 20 bis 30 Jahren haben wir viele sehr große Softwaresysteme in Java, C++ und auch in C# und PHP entwickelt. Diese Systeme enthalten sehr viel Business Value und frustrieren Ihre Entwicklungsteams, weil sie nur noch mit immer mehr Aufwand weiterentwickelt werden können. Das inzwischen 50 Jahre alte Rezept von David Parnas, um einen Ausweg aus dieser Situation zu finden, heißt Modularisierung. Haben wir eine modulare Architektur, so heißt es, dann haben wir unabhängige Einheiten, die von kleinen Teams verstanden und zügig weiterentwickelt werden können. Zusätzlich bietet eine modulare Architektur die Möglichkeit, die einzelnen Module getrennt zu deployen, sodass unsere Architektur skalierbar wird. Genau diese Argumente tauschen wir in Diskussionen unter Architekt:innen und Entwickler:innen aus und sind uns doch immer wieder nicht einig, was wir genau mit Modularität, Modulen, modularen Architekturen und Modularisierung meinen.

In meiner Doktorarbeit habe ich mich mit der Frage beschäftigt, wie man Softwaresysteme strukturieren muss, damit Menschen bzw. unser menschliches Gehirn sich darin gut zurechtfinden. Das ist besonders deswegen wichtig, weil Entwicklungsteams einen Großteil ihrer Zeit mit dem Lesen und Verstehen von vorhandenem Code verbringen. Erfreulicherweise hat die kognitive Psychologie mehrere Mechanismen identifiziert, mit dem unser Gehirn komplexe Strukturen erfasst. Einer von ihnen liefert eine perfekte Erklärung für Modularisierung: Er heißt Chunking. Auf der Basis von Chunking können wir Modularisierung sehr viel besser beschreiben als durch Entwurfsprinzipien und Heuristiken, die sonst oft als Begründungen herangezogen werden [1]. Zusätzlich liefert uns die kognitive Psychologie zwei weitere Mechanismen: Hierarchisierung und Schemata, die weitere wichtige Hinweise für Modularisierung mitbringen.

Stay tuned

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

 

Chunking ➔ Modularisierung

Damit Menschen in der Menge der Informationen, mit denen sie konfrontiert sind, zurechtkommen, müssen sie auswählen und Teilinformationen zu größeren Einheiten gruppieren. Dieses Bilden von höherwertigen Abstraktionen, die immer weiter zusammengefasst werden, nennt man in der kognitiven Psychologie Chunking (Abb. 1). Dadurch, dass Teilinformationen als höherwertige Wissenseinheiten abgespeichert werden, wird das Kurzzeitgedächtnis entlastet und weitere Informationen können aufgenommen werden.

Abb. 1: Chunking

Als Beispiel soll hier eine Person dienen, die das erste Mal mit einem Telegrafen arbeitet. Sie hört die übertragenen Morsezeichen als kurze und lange Töne und verarbeitet sie am Anfang als getrennte Wissenseinheiten. Nach einiger Zeit wird sie in der Lage sein, die Töne zu Buchstaben – und damit zu neuen Wissenseinheiten – zusammenzufassen, sodass sie schneller verstehen kann, was übermittelt wird. Einige Zeit später werden aus einzelnen Buchstaben Wörter, die wiederum größere Wissenseinheiten darstellen, und schließlich ganze Sätze.

Entwickler:innen und Architekt:innen wenden Chunking automatisch an, wenn sie sich neue Software erschließen. Der Programmtext wird im Detail gelesen, und die gelesenen Zeilen werden zu Wissenseinheiten gruppiert und so behalten. Schritt für Schritt werden die Wissenseinheiten immer weiter zusammengefasst, bis ein Verständnis des Programmtexts und der Strukturen, in die er eingebettet ist, erreicht ist.

Diese Herangehensweise an Programme wird als Bottom-up-Programmverstehen bezeichnet und von Entwicklungsteams in der Regel angewendet, wenn ihnen ein Softwaresystem und sein Anwendungsgebiet unbekannt sind und sie sich das Verständnis erst erarbeiten müssen. Bei Kenntnis des Anwendungsgebiets und des Softwaresystems wird von Entwicklungsteams eher Top-down-Programmverstehen eingesetzt. Top-down-Programmverstehen bedient sich hauptsächlich der beiden strukturbildenden Prozesse Bildung von Hierarchien und Aufbau von Schemata, die in den folgenden Abschnitten eingeführt werden.

Eine andere Form des Chunking kann man bei Expert:innen beobachten. Sie speichern die neuen Wissenseinheiten nicht einzeln im Kurzzeitgedächtnis ab, sondern fassen sie direkt durch Aktivierung bereits gespeicherter Wissenseinheiten zusammen. Wissenseinheiten können allerdings nur aus anderen Wissenseinheiten gebildet werden, die für die Versuchsperson sinnvoll zusammengehören. Bei Experimenten mit Experten eines Wissensgebiets und Anfängern wurden den beiden Gruppen Wortgruppen aus dem Wissensgebiet des Experten präsentiert. Die Experten konnten sich fünfmal so viele Begriffe merken wie die Anfänger. Allerdings nur, wenn die Wortgruppen sinnvoll zusammengehörige Begriffe enthielten.

An Entwickler:innen und Architekt:innen konnten diese Erkenntnisse ebenfalls nachgewiesen werden. Chunking funktioniert auch bei Softwaresystemen nur dann, wenn die Struktur des Softwaresystems sinnvoll zusammenhängende Einheiten darstellt. Programmeinheiten, die beliebige Operationen oder Funktionen zusammenfassen, sodass für die Entwicklungsteams nicht erkennbar ist, warum sie zusammengehören, erleichtern das Chunking nicht. Der entscheidende Punkt dabei ist, dass Chunking nur dann angewendet werden kann, wenn sinnvolle Zusammenhänge zwischen den Chunks existieren.

DIE KUNST DER SOTWARE-ARCHITEKTUR

Architecture & Design-Track entdecken

 

Module als zusammenhängende Einheiten

Für Modularisierung und modulare Architekturen ist es also essenziell, dass sie aus Bausteinen wie Klassen, Komponenten, Modulen, Schichten bestehen, die in sinnvoll zusammenhängenden Elementen gruppiert sind. In der Informatik gibt es eine Reihe von Entwurfsprinzipien, die diese Forderung nach zusammenhängenden Einheiten einlösen wollen:

  • Information Hiding (Geheimnisprinzip): David Parnas forderte 1972 als Erster, dass ein Modul genau eine Entwurfsentscheidung verbergen soll und die Datenstruktur zu dieser Entwurfsentscheidung in dem Modul gekapselt sein sollte (Kapselung und Lokalität). Parnas gab diesem Grundsatz den Namen Information Hiding [2].

  • Separation of Concerns: Dijkstra schrieb in seinem auch heute noch lesenswerten Artikel mit dem Titel „A Discipline of Programming“ [3], dass verschiedene Teile einer größeren Aufgabe möglichst in verschiedenen Elementen der Lösung repräsentiert werden sollten. Hier geht es also um das Zerlegen von zu großen Wissenseinheiten mit mehreren Aufgaben. In der Refactoring-Bewegung sind solche Einheiten mit zu vielen Verantwortlichkeiten als Code Smell unter dem Namen God Class wieder aufgetaucht.

  • Kohäsion: In den 1970er-Jahren arbeitete Myers seine Ideen über den Entwurf aus und führte das Maß Kohäsion ein, um den Zusammenhalt innerhalb von Modulen zu bewerten [4]. Coad und Yourdon erweiterten das Konzept für die Objektorientierung [5].

  • Responsibility-driven Design (Entwurf nach Zuständigkeit): In die gleiche Richtung wie das Geheimnisprinzip und die Kohäsion zielt Rebecca Wirfs-Brocks Entwurfsheuristik, Klassen nach Zuständigkeiten zu entwerfen: Eine Klasse ist eine Entwurfseinheit, die genau eine Verantwortung erfüllen und damit nur eine Rolle in sich vereinigen sollte [6].

  • Single Responsibility Principle (SRP): Als Erstes legt Robert Martin in seinen SOLID-Prinzipien fest, dass jede Klasse nur eine fest definierte Aufgabe erfüllen soll. In einer Klasse sollten lediglich Funktionen vorhanden sein, die direkt zur Erfüllung dieser Aufgabe beitragen. Effekt dieser Konzentration auf eine Aufgabe ist, dass es nie mehr als einen Grund geben sollte, eine Klasse zu ändern. Dafür ergänzt Robert Martin auf Architekturebene das Common Closure Principle. Klassen sollen in ihren übergeordneten Bausteinen lokal sein, sodass Veränderungen immer alle oder keine Klassen betreffen [7].

All diese Prinzipien wollen Chunking durch den inneren Zusammenhalt der Einheiten fördern. Modularität hat aber noch mehr zu bieten. Ein Modul soll nach Parnas außerdem mit seiner Schnittstelle eine Kapsel für die innere Implementierung bilden.

 

Module mit modularen Schnittstellen

Durch Schnittstellen kann Chunking erheblich unterstützt werden, wenn die Schnittstellen – welche Überraschung – sinnvolle Einheiten bilden. Die für Chunking benötigte Wissenseinheit kann in der Schnittstelle eines Moduls so gut vorbereitet werden, dass die Entwicklungsteams sich den Chunk nicht mehr durch die Analyse des Inneren des Moduls zusammensammeln müssen.

Eine gute zusammenhängende Schnittstelle entsteht, wenn man die Prinzipien aus dem letzten Abschnitt nicht nur beim Entwurf des Inneren eines Moduls anwendet, sondern auch für seine Schnittstelle [1], [7], [8]:

  • Explizite und kapselnde Schnittstelle: Module sollten ihre Schnittstellen explizit machen, d. h., die Aufgabe des Moduls muss klar erkennbar sein, und von der internen Implementierung wird abstrahiert.

  • Delegierende Schnittstellen und das Law of Demeter: Da Schnittstellen Kapseln sind, sollten die in ihnen angebotenen Dienste so gestaltet sein, dass Delegation möglich wird. Echte Delegation entsteht, wenn die Dienste an einer Schnittstelle Aufgaben komplett übernehmen. Dienste, die dem Aufrufer Interna zurückliefern, an denen der Aufrufer weitere Aufrufe ausführen muss, um zu seinem Ziel zu gelangen, verletzen das Law of Demeter.

  • Explizite Abhängigkeiten: An der Schnittstelle eines Moduls sollte direkt erkennbar sein, mit welchen anderen Modulen es kommuniziert. Erfüllt man diese Forderung, dann wissen Entwicklungsteams, ohne in die Implementierung zu schauen, welche anderen Module sie verstehen oder erzeugen muss, um mit dem Modul zu arbeiten. Dependency Injection passt direkt zu diesem Grundprinzip, denn es führt dazu, dass alle Abhängigkeiten über die Schnittstelle in ein Modul injiziert werden.

All diese Prinzipien haben als Ziel, dass Schnittstellen das Chunking unterstützen. Werden sie eingehalten, so sind Schnittstellen als eine Wissenseinheit schneller zu verarbeiten. Werden nun auch noch die Grundprinzipien der Kopplung beachtet, so haben wir für das Chunking beim Programmverstehen viel gewonnen.

Module mit loser Kopplung

Um ein Modul einer Architektur zu verstehen und ändern zu können, müssen sich Entwicklungsteams einen Überblick über das zu ändernde Modul selbst und seine benachbarten Module verschaffen. Wichtig sind dafür alle Module, mit denen das Modul zusammenarbeitet. Je mehr Abhängigkeiten es von einem Modul zum anderen gibt (Abb. 2), umso schwieriger wird es, die einzelnen Beteiligten mit der begrenzten Kapazität des Kurzzeitgedächtnisses zu analysieren und passende Wissenseinheiten zu bilden. Chunking fällt deutlich leichter, wenn weniger Module und Abhängigkeiten im Spiel sind.

Abb. 2: Stark gekoppelte Klassen (links) oder Packages/Directories (rechts)

Lose Kopplung ist das Prinzip in der Informatik, das an diesem Punkt ansetzt [9], [10], [11]. Kopplung bezeichnet den Grad der Abhängigkeit zwischen den Modulen eines Softwaresystems. Je mehr Abhängigkeiten in einem System existieren, desto stärker ist die Kopplung. Sind die Module eines Systems nach den Prinzipien der letzten beiden Abschnitte zu Einheiten und Schnittstellen entwickelt worden, so sollte das System automatisch aus lose gekoppelten Modulen bestehen. Ein Modul, das eine zusammenhängende Aufgabe erledigt, wird dazu weniger andere Module brauchen als ein Modul, das viele verschiedene Aufgaben durchführt. Ist die Schnittstelle nach dem Law of Demeter delegierend angelegt, dann braucht der Aufrufer nur diese Schnittstelle. Er muss sich nicht von Schnittstelle zu Schnittstelle weiterhangeln, um schließlich durch viel zusätzliche Kopplung seine Aufgabe abzuschließen.

Chunking hat uns bis hierhin geholfen, Modularisierung für das Innere und das Äußere eines Moduls und für seine Beziehung zu betrachten. Spannenderweise spielt auch der nächste kognitive Mechanismus in das Verständnis von Modularisierung hinein.

Modularisierung durch Muster

Der effizienteste kognitive Mechanismus, den Menschen einsetzen, um komplexe Zusammenhänge zu strukturieren, sind sogenannte Schemata. Unter einem Schema werden Konzepte verstanden, die aus einer Kombination von abstraktem und konkretem Wissen bestehen. Ein Schema besteht auf der abstrakten Ebene aus den typischen Eigenschaften der von ihm schematisch abgebildeten Zusammenhänge. Auf der konkreten Ebene beinhaltet ein Schema eine Reihe von Exemplaren, die prototypische Ausprägungen des Schemas darstellen. Jeder von uns hat beispielsweise ein Lehrerschema, das abstrakte Eigenschaften von Lehrer:innen beschreibt und als prototypische Ausprägungen Abbilder unserer eigenen Lehrer:innen umfasst.

Haben wir für einen Zusammenhang in unserem Leben ein Schema, können wir die Fragen und Probleme, mit denen wir uns gerade beschäftigen, sehr viel schneller verarbeiten als ohne Schema. Schauen wir uns ein Beispiel an: Bei einem Experiment wurden Schachmeister:innen und Schachanfänger:innen für ca. fünf Sekunden Spielstellungen auf einem Schachbrett gezeigt. Handelte es sich um eine sinnvolle Aufstellung der Figuren, so waren die Schachmeister:innen in der Lage, die Positionen von mehr als zwanzig Figuren zu rekonstruieren. Sie sahen Muster von ihnen bekannten Aufstellungen und speicherten sie in ihrem Kurzzeitgedächtnis. Die schwächeren Spieler:innen hingegen konnten nur die Position von vier oder fünf Figuren wiedergeben. Die Anfänger:innen mussten sich die Position der Schachfiguren einzeln merken. Wurden die Figuren den Schachexpert:innen und Schachlaien allerdings mit einer zufälligen Verteilung auf dem Schachbrett präsentiert, so waren die Schachmeister:innen nicht mehr im Vorteil. Sie konnten keine Schemata einsetzen und sich so die für sie sinnlose Verteilung der Figuren nicht besser merken.

 

Die in der Softwareentwicklung vielfältig eingesetzten Entwurfs- und Architekturmuster nutzen die Stärke des menschlichen Gehirns, mit Schemata zu arbeiten. Haben Entwickler:innen und Architekt:innen bereits mit einem Muster gearbeitet und daraus ein Schema gebildet, so können sie Programmtexte und Strukturen schneller erkennen und verstehen, die nach diesen Mustern gestaltet sind. Der Aufbau von Schemata liefert für das Verständnis von komplexen Strukturen also entscheidende Geschwindigkeitsvorteile. Das ist auch der Grund, warum Muster in der Softwareentwicklung bereits vor Jahren Einzug gefunden haben.

In Abbildung 3 sieht man ein anonymisiertes Tafelbild, das ich mit einem Team entwickelt habe, um seine Muster aufzunehmen. Auf der rechten Seite von Abbildung 3 ist der Source Code im Architekturanalysetool Sotograph in diese Musterkategorien eingeteilt und man sieht sehr viele grüne und einige wenige rote Beziehungen. Die roten Beziehungen gehen von unten nach oben gegen die durch die Muster entstehende Schichtung. Die geringe Anzahl der roten Beziehungen ist ein sehr gutes Ergebnis und zeugt davon, dass das Entwicklungsteam seine Muster sehr konsistent einsetzt.

Abb. 3: Muster auf Klassenebene = Mustersprache

Spannend ist außerdem, welchen Anteil des Source Codes man Mustern zuordnen kann und wie viele Muster das System schlussendlich enthält. Lassen sich 80 Prozent oder mehr des Source Codes Mustern zuordnen, dann spreche ich davon, dass dieses System eine Mustersprache hat. Hier hat das Entwicklungsteam eine eigene Sprache erschaffen, um sich die Diskussion über seine Architektur zu erleichtern.

Die Verwendung von Mustern im Source Code ist für eine modulare Architektur besonders wichtig. Wir erinnern uns: Für das Chunking war es entscheidend, dass wir sinnvoll zusammenhängende Einheiten vorfinden, die eine gemeinsame Aufgabe haben. Wie, wenn nicht durch Muster, lassen sich die Aufgaben von Modulen beschreiben? Modularisierung wird durch den umfassenden Einsatz von Mustern vertieft und verbessert, wenn für die jeweiligen Module erkennbar ist, zu welchem Muster sie gehören, und die Muster konsistent eingesetzt werden.

Hierarchisierung ➔ Modularisierung

Der dritte kognitive Mechanismus, die Hierarchisierung, spielt beim Wahrnehmen und Verstehen von komplexen Strukturen und beim Abspeichern von Wissen ebenfalls eine wichtige Rolle. Menschen können Wissen dann gut aufnehmen, es wiedergeben und sich darin zurechtfinden, wenn es in hierarchischen Strukturen vorliegt. Untersuchungen zum Lernen von zusammengehörenden Wortkategorien, zur Organisation von Lernmaterialien, zum Textverstehen, zur Textanalyse und zur Textwiedergabe haben gezeigt, dass Hierarchien vorteilhaft sind. Bei der Reproduktion von Begriffslisten und Texten war die Gedächtnisleistung der Versuchspersonen deutlich höher, wenn ihnen Entscheidungsbäume mit kategorialer Unterordnung angeboten wurden. Lerninhalte wurden von den Versuchspersonen mit Hilfe von hierarchischen Kapitelstrukturen oder Gedankenkarten deutlich schneller gelernt. Lag keine hierarchische Struktur vor, so bemühten sich die Versuchspersonen, den Text selbstständig hierarchisch anzuordnen. Die kognitive Psychologie zieht aus diesen Untersuchungen die Konsequenz, dass hierarchisch geordnete Inhalte für Menschen leichter zu erlernen und zu verarbeiten sind und dass aus einer hierarchischen Struktur effizienter Inhalte abgerufen werden können.

Die Bildung von Hierarchien wird in Programmiersprachen bei den Enthalten-Sein-Beziehungen unterstützt: Klassen sind in Packages oder Directories, Packages/Directories wiederum in Packages/Directories und schließlich in Projekten bzw. Modulen und Build-Artefakten enthalten. Diese Hierarchien passen zu unseren kognitiven Mechanismen. Sind die Hierarchien an die Muster der Architektur angelehnt, so unterstützen sie uns nicht nur durch ihre hierarchische Strukturierung, sondern sogar auch noch durch Architekturmuster.

Schauen wir uns dazu einmal ein schlechtes und ein gutes Beispiel an: Stellen wir uns vor, ein Team hat für sein System festgelegt, dass es aus vier Modulen bestehen soll, die dann wiederum einige Submodule enthalten sollen (Abb. 4).

Abb. 4: Architektur mit vier Modulen

Diese Struktur gibt für das Entwicklungsteam ein Architekturmuster aus vier Modulen auf der obersten Ebene vor, in denen jeweils weitere Module enthalten sind. Stellen wir uns nun weiter vor, dass dieses System in Java implementiert und aufgrund seiner Größe in einem einzigen Eclipse-Projekt organisiert ist. In diesem Fall würde man erwarten, dass dieses Architekturmuster aus vier Modulen mit Submodulen sich im Package-Baum des Systems wiederfinden sollte.

In Abbildung 5 sieht man den anonymisierten Package-Baum eines Java-Systems, für das das Entwicklungsteam genau diese Aussage gemacht hatte: „Vier Module mit Submodulen, das ist unsere Architektur!“. In der Darstellung in Abbildung 5 sieht man Packages und Pfeile. Die Pfeile gehen jeweils vom übergeordneten Package zu seinen Kindern.

Abb. 5: Das geplante Architekturmuster ist schlecht umgesetzt

Tatsächlich findet man die vier Module im Package-Baum. In Abbildung 5 sind sie in den vier Farben markiert, die die Module in Abbildung 4 haben (grün, orange, lila und blau). Allerdings sind zwei der Module über den Package-Baum verteilt und ihre Submodule sind zum Teil sogar unter fremden Ober-Packages einsortiert. Diese Umsetzung im Package-Baum ist nicht konsistent zu dem von der Architektur vorgegebenen Muster. Sie führt bei Entwicklern und Architekten zu Verwirrung. Das Einführen von jeweils einem Package-Root-Knoten für die orange- und die lilafarbene Komponente würde hier Abhilfe schaffen.

Eine bessere Abbildung des Architekturmusters auf den Package-Baum sieht man in Abbildung 6. Bei diesem System ist das Architekturmuster symmetrisch auf den Package-Baum übertragbar. Hier können die Entwickler sich anhand der hierarchischen Struktur schnell zurechtfinden und vom Architekturmuster profitieren.

Abb. 6: Gut umgesetztes Architekturmuster

Wird die Enthalten-Sein-Beziehung richtig eingesetzt, so unterstützt sie unseren kognitiven Mechanismus Hierarchisierung. Für alle anderen Arten von Beziehungen gilt das nicht: Wir können beliebige Klassen und Interfaces in einer Source-Code-Basis per Benutzungs- und/oder per Vererbungsbeziehung miteinander verknüpfen. Dadurch erschaffen wir verflochtene Strukturen (Zyklen), die in keiner Weise hierarchisch sind. Es bedarf einiges an Disziplin und Anstrengung, Benutzungs- und Vererbungsbeziehung hierarchisch zu verwenden. Verfolgt das Entwicklungsteam von Anfang an dieses Ziel, so sind die Ergebnisse in der Regel nahezu zyklenfrei. Ist der Wert von Zyklenfreiheit nicht von Anfang an klar, entstehen Strukturen wie in Abbildung 7.

Abb. 7: Zyklus aus 242 Klassen

Der Wunsch, Zyklenfreiheit zu erreichen, ist aber kein Selbstzweck! Es geht nicht darum, irgendeine technisch strukturelle Idee von „Zyklen müssen vermieden werden“ zu befriedigen. Vielmehr wird damit das Ziel verfolgt, eine modulare Architektur zu entwerfen.

Achtet man bei seinem Entwurf darauf, dass die einzelnen Bausteine modular, also jeweils genau für eine Aufgabe zuständig sind, dann entstehen in der Regel von selbst zyklenfreie Entwürfe und Architekturen. Ein Modul, das Basisfunktionalität zur Verfügung stellt, sollte nie Funktionalität aus den auf ihm aufbauenden Modulen benötigen. Sind die Aufgaben klar verteilt, dann ist offensichtlich, welches Modul welches andere Modul benutzen muss, um seine Aufgabe zu erfüllen. Eine umgekehrte und damit zyklische Beziehung entsteht erst gar nicht.

 

Stay tuned

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

 

Zusammenfassung: Regeln für Modularisierung

Mit den drei kognitiven Mechanismen Chunking, Schemata und Hierarchisierung haben wir das Hintergrundwissen bekommen, um Modularisierung in unseren Diskussionen klar und eindeutig zu verwenden. Eine gut modularisierte Architektur besteht aus Modulen, die den Einsatz von Chunking, Hierarchisierung und Schemata erleichtern. Zusammenfassend können wir die folgenden Regeln festlegen: Die Module einer modularen Architektur müssen

  1. In ihrem Inneren ein zusammenhängendes, kohärentes Ganzes bilden, das für genau eine klar definierte Aufgabe zuständig ist (Einheit als Chunk),

  2. Nach außen eine explizite, minimale und delegierende Kapsel bilden (Schnittstelle als Chunk),

  3. Nach einheitlichen Mustern durchgängig gestaltet sein (Musterkonsistenz) und

  4. Mit anderen Modulen minimal, lose und zyklenfrei gekoppelt sein (Kopplung zur Chunk-Trennung und Hierarchisierung).

Sind dem Entwicklungsteam diese Mechanismen und ihre Umsetzung in der Architektur klar, ist eine wichtige Grundlage für Modularisierung gelegt.

 

Links & Literatur

[1] Dieser Artikel ist ein überarbeiteter Auszug aus meinem Buch: Lilienthal, Carola: „Langlebige Softwarearchitekturen. Technische Schulden analysieren, begrenzen und abbauen“; dpunkt.verlag, 2019

[2] Parnas, David Lorge: „On the Criteria to be Used in Decomposing Systems in-to Modules“; in: Communications of the ACM (15/12), 1972

[3] Dijkstra, Edsger Wybe: „A Discipline of Programming“; Prentice Hall, 1976

[4] Myers, Glenford J.: „Composite/Structured Design“; Van Nostrand Reinhold, 1978

[5] Coad, Peter; Yourdon, Edward: „OOD: Objektorientiertes Design“; Prentice Hall, 1994

[6] Wirfs-Brock, Rebecca; McKean, Alan: „Object De-sign: Roles, Responsibilities, and Collaborations“; Pearson Education, 2002

[7] Martin, Robert Cecil: „Agile Software Development, Principles, Patterns, and Practices“; Prentice Hall International, 2013

[8] Bass, Len; Clements, Paul; Kazman, Rick: „Software Architecture in Practice“; Addison-Wesley, 2012

[9] Booch, Grady: „Object-Oriented Analysis and Design with Applications“; Addison Wesley Longman Publishing Co., 2004

[10] Gamma, Erich; Helm, Richard; Johnson, Ralph E.; Vlissides, John: „Design Patterns. Elements of Reusable Object-Oriented Software“; Addison-Wesley, 1994

[11] Züllighoven, Heinz: „Object-Oriented Construction Handbook“; Morgan Kaufmann Publishers, 2005

The post Modularisierung und kognitive Psychologie appeared first on JAX.

]]>
Services im Zwiegespräch: Synchrone Kommunikation zwischen REST Services mithilfe von OpenFeign https://jax.de/blog/software-architecture-design/services-im-zwiegespraech-synchrone-kommunikation-zwischen-rest-services-mithilfe-von-openfeign/ Fri, 18 Jan 2019 17:03:00 +0000 https://jax.de/?p=66668 Wer sich heutige Softwareprojekte oder -architekturen anschaut, steht immer wieder vor ähnlichen Herausforderungen. Eine davon ist die Kommunikation zwischen Services. Asynchron oder synchron, das ist hier die Frage.

The post Services im Zwiegespräch: Synchrone Kommunikation zwischen REST Services mithilfe von OpenFeign appeared first on JAX.

]]>

von Jörn Hameister
Ob es Microservices sein müssen oder ob es sich um Anwendungen in einer Systemlandschaft handelt, spielt keine Rolle. Wenn zwei Services miteinander kommunizieren sollen, besteht die Möglichkeit, dass die Kommunikation asynchron (etwa über Messages mit Kafka oder JMS) oder synchron (beispielsweise über REST) abläuft. Auf lange Sicht bietet die asychrone Kommunikation eine Reihe von Vorteilen, wie lose Kopplung von Services und Resilience. Allerdings wird trotzdem häufig die synchrone Kommunikation bevorzugt, weil sie leichter zu verstehen und zu implementieren ist. Zusätzlich fallen auch die Fehlersuche und das Debugging leichter. Deshalb stelle ich hier ein Framework vor, mit dem sich relativ einfach, elegant und übersichtlich die synchrone Kommunikation zwischen REST Services realisieren lässt: OpenFeign [1].

 

Erst nach dem Problem fragen

Wer kennt es nicht: In einer Systemlandschaft existiert ein Service mit einer REST-Schnittstelle, der Daten bereitstellt, die in einem anderen Service benötigt werden. Dazu gehören zum Beispiel Rechnungen, Kundendaten oder Wetterinformationen. Als Erstes stellt sich dann die Frage, mit welcher Technologie und welchem Framework der Service angebunden werden kann und welches Datenformat benutzt werden soll oder muss. JSON, XML, binär oder ein proprietäres Format?

In unserem Artikel gehen wir davon aus, dass JSON als Format zum Einsatz kommt. OpenFeign unterstützt auch alle anderen Formate und bietet die Möglichkeit, eigene proprietäre Formate zu ergänzen, sodass sie verarbeitet werden können. Wenn ein Service mit REST-Schnittstelle angesprochen werden soll, versucht man zuerst oft, den Service mit einem einfachen HTTP Call anzusprechen und die benötigten Daten abzufragen. Wenn von der Schnittstelle nur ein Integer oder String als Wert zurückkommt, ist das eventuell sogar ausreichend. Allerdings ist es oft so, dass komplexe Objekte (Entities, DTOs, …) an der Schnittstelle als JSON zurückgeliefert werden.

Für diesen Fall können wir beispielsweise den Jackson Mapper [2] ergänzen, um die Serialisierung und Deserialisierung der Objekte zu realisieren. Die Alternative ist das REST-Template von Spring. Es bietet sich an, wenn man sowieso im Spring-Boot-Umfeld unterwegs ist. Wer das REST-Template schon einmal eingesetzt hat, weiß, dass man jedes Mal überlegt, welche API-Methode (exchange, getForEntity usw.) man verwenden soll und wie die Parameter gesetzt werden müssen, um den gewünschten Wert abzufragen. Am Ende landet man meistens bei exchange, schaut wieder in die Dokumentation und sucht Codebeispiele, wie die Syntax genau aussieht.

Aus meiner Sicht ist der Java-Code mit seiner Fehlerbehandlung und dem Exception Handling immer wieder recht aufgebläht. Viel praktischer wäre es doch, wenn man einfach nur eine Clientschnittstelle beschreiben würde. Sie gibt an, wie die Service-Schnittstelle angesprochen werden soll. Das bedeutet, wir müssen uns nicht um die Fehlerbehandlung und die technischen Details kümmern. OpenFeign, ehemals Netflix Feign, ermöglicht beides. Schauen wir uns im ersten Schritt an einem kleinen Beispiel an, wie das funktioniert. Später wird an einem komplexeren API demonstriert, welche Möglichkeiten es gibt, um OpenFeign so zu erweitern, dass auch SPDY [3] verarbeitet werden kann.

ItemService

Anhand eines ItemStores, der Items (Dinge) verwaltet und über eine einfache REST-Schnittstelle angesprochen werden kann, erkennen wir, wie OpenFeign generell benutzt wird und funktioniert. Anfangs werfen wir einen kurzen Blick darauf, wie der Zugriff auf die Schnittstelle mit dem REST-Template oder einer http-Verbindung aussehen kann, um klar zu machen, welche Vorteile OpenFeign bietet. Die REST-Schnittstelle des ItemStore findet sich in Listing 1.

Listing 1: „ItemStore“ REST-Interface

@GetMapping(value = "/item")
ResponseEntity<List<Item>> getAllItems()

@PostMapping(value = "/item")
ResponseEntity<Item> createItem(@RequestBody Item item)

@PutMapping(value = "/item")
ResponseEntity<Item> updateItem(@RequestBody Item item)

@DeleteMapping(value = "/item/{id}")
ResponseEntity<Item> deleteItem(@PathVariable("id") long id)

@GetMapping(value = "/item/{location}")
ResponseEntity<List<Item>> getItemAtLocation(@PathVariable("location") String
location)

 

Es ist eine recht überschaubare Schnittstelle mit einer Methode zum Anlegen (createItem), Ändern (updateItem), Löschen (deleteItem) und Suchen (getItemAtLocation) von Items.

 

REST-Template

Wenn man mit der Klasse RestTemplate auf den Service zugreifen möchte, gestaltet sich das so:

RestTemplate restTemplate = new RestTemplate();
ResponseEntity<Item[]> responseEntity =
restTemplate.getForEntity("http://localhost:8080/item", Item[].class);
List<Item> listWithItems = Arrays.asList(responseEntity.getBody());

Hier verwenden wir die Methode getForEntity, um alle Items abzufragen. Eine weitere Variante, um lesend mit GET auf den Service zuzugreifen, kann so aussehen:


ResponseEntity<List<Item>> rateResponse =
  restTemplate.exchange("http://localhost:8080/item",
  HttpMethod.GET, null, new ParameterizedTypeReference<List<Item>>() {
  });
  List<Item> itemList = rateResponse.getBody();

Hier wird die exchange-Methode benutzt, um alle Items abzufragen und das Ergebnis in einer Liste zu erhalten. Sobald wir allerdings nicht nur lesend auf die Schnittstelle zugreifen möchten, sondern auch PUT und POST benutzen, müssen wir die Funktion exchange verwenden.

RestTemplate restTemplate = new RestTemplate();
HttpEntity<Item> request = new HttpEntity<>(item);
ResponseEntity<Item> response = restTemplate.exchange("http://localhost:8080/item",
HttpMethod.POST, request, Item.class);

In diesem Beispiel legen wir ein neues Item über das REST API an. Das bedeutet, die Methode createItem wird aus dem Interface in Listing 1 aufgerufen und die Response enthält das neu angelegte Item.

HttpConnection

Natürlich kann man den lesenden Zugriff auch mit einer einfachen HttpConnection und mit dem Jackson ObjectMapper lösen. Allerdings wird schon bei GET deutlich, dass extrem viel Boilerplate-Code entsteht und eine aufwendige Fehlerbehandlung dazukommt (Listing 2).

Listing 2: „HttpConnection“ für GET

private static List<Item> httpClientGet() throws IOException {
  URL url = new URL("http://localhost:8080/item");
  HttpURLConnection con = (HttpURLConnection) url.openConnection();
  con.setRequestMethod("GET");

  BufferedReader in = new BufferedReader(
  new InputStreamReader(con.getInputStream()));
  String inputLine;
  StringBuffer content = new StringBuffer();
  while ((inputLine = in.readLine()) != null) {
  content.append(inputLine);
  }
  in.close();

    ObjectMapper objectMapper = new ObjectMapper();
    return objectMapper.readValue(content.toString(), new
TypeReference<List<Item>>() { });
}

Außerdem ist bei diesem Ansatz auch die Gefahr größer, dass Fehler passieren. Beispielsweise weil vergessen wird, die Streams und Connections zu schließen. Eine andere Fehlerquelle liegt darin, dass wie im Beispiel kein Timeout von etwa fünf Sekunden mit con.setReadTimeout(5000); gesetzt wurde. Es ist eindeutig, dass das keine gute Lösung ist, die man für ein umfangreiches API implementieren und testen möchte.

Mit OpenFeign

Nachdem wir uns angeschaut haben, wie die REST-Schnittstelle mit dem REST-Template und mit HttpConnection angesprochen werden kann, kommen wir dazu, wie sich das mit OpenFeign lösen lässt. Um die Service-Schnittstelle aus Listing 1 mit OpenFeign anzusprechen, legen wir schlicht ein Interface an (Listing 3).

Listing 3: OpenFeign-Interface


package org.hameister.itemmanager;

import feign.Headers;
import feign.Param;
import feign.RequestLine;

import java.util.List;

public interface ItemStoreClient {

  @RequestLine("GET /item/")
  List<Item> getItems();

  @RequestLine("POST /item/")
  @Headers("Content-Type: application/json")
  Item createItem(Item item);

  @RequestLine("PUT /item/")
  @Headers("Content-Type: application/json")
  Item updateItem(Item item);

  @RequestLine("DELETE /item/{id}")
  void deleteItem(@Param("id") String id);

  @RequestLine("GET /item/{location}")
  List<Item> getItemAtLocation(@Param("location") String location);
}

Auf den ersten Blick ist deutlich, dass es nahezu identisch zum Service-Interface ist und keinerlei Boilerplate-Code enthält. Man beschreibt nur die Schnittstelle des Service mit dem Pfad der Operation und den Parametern und legt den Content-Type fest.

Mit der Annotation @RequestLine(“GET /item/”) geben wir die Operation und den Pfad an, der beschreibt, wo die Methode zu finden ist.

Um dieses Interface zu benutzen, lässt sich mit dem Feign.Builder einfach ein Client erzeugen und anschließend übers Interface auf die REST-Schnittstelle des ItemStore zugreifen (Listing 4).

Listing 4: OpenFeign-Client

package org.hameister.itemmanager;

import feign.Feign;
import feign.jackson.JacksonDecoder;
import feign.jackson.JacksonEncoder;

import java.time.LocalDate;
import java.util.List;

public class ItemManager {

  public static void main(String[] args) {

    ItemStoreClient api = Feign.builder()
      .encoder(new JacksonEncoder())
      .decoder(new JacksonDecoder())
      .target(ItemStoreClient.class, "http://localhost:8080");

    }
}

Dem Builder kommunizieren wir, welche Decoder und Encoder er verwenden soll, wo der Server mit der REST-Schnittstelle läuft und welches OpenFeign-Interface er verwenden soll. In unserem Beispiel nutzen wir den JacksonDecoder und JacksonEncoder. Diverse andere Standardencoder und -decoder zu verwenden, wäre ebenfalls möglich. Beispielsweise für Gson zum Serialisieren und Deserialisieren von Java-Objekten, JAXB zum Serialisieren und Deserialisieren von XML und SAX zum Serialisieren von XML.

Außerdem lässt sich im Builder das Logging konfigurieren, indem wir einen Logger ergänzen:


Feign.builder().logger(new Slf4jLogger())

Zusätzlich kann man einen Client definieren, der Dinge wie SPDY erledigt.


SwapiFeign api = Feign.builder()
 .client(new OkHttpClient())

Es ist auch möglich, Ribbon für das clientseitige Loadbalancing hinzuzufügen:


SwapiFeign api = Feign.builder().client(new RibbonClient())

Außerdem können wir eigene Encoder und Decoder implementieren und registrieren, sodass proprietäre Formate unterstützt werden können. Dazu muss nur das jeweilige Interface implementiert werden. Für den Decoder:


public class MyCustomDecoder implements Decoder {
  @Override
  public Object decode(Response response, Type type) throws IOException,
DecodeException, FeignException {
    return ...;
  }
}


Für den Encoder:


public class MyCustomEncoder implements Encoder {
  @Override
  public void encode(Object o, Type type, RequestTemplate requestTemplate) throws
EncodeException {
  ...
    }
}


Diese Encoder und Decoder müssen anschließend wie die Standardencoder und -decoder

registriert werden, wenn der Client mit dem Builder erstellt wird. Nicht zu vergessen, dass man bei der Definition des Clientinterface auch direkt Hystrix integrieren kann. Dafür gibt es einen HystrixFeign Builder, der genauso benutzt wird wie der Standard-Builder.


ItemStoreClient api = HystrixFeign.builder()
  .target(ItemStoreClient.class, "http://localhost:8080");


Er ermöglicht uns, die Schnittstelle um ein fehlertolerantes Verhalten zu erweitern. Beispielsweise, wenn der Service nicht oder nur langsam antwortet. Gerade in einem Umfeld, in dem mehrere Services miteinander kommunizieren, lässt sich dadurch verhindern, dass der Ausfall eines Service das Gesamtsystem zum Stehen bringt. Das war es auch schon. Anschließend dient der erstellte ItemStoreClient dazu, über die REST-Schnittstelle auf den ItemStore zuzugreifen.

In Listing 4 zeigt sich, wie der ItemStoreClient erstellt wird. Anschließend können die Methoden übers Interface direkt aufgerufen werden: List<Item> items = api.getItems();

Das Anlegen von Items funktioniert so:


Item item = new Item();
item.setDescription("New Item");
item.setLocation("Schrank 5A");
item.setItemdate(LocalDate.now());
Item newItem = api.createItem(item);


Ein Item zu ändern lässt sich analog durchführen:


newItem.setLocation("Schrank 5B");
Item updateItem = api.updateItem(newItem);


Um ein Item zu löschen, muss die jeweilige ID übergeben werden: api.deleteItem(“1”);.Das ist im Vergleich zum REST-Template oder dem HttpClient erheblich eleganter und verständlicher. Anzumerken ist, dass bei allen Ansätzen auf Clientseite ein DTO für das Item vorhanden sein muss. Entweder wir kopieren die Klasse aus dem Item-Service oder legen eine neue Klasse an (Listing 5).

Listing 5:  Item-DTO

@Data
public class Item {

  Long id;

  private String description;
  private String location;
  private LocalDate itemdate;

  public Item() {
  }
}

 

In dem DTO-Item haben wir Lombok [4] verwendet, um die Getter und Setter automatisch generieren zu lassen.

SWAPI

Wir haben uns angeschaut, wie OpenFeign generell bei einer einfachen Schnittstelle verwendet werden kann und welche Vorteile es gegenüber anderen Ansätzen mitbringt. Jetzt wenden wir uns dem zu, was OpenFeign noch bietet. Das soll am Beispiel von SWAPI (The Star Wars API) gezeigt werden. Dabei handelt es sich um eine öffentliche REST-Schnittstelle, über die man Personen, Filme, Raumschiffe und Planeten aus dem Star-Wars-Universum abfragen kann. Die Schnittstelle ist unter dem URL https://swapi.co zu erreichen. Wie auch schon bei dem Beispiel oben legen wir als Erstes ein Interface für die Schnittstelle an (Listing 6).

Listing 6:  SWAPI-Interface

public interface SwapiFeign {
  @RequestLine("GET /planets/{id}")
  Planet getPlanet(@Param("id") String id);

  @RequestLine("GET /planets/")
  GenericList<Planet> getPlanets();

  @RequestLine("GET /films/")
  GenericList<Film> getFilms();

  @RequestLine("GET /people/")
  GenericList<People> getPeople();

  @RequestLine("GET /starships/")
  GenericList<Starship> getStarships();

  @RequestLine("GET /vehicles/")
  GenericList<Vehicle> getVehicles();

}

Wir müssen für die Rückgabewerte noch DTOs anlegen. Im Vergleich zum anderen Beispiel benötigt man außerdem noch eine Generic List, weil das API die Rückgabewerte untereinander verlinkt. Das heißt, die Rückgabewerte enthalten immer einen Link auf das vorhergehende und nächste Element (Listing 7).

Listing 7:  „GenericList“

public class GenericList<T> {
  public int count;
  public String next;
  public String previous;

  public List<T> results;
}

Anschließend lässt sich wieder ein Client erstellen. Er ermöglicht, die Daten über die REST-Schnittstelle abzufragen. Auch im folgenden Beispiel werden ein JacksonEncoder und ein JacksonDecoder verwendet, um die JSON-Daten von der REST-Schnittstelle zu serialisieren und zu deserialisieren.

SwapiFeign api = Feign.builder()
  .encoder(new JacksonEncoder())
  .decoder(new JacksonDecoder())
  .client(new OkHttpClient())
  .target(SwapiFeign.class, "http://swapi.co/api");

Beim Erstellen des Clients fällt auf, dass der OkHttpClient() gesetzt wird. Das ist notwendig, damit SPDY, HTTP/2 und TLS des REST-Interface bedient werden können. Der komplette Quellcode zum OkHttpClient steht im GitHub Repository zu dem Artikel zur Verfügung [5].

Mit dem api-Objekt lässt sich nun die Schnittstelle ansprechen:

GenericList<Starship> starships = api.getStarships();

Listing 8 zeigt exemplarisch das DTO für das Starship.

Listing 8:  „Starship“

@Data
public class Starship {

  private String name;
  private String model;
  private String manufacturer;
  private String costs_in_credits;
  private String length;
  private String max_atmosphering_speed;
  private String crew;

  private String cargo_capacity;
  private String consumables;
  private String hyperdrive_rating;
  private String MGLT;
  private String starship_class;

  private List<People> pilots;
  private List<Film> films;
  private String created;
  private String edited;
  private String url;

  public Starship() {

  }
}

Um das Schema, also die Felder eines Starships herauszufinden, kann man einfach das API befragen, das unter [6] zu erreichen ist. Auch dies ist eine REST-Schnittstelle, die sich abfragen lässt:

Schema schema = getSchema("https://swapi.co/api/starships/schema");

Wobei das Schema-DTO aussieht wie in Listing 9

Listing 9:  Schema-DTO

@JsonIgnoreProperties(ignoreUnknown = true)
public class Schema {
  public List<String>required;
  public Map<String, Properties> properties;
  public String type;
  public String title;
  public String description;

  public Schema() {
  }
}

Und das verwendete Properties DTO so:

public class Properties {
  public String type;
  public String format;
  public String description;
}

Die getSchema()-Methode mit dem OkHttpClient zum Abfragen des Schemas findet sich in Listing 10.

Listing 10:  „getSchema“-Methode

private static Schema getSchema(String url) throws IOException {
  okhttp3.OkHttpClient okHttpClient = new okhttp3.OkHttpClient();

  Request request = new Request.Builder()
  .url(url)
  .get()
  .build();
  Response response = okHttpClient.newCall(request).execute();
  ObjectMapper objectMapper = new ObjectMapper();
  Schema schema =
objectMapper.readerFor(Schema.class).readValue(response.body().string());
  return schema;
}

 

Hier haben wir bewusst darauf verzichtet, OpenFeign einzusetzen, um zum Abschluss noch einmal zu verdeutlichen, dass der Quellcode ohne OpenFeign länger ist als mit OpenFeign. Zu beachten ist, dass das Exception Handling hier weitgehend ignoriert wurde, indem die IOExceptions einfach an den Aufrufenden zurückgeworfen und nicht behandelt werden.

Um noch einmal zu unterstreichen, wie einfach die Abfrage mit OpenFeign funktioniert, definieren wir zuerst ein Interface:

public interface SwapiSchemaClient {

  @RequestLine("GET ")
  Schema getSchema();
}

Anschließend kann ein Feign-Client mit dem Builder erstellt und daraufhin das Schema

abgefragt werden (Listing 11).

Listing 11:  „SchemaClient“

SwapiSchemaClient api = Feign.builder()
  .encoder(new JacksonEncoder())
  .decoder(new JacksonDecoder())
  .client(new OkHttpClient())
  .target(SwapiSchemaClient.class, "https://swapi.co/api/starships/schema");

Schema schema = api.getSchema();

 

Weniger selbst implementieren

OpenFeign ist eine elegante Möglichkeit, REST-Schnittstellen anzusprechen. Der Anwender bekommt eine Menge Features quasi geschenkt, die er normalerweise selbst implementieren müsste. Allerdings ist es nur eine von vielen Möglichkeiten. Wie so oft bei der Softwareentwicklung muss man immer genau schauen, in welchem Kontext man sich bewegt, welche Rahmenbedingungen es gibt und was dann die beste Lösung in dem Projekt ist. Einen kurzen Einführungsvortrag zu OpenFeign hat Igor Laborie bei der Devoxx 2016 in Belgien gehalten [7].

Anmerken sollte man vielleicht noch, dass ab Java 11 ein HttpClient fester Bestandteil von Java (JEP 321) ist, der sowohl synchrone, als auch asynchrone Requests absetzen kann [8].

 

Cheat-Sheet: Die neuen JEPs im JDK 12


Unser Cheat-Sheet definiert für Sie, wie die neuen Features in Java 12 funktionieren. Von JEP 189 „Shenandoah“ bis JEP 346 „Promptly Return Unused Committed Memory from G1“ fassen wir für Sie zusammen, was sich genau ändern wird!

Cheat-Sheet sichern!

Links & Literatur
[1] OpenFeign: https://github.com/OpenFeign/feign
[2] Jackson Mapper: https://github.com/FasterXML/jackson
[3] SPDY: https://de.wikipedia.org/wiki/SPDY
[4] Project Lombok: https://projectlombok.org
[5] https://github.com/hameister/ItemStoreFeignClient
[6] SWAPI: https://swapi.co/api/starships/schema
[7] OpenFeign in Action: https://youtu.be/kO3Zqk_6HV4
[8] Java 11 HttpClient, Gson, Gradle, and Modularization: https://kousenit.org/2018/09/22/java-11-httpclient-gson-gradle-and-modularization/

The post Services im Zwiegespräch: Synchrone Kommunikation zwischen REST Services mithilfe von OpenFeign appeared first on JAX.

]]>
Software-Architektur heute: „Es geht um Menschen, Technologien sind sekundär“ https://jax.de/blog/software-architecture-design/software-architektur-heute-es-geht-um-menschen-technologien-sind-sekundaer/ Thu, 25 Oct 2018 11:26:39 +0000 https://jax.de/?p=65554 Software-Architektur galt lange als die Disziplin, um in Software-Projekten für einen kohärenten Zusammenhang zu sorgen: Stabilität, Sicherheit, Planbarkeit stand im Vordergrund. Wir haben uns mit Henning Schwentner darüber unterhalten, wie sich dieses Bild verändert hat und welche Rolle Trends wie DevOps und DDD dabei spielen.

The post Software-Architektur heute: „Es geht um Menschen, Technologien sind sekundär“ appeared first on JAX.

]]>
W-JAX: Software-Architektur galt lange als die Disziplin, in Software-Projekten für einen kohärenten Zusammenhang zu sorgen: Es geht darum, Stabilität und Langlebigkeit zu gewährleisten, Standards einzuführen, für Sicherheit zu sorgen, Pläne und Dokumentationen zu erstellen, etc. Heute wird Software-Architektur oft auch anders diskutiert, und zwar im Sinne eines Change Management: Architekturen sollen flexibel, erweiterbar, austauschbar sein. Wie siehst du dich: Wie viel in deiner Arbeit ist Kirchenbauer, wie viel Change Manager?

Henning Schwentner: Beide Rollen sind untrennbar. Stabilität im Großen bekommen wir über Flexibilität im Kleinen. Das Interessante ist ja, dass insbesondere die großen Systeme, weil sie am längsten leben, gerade deshalb am flexibelsten sein müssen. Deswegen finde ich es gut, dass Trends wie Microservices und Self-Contained Systems die (im Prinzip alten) Ideen, wie man ein System vernünftig modularisiert, im Mainstream ankommen lassen. Der wichtigste Punkt ist: Wir wollen nicht ein großes Domänenmodell haben, sondern mehrere kleine. Die kleinen Domänenmodelle können dann verständlich, beherrschbar und flexibel sein.

W-JAX: Wie schafft man es, den richtigen Mix aus Stabilität und Flexibilität zu finden?

Henning Schwentner: Erstens: sich immer und immer wieder klar machen, dass Software kein Selbstzweck ist. Software (und auch Softwarearchitektur) bauen wir nicht für uns selbst, sondern für den Fachexperten. Zweitens: nicht nach dem einen Domänenmodell streben, das alle Probleme auf einmal löst. Das wird nämlich viel zu groß, fehleranfällig und schwer verständlich. Interessanterweise also gleichzeig instabil und unflexibel. Stattdessen wollen wir mehrere kleine Modelle.

W-JAX: Im Zuge der DevOps-Bewegung erweitert sich das Bild des Software-Architekten noch um eine weitere Facette: Es geht nämlich nicht nur um Anwendungsentwicklung, sondern immer mehr auch darum, wie sich Anwendungen in einer Continuous-Delivery-Landschaft einbetten. „You build it, you run it“ heißt da das Stichwort. Wie hat die DevOps-Bewegung die Rolle des Software-Architekten verändert? Was musst du als Architekt heute anders machen, als früher, als man die Anwendungen noch einfach über den Zaun hin zum Ops-Team geworfen hat?

Henning Schwentner: Mir geht es da wie vielen: Ich freue mich, dass die unnatürliche Trennung von Entwicklung und Betrieb aufgehoben wird. Als Entwickler und Architekt wird man jetzt von Anfang an darauf fokussiert, nicht eine ausführbare Datei, sonderen laufende Software auszuliefern. Und nur die hat Wert für den eigentlich wichtigen Menschen – unseren Anwender.

W-JAX: Ein weiterer Trend ist aktuell, das Design einer Software stark an den fachlichen Domänen auszurichten. Neben DDD als Theorie erobern gerade Microservices-Architekturen die Praxis. Neben den technologischen Aspekten, die Domänen-fokussierte Anwendungen mit sich bringen, geht es hier zentral auch darum, die beteiligten Leute erst einmal in ein Boot zu holen: Fachexperten, Entwickler und natürlich auch die Geschäftsleitung und Anwender bzw. Kunden. Ist man da als Software-Architekt nicht eigentlich zu 80% Projektmanager? Wie hältst du das persönlich: Wie stark nimmst du die Rolle des Projektmanagers ein, wie viel konzentrierst du dich auf Technologien?

Henning Schwentner: Von Jerry Weinberg wissen wir: »No matter how it looks at first, it’s always a people problem.« Die erste Aufgabe jedes Menschen, der mit Softwareentwicklung beschäftigt ist, ob er nun Programmierer, Architekt, Projektmanager oder wie auch immer heißt, ist, mit anderen Menschen zu kommunizieren. Mit Computern zu kommunizieren kommt frühestens auf Platz zwei.

Technologien sind nicht unwichtig, aber sekundär. Frag dich mal selbst: Von wievielen Programmen und Apps, die du einsetzt, weißt du, welche Programmiersprache, Frameworks usw. darin verwendet wird? Ist dir das wichtig? Oder willst du lieber, dass die ihren Job machen?

Die Gemeinheit ist: In der Software-Entwicklung gilt das Anna-Karenina-Prinzip, d.h. ein Projekt kann nur dann erfolgreich sein, wenn alle Faktoren stimmen. Technologien kennen und beherrschen wird deshalb immer wichtig und nötig sein.

W-JAX: Auf der W-JAX wird es ein Lab zum Thema „Event Storming“ geben. Wie funktioniert Event Storming – und weshalb sollte man es machen?

Henning Schwentner: Die Grundaufgabe von Software ist, unseren Anwender bei seiner Arbeit zu unterstützen. Damit wir das tun können, müssen wir seine Arbeit (d.h. die Domäne) verstehen. Event Storming ist ein Werkzeug, das uns dabei hilft. Bewaffnet mit haufenweise Klebezetteln wird Wissen aufgebaut, ausgetauscht und vertieft. Es hilft uns dabei, eine gemeinsame Sprache, Kontextgrenzen und Domänenmodelle zu finden.

W-JAX: Welchen Trend findest du im Bereich der Software-Architektur momentan besonders spannend – und warum?
Henning Schwentner: Mir ist das Thema „Domäne verstehen, um die richtige Software zu bauen“ sehr wichtig. Neben Event Storming ist Domain Storytelling ein tolles Werkzeug dafür. Wir lassen die Anwender ihre Geschichte erzählen. Dabei zeichnen wir sie mit einer einfach zu verstehenden Bildsprache auf. Die entstehenden Bilder (die sogenannten Domain Stories) verwendet man, um direkt rückzukoppeln, ob wir die Anwender richtig verstanden haben. Mehr Infos gibt’s auf domainstorytelling.org und hoffentlich bald in dem Buch, das Stefan Hofer und ich gerade darüber schreiben.

W-JAX: Vielen Dank für dieses Interview!

 

Cheat-Sheet: Die neuen JEPs im JDK 12


Unser Cheat-Sheet definiert für Sie, wie die neuen Features in Java 12 funktionieren. Von JEP 189 „Shenandoah“ bis JEP 346 „Promptly Return Unused Committed Memory from G1“ fassen wir für Sie zusammen, was sich genau ändern wird!

Cheat-Sheet sichern!

 

 

The post Software-Architektur heute: „Es geht um Menschen, Technologien sind sekundär“ appeared first on JAX.

]]>
Was Sie bei der Einführung von APIs wissen sollten https://jax.de/blog/software-architecture-design/was-sie-bei-der-einfuehrung-von-apis-wissen-sollten/ Thu, 04 Oct 2018 13:02:31 +0000 https://jax.de/?p=65319 Jede noch so gut definierte Schnittstelle kann an einen Punkt kommen, an dem sie weiterentwickelt werden muss. Welche Herausforderungen bei der Einführung von APIs zu bewältigen sind, verrät uns Arne Limburg im Interview.

The post Was Sie bei der Einführung von APIs wissen sollten appeared first on JAX.

]]>
W-JAX: Viele Unternehmen führen derzeit APIs ein, die ältere technische Lösungen wie SOA, ESBs und/oder monolithische Systeme ersetzen. Weshalb eigentlich? Warum hat die sogenannte API Economy momentan Konjunktur?

Arne Limburg: Das hat mehrere Aspekte. Da ist zum Einen der technologische Aspekt. Moderne REST-APIs sind viel schlanker als frühere Lösungen wie SOAP oder XML-RPC und das sowohl im Design als auch bei der Datenmenge, die über die Leitung geht.

Dann gibt es natürlich den architektonischen Aspekt. Man hat mittlerweile erkannt, dass Architekturen, die auf einem ESB basieren, nicht so leicht zu warten und weiterzuentwickeln sind, als wenn sich die Services direkt unterhalten. Damit das gelingt, benötigt man aber gute APIs, die auch stabil bleiben. Da spielt das Thema Abwärtskompatibilität und Versionierung eine wichtige Rolle.

Und last but not least haben viele Unternehmen erkannt, dass sich mit gut definierten Public APIs auch Geld verdienen lässt. Unternehmen erschließen sich neue Vertriebskanäle, in dem sie APIs anbieten, über die z.B. Mobile Clients angebunden werden können (Stichwort: Multi-Channel-Strategie). Die Clients müssen dann nicht immer vom Unternehmen selbst gebaut werden. Es gibt auch interessante Konstellationen, in denen Drittanbieter an dem Unternehmensumsatz partizipieren können.

W-JAX: Deine Session auf der W-JAX heißt „Abwärtskompatible APIs – Strategien für den Projektalltag.“ Dabei gehst du darauf ein, wie man Schnittstellen sinnvoll weiterentwickelt, wenn sich beispielsweise die Anforderungen verändert haben. Warum genügt es nicht einfach, eine Versionierung für APIs einzuführen?

Arne Limburg: Es geht vor allem um die Pflege alter Versionen. Wenn ich ein Public API betreibe, kann ich nicht einfach eine Version 2 des API zur Verfügung stellen und Version 1 abschalten. Damit erzeuge ich hohen Aufwand bei meinen Clients, die dann zeitgleich updaten müssten. Auf Dauer macht das kein externer Client-Anbieter mit.

Die Erfahrung zeigt aber, dass die Pflege alter Versionen serverseitig sehr aufwendig ist, wenn man es nicht richtig angeht. Es geht also darum, einen Weg zu finden, serverseitig alte Versionen einfach pflegen zu können und gleichzeitig den Clients leichte Wege zu eröffnen, auf die neueste Version zu aktualisieren.

W-JAX: Kannst du einmal einen Tipp geben, wie man Abwärtskompatibilität von APIs sicherstellen kann, ohne sich im Support alter Versionen zu verlieren?

Arne Limburg: In aller Ausführlichkeit erkläre ich das natürlich in meinem Talk auf der W-JAX. Kurz gesagt, geht es darum, einerseits gewisse Anforderungen an den Client zu stellen (Stichwort: Tolerant Reader Pattern) aber andererseits auf dem Server auch dafür zu sorgen, dass die API kompatibel bleibt, in dem innerhalb einer Version nur Attribute hinzukommen, aber niemals welche entfernt werden. Beim Versionssprung ist es wichtig, dass das Mapping zwischen alter und neuer Version nicht zu aufwendig ist.

 

 

W-JAX: In den meisten Fällen haben Unternehmen noch Legacy-Systeme am Laufen, die bei der Einführung von APIs integriert werden müssen. Welche technologischen Herausforderungen gilt es dabei zu meistern?

Arne Limburg: Hier gibt es zwei Arten von Legacy-Systemen. Für die einen ist das Unternehmen im Besitz des Source Codes und hat auch das Know-How, um die Systeme weiterzuentwickeln. Hier empfehlen wir immer, ein sauberes RESTful API für das Legacy-System zu realisieren, um es in die „neue Welt“ einzubinden. Häufig ist das gar nicht so schwer.

Sollte sich das nicht realisieren lassen, empfehlen wir einen Anti-Corruption-Layer, der dann die saubere Schnittstelle zur Verfügung stellt und eigentlich nichts anderes macht als zwischen Legacy und neuer Welt hin- und herzumappen. Das kann dann z.B. auch ein Caching beinhalten, wenn das Legacy-System nicht auf eine so hohe Anzahl von Requests ausgelegt ist oder wenn es sogar nur im Batch-Betrieb läuft.

W-JAX: Bei der Einführung von APIs bleibt es aber ja nicht bei den technologischen Herausforderungen. Weshalb hat das auch Konsequenzen auf die gesamte Organisation eines Unternehmens?

Arne Limburg: In vielen Unternehmen ist es nach wie vor so, dass die Entwicklung sehr Projekt-getrieben ist. Die Einführung eines neuen Features ist ein eigenes Projekt, für das es ein separates Budget gibt und häufig auch noch ein Plichten- und Lastenheft.

Moderne APIs müssen allerdings als Produkt betrieben werden, das kontinuierlich weiterentwickelt wird und auf diese Weise benötigte Features zur Verfügung stellt. Die Art und Weise, wie neue Themen in die IT eingebracht werden, muss sich daher häufig komplett ändern.

W-JAX: Wie sollte man ein API-basiertes Projekt deiner Erfahrung nach angehen? Es gibt da ja verschiedenste Ansätze: Startet man technologisch, oder muss man zuerst das Unternehmen umstrukturieren? Braucht es zunächst ein ausgefeiltes Konzept zum API Management, oder ist der MVP-Ansatz hier besser: erstmal klein starten, Feedback einholen, weiterentwickeln?
Arne Limburg: Das Vorgehen, erst einmal klein anzufangen, um Erfahrung zu sammeln, ist auf jeden Fall ein Vorgehen, das sich bewährt hat. Dennoch sollte man sich beim Design einer API nicht von der Technologie treiben lassen. Es geht ja nicht primär darum, wie ich den Server schnell realisieren kann, sondern darum, eine API so aufzubauen, dass viele unbekannte Clients sie leicht nutzen können und gerne nutzen. Mein Ansatz ist deshalb immer der sogenannte Contract-First-Ansatz, wobei der Name etwas irreführend ist, weil er nicht das Ziel widerspiegelt. Eigentlich müsste man den Ansatz Client-First nennen. Gute APIs sind in der Regel die, bei denen das Design mit der Überlegung ausgeführt wurde: Was benötigt der Client?

W-JAX: Vielen Dank für dieses Interview!

 

 

Cheat-Sheet: Die neuen JEPs im JDK 12


Unser Cheat-Sheet definiert für Sie, wie die neuen Features in Java 12 funktionieren. Von JEP 189 „Shenandoah“ bis JEP 346 „Promptly Return Unused Committed Memory from G1“ fassen wir für Sie zusammen, was sich genau ändern wird!

Cheat-Sheet sichern!

 

 

The post Was Sie bei der Einführung von APIs wissen sollten appeared first on JAX.

]]>
Microservices sind kein Allheilmittel! https://jax.de/blog/software-architecture-design/microservices-sind-kein-allheilmittel/ Tue, 11 Sep 2018 09:16:40 +0000 https://jax.de/?p=65214 In Zeiten von Agile, DevOps und DDD verändert sich auch die Rolle des Software-Architekten. Wir haben uns mit Ralf D. Müller, darüber unterhalten, wie man als Software-Architekt den richtigen Mix aus Stabilität und Flexibilität findet, welche Impulse von der DevOps-Bewegung ausgehen und wie DDD dabei hilft, wertschöpfende Software zu bauen.

The post Microservices sind kein Allheilmittel! appeared first on JAX.

]]>
W-JAX: Software-Architektur galt lange als die Disziplin, in Software-Projekten für einen kohärenten Zusammenhang zu sorgen: Es geht darum, Stabilität und Langlebigkeit zu gewährleisten, Standards einzuführen, für Sicherheit zu sorgen, Pläne und Dokumentationen zu erstellen, etc. Heute wird Software-Architektur oft auch anders diskutiert, und zwar im Sinne eines Change Management: Architekturen sollen flexibel, erweiterbar, austauschbar sein. Wie siehst du dich: Wie viel in deiner Arbeit ist Kirchenbauer, wie viel Change Manager?

Ralf D. Müller: Beide Aspekte der Architektur – Stabilität und Flexibilität – müssen wie immer ausgewogen vorhanden sein und bauen aufeinander auf. Erst wenn gewisse Standards existieren und die Vorgehensweise, Schnittstellen etc. dokumentiert sind, lässt sich eine Architektur auch gut ändern, ohne die Stabilität zu riskieren. Deshalb ist es ja auch so wichtig, dass die Architektur gut kommuniziert und die Pfade zur Umsetzung der architekturellen Aspekte ausgetrampelt werden.

Nur wenn jeder im Team weiß, auf was es architekturell ankommt, entsteht die benötigte Stabilität, um später flexibel auf geänderte Anforderungen reagieren zu können. Das arc42-Template von Gernot Starke und Peter Hruschka hilft hier bei der Strukturierung der Dokumentation.

W-JAX: Wie schafft man es, den richtigen Mix aus Stabilität und Flexibilität zu finden? Hast du da vielleicht einen Tipp aus deinen langjährigen Erfahrungen?

Ralf D. Müller: Jedes Projekt ist anders und bringt unterschiedliche Anforderungen bezüglich Stabilität und Flexibilität mit. Deswegen ist es wichtig, einen Blick auf die Requirements zu werfen und nicht einfach eine interessante Architektur eines anderen Projekts zu übernehmen. Die Requirements geben meist vor, welcher Teil der Architektur flexibel und welcher stabil sein muss.

Soll z.B. ein White-Label Produkt erstellt werden, dann ist das Design sicherlich flexibler zu halten als bei einer Anwendung zur internen Verwendung. Aber die beiden Attribute müssen sich auch nicht widersprechen: Erst die Stabilität in den Schnittstellen zwischen wohldefinierten Modulen ermöglicht die Flexibilität zur Änderung einzelner Module.

 

W-JAX: Im Zuge der DevOps-Bewegung erweitert sich das Bild des Software-Architekten noch um eine weitere Facette: Es geht nämlich nicht nur um Anwendungsentwicklung, sondern immer mehr auch darum, wie sich Anwendungen in einer Continuous-Delivery-Landschaft einbetten. „You build it, you run it“ heißt da das Stichwort. Wie hat die DevOps-Bewegung die Rolle des Software-Architekten verändert? Was musst du als Architekt heute anders machen, als früher, als man die Anwendungen noch einfach über den Zaun hin zum Ops-Team geworfen hat?

Ralf D. Müller: Hat man das früher gemacht – Anwendungen einfach über den Zaun hin zum Ops-Team geworfen?  Der Betrieb der Software war schon immer ein wichtiger Aspekt der Architektur. Eine Applikation wird meist länger betrieben, als entwickelt. Somit ist der Aspekt des Betriebs für den Erfolg der Software mindestens genauso wichtig wie z.B. der Aspekt des Clean Code.

Aus meiner Sicht hat sich in diesem Bereich die wichtigste Änderung nicht direkt durch DevOps ergeben, sondern durch die iterativen Entwicklungszyklen eines agilen Projekts. Es gibt nicht mehr das Upfront-Design der Architektur, sondern man kann ein Projekt nun über mehrere Release-Zyklen begleiten. Dadurch sieht der Architekt vor allem, wie die Architektur die Qualitätskriterien des Projekts auch tatsächlich implementiert und kann die Architektur entsprechend anpassen.

W-JAX: Ein weiterer Trend ist aktuell, das Design einer Software stark an den fachlichen Domänen auszurichten. Neben DDD als Theorie erobern gerade Microservices-Architekturen die Praxis. Neben den technologischen Aspekten, die Domänen-fokussierte Anwendungen mit sich bringen, geht es hier zentral auch darum, die beteiligten Leute erst einmal in ein Boot zu holen: Fachexperten, Entwickler und natürlich auch die Geschäftsleitung und Anwender bzw. Kunden. Ist man da als Software-Architekt nicht eigentlich zu 80% Projektmanager? Wie hältst du das persönlich: Wie stark nimmst du die Rolle des Projektmanagers ein, wie viel konzentrierst du dich auf Technologien?

Ralf D. Müller: Es stimmt schon, dass Software-Architektur streckenweise mehr mit Management als mit Technologie zu tun hat. Aber wie bei allem hängt es sehr stark vom eigentlichen Projekt und des Typs „Architekt“ ab, den man verkörpert. Mich selbst würde ich weniger als Projekt-, sondern mehr als Architekturmanager sehen. Die Architektur, die in meinem Kopf ist, muss irgendwie raus in die Umsetzung. Das geschieht durch Dokumentation, Kommunikation und auch Management.

 

 

W-JAX: Auf der W-JAX hältst du einen Talk namens „Docs-as-Code“. Wo liegt der große Unterschied zwischen dem Docs-as-Code-Ansatz, den ihr beschreibt, und der traditionellen Art und Weise, Software zu dokumentieren?

Ralf D. Müller: Der Unterschied ist recht groß und vielfältig. Ich denke, jeder kennt die klassische, mit einer Textverarbeitung erstellte Dokumentation, die getrennt vom Code verwaltet wird. Dokumentation gehört aber, wie Tests auch, zum Code und sollte mit diesem verwaltet werden. Dadurch ist immer klar, wo die aktuelle Version liegt.

Sobald die Dokumentation zusammen mit dem Code verwaltet wird, können auch weitere Aspekte der Softwareentwicklung auf die Dokumentation übertragen werden. So kann z.B. das Aktualisieren von Diagrammen automatisiert im Build umgesetzt und die Dokumentation sogar automatisiert getestet werden. Wird eine Änderung am Code vorgenommen, so gehört es mittlerweile zur Definition of Done, auch die Tests anzupassen. Mit Docs-as-Code wird im gleichen Schritt auch die Dokumentation gepflegt, weil ein Pull-Request sonst nicht als vollständig im Sinne der DoD angesehen wird.

W-JAX: Welchen Trend findest du im Bereich der Software-Architektur momentan besonders spannend – und warum?

Ralf D. Müller: Ich beobachte momentan, wie der Ansatz der Microservices reift. Zum Einen setzt sich die Erkenntnis durch, dass auch Microservices nicht die Lösung für jedes Problem sind und man abwägen muss. Aber auch die Art der Implementierung von Microservices auf der JVM entwickelt sich weiter. So steht mit micronaut.io mittlerweile ein Framework zur Verfügung, welches zielgerichtet auf Microservices hin entwickelt wurde und nicht als Full-Stack Framework entstand. Auch der Serverless-Ansatz ist in diesem Zusammenhang spannend. Solche Entwicklungen sorgen dafür, dass die Arbeit als Software-Architekt immer spannend bleiben wird.

Vielen Dank für dieses Interview!

 

Cheat-Sheet: Die neuen JEPs im JDK 12


Unser Cheat-Sheet definiert für Sie, wie die neuen Features in Java 12 funktionieren. Von JEP 189 „Shenandoah“ bis JEP 346 „Promptly Return Unused Committed Memory from G1“ fassen wir für Sie zusammen, was sich genau ändern wird!

Cheat-Sheet sichern!

The post Microservices sind kein Allheilmittel! appeared first on JAX.

]]>
Wie werde ich ein erfolgreicher Software-Architekt? https://jax.de/blog/software-architecture-design/wie-werde-ich-ein-erfolgreicher-software-architekt-blog/ Mon, 03 Sep 2018 09:26:28 +0000 https://jax.de/?p=65170 In Zeiten von Agile, DevOps und DDD verändert sich auch die Rolle des Software-Architekten. Wir haben uns mit Eberhard Wolff, darüber unterhalten, wie man als Software-Architekt den richtigen Mix aus Stabilität und Flexibilität findet, welche Impulse von der DevOps-Bewegung ausgehen und wie DDD dabei hilft, wertschöpfende Software zu bauen.

The post Wie werde ich ein erfolgreicher Software-Architekt? appeared first on JAX.

]]>
W-JAX: Software-Architektur galt lange als die Disziplin, in Software-Projekten für einen kohärenten Zusammenhang zu sorgen: Es geht darum, Stabilität und Langlebigkeit zu gewährleisten, Standards einzuführen, für Sicherheit zu sorgen, Pläne und Dokumentationen zu erstellen, etc. Heute wird Software-Architektur oft auch anders diskutiert, und zwar im Sinne eines Change Management: Architekturen sollen flexibel, erweiterbar, austauschbar sein. Wie siehst du dich: Wie viel in deiner Arbeit ist Kirchenbauer, wie viel Change Manager?

Eberhard Wolff: Stabilität und Standards sind keine Ziele, sondern Mittel, um ein wartbares System zu erstellen. Wenn die Software einheitlich gestaltet ist, können Entwickler sich leichter einarbeiten und die Systeme einfacher ändern. Also ist dieses Vorgehen nur dazu da, um Änderbarkeit zu erreichen. Aber dieser Ansatz funktioniert nur theoretisch.

In der Praxis verlieren große Systeme mit der Zeit ihre Struktur und ihre Einheitlichkeit. So werden sie immer schwerer wartbar, was die Langlebigkeit begrenzt. Daher setzen aktuelle Ansätze darauf, Komponenten ersetzbar zu machen, um so der mangelnden Langlebigkeit zu entgehen. Das bieten Microservices. Sie können ersetzt werden, und zum Ersetzen können auch neue Technologien genutzt werden. Ebenso ist es möglich, Microservices mit unterschiedlichen Technologien zu implementieren und so mit der Vielfalt besser umzugehen.

W-JAX: Wie schafft man es, den richtigen Mix aus Stabilität und Flexibilität zu finden? Hast du da vielleicht einen Tipp aus deinen langjährigen Erfahrungen?

Eberhard Wolff: Meiner Meinung nach liegt das Problem auf einer anderen Ebene: Stabilität und Flexibilität sind nur unterschiedliche Wege, um das Ziel Wartbarkeit zu erreichen. Meistens treffe ich in Projekten auf das Problem, dass die Ziele des Projektes unklar sind oder nicht in der Architektur abgebildet worden sind.

Sicher ist Wartbarkeit wichtig, damit man auch in der Zukunft noch das System anpassen kann. Aber wenn das System gar nicht in Produktion geht, weil es Compliance-Richtlinien nicht einhält, die notwendige Performance nicht erreicht oder die Vorgaben im Betrieb nicht erfüllt, hilft die Wartbarkeit nichts. Und wenn das System in den Betrieb geht, aber kein sinnvolles Geschäftsziel unterstützt, ist das System ebenfalls sinnlos.

Architektur bedeutet, eine technische Lösung für ein Problem zu finden. Das wiederum bedeutet, das Problem zu kennen. Zu oft wird einfach nur Wartbarkeit angestrebt – ohne die wirklichen Probleme überhaupt zu lösen oder zu analysieren.

 

W-JAX: Im Zuge der DevOps-Bewegung erweitert sich das Bild des Software-Architekten noch um eine weitere Facette: Es geht nämlich nicht nur um Anwendungsentwicklung, sondern immer mehr auch darum, wie sich Anwendungen in einer Continuous-Delivery-Landschaft einbetten. „You build it, you run it“ heißt da das Stichwort. Wie hat die DevOps-Bewegung die Rolle des Software-Architekten verändert? Was musst du als Architekt heute anders machen, als früher, als man die Anwendungen noch einfach über den Zaun hin zum Ops-Team geworfen hat?

Eberhard Wolff: Eigentlich geht es immer noch um dasselbe: Ein System ist für Anwender nur nützlich, wenn es in Produktion ist. Das ist mittlerweile durch Continuous Delivery und DevOps offensichtlich. Daher sollte der Architekt auch diesen Aspekt betrachten. Das klassische Ziel der einfachen Änderbarkeit kann durch Continuous Delivery ebenfalls besser erreicht werden: Software, die schneller und einfacher in Produktion gebracht werden kann, ist einfacher änderbar, weil Änderungen schneller dahin gebracht werden, wo es zählt: In die Hände der Nutzer und zwar abgesichert durch Tests.

Auf der anderen Seite ist die einfache Betreibbarkeit einer Anwendung eine Voraussetzung für eine möglichst einfache Continuous-Delivery-Pipeline. Das kann die Auswahl der Technologien einschränken oder dazu führen, dass Ops-Anforderungen wie Monitoring oder Logging schon frühzeitig betrachtet werden. Das verringert das Risiko und macht allen – Betrieb und Entwicklung – das Leben einfacher.

W-JAX: Ein weiterer Trend ist aktuell, das Design einer Software stark an den fachlichen Domänen auszurichten. Neben DDD als Theorie erobern gerade Microservices-Architekturen die Praxis. Neben den technologischen Aspekten, die Domänen-fokussierte Anwendungen mit sich bringen, geht es hier zentral auch darum, die beteiligten Leute erst einmal in ein Boot zu holen: Fachexperten, Entwickler und natürlich auch die Geschäftsleitung und Anwender bzw. Kunden. Ist man da als Software-Architekt nicht eigentlich zu 80% Projektmanager? Wie hältst du das persönlich: Wie stark nimmst du die Rolle des Projektmanagers ein, wie viel konzentrierst du dich auf Technologien?

Eberhard Wolff: Fachliche Anforderungen zu verstehen ist zentral, um die richtigen Probleme zu lösen. Außerdem ist Architektur eigentlich die Strukturierung der Fachlichkeit. Das geht nur mit fachlichem Wissen und dem Austausch mit fachlichen Experten. Das ist aber kein Projektmanagement und auch nichts Neues. Domain-driven Design ist auch schon fast 15 Jahre alt. Am Ende sollte der Architekt wie alle anderen auch seinen Teil dazu beitragen, dass das Projekt erfolgreich ist. Dazu ist die Fachlichkeit und ihre Strukturierung meist wichtiger als die Technik.

 
 

 

W-JAX: Auf der W-JAX hältst du einen Talk namens „Wie werde ich ein erfolgreicher Softwarearchitekt?“ Dabei gehst du auf Voraussetzungen für einen guten Softwarearchitekten ein, auf die man zunächst vielleicht nicht gleich kommt. Kannst du da einmal ein Beispiel nennen?

Eberhard Wolff: Softwarearchitekten arbeiten zwar mit technischen Herausforderungen, aber zentral ist die gemeinsame Arbeit an den zu lösenden Problemen. Daher geht es in dem Vortrag vor allem darum, wie Architekten ihre Rolle leben sollten, ihr Wissen so einbringen können, dass es auch wirklich umgesetzt wird, und wie man auch das Wissen der anderen Team-Mitglieder nutzbar macht. Aber es gibt natürlich auch ein paar ganz praktische Tipps.

W-JAX: Welchen Trend findest du im Bereich der Software-Architektur momentan besonders spannend – und warum?

Eberhard Wolff: Der nächste Trend sollte sein, sich mit den Zielen und Herausforderungen des jeweiligen Projekts auseinanderzusetzen und dafür passende technische Lösungen zu finden. Andere Trends können sicher helfen, um neue Lösungsmöglichkeiten kennen zu lernen. Es ist gut, wenn man sie unvoreingenommen für die passenden Szenarien nutzt. Aber ich sehe zu oft Architekturen, die dem letzten Trend entsprechen, aber keiner kann sagen, welche Ziele damit wie erreicht werden sollen. Das finde ich schade, denn die Aufgabe eines Architekten ist eben, eine technische Lösung zum Erreichen der Ziele des Projekts zu finden.

 

Erfahren Sie mehr über Software Architecture auf der W-JAX 2018:


● Integration Patterns for Microservices
● Wie werde ich ein erfolgreicher Softwarearchitekt?

 

 

Cheat-Sheet: Die neuen JEPs im JDK 12


Unser Cheat-Sheet definiert für Sie, wie die neuen Features in Java 12 funktionieren. Von JEP 189 „Shenandoah“ bis JEP 346 „Promptly Return Unused Committed Memory from G1“ fassen wir für Sie zusammen, was sich genau ändern wird!

Cheat-Sheet sichern!

The post Wie werde ich ein erfolgreicher Software-Architekt? appeared first on JAX.

]]>
Jenkins Tutorial: So baut man einen Jenkins-Cluster https://jax.de/blog/software-architecture-design/jenkins-cluster-verteilung-eines-deployment-servers/ Thu, 28 Jun 2018 12:11:07 +0000 https://jax.de/?p=64220 Sobald der Deployment-Prozess mit Jenkins mehrere Stufen annimmt und zusätzlich noch automatisierte Tests in größeren Projekten dazukommen, muss man sich mit dem Thema Skalierung auseinandersetzen. Erschwerend kann hinzukommen, dass mehrere Teams mit Jenkins arbeiten und die fertigen Applikationen für mehrere Kunden in unterschiedlichen produktiven Umgebung bereitstellen sollen. Eine Möglichkeit, Jenkins zu skalieren, ist der Aufbau eines Jenkins-Clusters.

The post Jenkins Tutorial: So baut man einen Jenkins-Cluster appeared first on JAX.

]]>

von Jörg Jackisch

Sobald wir mit mehreren Teams arbeiten und mehrere Projekte abwickeln und bereitstellen, müssen wir an die Skalierung von Jenkins denken. Dies lässt sich aber mit einer Standardinstallation von Jenkins nur sehr schwer umsetzen, wenn überhaupt. Denn die einzelnen Build-Prozesse auf dem überforderten Jenkins-Server nehmen immer mehr Zeit in Anspruch, was bei der produktiven Zeit der Entwickler und Tester verloren geht. Bei weiter steigender Last auf dem Server wird dieser außerdem zunehmend instabil und fällt öfter aus. Der Frust sowohl bei den Entwickler- als auch Testteams und Teamleitern bis hin zu den Managern ist dann groß. Dieser Frust durch instabile Infrastrukturkomponenten schlägt sich schließlich auch in der Produktivität und Qualität des gesamten Projekts nieder. Das wollen wir natürlich mit allen Mitteln vermeiden. Also muss Skalierung her.

Jenkins skalieren – wie geht das?

Jenkins lässt sich sowohl horizontal als auch vertikal skalieren. In der vertikalen Skalierung kann man den Server mit mehr Hardwareressourcen ausstatten, damit die Jenkins-Applikation performanter ist. Dazu gibt es mehrere Möglichkeiten. Die einfachste ist, die Anzahl der Prozessoren und den Arbeitsspeicher zu erweitern. Auch die I/O-Performance kann man verbessern, indem man bei den Speichermedien zu SSD wechselt und die unterschiedlichen Komponenten mit Fibre Channel verbindet, wenn das Serversystem es zulässt. Bei der vertikalen Skalierung stößt man aber häufig auf technische Grenzen. Hinzu kommt, dass auch die eingesetzten Werkzeuge und Programme, die innerhalb von Jenkins genutzt werden, Multithreading unterstützen müssen, sonst bringt Skalierbarkeit im vertikalen Sektor nicht den gewünschten Effekt. Außerdem bringt das Aufrüsten der Serverhardware keine Ausfallsicherheit mit.

Lesen Sie auch: Grundkurs Microservices: Warum Frameworks nicht genug sind

Da die vertikale Skalierung an Grenzen stößt und man damit keine nachhaltige und langfristige Steigerung erzielen kann, sollte man rechtzeitig die horizontale Skalierbarkeit prüfen und umsetzen. Die horizontale Skalierung beschreibt dabei das Clustering der Anwendung. Jenkins setzt dabei auf eine Master-Agent-(Slave-)Skalierung. Ziel ist es, die Anwendung auf viele Server zu verteilen. Dabei gibt es zwei Alternativen: Zum einen spricht man von einem High-Performance-Computing-(HPC-)Cluster, das dazu dient, die Rechenkapazität zu erhöhen. Zum anderen gibt es High-Available-(HA-)Cluster, die größeren Wert auf die Ausfallsicherheit des Systems legen. Man unterscheidet grundsätzlich zwischen Hardware- und Software-Clustern. In beiden Kategorien gibt es unterschiedliche Methoden der Umsetzung.

 

Das Jenkins-Cluster in der Theorie

Bei der Einführung eines Jenkins-Clusters beginnt man mit der Einrichtung und Installation der Stand-alone-Variante von Jenkins. Er bildet die Basis des Clusters, verwaltet die gesamte Build-Umgebung und führt über seinen eigenen Executor die Build-Prozesse aus. Der erste Schritt ist es, den Jenkins-Server vertikal zu skalieren, zusätzlich passt man die JRE an. Damit wird man vorübergehend wieder eine stabile Infrastruktur erreichen.

Verschaffen Sie sich den Zugang zur Java-Welt mit unserem kostenlosen Newsletter!

In der Regel ist es allerdings nicht zu empfehlen, die Stand-alone-Variante in einer großen und produktiven Umgebung einzusetzen. Das sollte nur der erste Schritt bei der Einführung von Jenkins sein, denn hier kommt ein großes Sicherheitsrisiko hinzu. Eine Webapplikation führt die Prozesse mit dem Benutzer aus, mit dem der Jenkins-Server gestartet wurde. Meistens ist das ein Benutzer, der erhöhte Rechte besitzt. Das bedeutet, dass der Benutzer der Webapplikation Zugriff auf alle Ressourcen besitzt. Wenn ein Angreifer also Schadcode einschleusen kann, hat er Zugriff auf private oder geheime Daten.

Um vom einzelnen Server zu einer verteilten Lösung zu kommen, bietet Jenkins die Möglichkeit, mehrere sogenannte Worker-Prozesse auf unterschiedliche Server zu verteilen. Dabei bleibt der erste einzelne Server der Master und die weiteren dienen als sogenannte Agents. Dieses Prinzip nennt man Master/Agent Clustering. Dabei verwaltet der Masterserver die komplette Umgebung. Er verteilt einerseits die Deployment-Jobs, andererseits überwacht er, ob die jeweiligen Agents noch verfügbar sind. Der Master dient lediglich dazu, Informationen zu sammeln, Prozesse zu verwalten und letztendlich als grafische Oberfläche des gesamten Schwarms. Die Agents oder Worker sind Server, die nur vom Master Build-Prozesse entgegennehmen und bearbeiten.
Die Master/Agent-Methode gehört zu den HPC-Cluster-Methoden. Die Verteilung der Build-Jobs wird dabei auf alle vorhandenen Worker-Prozesse ausgelagert. Die Verteilung der Jobs lässt sich allerdings auch so konfigurieren, dass bestimmte Abläufe und Prozesse nur auf bestimmten Agents ausgeführt werden.

Damit der Master mit seinen Agents kommunizieren und nachvollziehen kann, ob sie noch verfügbar sind, kann man drei unterschiedliche bidirektionale Kommunikationsmethoden verwenden: Kommunikation per SSH (Secure Shell), JNLP-TCP (JNLP, Java Native Launch Protocol) oder JNLP-HTTP (Java Webstart). Falls es über keinen der vorhandenen Konnektoren möglich ist, eine Verbindung zwischen den Agents und dem Master aufzubauen, kann man auch ein eigenes Skript entwickeln. Als Programmiersprachen für eigene Konnektoren eignen sich vor allem Groovy oder Grails, doch auch Implementierungen mit der Programmiersprache Python sind gängig.

Bei der Methode mit dem SSH-Konnektor fungiert jeder Agent als SSH-Server und der Jenkins-Master ist der SSH-Client; in der Regel via Port 22 mithilfe eines SSH-Schlüssels, der auf dem Agent erstellt wird. Man kann die Verbindung aber auch ohne Schlüssel aufbauen. Dann wird sich der Server wie bei einer gewöhnlichen SSH-Kommunikation mit einem Benutzernamen und einem Passwort anmelden. Es ist empfehlenswert, den Benutzer zu nehmen, mit dem auch der Jenkins-Master läuft. Agents, die auf Microsoft Windows basieren, lassen sich mithilfe des Programms Cygwin verbinden. Cygwin [1] ist eine Sammlung von Open-Source-Werkzeugen, die unter Windows Funktionalitäten eines Linux-Systems bereitstellen.

Jenkins bietet mit dem JNLP-HTTP- und dem JNLP-TCP-Konnektor zwei Varianten, mit denen man mithilfe der Java-internen Protokolle Master und Agent miteinander kommunizieren lassen kann. Um JNLP zu verwenden, arbeitet Jenkins mit der Java-Web-Start-Technologie. Das ist wahrscheinlich die einfachste Art und Weise, Agent und Master zu verknüpfen, da man auf dem Agent lediglich die Java-Applikation des Masters ausführen muss und so bereits die Verbindung aufgebaut hat.

 

Der Software Architecture Track auf der JAX 2020

 

Das Jenkins-Cluster in der Praxis

Nach der ganzen Theorie erstellen wir jetzt ganz praktisch mit Docker einen Jenkins-Schwarm. Bei diesem Beispielszenario verbinden wir auf unterschiedliche Art und Weise vier Agents mit einem Masterserver. Es gibt also insgesamt fünf Server. Der Masterserver basiert auf einem Debian Linux, ebenso wie drei der Agents. Zusätzlich wird ein Server hinzugenommen, auf dem Windows läuft. Auf dem Masterknoten des Jenkins-Schwarms läuft das Jenkins Backend. Eine solch heterogene Verteilung bringt den Vorteil, dass man bestimmte Schritte innerhalb des Deployment-Prozesses auf einem bestimmten Betriebssystem ausführen kann. Als Beispiel dienen hier automatisierte Browsertests: Ich möchte meine Java-EE-Applikation sowohl auf einem Windows-Betriebssystem mit Microsoft Edge als auch auf einem Linux-Betriebssystem mit Mozilla Firefox testen.

Basierend auf einem Debian Linux oder einer ähnlichen Linux-Distribution muss man folgende Befehle ausführen, um eine lauffähige Jenkins-Umgebung zu erstellen:

 

 $> apt-get update && apt-get upgrade

 

Mit apt-get update wird der Paketmanager APT anhand seiner Konfiguration aktualisiert und mit dem Kommando apt-get upgrade das Betriebssystem. Das sollte man stets vor einer Installation einer neuen Software durchführen. Mit dem Befehl $> apt-get install default-jre wget wird die JRE mit all ihren Abhängigkeiten installiert. Hinzu kommt auch das Programm Wget, mit dem Jenkins zur sources-list-Datei von APT hinzugefügt wird. Zum Hinzufügen der Sources für APT benutzt man die Zeilen aus der Dokumentation von Jenkins [2]:

 

$> wget -q -O - https://pkg.jenkins.io/debian/jenkins-ci.org.key | apt-key add -
$> sh -c 'echo deb http://pkg.jenkins.io/debian-stable binary/ > /etc/apt/sources.list.d/jenkins.list'
$> apt-get update
$> apt-get install jenkins

 

Letztendlich wird mit dem letzten Kommando der Jenkins-Server installiert. Mit Docker kann man diese Installation abkürzen und virtualisiert den Server innerhalb eines Containers. Das Image, das dabei benutzt wird, befindet sich online im Docker Hub. Mit einem Befehl wird ein Container mit Linux erzeugt, auf dem ein Jenkins-Deployment-Server läuft:

 

$> docker run  --name=Master –link=slave3:2222 -d -ti -p 8080:8080 -p 50000:50000 jenkins:latest /bin/bash

 

Mit den Parametern –p werden die Ports 8080 und 50000 vom Container auf dieselben Ports auf dem Host gemappt. Docker lädt dabei jeden Image-Layer des Docker-Files von Jenkins einzeln herunter und richtet eine lauffähige Umgebung ein. Dabei wird die Bash auf dem Server gestartet, und man kann sich mit dem Befehl docker attach und der Container-ID mit der Bash des Servers verbinden. Zum Starten des Servers führt man den Befehl zum Starten von Jenkins aus: $> java -jar /usr/share/jenkins/jenkins.war &.

Nachdem der Jenkins-Masterserver gestartet ist, kann man in der Docker-Toolbox mit der Tastenkombination STRG + P + Q den Server wieder detachen.

Egal mit welcher Variante der Masterserver eingerichtet wurde, navigiert man im Browser seiner Wahl auf die jeweilige IP-Adresse des Servers. Bei Docker ist das die IP-Adresse des Docker Containers, auf dem der Jenkins-Server läuft, jeweils mit dem Port 8080. Unter http://IP_ADRESSE:8080/ wird jetzt der Set-up-Screen von Jenkins dargestellt. Dort muss man das initiale Passwort eingeben, das bei der Installation angezeigt wurde.

Falls man es übersehen hat, findet man es unter /var/jenkins_home/secrets/initialAdminPassword auf dem Server. Nach dem Set-up kann man sich beim Masterserver anmelden. Zu diesem Zeitpunkt hat man einen Stand-alone-Server eingerichtet, auf dem man die jeweiligen Projekte konfigurieren und einrichten kann.
Zum Erstellen der Slaveserver benutzt man wieder ein Docker Image, das ein Standard-Debian-Betriebssystem bereitstellt. Zum Starten der Server wird folgender Befehl ausgeführt:

 

docker run -ti --name=Slave1 -d debian:latest /bin/bash
docker run -ti --name=Slave2 -d debian:latest /bin/bash
docker run -ti --name=Slave3 –hostname=slave3 -p 2222:22 -d debian:latest /bin/bash

 

Dieser Befehl lädt das aktuelle Debian Image aus dem Docker Hub herunter und startet es als Daemon im Hintergrund (-d = detached). Mit dem Befehl docker ps –format “table {{.ID}}\t{{.Image}}\t{{.Names}}” sollten jetzt alle vier Linux-Server sichtbar sein.

Um die Linux Agents mit dem Masterserver zu verbinden, werden zuerst zwei der Linux-Server per JNLP verbunden. Einer wird per SSH die Verbindung mit dem Masterserver aufbauen. Im Jenkins-Master richtet man für die zwei weiteren Linux-Server jeweils einen weiteren Agent bzw. Knoten ein und lädt dann die Datei slave.jar auf den jeweiligen Slaveserver herunter. Die Knoten sollten sogenannte permanente Knoten sein, und als Startmethode wählt man LAUNCH AGENT VIA JAVA WEB START. Nach der Einrichtung der Agents wird dieser Befehl angezeigt:

 

Java -jar slave.jar -jnlpURL URL_DES_JENKINS_SERVERS -secret SECRET 

 

Als Parameter für die slave.jar werden die Verbindungsparameter benötigt. Der erste ist –jnlpURL, er teilt der slave.jar mit, wo sich der Server befindet. Der zweite ist ein Secret zur Verbindung (-secret). Den kompletten Befehl, um den Agent zum Master zu verbinden, sieht man, wenn man im Jenkins Backend durch die Punkte JENKINS VERWALTEN | KNOTEN VERWALTEN | SLAVE1 navigiert. Das Gleiche gilt für den zweiten Agent: JENKINS VERWALTEN | KNOTEN VERWALTEN | SLAVE2.

Der dritte Linux-Server kommuniziert per SSH mit dem Masterserver. Dazu geht man wieder mit dem Befehl docker attach in die Bash des Slaves und lädt als Erstes die slave.jar-Datei herunter. Ich benutze dafür Wget und verschiebe die Datei danach in das /bin-Verzeichnis. Zusätzlich muss man hier einen SSH-Server installieren, unter Debian Linux mit dem Befehl apt-get install openssh-server. Im nächsten Schritt legt man einen neuen Benutzer an und gibt ihm ein Passwort.

$> groupadd jenkins
$> useradd -g jenkins -d /home/jenkins -s /bin/bash jenkins
$> passwd jenkins

 

Nun erzeugt man einen SSH-Schlüssel, hinterlegt ihn auf dem Jenkins-Server und testet von ihm aus den Log-in.
Im Backend des Masterservers wird auch der dritte Knoten angelegt. Als Startmethode wird hier allerdings LAUNCH AGENT VIA EXECUTION OF COMMAND ON THE MASTER ausgewählt. Nach dieser Auswahl öffnet sich ein neues Konfigurationsfeld, in das man das Startkommando eintragen kann:

ssh -p 2222 [email protected] java -jar /bin/slave.jar

 

Alternativ bietet Jenkins auch die Startmethode STARTE SLAVE ÜBER SSH. Dort kann man den Hostnamen, Port, Benutzernamen und das Passwort eingeben. Der Jenkins-Server wird sich dann per SSH-Client auf den Server verbinden und ihn als Slave starten. In der Übersicht unter JENKINS VERWALTEN | KNOTEN sollten nun alle Agents dargestellt werden und online sein.

Zur Installation des Windows-Servers empfiehlt es sich, eine virtuelle Maschine zu benutzen. Dann folgt man den Installationsanweisungen des Set-ups von Microsoft. Man sollte allerdings bei der Auswahl der Version darauf achten, was man tatsächlich auf diesem Server ausführen möchte. In diesem Szenario empfiehlt sich Windows in der Clientversion 10, da man dort den Browser Edge zur Verfügung hat. Somit kann man seine Applikation auf Edge testen. Da man keine Serverversion des Betriebssystems benutzt, kann man auch weitere Versionen des Internet Explorers installieren.

Lesen Sie auch: Das Beste aus beiden Welten – Objektfunktionale Programmierung mit Vavr

Unter Windows benutzt man nahezu immer den Verbindungsaufbau per JNLP-HTTP, da dies eine sehr einfache Variante ist, Slaves mit dem Java Web Start zum Jenkins-Masterserver hinzuzufügen. Um den Windows-Server zum Schwarm als Agent hinzuzufügen, verbindet man sich per Remote Desktop zum Windows-Server. Auf diesem benutzt man einen Browser und navigiert zum Jenkins-Master. Nachdem man sich dort eingeloggt hat, klickt man in der Jenkins-Oberfläche auf den Menüpunkt JENKINS VERWALTEN | KNOTEN VERWALTEN und wählt den Menüpunkt NEUER KNOTEN, um einen neuen Agent hinzuzufügen. Im nächsten Fenster füllt man das Textfeld mit dem Namen seines Knotens aus, hier MS Windows Server, und wählt PERMANENT AGENT. Nach der Bestätigung kommt man zur Konfiguration des Knotens.

Das wichtigste Feld bei diesem Formular ist die Startmethode. Dort wählt man in unserem Beispiel LAUNCH AGENT VIA JAVA WEB START. Nach dem Speichern dieses Formulars kommt man zurück zur Übersicht der Knoten. Dort ist nun unser angelegter Windows-Knoten verfügbar, der allerdings mit einem roten Kreuz markiert ist, was bedeutet, dass der Agent nicht verbunden ist. Mit einem Klick auf den Agent kommt man zur Übersichtsseite, wo man mit Klick auf den Java-Webstart-Launch-Knopf die JNLP-Verdingung herstellen kann. Dazu wird eine slave-agent.jnlp-Datei heruntergeladen, die gestartet werden muss, und schon ist der Agent verbunden.

Quarkus-Spickzettel


Quarkus – das Supersonic Subatomic Java Framework. Wollen Sie zeitgemäße Anwendungen mit Quarkus entwickeln? In unserem brandaktuellen Quarkus-Spickzettel finden Sie alles, was Sie zum Loslegen brauchen.

 

Jetzt herunterladen!

 

Fazit

Zu Beginn eines Projekts sollte die Planung für den Deployment-Prozess und die Skalierung in der Architekturplanung enthalten sein. Man sieht, wie schnell und einfach man mit Docker einen Jenkins-Schwarm erstellen kann. Läuft der Cluster in einer produktiven Umgebung, kann langfristig und nachhaltig eine qualitativ hochwertige Software garantiert werden.

Links & Literatur
[1] Cygwin: https://www.cygwin.com
[2] Jenkins: https://wiki.jenkins-ci.org/display/JENKINS/Installing+Jenkins+on+Ubuntu

 

The post Jenkins Tutorial: So baut man einen Jenkins-Cluster appeared first on JAX.

]]>
RESTful APIs richtig gemacht – Anleitung für bessere REST-Schnittstellen https://jax.de/blog/software-architecture-design/restful-apis-richtig-gemacht/ Mon, 05 Mar 2018 11:04:47 +0000 https://jax.de/?p=62830 Wer schon einmal eine Domäne mit Microservices aufgebaut hat, wird es bereits wissen: APIs für die Service-zu-Service-Kommunikation sind von zentraler Bedeutung. Da jedes Team seinen eigenen Stil hat und Schnittstellen jeweils anders implementiert, kommt es über kurz oder lang zu einem Wildwuchs von verschiedenen Ansätzen. Gleich zu Projektbeginn einen Leitfaden mit Richtlinien und Beispielen zu definieren, hilft, einheitliche und möglichst selbsterklärende APIs zu gewährleisten.

The post RESTful APIs richtig gemacht – Anleitung für bessere REST-Schnittstellen appeared first on JAX.

]]>
von Thomas Spiegl und Manfred Geiler
REST mit JSON ist der heute am weitesten verbreitete Ansatz für neue Programmierschnittstellen. Unzählige Bücher, Vorträge, Blogs und andere Quellen im Internet beschäftigen sich mit dem Thema. Trotzdem scheint es große Auffassungsunterschiede in der Entwicklergemeinde zu geben, wie Webschnittstellen auszusehen haben. Die JSON-API-Spezifikation [1] legt genau fest, wie ein RESTful API basierend auf einem einheitlichen Standard implementiert werden sollte.
Zunächst aber noch ein paar Worte zu APIs. Bei der Definition eines API sollte sich der Entwickler genug Zeit nehmen, denn gut entworfene APIs führen automatisch zu besseren Produkten. Es empfiehlt sich, zu Beginn eines Projekts Richtlinien zu entwickeln, die die zentralen Anforderungen an ein API festhalten. Mithilfe von Schlüsselwörtern laut RFC 2119 [2] lässt sich festlegen, wie wichtig (oder zwingend) eine einzelne Anforderung ist. Themen für einen Richtlinienkatalog sind beispielsweise API-Namensgebung, HTTP Header und Operationen, Anfrageparameter oder Dokumentstruktur.
Richtlinien für den API-Entwurf helfen, ein gemeinsames Verständnis zu entwickeln. Grundsätzlich lässt sich sagen, dass Services, die einheitliche Standards verfolgen, leichter zu verstehen und einfacher zu integrieren sind. Daneben lassen sie sich effizienter umsetzen (gemeinsame Tools), sind stabiler gegenüber Änderungen und einfacher zu warten.

RESTful Styleguide für JSON-APIs

In der JSON-API-Spezifikation [1] finden sich konkrete Vorschläge, wie ein gut entworfenes REST API aussehen kann. Aus unserer Sicht ist die Spezifikation ein guter Einstiegspunkt ins Thema. Für unsere Beispiele begeben wir uns gedanklich in die Welt der Sportvereine. Wir wollen mit unserer gedachten IT-Lösung Teams, Manager und Spieler verwalten.

 


Abb. 1: Datenmodell

 

Das Diagramm (Abb. 1) zeigt die Entität Team und seine Beziehungen. Ein Team wird von einem Manager trainiert. Einem Team werden mehrere Spieler (Player) zugeordnet. Manager und Player sind jeweils vom Typ Person.

 

Manager oder Coach?

Die Begriffe Manager und Head Coach werden im englischen Fußball synonym für den Cheftrainer einer Mannschaft verwendet. Zum Zwecke der Lesbarkeit verwenden wir hier den Begriff Manager.

 

Das API: Mit wem dürfen wir sprechen?

Den Einstieg ins API bietet für unsere Beispielapplikation die zentrale Domain Entity Team. Über einen API-URL-Pfad werden klassische Operationen wie Lesen, Schreiben, Ändern, Löschen oder die Verwaltung von Beziehungen ermöglicht. Der Pfad dient auch der Dokumentation für den API-Anwender und sollte daher klar und einfach zu deuten sein. Als Einstiegspunkt für die Ressource Team bietet sich der Pfad /teams an. Der Pfadname ist in der Mehrzahl angegeben, schließlich sollen ja mehrere Teams gleichzeitig verwaltet werden können.
Wir legen daher im API fest, dass eine Entität über den Ressourcenpfad /{entity-type} verwaltet werden kann. Es handelt sich dabei typischerweise um eine Collection (1 bis n Teams). Als Namenskonvention gilt: ein Entity Type ist (oder endet auf) ein Nomen in der Mehrzahl, enthält nur Kleinbuchstaben, und einzelne Worte werden durch ein Minus getrennt:

 

richtig: /teams, /team-memberships

falsch: /Teams, /team, /teamMemberships, /team-search

Stay tuned

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

 

Datenobjekte abfragen: Welche Mannschaften gibt es eigentlich?

Beginnen wir mit einem einfachen HTTP GET auf den API-Pfad /teams:

GET /teams HTTP/1.1Accept: application/vnd.api+json

Im HTTP Header Accept mit dem Wert application/vnd.api+json wird festgelegt, dass in der Antwort ein JSON-Dokument erwartet wird. Jeder HTTP Request sollte diesen Header setzen. Das zurückgelieferte Dokument enthält ein Array aller Team-Objekte. Eine Response könnte also etwa so aussehen wie in Listing 1.

Listing 1: Array aller „Team“-Objekte


{

   "data": [

       {

           "id": "1",

           "type": "teams",

             "attributes": {  

                 "name": "FC Norden Jugend",  

                 "category": "juniors"  

             },  

             "links": {  

                 "self": "http://example.com/teams/1"  

             }  

         },  

         {  

             "id": "2",  

             "type": "teams",  

             "attributes": {  

                 "name": "FC Essen",  

                 "category:" "masters"  

             },  

             "links": {  

                 "self": "http://example.com/teams/2"  

               }

       }

   ]

}

Die Dokumentstruktur: Ordnung ist das halbe Leben

Zu einem gut strukturierten API gehört auch eine vorgegebene Dokumentstruktur. Sie hilft, wiederverwendbare Werkzeuge zu bauen, auch um das Zusammenspiel von Komponenten und den Daten im User Interface zu erleichtern. Ein API mit einheitlicher Struktur wirkt wie aus einem Guss – auch dann, wenn unterschiedliche Entwicklerteams an seiner Definition arbeiten. Die Struktur eines JSON-Dokuments wird von der JSON API Specification [3] genau festgelegt. Jedes Dokument besitzt immer eines von zwei Elementen: data oder errors. Optional können auch die Elemente meta, jsonapi, links oder included auf oberster Ebene enthalten sein. Das Element data enthält die eigentlichen Daten und kann entweder aus einem einzelnen Resource-Objekt oder aus einem Array von Resource-Objekten bestehen. Zusätzlich können Objektreferenzen oder auch null enthalten sein. Im Dokument aus dem letzten Beispiel wird im data-Element ein Ressource-Array, eine Liste von Datenobjekten vom Typ teams, geliefert. Das Datenobjekt wiederum hat die Elemente id, type, attributes und links. Die Attribute id und type stellen eine Referenz {“id”: “1”, “type”: “teams”} auf eine Entität vom Typ teams dar. Jede Ressource muss diese beiden Attribute besitzen. Datenobjekte bleiben so auch losgelöst vom API noch eindeutig identifizierbar. Der Typ deckt sich mit dem Pfadnamen im API und ist so immer ein Nomen im Plural. Die eigentlichen Daten (also z. B. “name”: “FC Essen”) der Entität werden im Element attributes geführt. Es scheint untypisch, dass Daten von der Objektreferenz getrennt gehalten werden. Im klassischen RDBMS oder in JPA werden Daten und Identifier meist gleichberechtigt in der Entity geführt. Die Trennung ist dennoch sinnvoll, da type und id rein technische Auszeichnungen des Objekts sind und nicht mit den fachlichen Attributen vermischt werden sollten. Durch Weglassen aller anderen Attribute erhalten wir außerdem jederzeit die Objektreferenz {“id”: “1”, “type”: “teams”}.

 

Nach Datenobjekten suchen: Wo ist mein Team?

Für die Suche nach unseren Teams hängen wir wie gewohnt URL-Abfrageparameter an den URL-Pfad. Um nach Attributwerten zu filtern, sieht die Spezifikation Parameter mit dem Namensmuster filter[{attribute_name}] vor. Die Unterscheidung in den Attributen erfolgt über den assoziativen Parameter {attribute_name}. Die Suche nach dem Team „FC-Norden“ sieht dann beispielsweise folgendermaßen aus:

 

GET /teams?filter[name]=FC+Norden

Filterparameter lassen sich mit einem logischen UND verknüpfen:

GET /teams?filter[name]=FC+Norden+Jugend&amp;filter[category]=juniors

oder können ein Set von Werten enthalten, was einem logischen ODER entspricht:

 

GET /teams?filter[category]=juniors,masters

 

Der reservierte Name filter hat den großen Vorteil, dass für den Anwender des API sofort klar wird, wozu der URL-Parameter verwendet wird.

 

Im Ergebnis blättern: vor und zurück …

Das Prinzip von assoziativen Parametern ist auch beim Blättern in der Ergebnisliste eine elegante Lösung: Zwei URL-Parameter, page[number] und page[size], genügen. Für den Anwender ist auch ohne Dokumentation die Bedeutung des Parameters sofort ersichtlich. Folgende Abfrage liefert Teams auf Seite drei mit maximal zehn Ergebnissen pro Seite (Listing 2).

Listing 2: Blättern in der Ergebnisliste

GET /teams?page[number]=3&amp;page[size]=10 HTTP/1.1
Accept: application/vnd.api+json
  
{
   "data": [
   { /*...*/ },
   { /*...*/ },
   /*...*/
   ],
   "links": {
      "first": "http://example.com/teams?[number]=1&amp;page[size]=10",
      "prev": "http://example.com/teams?page[number]=2&amp;page[size]=10",
      "next": "http://example.com/teams?page[number]=4&amp;page[size]=10"
      "last": "http://example.com/teams?page[number]=200&amp;page[size]=10"
   }
}

Im Antwortdokument werden die Links für das Blättern geliefert. Die Angabe dazu findet sich im links-Element parallel zu data. Somit wird auch klar, warum es vorteilhaft ist, die eigentlichen Daten in einem eigenen data-Element zu liefern. Auf der obersten Ebene lassen sich zusätzlich weitere wichtige Daten an den Empfänger des Ergebnisses übermitteln. Diese Eigenschaft des JSON-API-Dokuments ist auch bei der Behandlung von Fehlern nützlich.

 

Ein neues Datenobjekt anlegen: Willkommen FC Oldenburg

Wie legen wir nun ein neues Team an? Auch hier kommt der API-Pfad /teams zum Einsatz. Allerdings verwenden wir die HTTP-Methode POST. Der Request sieht nun so aus wie in Listing 3 dargestellt.

Listing 3

  POST /teams HTTP/1.1  
  Content-Type: application/vnd.api+json  
  Accept: application/vnd.api+json  
  {  
     "data": {  
        "type": "teams",  
        "attributes": {  
           "name": "FC Oldenburg",  
           "category": "seniors"
        }
     }
}

Wieder benutzen wir das Dokumentattribut data zur Übertragung der eigentlichen Teamattribute. Die id wird in diesem Beispiel vom Server vergeben und ist im Dokument daher nicht angegeben. Es stellt sich die Frage, warum die Angabe “type”: “teams” nicht ebenfalls entfallen kann: Lässt sich der type nicht ohnehin aus dem API-Pfad /teams ableiten? Das ist nur bedingt der Fall. Handelt es sich bei der Angabe im Pfad nämlich um einen abstrakten Basistyp, der verschiedene polymorphe Unterklassen kennt, dann benötigen wir für die Erzeugung einen konkreten (nicht abstrakten) Datentyp. Um also robuster gegen spätere Erweiterungen im Datenmodell zu sein, definieren wir das Feld type daher von vorn herein als zwingend. Als Antwort auf den POST Request wird das angelegte Team als Dokument geliefert (Listing 4).

Listing 4

{
   "data": {
      "type": "teams",
      "id": "3",
      "attributes": {
         "name": "FC Oldenburg",
         "category": "seniors"
      }    
      "links": {
         "self": "http://example.com/teams/3"
      }
   }
}

Die id wurde vom Server vergeben und ist nun ebenfalls im Dokument enthalten. Ein Link zur Ressource selbst wird im Feld self zurückgegeben.

HTTP-Methode POST

Wichtig zu wissen ist, dass ein POST per Definition den Status in einer Domäne verändert und somit nicht idempotent ist. Jeder POST führt zu einem neuen Ergebnis. Der Request POST /teams HTTP/1.1 erzeugt also jedes Mal eine neue Mannschaft. Soll ein Datenobjekt geändert werden, darf niemals POST, sondern es muss stattdessen PATCH oder PUT verwendet werden.

 

Ein Datenobjekt lesen: Zeig mir das Team mit der Nummer 3

Um ein einzelnes Datenobjekt zu lesen, wird ein API-Pfad verwendet, der dem Muster /{entity-type}/{entity-id} entspricht. Der Pfad wird also noch um die id der Entität erweitert. Es handelt sich dabei um den eindeutigen URI der zugehörigen Ressource. Der self-Link im vorangegangenen Beispiel entspricht genau solch einem URI. Das Team mit der Nummer 3 wird also mit einem einfachen HTTP GET gelesen:

 

GET /teams/3 HTTP/1.1
Accept: application/vnd.api+json

 

Als Antwort wird das Dokument mit der Referenz {“type”:”teams”, “id”:”3″} geliefert.

 

 

Ein Datenobjekt ändern: FC Essen ist gealtert …

Um ein Team zu ändern, benutzen wir wieder den URI der Ressource nach dem Muster /{entity-type}/{entity-id}. Mit der Methode PATCH können einzelne Attribute geändert werden, ohne das gesamte Objekt zu überschreiben (Listing 5).

Listing 5

PATCH /teams/2 HTTP/1.1
Accept: application/vnd.api+json
{
 "data": {
   "id": "2"
   "type": "teams",
   "attributes": {
    "category": "seniors"
   }
 }
}

Wir ändern lediglich das Attribut category, das Attribut name hingegen bleibt unverändert. Als Response wird das gesamte (geänderte) Dokument zurückgeliefert.
Eine HTTP-PATCH-Anfrage ist idempotent. Jeder Aufruf mit den gleichen Daten führt zum gleichen Status in der Domäne. Ein wiederholter, gleichlautender Aufruf ändert keine Daten am Server bzw. in unserer Datenbank.

 

Ein Datenobjekt löschen: FC Oldenburg gibt’s nicht mehr

Um ein Team zu löschen, verwenden wir, genau wie beim Lesen und Ändern, den URI der Ressource nach dem Muster /{entity-type}/{entity-id}. Als HTTP-Methode setzen wir jetzt einfach DELETE ein:

 

DELETE /teams/3 HTTP/1.1

 

Als Antwort erhalten wir einen entsprechenden HTTP-Statuscode, also 200 OK, wenn das Löschen erfolgreich war, oder 404 Not Found, wenn das zu löschende Objekt nicht existiert.

 

 

Über Beziehungen: Wer gehört zu wem?

Aus dem Datenmodell für unser Beispiel geht hervor, dass einem Team ein Manager und mehrere Spieler zugeordnet werden können. Sowohl Manager als auch Spieler sind vom Typ persons. Um den Manager und die Spieler eines Teams über das API verwalten zu können, wird der Begriff Relationship eingeführt. Das folgende Beispiel zeigt, wie die Beziehung manager im Element relationships abgebildet wird (Listing 6).

Listing 6: Beziehung „manager“-„relationships“

GET /teams/3 HTTP/1.1
Content-Type: application/vnd.api+json
Accept: application/vnd.api+json
{
"data": {
  "id:": "3"
  "type": "teams",
  "attributes": {
   "name": "FC Oldenburg"
},
  "relationships": {
   "manager": {
            "data": { "type": "persons", "id": "55" },
            "links": {
               "self": "http://example.com/teams/3/relationships/manager",
               "related": "http://example.com/teams/3/manager"
            }
         }
      }
   }
}

 

Beziehungen werden im Datenobjekt im Element relationships geliefert. Jede Relationship wird aus Sicht der verweisenden Entität über einen eindeutigen Namen identifiziert und enthält die Elemente data und links. Das data-Element kann ein oder mehrere Datenobjekte enthalten, je nach Kardinalität der Beziehung. Im konkreten Beispiel handelt es sich beim Manager um eine 1:1-Beziehung, es wird daher genau eine Referenz auf das Personen-Datenobjekt des Managers geliefert. Das links-Element enthält zwei Referenzen: Über den self-Link kann die Beziehung selbst verwaltet werden. Das Muster für den API-Pfad lautet hier: /{entity-type}/{entity-id}/relationship/{relationship-name}.
Wichtig zu erkennen ist hier, dass über den relationship-Link lediglich die Beziehung angesprochen wird. Der Link http://example.com/teams/3/relationships/manager liefert also die Relationship mit dem Namen manager, nicht das Dokument der referenzierten Person. Dieses wiederum bekommen wir mittels des zweiten Links mit dem Namen related. Der Link http://example.com/teams/3/manager liefert das Dokument mit den Daten zur Person. Auch hier sieht man, dass sich der type im Dokument nicht immer aus dem Request-URL ableiten lässt.

 

Die 1:1-Beziehung: Unser Team braucht einen Trainer!

Für das Team mit der Nummer 3 soll nun der Manager mit der Nummer 55 gesetzt werden. Für das Schreiben einer Beziehung kommt die HTTP-Methode PATCH zum Einsatz:

PATCH /teams/3/relationships/manager HTTP/1.1
Content-Type: application/vnd.api+json
Accept: application/vnd.api+json
{
   "data": { "type": "persons", "id": "55" }
}

Eine Beziehung, sofern sie optional ist, kann auch gelöscht werden. Das data-Element wird dazu auf null gesetzt:

PATCH /teams/3/relationships/manager HTTP/1.1
Content-Type: application/vnd.api+json
Accept: application/vnd.api+json
{
   "data": null
}

Der Weg zur digitalen Transformation

Developer Experience-Track entdecken

 

Die 1:n-Beziehung: Wer spielt mit?

Ähnlich verhält es sich mit der Verwaltung der Spielerbeziehungen. Im Unterschied zum Teammanager handelt es sich hier allerdings um eine 1:n-Beziehung zwischen Mannschaft und Spielern. Wir haben die Wahl zwischen HTTP-PATCH und –POST (Listing 7).

 

Listing 7

PATCH /teams/3/relationships/players HTTP/1.1
{
   "data": [
      { "type": "persons", "id": "10" },
      { "type": "persons", "id": "11" },
      { "type": "persons", "id": "12" },
      { "type": "persons", "id": "13" }
   ]
}
  
POST /teams/3/relationships/players HTTP/1.1
{
   "data": [
      { "type": "persons", "id": "17" },
      { "type": "persons", "id": "18" }
   ]
}

 

Der Unterschied ist auf den ersten Blick nicht gleich ersichtlich, wird aber schnell klarer: PATCH ersetzt die komplette Beziehung, also alle Spieler unserer Mannschaft. POST hingegen fügt die angegebenen Personen zu den bereits bestehenden Spielerbeziehungen hinzu, sofern sie noch nicht existieren. Analog können eine oder mehrere Spielerbeziehungen über ein HTTP-DELETE wieder gelöscht werden.

DELETE /teams/3/relationships/players HTTP/1.1
{
"data": { "type": "persons", "id": "10" }
}

Auch beim DELETE kann das data-Element wieder eine einzelne Referenz oder ein Array, eine Liste von Referenzen, enthalten.

Zugehörige Objekte einbinden: Ich will alles sehen!

Mit den bisher gezeigten Möglichkeiten wäre es sehr umständlich, alle Daten für ein bestimmtes Team, also die gesamte Mannschaft inklusive Trainer- und Spielernamen, zu laden. Zunächst müsste man das Teamobjekt lesen. Daraufhin müsste der Entwickler für jede Relation einen weiteren API-Aufruf absetzen, um die zugehörigen Personenobjekte zu laden. Das wäre weder performant, noch wäre der Spaß beim Programmieren sehr groß. JSON API spezifiziert hier eine elegante Möglichkeit, mit einem einzigen API-Aufruf alle benötigten Beziehungsobjekte gleich mitzuladen. Der URL-Parameter include ist dafür reserviert. Soll zum Team auch die Person zur Beziehung manager geladen werden, setzen wir als URL-Parameter ?include=manager (Listing 8).

 

 

Listing 8: Beziehungsobjekte laden

GET /teams/3?include=manager HTTP/1.1
Accept: application/vnd.api+json
  
{
   "data": {
      "id:": "3"
      "type": "teams",
      "attributes": {
         "name": "FC Oldenburg"
      },
      "relationships": {
         "manager": {
            "data": { "type": "persons", "id": "55" },
            "links": {
               "self": "http://example.com/teams/3/relationships/manager",
               "related": "http://example.com/teams/3/manager"
            }
         }
      }
   },
   "included": [
      {
         "id:": "55"
         "type": "persons",
         "attributes": {
            "name": "Coach Maier"
         }
      }
   ]
}

 

Die Daten des Trainers werden nun im Element included parallel zum Element data geliefert. Analog lassen sich auch die Spieler mit ein- und demselben Aufruf anfordern (Listing 9).

 

Listing 9: Spieler anfordern

GET /teams/3?include=manager,players HTTP/1.1
Accept: application/vnd.api+json
  
{
   "data": {
      "id:": "3", "type": "teams",
      "attributes": { "name": "FC Oldenburg" },
      "relationships": {
         "manager": {
            "data": { "type": "persons", "id": "55" },
            "links": { "related": "http://example.com/teams/3/manager" }
         },
         "players": {
            "data": [
               { "type": "persons", "id": "10" },
               { "type": "persons", "id": "11" }
            ],
            "links": { "related": "http://example.com/teams/3/players" }
         }
      }
   },
   "included": [
      {
         "id:": "55", "type": "persons",
         "attributes": { "name": "Coach Maier" }
      },
      {
         "id:": "10", "type": "persons",
         "attributes": { "name": "Johnny Wirbelwind" }
      },
      {
         "id:": "11", "type": "persons",
         "attributes": { "name": "Franz Luftikus" }
      }
   ]
}

 

Der include-Parameter eignet sich idealerweise auch bei der Suche, also der Abfrage einer Liste von Datenobjekten:

GET /teams?include=manager

 

Die include-Methode und die zugehörige Ergebnisstruktur haben ein paar wesentliche Vorteile gegenüber herkömmlichen Ansätzen. Die Daten der einzelnen Objekte bleiben streng getrennt. Ein inkludiertes Objekt ist nur einmal im Dokument vorhanden, auch wenn es mehrfach referenziert wird. Schließlich wird auch vermieden, dass das API speziell auf die Anforderungen verschiedener API-Konsumenten zugeschnitten werden muss (z. B. /teams/readAllWithManager?id=3 oder ähnliche Auswüchse).

Hier gilt es zu beachten, dass serverseitig ein solcher Request zu sehr aufwändigen Datenbankabfragen führen kann. In unserem Beispiel würde etwa die Anfrage /teams?include=manager,players die gesamte Mannschaftenliste inklusive aller Trainer- und Spielerdaten ausliefern. Denkt man kurz über die entsprechende SQL-Datenbankabfrage mittels Joins nach, kann man erahnen, dass das bei großen Datenmengen nicht unbedingt ideal ist. Es kann also sinnvoll sein, das Inkludieren bestimmter Beziehungen nicht automatisch für alle API-URL-Pfade zu erlauben. Wollen wir lediglich die Spieler eines Teams in einer Anfrage lesen, kann wieder der self-Link aus dem Relationship-Element players seinen Dienst tun. Der Link aus dem Beispiel http://example.com/teams/3/players liefert als Antwort alle Objekte vom Typ persons aus der Beziehung players.

Stay tuned

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

 

Aktionen ausführen: Mach etwas!

Bisher wurden die klassischen Aktionen wie Create, Read, Update und Delete vorgestellt. Genügen diese Basisoperationen nicht, muss das API entsprechend erweitert werden. Eine Aktion (wie ‘suspend player from team’) führt meist auch ein Verb im Namen der Operation. Das REST-Design-Pattern empfiehlt hingegen, keine Verben im URL zu verwenden. Wie lassen sich weitere Serveroperationen in das API mit aufnehmen? Die JSON-API-Spezifikation geht auf Aktionen im API nicht näher ein. An einem Beispiel stellen wir ein paar Ansätze vor: Ein Spieler verletzt sich, und der Teammanager soll per Nachricht darüber informiert werden. Die entsprechende Methode steht bereits serverseitig zur Verfügung und soll nun in das API aufgenommen werden.

Action Verb: Ruf mich auf!

In der ersten Variante wird die Aktion in den Pfad aufgenommen: /players/1/notifyInjury. Die Verwendung des Verbs notify deutet auf eine Art Remote Procedure Call hin – etwas, das wir beim RESTful-Ansatz eigentlich vermeiden wollen. Jedenfalls handelt es sich bei notifyInjury nicht um eine Ressource im herkömmlichen Sinn. Es ist völlig unklar, welche der HTTP-Methoden GET, POST oder PATCH auf diese Pseudoressource angewandt werden sollen. Diese Methode ist daher nicht zu empfehlen.

Action Patch: Reagiere auf meine Änderung!

Soll das Verb nicht im API-Pfad vorkommen, lässt sich die serverseitige Methode auch durch einen einfachen PATCH auslösen (Listing 10).

Listing 10

PATCH /players/1 HTTP/1.1
{
   "data": {
      "id": "1"
      "type": "players",
      "attributes": {
         "condition": "injured"
      }
   }
}

 

Am Server werden Updates auf das Attribut condition entsprechend überwacht, und bei einer Änderung des Status wird der Teammanager über die Verletzung seines Spielers informiert. Die serverseitige Action muss bei dieser Variante idempotent implementiert werden: Der Manager darf nur bei einer Änderung des Status benachrichtigt werden, nicht bei jedem PATCH auf diese Ressource.

Action Metadaten: Schick mir einen Hinweis!

Eine weitere Möglichkeit bietet das (im JSON API spezifizierte) meta-Element (Listing 11).

 

Listing 11

POST /players/1 HTTP/1.1
{
   "data": {
      "id": "1", "type": "players"
   },
   "meta": {
   "action": "notifyInjury"
   }
}

 

Ein POST auf eine Ressource wird hier als Senden einer Aktion interpretiert. Die auszuführende Aktion wird dabei im Block meta als action mitgegeben. Zu beachten ist, dass ein POST nicht idempotent ist. Sollte der Spieler bereits verletzt sein, muss der Server mit einem Fehler reagieren. Der Vorteil dieser Variante: Kein zusätzlicher URL-Pfad wird definiert, sondern lediglich dem POST auf eine vorhandene Ressource eine Bedeutung zugeordnet.

Action Queue: Schick mir deine Aktion!

Will man die Aktion dennoch im API-Pfad führen, besteht die Möglichkeit, eine Action Queue [4] zu definieren. Der URL /players/1/actions bietet die Möglichkeit neue Actions (also Ressourcen vom Typ actions) wie gewohnt mittels POST über das API anzulegen. Optional könnten hier sogar vergangene Actions mittels GET ausgelesen werden. Der Vorteil dieser Variante: Eine Aktion ist eine (Pseudo-)Ressource und entspricht dem REST-Pattern. Der Nachteil: Es wird ein zusätzlicher Pfad im URL benötigt.

Fehlerbehandlung: Was läuft hier schief?

Statuscodes dienen dazu, den Schnittstellenbenutzer über den Ausgang der Request-Verarbeitung zu informieren. Das HTTP-Protokoll gibt Auskunft über mögliche Ergebnisse (Tabelle 1).

 

Statusgruppe Bedeutung2xx Verarbeitung ok3xx Umleitung4xx Fehlerhafte API-Bedienung5xx Serverseitiger FehlerTabelle 1: HTTP-Statuscodes

 

Die JSON-API-Spezifikation bietet die Möglichkeit, zusätzlich zum HTTP-Statuscode auch Fehlermeldungen im Element errors zu liefern (Listing 12).

Listing 12

HTTP/1.1 400 Bad Request
Content-Type: application/vnd.api+json
{
   "errors": [
      {
         "code": "100",
         "status": "400",
         "source": {"pointer": "data.attributes.name"},
         "title":   "Mandatory field",
         "detail": "Attribute ‘name‘ must not be empty"
      }
   ]
}

Hier wird auf eine fehlerhafte Bedienung des API durch den Client hingewiesen. Die Antwort enthält kein data-Element und liefert stattdessen neben dem HTTP-Status 400 (Bad Request) eine oder mehrere Fehlermeldungen mit weiteren Details. Die JSON-API-Spezifikation nennt für ein Error-Objekt noch weitere Attribute [5]. Den Einsatz von HTTP-Statuscodes sollte man nicht übertreiben. Auf HTTP-Ebene genügen in der Regel ein paar wenige Codes. Eine detaillierte Beschreibung liefert ohnehin das errors-Element des Dokuments.

 

Datentypen: Was bist du?

Auf die Verwendung von Datentypen geht die JSON-API-Spezifikation kaum ein. An dieser Stelle sei lediglich erwähnt, dass die Definition von unterstützten Typen für Attributwerte unbedingt Teil eines Richtlinienkatalogs sein sollte. Einheitliche Formate und die entsprechende JSON-Darstellung für Datum, Zeitstempel oder auch eigene Klassen wie Money und Ähnliches sollten unbedingt im Vorfeld festgelegt werden.

API-Versionierung: Welchen Dialekt sprichst du?

Der erste Schritt ist schnell gemacht. Ein API wurde entworfen und ist im Einsatz. Ab diesem Zeitpunkt sollte die Schnittstelle möglichst stabil gegenüber Änderungen sein. Zusätzliche Attribute führen in der Regel zu keinen Verwerfungen mit den Konsumenten der Schnittstelle. Clients sollten gegenüber solchen Anpassungen robust implementiert werden. Was aber, wenn sich am Schema des API Grundlegendes ändert? Sind andere Entwicklerteams oder auch externe Partner von der Änderung betroffen, kann die Synchronisierung aller notwendigen Tätigkeiten aufwendig und schwierig werden. Daher muss sich der API-Entwickler auch Gedanken über unterschiedliche Versionen eines API machen. Die JSON-Spezifikation bietet hierzu keine vordefinierte Lösung. Sofern man rückwärtskompatible, alte Aufrufe weiterhin unterstützen möchte (oder muss) bietet sich die URI-Versionierung als Lösungsansatz an:


/v1/teams
/1.0/teams
/v1.1/teams

Stay tuned

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

 

Wer den RESTful-Ansatz ernst nimmt, dem muss hier allerdings bewusst sein, dass mit jeder Einführung einer neuen Version aus der Sicht von außen ein vollständiges, neues Datenmodell entsteht. Niemals sollte die Verlinkung der Ressourcen untereinander auf verschiedenen API-Versionen basieren. Für einen Client sieht es so aus, als ob es sich um unabhängig voneinander existierende Datenquellen mit unterschiedlichen Datensätzen handeln würde.
Zu den Vorteilen in diesem Zusammenhang zählen die Einfachheit in der Umsetzung (abgesehen von der Rückwärtskompatibilität) und die unkomplizierte Anwendung. Ein Nachteil ist, dass dieselbe Ressource unter mehreren Pfaden abrufbar ist. Der URL für ein und dieselbe REST-Ressource ist nicht für alle Zeiten fix, und Links in externen Systemen müssen eventuell angepasst werden.
Außerdem fehlt ein kanonischer Identifikator: Zwei unterschiedliche URIs können auf dieselbe dahinterliegende Resource verweisen, ohne dass dies dem Client bewusst ist. Für weitere Ansätze zum Thema Versionierung verweisen wir auf das Buch „Build APIs You Won’t Hate“ [6].

Fazit: Wir sind gewappnet

Ein starker Trend in der Softwareindustrie geht weg vom Monolith hin zu kleinen Domain- und Microservices. Diese Entwicklung erfordert stabile und gut durchdachte APIs, um nicht im Schnittstellenchaos zu versinken. Die JSON-API-Spezifikation bietet sehr viele und gute Lösungsansätze rund um das Thema RESTful JSON. Eigene Richtlinien lassen sich aus ihr leicht ableiten. Von Anfang an gut gegen den API-Wildwuchs gewappnet, steht einer erfolgreichen Lösung nichts mehr im Weg.

 

Links & Literatur
[1] JSON-API-Spezifikation: http://jsonapi.org
[2] Keywords for use in RFCs to Indicate Requirement Levels: https://www.ietf.org/rfc/rfc2119.txt
[3] JSON API/Document Structure: http://jsonapi.org/format/#document-structure
[4] Thoughts on RESTful API Design: http://restful-api-design.readthedocs.io/en/latest/methods.html#actions
[5] JSON API/Errors: http://jsonapi.org/format/#errors
[6] Sturgeon, Philip: „Build APIs You Won’t Hate“: https://leanpub.com/build-apis-you-wont-hate

The post RESTful APIs richtig gemacht – Anleitung für bessere REST-Schnittstellen appeared first on JAX.

]]>