Über unsMediaKontaktImpressum
Siegfried Steiner 21. Februar 2018

Sind Frameworks in der Softwareentwicklung alternativlos?

Auf der Suche nach großem Funktionsumfang und gleichsam eleganter Umsetzung greifen Architekten und Entwickler gerne zu bekannten aber oftmals aufgeblähten Frameworks. Dabei lassen sich die mit solchen Frameworks adressierten Probleme durch selbstentwickelte und passgenaue Bibliotheken mitunter schneller und besser lösen.

Meist wird der Griff zu einem großen und nicht selten übermächtigen Framework mit Zeitersparnis und Effizienz begründet. Schließlich sei darin bereits alles vorhanden, um mehrere vorliegende und eventuell zukünftige Probleme zu lösen. Insbesondere im Zeitalter von Microservices und dem Internet of Things scheint es übertrieben, zwischen der Java Virtual Machine als Laufzeitumgebung und der eigentlichen Applikation noch eine weitere Laufzeitumgebung in Form eines Frameworks zu schieben. In diesem Umfeld sollte der Vorsatz gelten, die Komplexität im Einzelnen stark zu reduzieren. Schließlich müssen es gar nicht immer die großen Standard-Frameworks sein, um ein Problem effizient zu lösen.

Die beinahe in Vergessenheit geratenen Bibliotheken können ebenfalls eine adäquate Alternative sein. Bibliotheken vereinen sogar mehrere Vorteile in sich: Man kann klein starten und dennoch groß wachsen, ohne dabei in Komplexität zu versinken. Vorausgesetzt, sie wurden planvoll konzipiert und entwickelt. Darüber hinaus vertragen sich Bibliotheken mit Frameworks und Application Servern gleichermaßen gut und lassen sich auch noch potentiell in alle möglichen anderen Klassen von Software integrieren.

Die Zutaten für eine gute Bibliothek sind ein wenig syntaktischer Zucker, Method Chaining sowie eine Handvoll Pattern, die es den bequemen Softwareentwickelnden erlauben, sich viele Mühen zu sparen. Anhand eines konkreten Umsetzungsbeispiels schildert dieser Artikel, wie die Zutaten zu vermengen sind.

Unterschied zwischen Application Server, Framework und Bibliothek
An der Frage nach dem Unterschied zwischen Application Server, Framework und Bibliothek scheiden sich die Geister und es entbrennen hitzige Diskussionen unter Softwareentwicklern. Ein Framework ist unter anderem aktiv und übernimmt die Kontrolle. Es besteht aus einer Menge kooperierender Klassen und ist übergreifend und eingreifend in der Anwendung aktiv. Dabei dreht es den Programmfluss der Anwendung um, auch bekannt als "inversion of control". Ähnliches gilt für Application Server. Eine Bibliothek verhält sich passiv und wird verwendet, API-Aufrufe korrelieren mit dem Code, sie ist punktuell erweiterbar und wirkt lokal begrenzt. Sie stellt kapselbare Bausteine zur Verfügung.

Eine Bibliothek für Konfigurationsmanagement

Geistlos und ohne jeden Mehrwert die Funktionalität eines beliebigen Frameworks als Bibliothek nachzubauen, wäre Zeitverschwendung. Stattdessen dient in diesem Artikel ein fiktiver Microservice als Ausgangsbasis für eine Gegenüberstellung zwischen dem Framework Spring Boot und einer beispielhaft programmierten Bibliothek.

  1. Der Microservice besitzt zwei Zuständigkeiten, die abstrakt als "This" und "That" bezeichnet sind.
  2. Der Microservice wird mit mindestens zwei unterschiedlichen JSON-Dateien konfiguriert. Für jede funktionale Zuständigkeit gibt es eine JSON-Datei.
  3. Der Microservice soll in der Lage sein, seine Konfigurationen nicht nur zu lesen, sondern auch wieder zurück in die jeweiligen JSON-Dateien zu sichern.
  4. Der Microservice muss JSON-Dateien observieren und selbständig Änderungen erkennen. Wird etwa ein Konfigurationsparameter in Form eines Key-Value-Paares in einer der Dateien geändert, so soll ein Observer im Microservice benachrichtigt werden.
  5. Im Sinne eines Servers soll der Microservice mindestens zwei TCP-Ports mit unterschiedlicher Funktionalität je Port binden, um HTTP-Anfragen zu beantworten.

Wir benötigen ein cleveres Konzept!

Die Bibliothek soll eine angemessene Funktionsvielfalt aufweisen, ihr Implementationsaufwand aber gleichzeitig möglichst gering bleiben. Angemessene Funktionsvielfalt meint, dass nur benötigte Funktionalität implementiert und mögliche zukünftige Funktionalität nur berücksichtigt wird. Angemessene Funktionsvielfalt hat also nur den Anspruch, dem geplanten Zweck gerecht zu werden. Entsprechend fällt der Funktionsumfang der Bibliothek zunächst klein aus und wird dann mit jedem weiteren Schritt in die eine oder andere Richtung ergänzt.

Treffen wir für dieses inkrementelle Vorgehen keine Vorkehrungen, erhalten wir schnell ein komplexes Konstrukt, das die Funktionsvielfalt im Programmcode mit jedem Inkrement ungünstiger abbildet. Das würde zu aufgeblähtem Programmcode mit unzähligen Fallunterscheidungen und Redundanzen führen. Also müssen wir uns überlegen, wie sich eine angemessene Funktionsvielfalt mit geringem Aufwand ermöglichen lässt. Mit anderen Worten: "Wir benötigen ein cleveres Konzept!"

Funktionsvielfalt lässt sich etwa durch die Kombination beliebiger einfacher und dedizierter Bestandteile erreichen. Diese Bestandteile erfüllen einen ganz klaren Zweck und weisen somit eine starke Kohäsion auf. Die Bestandteile müssen so konstruiert sein, dass sie sich mit anderen analog konstruierten Bestandteilen aber gänzlich anderem Funktionsumfang kombinieren lassen. Jeder weitere hinzugefügte Bestandteil reichert den Funktionsumfang an.

Über das Verarbeiten von Konfigurationen

Konfigurationsparameter können aus unterschiedlichen Quellen und Ressourcen stammen und eine unterschiedliche Spezifizität aufweisen (s. Abb. 2). Das erschwert die Wartbarkeit und Fehlersuche. Deshalb ist es entscheidend, die Dimension zu verstehen.

Die Quelle bestimmt die Art, wie Parameter zugeführt werden. Die Bandbreite reicht von Konfigurationsdateien über Umgebungsvariablen bis hin zu Kommandozeilenargumenten. Je volatiler die Quelle, desto höher die Priorität beim Auswerten durch das Framework.

Die Ressource bestimmt, woher die Parameter geladen werden sollen. Die Parameter können dabei aus dem lokalen Dateisystem oder aus dem Programmarchiv (JAR) der Applikation geladen werden. Auch ein entferntes GIT-Repository ist möglich. Je relevanter für den Betrieb, desto höher die Priorität der Ressource.

Profile bilden die Spezifität der Parameter ab, die je nach Aktivierung eine höhere oder niedrigere Priorisierung aufweisen. Dementsprechend stechen Parameter aus einem höher priorisierten Profil solche aus einem niedriger priorisierten Profil. Profile trennen dazu Teile der Konfigurationsdateien, um sie nur in bestimmten Umgebungen zu berücksichtigen. Sie werden innerhalb der Konfiguration oder im Programmcode aktiviert.

Aus diesen drei Dimensionen Quelle, Ressource und Spezifität ergibt sich ein sehr großer Lösungsraum für die Herkunft einzelner Parameter, was die Wartbarkeit und Fehlersuche bei Fehlkonfigurationen erschweren kann. Anzumerken ist, dass nicht jede Kombination gültig ist. So existieren keine weiteren Dimensionen für die Umgebungsvariablen bis hinunter zu den Kommandozeilenargumenten.

Initiieren: Den Service konfigurieren

Der Microservice soll beim Start zwei Konfigurationsdateien in JSON-Notation laden sowie die darin enthaltenen Parameter parsen und für die Initialisierung auswerten. Für das Fallbeispiel tragen die Dateien schlicht die Namen this.json und that.json.

Listing 1: this.json

{
    "unit": "CELSIUS",
    "resolution": "10",
    "frequency": "100",
    "port": "8082"
}

Listing 1: that.json

{
    "unit": "METER",
    "resolution": "100",
    "frequency": "42",
    "port": "8083"
}

So geht’s in Spring Boot

Spring Boot unterstützt die JSON-Notation nicht direkt [1]. Allerdings erlaubt Spring Boot sowohl den Einsatz von Java-Properties [2] als auch von YAML-Properties. Anstatt also JSON-Dateien zu parsen, weichen wir für Spring Boot vom Beispiel ab und schreiben die Konfigurationen als Java-Properties um.

In der Standardkonfiguration lädt Spring Boot jedoch nur eine Datei neben ihren YAML- und profilbasierten Pendants [3]. Allerdings haben wir es im Fallbeispiel mit zwei zu ladenden Konfigurationsdateien zu tun. Die Lösung: Zusätzlich zu berücksichtigenden Dateien lassen sich in Spring Boot mittels der Annotation @PropertySource einer als @Configuration annotierten Konfigurationsklasse mitteilen.

Listing 2: ThisConfig.java

@Configuration
@PropertySource( 
    "classpath:this.properties", "classpath:that.properties"
)
public class ThisConfig {
    ...
} 

Eine entsprechend annotierte Klasse enthält dann die Deklarationen für die zusätzlichen Dateien, s. Listing 2. Dieser über @PropertySource eingeleitete Mechanismus würde aber dafür sorgen, dass alle gelesenen Konfigurationsparameter in einem gemeinsamen Topf landen. Gleichnamige Parameter aus verschiedenen Dateien würden sich dann gegenseitig überschreiben. Deshalb müssen die Parameter dateiübergreifend eindeutig sein. Aus dem Parameter unit der JSON-Beispiele wird deshalb this.unit in der Datei this.properties und that.unit in der Datei that.properties. Gleiches gilt für die anderen Parameter, wie Listing 3 zeigt.

Listing 3: this.properties

this.unit=CELSIUS
this.resolution=10
this.frequency=100
this.port=8082

Listing 3: that.properties

that.unit=METER
that.resolution=100
that.frequency=42
that.port=8083

Für den Zugriff auf Konfigurationsparameter annotieren wir die entsprechenden Member-Variablen einer Spring Bean noch mit der @Value-Annotation. Für den Parameter this.unit lautet die entsprechende Member-Variable dann @Value("${this.unit}").

Listing 4: ThisService.java

@Service
public class ThisService {
   @Value("${this.unit}")
   private UnitOfLength unit;
   ...
} 

Mit Spring Boot können wir zwar weder Konfigurationsdateien in JSON-Notation verarbeiten, noch diese gesondert verarbeiten, dafür erhalten wir mit Spring Boot aber reichhaltige Möglichkeiten bezüglich Quelle, Ressource und Spezifität der Konfigurationsparameter.

So macht’s die Bibliothek

Unsere Bibliothek sollte flexible Mechanismen zum Laden von Konfigurationsparametern bereitstellen. Sie sollten also nicht nur aus Dateien stammen können, sondern etwa auch aus I/O-Streams. Damit würden wir uns vom Dateisystem lösen. Die Konfigurationsparameter können zudem unterschiedlichen Ursprungs sein, etwa aus Ressourcen geladen oder aber aus den Umgebungsvariablen respektive Java System Properties ermittelt werden.

Um effektiv mit diesen Möglichkeiten zu arbeiten, bietet sich ein einheitliches Konzept zum Umgang mit Konfigurationsparametern an: Eine generische Schnittstelle ermöglicht einen einheitlichen Umgang mit Parametern unterschiedlicher Herkunft. Für verschiedene Anwendungsfälle erstellen wir verschiedene Implementierungen dieser generischen Properties.

Ein einheitliches Modell hilft uns, die hierarchischen Strukturen, wie sie in JSON oder YAML möglich sind, abzubilden. Die in JSON oder YAML definierten Strukturen sollen sich einerseits in dieses einheitliche Modell überführen lassen. Andererseits sollen sich aus dem Modell die hierarchischen Strukturen auch wieder generieren und als JSON oder YAML speichern lassen. Das Entwurfsmuster Canonical Model steht genau für derlei Anforderungen. Damit öffnen wir ferner die Tür für eine ganze Reihe weiterer Notationen, etwa TOML oder XML, ohne uns im Vorfeld auf diese festlegen oder einschränken zu müssen. Unser Canonical Model wird dabei von der generischen Schnittstelle ummantelt.

Beim Erstellen von Profilen und beim Priorisieren von Konfigurationsparametern helfen uns außerdem das Decorator Pattern sowie das Composite Pattern, die sich wunderbar mit dem Ansatz einer generischen Schnittstelle vertragen. Wichtig: Die generische Schnittstelle unterscheidet einen unveränderlichen und einen veränderlichen Teil, auch als immutable und mutable bekannt. Ändernde Operationen an einer Implementierung mit mehreren enthaltenen Konfigurationen können nicht immer eindeutig der tatsächlich zu ändernden Konfiguration zugeordnet werden. Diese implementieren nur den immutable-Teil. Jede potentielle Erweiterung der Bibliothek ermöglicht somit, die Properties eindeutig zu lesen, aber nicht unbedingt sie eindeutig zu ändern. Der unveränderliche Teil ist somit der kleinste gemeinsame Nenner, der als Input für Instanzen des veränderlichen Teils dienen kann.

Damit wir unsere Funktionalität auch komfortabel nutzen können, bietet sich syntaktischer Zucker in Form von statischen Imports und Method Chaining an. Zuerst lädt die Bibliothek die Konfigurationsparameter aus einer JSON-Datei in eine Properties-Instanz, s. Listing 5. Eine zweite Datei würde sich analog und unabhängig von der ersten in eine eigene, weitere Properties-Instanz laden lassen.

ResourcePropertiesBuilder that = seekFromJsonProperties( "that.json" );

Listing 5: ThisService.java

import static org.refcodes.configuration.PropertiesSugar.*;
public class ThisService {
    public ThisService() {
        ResourcePropertiesBuilder properties = seekFromJsonProperties(
            "this.json"
        );
        ThisConfig config = properties.toType( ThisConfig.class );
    }
} 

Listing 5: ThisConfig.java

public class ThisConfig { 
    public int resolution;
    ...
}

Die Konvertierung der Properties (Zeile 7) ist gar nicht notwendig, da die Properties wie eine Map benutzt werden können, um auf die darin enthaltenen Werte zuzugreifen:

value = properties.get("key"); 

So gewähren die Properties Zugriff mittels Key-Value-Paaren. Dadurch, dass die Properties die Map erweitern, werden der Bibliothek eine Vielzahl weiterer Einsatzmöglichkeiten eröffnet. Mit einer solchen Erweiterung laufen wir zwar unter Umständen in ein Fragile Base Class-Problem [4], das sich aber mit dem Einsatz von Default-Methoden mindern lässt und dennoch eine nahtlose Integration mit dem Java Collections-Framework erlaubt [5].

Ohne viel Aufwand bieten unsere Properties auch folgende Möglichkeit:

int value =  properties.getInteger("key");

Solche Methoden lassen sich komfortabel als Default-Methoden in der einheitlichen Schnittstelle implementieren, da nur die vorhandenen Methoden der Schnittstelle selber genutzt werden müssen. Selbst nachträgliche Erweiterungen der Schnittstelle ohne notwendige Anpassungen an den Implementierungen sind damit möglich!

Kombinierbarkeit: Die Bibliothek erweitern

Bereits im vorangegangen Kapitel haben wir das Ziel erreicht und können für jeden Teilbereich unseres Microservice eine unabhängige Instanz der JSON-Properties erzeugen. Im nächsten Schritt soll die Funktionalität der Microservices erweitert werden, indem wir Umgebungsvariablen oder Java System Properties auf unsere Properties abbilden, Properties priorisieren und Properties auf Profile projizieren. Wenn wir diese Funktionalität kombinierbar gestalten, dann schaffen wir einen Lösungsraum beliebiger Komplexität mit geringem Aufwand.

So macht’s die Bibliothek

Ein wenig syntaktischer Zucker reicht, damit unsere Bibliothek weder eine komplexe noch eine schlecht lesbare Umsetzung aufweist. Wie Listing 6 zeigt, bekommen die Java System Properties von uns höchste Priorität, gefolgt von den Umgebungsvariablen und am Ende den JSON-Properties mit der niedrigsten Priorität. Wir erhalten also einen klar formulierten Lösungsraum (s. Abb. 4).

Listing 6: ThisService.java

import static org.refcodes.configuration.PropertiesSugar.*;
public class ThisService {
    public This() {
        Properties properties = fromProfile(
            toPrecedence(
                fromSystemProperties(),
                fromEnvironmentVariables(),
                seekFromJsonProperties("this.json")
            )
        );
        ThisConfig theConfig = properties.toType( ThisConfig.class );
    }
}

Das Ergebnis dieser Priorisierung mit toPrecedence() ist ein PropertiesPrecedenceComposite-Typ, der sich wieder wie eine Properties-Instanz verhält. Wie der Name schon sagt, handelt es sich um ein Kompositum (Composite Pattern), das sich aber wie die immutable-Properties verhält, da das Setzen von Properties-Einträgen nicht mehr eindeutig weder den darin enthaltenen Java System Properties noch den Umgebungsvariablen oder den JSON-Properties zugeordnet werden kann.

Ein Dekorierer (Decorator Pattern) wiederum erzeugt mittels fromProfile() eine Projektion von darin enthaltenen Properties-Instanzen aus der Sicht eines oder mehrerer Profile und verhält sich natürlich auch wieder wie eine Properties-Instanz.

Auch hier wäre beim Setzen von Properties-Einträgen nicht mehr klar, welches der projizierten Profile denn Ziel der Operationen wäre, deshalb haben wir dafür auch den immutable Properties-Typ gewählt.

Abb. 5 veranschaulicht die Kombination der Bestandteile untereinander: Am Ende erhalten wir einen Dekorierer (1), der das Profil generiert. Dieser kapselt das Kompositum, was eine Priorisierung der darin enthaltenen Properties ermöglicht. Und darin enthalten sind Properties unterschiedlicher Herkunft, siehe (3.1), (3.2) und (3.3).

Der syntaktische Zucker aus der Klasse PropertiesSugar bietet uns noch weitere Möglichkeiten.

import static org.refcodes.configuration.PropertiesSugar.*;
...
PropertiesBuilder properties = fromProperties(
    toProperty( "key", "value" ), toProperty( "foo", "bar" )
);
...

Mit effektiv einer Codezeile – die sich im Listing der Lesbarkeit halber über drei Zeilen erstreckt – haben wir eine Properties-Instanz mit zwei Key-Value-Paaren erzeugt. Dabei kommen die statisch importierten Methoden der Klasse PropertiesSugar als Factory-Methoden zum Einsatz, um einzelne Property-Elemente zu erstellen. Außerdem nutzen wir den Mechanismus der Variable Length Argument Lists, um daraus die Properties-Instanz fertigzustellen.
Method Cascading, eine Spezialform des Method Chaining, serviert weiteren syntaktischen Zucker. Damit lassen sich verkettete Methodenaufrufe formulieren, ohne Variablen für temporäre Ergebnisse zu deklarieren. Der Unterschied: Während bei Method Chaining ein beliebiges Objekt zurückgegeben wird, wird bei Method Cascading das Objekt der Methode selbst als Rückgabewert verwendet. Der Rückgabewert von properties.withPut(key, value) entspricht also der Properties-Instanz selbst, auf welche dann weitere Aufrufe getätigt werden.

PropertiesBuilder properties = new PropertiesBuilderImpl(); 
properties.withPut( "key", "value" ), withPut( "foo", "bar" );

Observieren: Auf Änderungen reagieren

Das Fallbeispiel sieht vor, dass ein Observer (Observer Pattern) aufgerufen wird, sobald sich in einer der beiden Konfigurationsdateien etwas ändert. Unsere Konfigurationsparameter müssen sich also observieren lassen können, damit ein Listener (Observer) bei Änderungen aufgerufen wird.

So geht’s mit Spring Boot

Zum Zeitpunkt der Recherche zu diesem Artikel war keine Spring Boot-basierte Lösung aufzufinden, die das Observieren von Konfigurationsparametern im Sinne des Observer Patterns ermöglicht. Wir bewegen uns ja abseits des Standardfalls.

So macht’s die Bibliothek

Abermals übernimmt syntaktischer Zucker einen Großteil der Arbeit, dieses Mal verwenden wir die Klasse ObservablePropertiesSugar, eine Erweiterung der Klasse PropertiesSugar. Das liegt darin begründet, dass die observierbaren Properties in einem eigenen Artefakt [6] zusammengefasst sind, um die Komplexität je Artefakt gering zu halten und die einzelnen Artefakte nicht mit Funktionalität zu überfrachten (starke Kohäsion).

Listing 7: ThisService.java

import static
org.refcodes.configuration.ext.observer.ObservablePropertiesSugar.*;

public class ThisService {
    public ThisService() {
        ObservableResourcePropertiesBuilder observable = observe(
            seekFromJsonProperties( "this.json" )
        );
        observable.subscribeObserver( event -> {
                // ... Use "event.getKey()", "event.getValue()"
                // and "event.getAction()" ...
            );
        } );
        schedule( observable, 3000, ReloadMode.ORPHAN_REMOVAL );
    }
}

Zuerst erstellen wir unsere observierbaren Properties aus den JSON-Properties. Dafür haben wir den ObservableResourcePropertiesBuilder definiert, der es uns erlaubt, Observer zu registrieren. Dieser verhält sich wie ein ResourcePropertiesBuilder mit zusätzlicher Funktionalität zum Observieren. Am Ende lässt sich solch eine Instanz wieder wie ein Properties-Typ verwenden.

An dieser Instanz registrieren wir einen Observer in Lambda-Notation, die eine kompaktere Schreibweise ermöglicht, als dies bei einem zu implementierenden Interface möglich ist. Hinter dem Lambda-Ausdruck steckt jedoch ein Interface, das nur die Implementierung einer Methode erzwingt, also ein Functional Interface.

Diese observierbaren Properties dekorieren (Decorator Pattern) wir mit einem Scheduler, welcher periodisch diese observierbaren Properties triggert, um die darin enthaltenen JSON-Properties neu einzulesen. Zu diesem Zweck verfügen diese observierbaren Properties, wie auch jede andere ResourcePropertiesBuilder-Erweiterung, über eine reload-Methode. Bei Änderungen in Form von Create, Update oder Delete an den JSON-Properties benachrichtigen die observierbaren Properties unseren Lambda-Ausdruck.

Damit die observierbaren Properties (ObservableResourcePropertiesBuilder) tatsächlich die veränderten Properties-Einträge der JSON-Properties (ResourcePropertiesBuilder) identifizieren können, musste im Nachhinein die reload-Methode einem Refactoring unterzogen werden.

Das zeigt, dass wir bei der Evolution unserer Bibliothek durchaus auch einmal einen Schritt zurückgehen müssen. In diesem Fall bedeutet es also, zurück zur Basisfunktionalität zu springen, um Anpassungen für potentielle Erweiterungen vorzunehmen. Daraufhin springen wir wieder zurück zur darauf aufsetzenden Funktionalität, die diese neuen Möglichkeiten dann nutzt. Der Ordnung halber ziehen wir die Versionsnummer der Bibliothek in solchen Fällen immer hoch.

Persistieren: Zurückschreiben der Konfigurationsdateien

Nachdem die Konfigurationsparameter dem Service jetzt zugänglich sind und wir Änderungen an diesen Parametern vorgenommen haben, sollen diese wieder zurück in die ursprünglichen Konfigurationsdateien geschrieben werden.

So geht’s in Spring Boot

Bei Spring Boot ergeben sich die effektiven Konfigurationsparameter aus den verschiedenen Quadern des Lösungsraumes (s. Abb. 7). Je nach Relevanz der Quader entscheidet sich auch erst am Ende, welche Parameter tatsächlich verwendet werden. Auf die einzelnen Quader kann nachträglich nicht mehr dediziert zugegriffen und davon abgeleitet geschrieben werden. Für das Schreiben von Konfigurationen mit Spring Boot würde sich eine Bibliothek anbieten.

So macht’s die Bibliothek

Bei der Bibliothek mit ihren dedizierten Bestandteilen schreiben wir die Konfigurationsparameter genau dorthin, wo wir sie hergeholt haben.

Listing 8: ThisService.java

import static org.refcodes.configuration. PropertiesSugar.*;

public class ThisService {
    public ThisService() {
        ResourcePropertiesBuilder properties = seekFromJsonProperties(
            "this.json"
        );
        properties.put( "port", "5161" );
        properties.flush();
    }
}

Das Laden der Konfigurationsdatei kennen wir ja schon (Listing 8), wir verändern die Einträge in unseren Properties und schreiben die Änderungen wieder zurück. Dabei verwenden wir genau das Objekt, mit dem wir die Datei geladen haben. Die Properties können auch in eine andere Datei gesichert werden.

properties.fileTo( "other.json" );

Durch die starke Kohäsion der Bestandteile der Bibliothek haben wir die Funktionalität zum Speichern mit unserem ResourcePropertiesBuilder bereits geschenkt bekommen. Dieser sieht sowohl das Laden aus einer Ressource als auch das Speichern als seine Verantwortung an und sorgt dafür, dass unsere JSON-Properties diese Verantwortung auch implementieren müssen.

Fazit

In diesem Beispiel kamen diverse Möglichkeiten zum Einsatz, die dem Ruf bequemer Entwickler mehr als gerecht werden.

Standards: Der Typ PropertiesBuilder implementiert die Map-Schnittstelle aus dem Java Collections Framework. Überall dort, wo im Java-Ökosystem eine Map erwartet wird, können diese Typen verwendet werden. Durch diese Integration wird die Interaktion mit bestehenden Systemen vereinfacht und teilweise überhaupt erst ermöglicht.

Funktionalität: Indem wir ein Canonical Model verwenden, haben wir uns viel Arbeit erspart. So haben wir einen Mittler geschaffen, der uns die Kommunikation von und zu externen Datenquellen ermöglicht, ohne jede Kombination von Quell- und Zielsenke dediziert betrachten zu müssen.

Kombinationsmöglichkeiten: Dank gemeinsamer Schnittstellen und unseres Canonical Models können wir Daten etwa in XML-Notation einlesen, diese manipulieren, um sie dann in JSON- oder TOML-Notation auszugeben. Auch wenn wir diese Funktionalität nie explizit so in der Bibliothek ausprogrammiert haben. Gemeinsame Schnittstellen ermöglichen es uns also, unsere verschiedenen Ausprägungen miteinander zu kombinieren und neue Dimensionen der Funktionalität zu nutzen.

Kohäsion: Die vielfältigen Kombinationsmöglichkeiten werden nicht zuletzt auch durch einfache Bausteine ermöglicht, die einem ganz bestimmten Zweck dienen, also über eine starke Kohäsion verfügen. Damit bleiben sie wartbar, wenngleich die mögliche Funktionalität insgesamt wächst. Dank der gemeinsamen Schnittstelle können die Bausteine mannigfaltig untereinander kombiniert werden.

Dekorierer: Wir haben das Decorator Pattern genutzt, um Funktionalität bruchfrei zu ergänzen. So haben wir Bausteine erstellt, die andere Bausteine um Funktionen erweitern, etwa das Abbilden von Profilsichten auf unsere Properties. Dank der gemeinsamen Schnittstellen lassen sich diese Dekorierer wieder für neue Kombinationsmöglichkeiten nutzen.

Kompositum: Das Composite Pattern hat es uns erlaubt, Funktionalität verschiedener Bausteine zu einem neuen Baustein mit kohäsiver Funktionalität zu bündeln. Wiederum dank der gemeinsamen Schnittstellen lassen sich diese Komposita wieder für neue Kombinationsmöglichkeiten nutzen.

Kaskadierung: Die Ausdrucksstärke beim Einsatz der Bibliothek haben wir mittels Method Chaining und Method Cascading erhöht, indem wir einerseits auf Zwischenvariablen verzichten, andererseits auch eine ganze Reihe von Operationen kompakt formulieren können. Auch eine Art des syntaktischen Zuckers.

Functions: Die Ausdrucksstärke haben wir ebenso mit Functional Interfaces im Blick gehabt. Diese ermöglichten es uns, an Stellen Lambda-Ausdrücke zu formulieren, an denen sonst eine implementierte Schnittstelle erwartet worden wäre. Man kann den Java-Compiler übrigens mittels der Annotation @FunctionalInterface anweisen, beim Verletzen der Regeln für Functional Interfaces mit einer Fehlermeldung die Arbeit zu quittieren.

Import: Das statische Importieren von statisch deklarierten Methoden ermöglichen es, recht komplexe Sachverhalte erstaunlich einfach auszudrücken. Dabei verhalten sich die statisch importierten Methoden zumeist wie Factory-Methoden mit geschickt gewählten Eingabeargumenten und geschickt gewähltem Rückgabewert. Dieser syntaktische Zucker sorgt für Zugänglichkeit der Bibliothek.

Autor

Siegfried Steiner

Siegfried Steiner arbeitet bei msg im Bereich Applied Technology Research als Lead IT Consultant. Sein Steckenpferd seit Anfang der 2000er Jahre ist Java
>> Weiterlesen
botMessage_toctoc_comments_9210