Java Magazin   7.2017 - Eclipse Oxygen

Erhältlich ab:  Juni 2017

Autoren / Autorinnen: 
Roland Grunberg ,  
Sven Kölpin ,  
,  
Konstantin Diener ,  
Lars Vogel ,  
Ralf Bruchmüller ,  
Manfred Steyer ,  
Dominik Mohilo ,  
Fabian PfaffLars Vogel ,  
Mikaël BarberoRalph MüllerWayne Beaton ,  
Robert WinklerTim Riemer ,  
Denis Kuniß ,  
Norman Christ ,  
Alexandru Jecan ,  
Vadym KazulkinRodion Alukhanov ,  
Sascha MölleringDimitrij Zub ,  
Johannes Dienst

Wären Entwickler nicht so schlau, wie sie es nun einmal sind, könnte man in den Büros der Softwareschmieden vermutlich nur noch selten ruhig arbeiten. Die Nutzung einfacher Texteditoren ohne Helferlein wie Syntax-Highlighting und Codevervollständigung würde eine ohrenbetäubende Kulisse aus wütendem Schnauben, hoffnungslosem Seufzen und genervtem Stöhnen evozieren. Da Entwickler aber eben clever sind, haben sie Entwicklungsumgebungen erfunden. So ist es über vierzig Jahre her, seit Maestro I, die erste integrierte Entwicklungsumgebung, für allgemeines Aufatmen in der Welt der Softwareentwicklung sorgte. Als dann schließlich Java zu Beginn der 90er das Licht der Welt erblickte und der Duke seinen ersten Atemzug tat, stand mit Xelfi die erste IDE praktisch schon in den Startlöchern.

Es dauerte allerdings nicht lange, bis weitere Entwicklungsumgebungen verfügbar waren, die mit dem Ziel entworfen wurden, nicht nur das Leben der Nutzer stressfreier zu gestalten, sondern groß angelegte IT-Projekte und deren Wartung erst zu ermöglichen. Ein Wettbewerb um die Gunst der Nutzer entbrannte, der kaum eine Verschnaufpause zulässt. Glücklicherweise wird dieser nicht mit Waffengewalt, sondern mit kontinuierlichen Updates ausgetragen. Man versucht sich also mit Performanz, Funktionalität und Nutzerfreundlichkeit, kurz: dem besseren Produkt, gegenseitig vom Siegespodest zu schubsen. Als Gewinner haben sich in der Java-Welt bis dato drei IDEs erwiesen: NetBeans (das ehemalige Xelfi), IntelliJ IDEA und Eclipse, um dessen neueste Version sich der Schwerpunkt der Ausgabe dreht. Eclipse ist allerdings mehr als eine reine Entwicklungsumgebung. Längst haben sich zahlreiche Projekte, die nur wenig oder gar nichts mit der eigentlichen IDE zu tun haben, im Eclipse-Okösystem angesiedelt, und in den Eclipse Working Groups arbeiten einzelne Entwickler und Organisationen miteinander an innovativen Softwarelösungen.

Am so genannten Simultaneous Release nehmen in diesem Jahr über achtzig Projekte teil, darunter das Docker Tooling für Eclipse, über dessen Neuerungen Roland Grunberg in seinem Artikel informiert. In diesem Jahr war übrigens der Sauerstoff Namenspate für das am 28. Juni erscheinende Eclipse Oxygen.

Nach der Veröffentlichung von Eclipse Oxygen heißt es dann noch einmal tief Luft holen und sich auf den Juli vorbereiten: Seitens der ­Eclipse Foundation wurde nämlich die vollständige Unterstützung von Java 9 zu dessen Veröffentlichung versprochen. Wie das Java Tooling aussehen wird, haben Lars Vogel und Florian Pfaff für uns zusammengefasst.

Natürlich können wir nicht für Sie entscheiden, welche Entwicklungsumgebung Ihre Bedürfnisse am besten befriedigt. Vielleicht finden Sie aber die Argumente der Autoren dieser Ausgabe für überzeugend genug, sich Eclipse Oxygen einmal anzusehen.

In diesem Sinne viel Spaß beim Lesen und Frischluft schnappen.

mohilo_dominik_sw.tif_fmt1.jpgDominik Mohilo | Redakteurin

Website Twitter Google Xing Mail

Mit Java 8 hat die funktionale Programmierung in Java Einzug gehalten. Allerdings fehlen in der Standardbibliothek noch viele Features, die man von anderen Programmiersprachen wie Scala kennt. Das Open-Source-Projekt Vavr möchte diese Lücke füllen und stellt unter anderem persistente Datenstrukturen, algebraische Datentypen und bessere funktionale Schnittstellen für Java 8 bereit.

Vavr [1] stellt viele neue persistente Datenstrukturen, algebraische Datentypen – Summen- und Produkttypen – und bessere funktionale Schnittstellen für Java 8 bereit. Alle Datentypen bauen auf einen von drei Basisdatentypen auf: Tuple, Lambda und Value. Jeder Datentyp in Vavr, der einen Wert repräsentiert, ist vom Basisdatentyp Value abgeleitet. Bei der Namensgebung der Datentypen orientiert sich Vavr stark an Scala (Abb. 1). Das Besondere an den Datenstrukturen von Vavr ist, dass sie persistent und somit unveränderlich sind.

winkler_javaslang_1.tif_fmt1.jpgAbb. 1: Vavr-Übersicht

Unter dem Begriff „Persistenz“ versteht man in der Informatik häufig, dass Daten auf einem nicht flüchtigen Datenträger gespeichert werden. Mit dem Begriff „persistente Datenstruktur“ ist dieses Verhalten nicht gemeint. Es meint eher, dass eine persistente Datenstruktur, wenn sie verändert wird, immer eine veränderte Kopie von sich selbst zurückliefert und ihren vorherigen Zustand behält. Man kann also sagen, dass persistente Datenstrukturen unveränderlich sind, da sie effektiv nach ihrer Erstellung nicht mehr modifiziert werden können. Damit das Verändern von persistenten Datenstrukturen nicht zu speicherintensiv ist, können sie sich im Speicher vorgehaltene Werte teilen. Das lässt sich am besten an einem Beispiel einer verketteten Liste erläutern. Listing 1 zeigt, wie mit Vavr eine Liste erstellt und modifiziert werden kann. Zunächst wird eine Liste mit drei Werten erstellt und anschließend der erste Wert der Liste modifiziert (Abb. 2). Der zweite Aufruf erzeugt eine Kopie der Liste, die sich die gemeinsamen Knoten mit der ursprünglichen Liste teilt (Abb. 3).

winkler_javaslang_2.tif_fmt1.jpgAbb. 2: Liste mit drei Werten, der erste Wert modifiziert
winkler_javaslang_3.tif_fmt1.jpgAbb. 3: Kopie der Liste, teilt sich Knoten mit ursprünglicher Liste

Listing 1: Mit Vavr eine Liste erstellen und modifizieren

// = List(1, 2, 3)
List<Integer> list1 = List.of(1, 2, 3);
 
// = List(0, 2, 3)
List<Integer> list2 = list1.replace(1,0);

Das gleiche Verfahren funktioniert auch bei komplexeren Datenstrukturen wie Queues, TreeSets oder TreeMaps. Vavrs Datenstrukturen haben viele hilfreiche Operatoren, die man aus funktionalen Programmiersprachen oder RxJava kennt, wie map, flatMap, foldLeft, fold­Right, scanLeft, scanRight, sortBy, groupBy, distinct, drop­Right oder dropUntil. Es wird großen Wert darauf gelegt, dass es einfach ist, Java-8-Datentypen in Vavr-Datentypen zu konvertieren und auch wieder zurück.

Hier kommen die Tupel

Tupel haben in der Standardbibliothek von Java schon immer gefehlt. Es gibt viele Bibliotheken, die Tupel mit zwei oder drei Elementen bereitstellen. Vavr bietet persistente Tupel mit bis zu acht Elementen an. Listing 2 zeigt, wie man Tupel erstellen, modifizieren und auf die Elemente zugreifen kann. Tupel sind praktisch, wenn eine Funktion mehr als einen Rückgabewert hat und es sich nicht um ein Entweder-oder-Ergebnis handelt. Bei einem Entweder-oder-Ergebnis würde sich der algebraische Datentyp Either als Rückgabewert eignen. Hierzu aber mehr im Verlauf des Artikels.

Listing 2: Tupel erstellen, ­modifizieren und auf ­Elemente zurückgreifen

// = (Java, 8)
Tuple2<String, Integer> Java8 = Tuple.of("Java", 8);
 
// = (Vavr, 2)
Tuple2<String, Integer> Vavr2 = Java8.map(
  s -> s + "slang",
  i -> i / 4
);
 
// = "Vavr"
String first = Vavr2._1;
// = 2
int second = Vavr2._2;

Funktionen mit bis zu acht Parametern

In der funktionalen Programmierung dreht sich alles um Funktionen, deren Komposition und die Transformationen von Werten mithilfe von Funktionen. Java 8 stellt hierfür lediglich die Schnittstellen Supplier bereit, die keinen Parameter entgegennimmt, Function, die einen Parameter entgegennimmt, und BiFunction, die zwei Parameter entgegennimmt. Im Gegensatz hierzu bietet Vavr Funktionen mit bis zu acht Parametern an. Diese stehen unter den Namen Function0 bis Function8 zur Verfügung. Ähnlich dazu gibt es auch Funktionen, die eine Checked Exception werfen können. So erzeugt man mit einem Lambda-Ausdruck eine Vavr-Funktion, die zwei Integer summiert:

Function2<Integer, Integer, Integer> sum = (a, b) -> a + b;

Vavrs funktionale Schnittstellen bieten weitere Methoden: Komposition, Lifting, Currying und Memorization.

Zusammenhänge schaffen

Durch Komposition ist es möglich, Funktionen miteinander zu verketten. Das Prinzip ist der Mathematik bekannt: Durch Anwendung einer Funktion auf dem Resultat einer anderen Funktion entsteht eine neue Funktion. Die Funktionen f: X -> Y und g: Y -> Z können zu einer Funktion h: g(f(x)) mit h: X -> Z verkettet werden. In Vavr stehen dafür die Methoden andThen sowie compose zur Verfügung (Listing 3 und 4). Diese Methoden haben aber auch mit dem Function-Interface in Java 8 Einzug gehalten.

Listing 3: Methode „andThen“

Function1<Integer, Integer> plusOne = a -> a + 1;
Function1<Integer, Integer> multiplyByTwo = a -> a * 2;
 
Function1<Integer, Integer> add1AndMultiplyBy2 = plusOne.andThen(multiplyByTwo);
 
then(add1AndMultiplyBy2.apply(2)).isEqualTo(6);

Listing 4: Methode „compose“

Function1<Integer, Integer> plusOne = a -> a + 1;
Function1<Integer, Integer> multiplyByTwo = a -> a * 2;
 
Function1<Integer, Integer> add1AndMultiplyBy2 = multiplyByTwo.compose(plusOne);
 
then(add1AndMultiplyBy2.apply(2)).isEqualTo(6);

Funktionen mit Lifting umwandeln

Lifting erlaubt es, eine partielle Funktion in eine totale Funktion umzuwandeln, die den Typ Option zurückgibt. Wie bei der Komposition erklären wir das Vorgehen kurz von der mathematischen Seite. Eine partielle Funktion von X nach Y ist eine Funktion f: X' -> Y, wo X' eine Teilmenge von X ist, das heißt die Funktion f erfordert nicht, dass jedes Element aus X auf ein Element in Y abgebildet werden muss. Somit arbeitet eine partielle Funktion nur für einige Eingabewerte korrekt und wirft bei einer ungültigen Eingabe einen Fehler. Die Methode divide in Listing 5 ist eine partielle Funktion, die nur einen Divisor ungleich 0 akzeptiert, ansonsten wird eine ArithmeticException geworfen. Mithilfe des Operators lift kann nun aus divide eine totale Funktion erzeugt werden, die jede Eingabe verarbeitet, ohne eine ArithmeticException zu werfen. Der Operator lift fängt alle Exceptions einer Funktion und liefert dann ein leeres Option zurück. In Listing 5 wird daher bei einem ungültigen Divisor die ArithmeticException gefangen und None zurückgegeben. Andernfalls wird das Ergebnis zurückgeliefert.

Listing 5: Partielle Funktion

Function2<Integer, Integer, Integer> divide = (a, b) -> a / b;
 
Function2<Integer, Integer, Option<Integer>> safeDivide = Function2.lift(divide);
 
// = None
Option<Integer> i1 = safeDivide.apply(1, 0); 
 
// = Some(2)
Option<Integer> i2 = safeDivide.apply(4, 2); 

Partielle Anwendung mit Currying

Currying erlaubt die partielle Anwendung einer Funktion durch das Umwandeln einer Funktion mit n Argumenten in eine modifizierte Funktion mit n – 1 Argumenten. Mathematisch bedeutet das beispielsweise, dass eine Funktion mit zwei Argumenten f: A1 x A2 -> B in eine modifizierte Funktion mit einem Argument f': A1 -> (A2 -> B) umgewandelt wird. Das Festsetzen eines oder mehrerer Parameter erfolgt in Vavr von links nach rechts (Listing 6).

Listing 6: Festsetzen von Parametern

Function2<Integer, Integer, Integer> sum = (a, b) -> a + b;
// Parameter a wird mit 2 // festgeschrieben
Function1<Integer, Integer> add2 = sum.curried().apply(2); 
 
then(add2.apply(4)).isEqualTo(6);

Memoization: nur beim ersten Mal

Memoization ermöglicht es, dass eine Methode nur beim ersten Aufruf ausgeführt wird und bei weiteren Aufrufen der gespeicherte Wert zurückgeliefert wird. Das Beispiel in Listing 7 erzeugt bei der ersten Ausführung eine Zufallszahl und liefert danach immer den gespeicherten Wert zurück.

Listing 7: Memoization

Function0<Double> hashCache = Function0.of(Math::random).memoized();
 
double randomValue1 = hashCache.apply();
double randomValue2 = hashCache.apply();
 
then(randomValue1).isEqualTo(randomValue2);

Algebraische Datentypen für jeden Tag

Vavr bietet mit Option, Try und Either interessante algebraische Datentypen, die für den täglichen Gebrauch geeignet sind. Man nennt diese Datentypen auch Summen- oder Variantentypen, da es für jeden Typ (Interface) nur eine fixe Anzahl an Varianten (Instanzen) gibt. Für Option wären das Some und None. Für Try wären das Success und Failure. Für Either wären das Left or Right. Im Folgenden zeigen wir praktische Anwendungsfälle für diese Datentypen.

Optionale Werte

Vavrs Option kennt man als Optional in Java 8. Es repräsentiert einen optionalen Wert. Warum sollte man Vavrs Option benutzen? Es verfügt über mehr Operatoren und kann in andere Datentypen wie Try oder Either konvertiert werden. Ein weiterer Vorteil von Vavrs Option ist, dass die Klasse serialisierbar ist. Listing 8 zeigt, dass sich das API nicht stark von Optional unterscheidet.

Listing 8: Vavrs ­„Option“

// = "Hello world"
String helloWorld = Option.of("Hello")
  .map(value -> value + " world")
  .getOrElse(() -> "Recover");

Try: Erfolg oder Misserfolg?

Interessanter ist der Datentyp Try, der eine Berechnung repräsentiert, die entweder erfolgreich durchgeführt (Success) oder mit einem Fehler beendet wurde (Failure). Viele Java-Applikationen sind voll von Seiteneffekten, da Methoden Laufzeitfehler (Unchecked Exceptions) schmeißen können und dieses Verhalten nicht an der Signatur der Methode erkennbar ist. Diese Laufzeitfehler durchbrechen den normalen Ablauf eines Programms und können zu schwerwiegenden Fehlern führen, wenn sie nicht alle irgendwo im Programm abgefangen und behandelt werden. Besser ist es, wenn die Signatur einer Methode klarstellt, dass die Methode einen Fehler schmeißen kann, aber nicht zwischen Unchecked und Checked Exceptions unterschieden wird. Hierfür kann der Datentyp Try als Rückgabewert von einer Methode verwendet werden. Im Gegensatz zu Checked Exceptions wird der Entwickler nicht dazu gezwungen, alle Checked Exceptions zu behandeln oder weiterzuleiten. Listing 9 zeigt, dass Try einige Operatoren hat, die nur ausgeführt werden, wenn die Berechnung erfolgreich durchgeführt wurde. Andere wurden nur ausgeführt, wenn die Berechnung mit einem Fehler beendet wurde. Der Operator map wird nur ausgeführt, wenn es ein Success ist, ansonsten kann mit dem recover-Operator ein Failure behandelt und ein anderer Wert zurückgeliefert werden.

Listing 9: Operatoren des Datentyps „Try“

// Serviceinterface mit Checked // exceptions
public interface HelloWorldService {
  String sayHelloWorld(String name) throws BusinessException;
}
 
// Service mit Try als Rückgabewert
public class HelloWorldService {
  Try<String> sayHelloWorld(String name){
    return Try.of(() -> backendDao.sayHelloWorld(input));
  }
}
 
// Businessservice aufrufen
String result = helloWorldService.sayHelloWorld("Robert")
  .map(value -> value + " and all readers")
  .recover(throwable -> "Handle exception and recover")
  .get();

Either als Rückgabewert

Der DatenTyp Either eignet sich als Rückgabewert, wenn Funktionen ein Entweder-oder-Ergebnis zurückliefern. Either kann auch als Alternative für Option und Try verwendet werden. Per Konvention wäre dann Left mit None oder Failure vergleichbar und Right mit Some oder Success. Aufgrund der Konvention operiert der Operator map nur, wenn Either eine Instanz von Right ist und macht nichts, wenn Either eine Instanz von Left ist (Listing 10).

Listing 10: Either als Instanz von „Right“ oder „Left“

Either<Integer, String> either = Either.right("Hello world");
 
// = "HELLO WORLD"
String result = either.map(value -> value.toUpperCase())
  .get();

Lazy lässt sich Zeit

Da es in Java 8 kein lazy Keyword wie in Scala gibt, gibt es in Vavr den Datentyp Lazy, der einen Wert repräsentiert, der unter Umständen noch nicht berechnet wurde. Lazy könnte man mit einem Supplier vergleichen. Sie haben aber einen wesentlichen Unterschied: Bereits berechnete Werte werden gecacht (Listing 11).

Listing 11: Lazy in Vavr

Lazy<Double> lazy = Lazy.of(Math::random);
lazy.isEvaluated(); // = false
lazy.get();         // = 0.123 (random // generated)
lazy.isEvaluated(); // = true
lazy.get();         // = 0.123 (cached)

CircuitBreaker, Retry und RateLimiter

Das Open-Source-Project Resilience4j [2] ist eine leichtgewichtige Fehlertoleranzbibliothek, die von Netflix Hystrix inspiriert wurde, aber speziell für funktionale Programmierung ausgelegt wurde. Sie ist leichtgewichtig, weil sie nur Vavr und RxJava als Abhängigkeit hat, die wiederum keine weiteren Abhängigkeiten haben. Netflix Hystrix hingegen hat eine Abhängigkeit zu Net­flix Archaius, das weitere Abhängigkeiten zu Guava oder Apache Commons Configuration mit sich bringt. Re­sil­ience4j stellt Funktionen höherer Ordnung (Higher-order Functions) bereit, mit denen funktionale Schnittstellen, Lambda-Ausdrücke und Methodenreferenzen mit einem Circuit Breaker geschützt werden können. Man kann diese Funktionen höherer Ordnung mit dem Decorator-Pattern vergleichen, nur dass keine Decorator-Klassen erstellt werden müssen. Neben dem CircuitBreaker Decorator werden auch Funktionen höherer Ordnung (Decorator) angeboten, um fehlgeschlagene Funktionen automatisch zu wiederholen (Retry) oder die Rate von Funktionsaufrufen zu limitieren (RateLimiter). Man kann mehrere dieser Decorators kombinieren.

Listing 12 zeigt, wie man einen Lambda-Aufruf, der ein Backend aufruft, mit einem CircuitBreaker schützen und fehlgeschlagene Aufrufe, die in einer IOExcep­tion resultieren, automatisch wiederholen kann. Sowohl CircuitBreaker als auch Retry haben viele Konfigurationsmöglichkeiten. Mithilfe von Try kann man einen Fallback ausführen, wenn der Aufruf trotz Wiederholungen fehlgeschlagen ist.

Listing 12: Lambda-Aufruf mit „CircuitBreaker“ schützen

// = CircuitBreaker-Instanz mit default-Konfiguration
CircuitBreakerConfig circuitBreakerConfig = CircuitBreakerConfig.custom()
  .recordFailure(throwable -> throwable instanceof IOException)
  .build();
CircuitBreaker circuitBreaker = CircuitBreaker.of("testName", circuitBreakerConfig);
 
// = Maximal 3 Wiederholungen und ein fixes Intervall zwischen den // Wiederholungen
RetryConfig config = RetryConfig.custom()
  .maxAttempts(3)
  .waitDuration(Duration.ofMillis(100))
  .retryOnException(throwable -> throwable instanceof IOException)
  .build();
Retry retryContext = Retry.of("id", config);
 
// Dekoriere den Aufruf zum HelloWorldService
Try.CheckedSupplier<String> decoratedSupplier = Decorators
  .ofCheckedSupplier(() -> helloWorldService.sayHelloWorld("Robert"))
  .withCircuitBreaker(circuitBreaker)
  .withRetry(retryContext)
  .decorate();
 
Try<String> result = Try.of(supplier)
  .recover(throwable -> "Hello Recovery");

Der CircuitBreaker ist eine Zustandsmaschine mit den Zuständen CLOSED, OPEN und HALF_OPEN. Der initiale Zustand ist CLOSED. Der CircuitBreaker speichert den Status – erfolgreich oder fehlgeschlagen – von Aufrufen in einem Ring-Bit-Buffer ohne rollendes Zeitfenster. Erfolgreiche Aufrufe werden als 1 Bit und fehlgeschlagene Aufrufe als 0 Bit gespeichert. Der Ring-Bit-Buffer hat eine konfigurierbare fixe Größe und speichert die Bits in einem long-Array. Das bedeutet, dass man in einem Array mit 16 long-(64-Bit-)Werten den Status der letzten 1024 Aufrufe speichern kann. Diese Speicherform spart viel Arbeitsspeicher. Der Vorteil an einem Ring-Bit-Buffer ohne rollendes Zeitfenster ist, dass man nicht vorab wissen muss, mit welcher Frequenz das Backend aufgerufen wird, da kein Status gelöscht wird, wenn ein Zeitfenster abgelaufen ist. Wenn die Anzahl der fehlgeschlagenen Aufrufe einen konfigurierbaren Schwellwert überschreitet, wechselt der CircuitBreaker in den Zustand OPEN und weist weitere Aufrufe mit einer Resilience4jOpenException für ein konfigurierbares Zeitintervall ab. Nach Ablauf des Zeitintervalls wechselt der CircuitBreaker in den Zustand HALF_OPEN und lässt eine konfigurierbare Anzahl an Aufrufen wieder durch, um festzustellen, ob das Backend wieder zuverlässig antwortet. Wenn die meisten Aufrufe erfolgreich sind und der Schwellwert unterschritten wird, dann wechselt der CircuitBreaker wieder in den Zustand CLOSED, ansonsten wechselt er erneut in den Zustand OPEN und der Kreislauf beginnt aufs Neue. Netflix Hystrix macht im Vergleich nur einen einzigen Aufruf im HALF_OPEN-Zustand. Dieses Verhalten ist unzuverlässig, wenn ein Backend hinter einem Load Balancer aus mehreren Hosts besteht und nur ein einziger Host unzuverlässig antwortet, aber genau dieser getestet und dann das gesamte Backend erneut blockiert wird.

Mit dem RateLimiter kann die Frequenz von Aufrufen zu einem Backend limitiert werden. Zum Beispiel kann limitiert werden, dass ein Backend nur einmal pro Sekunde aufgerufen werden darf, bevor der Aufruf mit einer RequestNotPermittedException abgewiesen wird (Listing 13).

Listing 13: Frequenz von Aufrufen zu ­einem ­Backend limitieren

RateLimiterConfig config = RateLimiterConfig.custom()
  .limitRefreshPeriod(Duration.ofSeconds(1))
  .limitForPeriod(1)
  .build();
RateLimiter rateLimiter = RateLimiter.of("backendName", config);
 
// Dekoriere den Aufruf zum HelloWorldService
Try.CheckedSupplier<String> supplier = Decorators
  .ofCheckedSupplier(() -> helloWorldService.sayHelloWorld("Robert"))
  .withRateLimiter(rateLimiter)
  .decorate();
Try<String> result = Try.of(supplier);

Sowohl CircuitBreaker, Retry als auch RateLimiter emittieren einen Eventstream mithilfe von RxJava, falls es einen Konsumenten gibt. Diese Events enthalten Informationen, die für das Monitoring eines Programms interessant sind; zum Beispiel die Antwortzeit von Aufrufen, die Fehler, die dazu führen, dass der Circuit­Break­er getriggert wird, oder Zustandswechsel. Listing 14 zeigt, wie man nur die Error-Events eines Resilience4j konsumieren und in einem Circular-Buffer speichern kann, um nur die letzten zehn Events zu speichern. RxJava macht es mit Operatoren wie buffer oder window einfach, Events zwischenzuspeichern und sie in einem bestimmten Intervall stapelweise in eine Metrikendatenbank wie InfluxDB oder Prometheus zu schreiben.

Listing 14: Error-Events von „CircuitBreaker“ ­konsumieren, in Circular-Buffer speichern

CircuitBreaker circuitBreaker = CircuitBreaker.ofDefaults("testName");
CircularEventConsumer<CircuitBreakerOnErrorEvent> circularEventConsumer = new CircularEventConsumer<>(10);
circuitBreaker.getEventStream()
  .filter(event -> event.getEventType() == Type.ERROR)
  .cast(CircuitBreakerOnErrorEvent.class)
  .subscribe(circularEventConsumer);
 
List<CircuitBreakerOnErrorEvent> bufferedEvents = circularEventConsumer.getBufferedEvents();

Resilience4j bietet außerdem eine Schnittstelle an, mit der Metriken, wie die Anzahl fehlgeschlagener Aufrufe oder die aktuelle Fehlerrate, überwacht werden können (Listing 15).

Listing 15: Aktuelle Fehlerrate

CircuitBreaker.Metrics metrics = circuitBreaker.getMetrics();
float failureRate = metrics.getFailureRate();
int bufferedCalls = metrics.getNumberOfBufferedCalls();
int failedCalls = metrics.getNumberOfFailedCalls();

Fazit

Funktionale Programmierung als Programmierparadigma ist zu Beginn für einen Java-Entwickler sehr gewöhnungsbedürftig. Aber die Eingewöhnungsphase lohnt sich. Nach einiger Zeit macht man sich immer häufiger Gedanken über referenzielle Transparenz und passt auf, dass Funktionen keine Seitteneffekte haben. Die Testbarkeit des Codes verbessert sich somit und Funktionen lassen sich miteinander kombinieren. Die Vavr-Community ist mitlerweile schon recht groß, und man kann in Zukunft sicher noch einige neue Features in Vavr erwarten.

winkler_robert_sw.tif_fmt1.jpgRobert Winkler arbeitet als Softwarearchitekt bei der Deutschen Telekom AG – Group Innovation in Darmstadt. Seine Schwerpunkte liegen auf den Themen Softewarearchitektur, Continuous Delivery und Entwicklung von JVM-basierten verteilten Systemen. Er ist der Ersteller von Resilience4j.

Web Twitter

riemer_tim_sw.tif_fmt1.jpgTim Riemer ist als Softwarearchitekt mit Schwerpunkt Integration bei der FNT GmbH in Heiligenhaus beschäftigt. Neben seinem Job befasst er sich mit aktuellen Themen rund um Softwarearchitektur, Spring Boot, Build-Automation und Kotlin.

Twitter

TypeScript ist eine echte Obermenge von JavaScript mit optionaler statischer Typisierung. Es bietet schon heute zahlreiche Features aus den zukünftigen ECMAScript-Standards. Dabei transpiliert es zu JavaScript in der ECMAScript-Version 3/5 und ist damit kompatibel zu allen Browsern. Vor Kurzem wurde Version 2.0 veröffentlicht. Der richtige Zeitpunkt, sich TypeScript genauer anzusehen.

Mitte September 2016 haben die Entwickler Version 2.0 von TypeScript [1] veröffentlicht. Die ausgereifte Obermenge von JavaScript erreicht damit einen weiteren Meilenstein ihrer Entwicklung. Das wichtigste Feature ist die optionale statische Typisierung, die laut Projektleiter Anders Hejlsberg JavaScript-Entwicklung in großem Stil ermöglicht. Die 2012 als Open Source veröffentlichte Sprache wird unter großem Einfluss der Community weiterentwickelt. Sie transpiliert wahlweise zu ECMAScript 3, 5 oder 6 und garantiert dadurch auch, dass das erzeugte JavaScript auf allen Browsern lauffähig ist. Abgerundet wird das Paket durch ein Tool­ing, das sich in das Ökosystem rund um Node.js und npm einfügt.

Etablierte IDEs und Editoren profitieren von der Architektur des TypeScript-Compilers, durch die Plug-ins mit Codevervollständigung und Refaktorisierungsmöglichkeiten gebaut werden können. Mit den vorhandenen Werkzeugen sind große JavaScript-Codebasen handhabbar. Spätestens seit Google für sein beliebtes Framework Angular auf TypeScript setzt, kann man davon ausgehen, dass das Projekt ein langes Leben haben wird.

Alles zu TypeScript 5

TypeScript ist aktuell in Version 5 verfügbar. In seinem Artikel TypeScript 5: Neuerungen und Breaking Changes gibt JavaScript-Experte Karsten Sitterberg einen Überblick über alle Innovationen in der Programmiersprache.

Hauptmerkmal von TypeScript ist die optionale statische Typisierung. Dafür stellt es eine ganze Reihe von Basistypen bereit, die in Listing 1 in Aktion gezeigt werden. Neben den klassischen Typen für boolesche Werte boolean, string für Strings und number für Zahlenwerte im Format dezimal, hexadezimal, binär und oktär, lassen sich auch Arrays definieren, sogar auf zwei Arten: klassisch zum Beispiel ein Array aus strings mit

  let aArray: string[] = ['Hello', 'World', '!'];

oder mit Generics

  let aArray: Array<string> = ['Hello', 'World', '!'];

Ein letzter Basistyp ist enum, mit dem sich Aufzählungen definieren lassen, die aufsteigend von 0 angelegt werden. Alternativ können auch die numerischen Werte selbst belegt werden.

Listing 1: Basistypen

let isDone: boolean = false;
let decimal: number = 6;
let color: string = 'berlin';
 
// Getype Arrays auf zwei Arten
let aArray: number[] = [1, 2, 3];
let aGenericArray: Array<number> = [4,5,6];
 
enum Color {Red, Green, Blue};
let c: Color = Color.Green;

Die Migration von JavaScript auf TypeScript bringt mitunter Fehler ans Tageslicht. Diese betreffen die Typisierung der Codebasis und Spezialfälle mit nicht klar aufgelösten Typen. Dafür gibt es auch einige Spezialtypen, die die Integration mit JavaScript vereinfachen. Listing 2 zeigt zuerst den Typ any, der immer dann zum Einsatz kommen kann, wenn eine Variable Werte von unterschiedlichen Typen annehmen kann. Arrays können auch verschiedene Typen enthalten, so wie:

let tuple: [string, number];

Zu beachten ist hierbei, dass damit eine Reihenfolge verbunden ist und ab Index 2 jeder der beiden Typen erlaubt ist. Folgende Zuweisungen sind also korrekt:

tuple = ["Hello", 42];
tuple = ["Hi", 42, 27, "Zoe"];

Listing 2: Spezialtypen

// Kann jeden Typ enthalten
let notSure: any = 4;
notSure = 'I am undecided';
 
let tuple: [string, number];
tuple = ["Hello", 42];

Seit Version 2.0 gibt es einen neuen Typ never, der immer dann zum Einsatz kommt, wenn eine Methode nicht korrekt terminiert. Das wird gerne mit void verwechselt, wenn eine Methode nichts zurückgibt. Die Methode in Listing 3 wirft immer einen Fehler, terminiert also nicht korrekt. Deswegen bekommt sie als Rückgabewert den sogenannten Bottom Type never, der genau dieses Verhalten ausdrückt. Das lässt sich dazu benutzen, Checks zur Lauf- und Kompilierzeit einzubauen, die auf dieses Verhalten prüfen. Eine genaue Erläuterung findet sich in [2] und [3].

Listing 3

// Falls Methoden keinen Rückgabewert // haben: never
function error(message: string): never {
  throw new Error(message);
}

Warum Sie zuschlagen sollten

Größtes Verkaufsargument von TypeScript ist die optionale statische Typisierung. Tatsächlich ist sie ziemlich ausgereift und der Compiler zuverlässig und schnell. Typen lassen sich mit Interfaces relativ leicht erzeugen. Listing 4 definiert ein Interface Robot. Es kann sowohl Properties als auch Funktionssignaturen enthalten. Eine Besonderheit stellen sogenannte optionale Properties dar. Sie werden mit einem ? markiert, wie die Property model. In einer konkreten Ausprägung können sie dann vorhanden sein oder auch nicht.

Listing 4: Ein ­einfaches Interface

interface Robot {
  name: string;
  model?: number;
  isDiscontinued?: boolean;
  sayName(): void;
}

Eine Klasse kann dieses Interface implementieren und der Compiler überprüft die Einhaltung des Vertrags. Hier zeigt sich schon der große Vorteil von statischer Typisierung. Bei einer großen Codebasis ist es nicht immer leicht, diese Einhaltung nur durch eine hohe Testabdeckung zu garantieren. In TypeScript gibt es mit dem Compiler ein Sicherheitsnetz für diesen Fall. Die Klasse C3PO ist eine gültige Implementierung, obwohl die optionalen Properties model und isDiscontinued fehlen. Sie werden vom Compiler nicht gefordert.

Listing 5: Abgeleitete Klasse

class C3PO implements Robot {
  name: string;
 
  constructor(name: string) {
    this.name = name;
  }
 
  sayName(): void {
    alert(this.name);
  }
}

Interessanter aber ist es zu sehen, dass zwar Properties fehlen können, jedoch keine übermäßigen vorhanden sein dürfen, um das Interface zu implementieren. Folgender Code wäre also nicht gültig:

let wrongC3PO: Robot = {name : 'Falsum', sayName : function(){}, excess : true};

Um wahre Objektorientierung bereitzustellen, fehlt noch ein wichtiges Merkmal: Datenkapselung. Dazu gibt es in TypeScript die teilweise aus anderen Sprachen bekannten Modifier public, protected, private und readonly (Listing 6). Die ersten drei verhalten sich dabei so, wie man es aus Sprachen wie Java gewohnt ist. Lediglich die package-Sichtbarkeit bei protected verhält sich aufgrund der fehlenden package-Struktur in JavaScript anders. Nur von einer Klasse erbende Unterklassen können auf eine mit protected gekennzeichnete Property zugreifen. Der Modifier readonly ist das Pendant zu final in anderen Sprachen. Mit readonly versehene Properties lassen sich genau einmal bei der Erzeugung eines Objekts setzen.

Listing 6: Modifier

class TinyThing {
  name: string;
  protected weight: number;
  private age: number;
  readonly color?: string;
}

Eigenschaften der Typisierung

Das Typsystem von TypeScript ist grundsätzlich nicht „sound“ aufgebaut. Das heißt, dass es in Spezialfällen keinen Fehler generiert, wenn zur Kompilierungszeit keine korrekte Typisierung ermittelt werden kann. Das hat unter anderem auch mit der Natur von JavaScript zu tun, das sich oft solche Spezialfälle zu Nutze macht, für eine Erklärung dieser Fälle siehe [4]. Ansonsten hat das Typsystem vier Eigenschaften (Listing 8) und ist zudem optional. Der Compiler meckert also nicht, wenn keine Typisierung angewandt wird. In Kombination mit struktureller Typisierung ergibt sich dadurch sogar der nette Nebeneffekt, dass der Compiler den Typ in fast allen Fällen selbst richtig ableiten kann, eine Eigenschaft, die auch als Duck Typing bezeichnet wird. Interessant und mächtig ist auch die lokale Typinferenz. Wird einer ungetypten Variablen ein neu erzeugtes Objekt zugewiesen, z. B. mit

let c3po = new C3PO('c1po');

dann weiß der Compiler, dass in c3po ein Objekt des Typs C3PO steckt. Als letzte Eigenschaft ist die kontextuelle Typisierung zu nennen. Hier kann der Compiler aufgrund der Umstände den Typ ermitteln. So weiß er, dass bei window.onmousedown ein MouseEvent an die Funktion übergeben wird, das bestimmte Eigenschaften (nicht) hat (Listing 7).

Listing 7

// optional
let myRobot = {name: 'Arnold', sayName: function(){}};
 
// structural
printRobotInfo(myRobot);
 
// local
let c3po = new C3PO('c1po');
c3po = 42; // error
 
// contextual
window.onmousedown = function(mouseEvent) {
  console.log(mouseEvent.buton); // <- Error
};

Wann wir Funktionstypen brauchen

JavaScript ist von jeher eine funktionale Sprache. Es ist nur konsequent, Typen für Funktionen einzuführen. Diese können in TypeScript sogar über ein Interface definiert werden (Listing 8). Dabei wird in Klammern die Parameterliste angegeben und anschließend der Rückgabetyp. Mit void zeigt man an, dass eine Funktion keinen Rückgabewert besitzt. Wird nicht der Weg über das Interface gegangen, so kommt die Schreibweise mit Fat Arrow zum Einsatz: (p1, p2, …) => T. Besonders nützlich sind Funktionstypen, wenn in einer Codebasis Callbacks verwendet werden, oder generell funktional programmiert wird. Beide Fälle sind vor allem in JavaScript-Codebasen mit asynchronen Aufrufen häufig anzutreffen.

Listing 8: Typen über Interface definieren

interface LameFunc {
  (): void;
}
 
let funcType: LameFunc;
let funcType2: (robot)=>void;
 
funcType = printRobotInfo; // Error: // Wrong function type
funcType2 = printRobotInfo;

Ein paar Worte zur Typdefinition

In TypeScript kann ein Interface sowohl die statische als auch die Instanzseite gleichzeitig beschreiben. Das klingt zuerst verwirrend. Tatsächlich trägt das Handbuch auch nicht zur Klärung bei. Sehen wir uns dazu das Beispiel in Listing 9 an. Das Interface definiert auf der Instanzseite eine Property age. Auf der statischen Seite wird der Konstruktor mit dem Schlüsselwort new definiert. Die Klasse Mortal implementiert das Interface, aber es gibt einen Fehler, da der Compiler die Methode new nicht finden kann. Warum ist das so, obwohl ein Konstruktor vorhanden ist, der diese Signatur aufweist?

Listing 9: Interface beschreibt statische und ­Instanzseite

interface MortalInterface {
  age: number;
  new (age: number);
}
 
// Type Mortal provides no match for the signature 'new (age :number) :any'
class Mortal implements MortalInterface {
  age: number;
  constructor(age: number) { }
}

Erstellt man ein Objekt mit let aMortal = new Mortal(42), wird der Konstruktor aufgerufen. Anschließend ist er nicht mehr zugreifbar, da er nicht als Methode des Objekts vorhanden ist. Ein Aufruf wie aMortal.new(42) ist also nicht möglich. Das Interface MortalInterface fordert aber, dass eine solche Methode vorhanden ist. An dieser Stelle hilft eine Verbildlichung wie in Abbildung 1. Ein Interface sollte immer nur die Instanzseite oder die statische Seite beschreiben. Beides zusammen ist zwar möglich, aber der Compiler überprüft immer nur die Instanzseite – man kann dann keine Klasse implementieren, die diese Vorgaben erfüllt.

Abb. 1: Zusammenhang zwischen statischer und Instanzseite von Interfaces und Klassen

Abb. 1: Zusammenhang zwischen statischer und Instanzseite von Interfaces und Klassen

Die Definition der statischen Seite ergibt nur Sinn, wenn eine Factory gebaut werden soll. In Listing 10 werden zwei Interfaces zur Konstruktion eines Objekts der Klasse Human definiert. HumanConstructor beschreibt die Signatur des Konstruktors, HumanInterface die Properties, die das Objekt beinhalten soll. Will man jetzt eine konkrete Klasse Human erstellen, dann implementiert sie das Instanzinterface HumanInterface und sie muss einen Konstruktor wie in HumanConstructor bekommen.

Die Fabrikmethode createHuman erwartet ein Objekt, das die Konstruktorvorgabe von HumanConstructor erfüllt, und gibt ein Objekt des Typs HumanInterface zurück. Voilà, damit wurde die statische Seite mit der Instanzseite kombiniert.

Listing 10: Zwei Interfaces zur ­Konstruktion eines Objekts der Klasse „Human“

interface HumanConstructor {
  new (age: number): HumanInterface;
}
interface HumanInterface {
  age: number;
}
 
class Human implements HumanInterface {
  age: number;
  constructor(age: number) {
    this.age = age;
  }
}
 
function createHuman(ctor: HumanConstructor, age: number): HumanInterface {
  return new ctor(age);
}
 
let dijkstra = createHuman(Human, 86);

Klassen und Methoden parametrisieren mit Generics

Generics werden allgemein benutzt, um Klassen und Methoden mit Typen parametrisieren zu können. Dadurch können z. B. die Typen der Elemente, die in einem Array enthalten sind, dem Compiler mitgeteilt werden:

Array<string>

Für Entwickler ist das insofern hilfreich, dass dadurch schon zur Kompilierzeit eine Typprüfung stattfindet. Noch dazu ist der Typ der enthaltenen Elemente bekannt und kann zum Beispiel für IntelliSense benutzt werden. Im folgenden Beispiel wird ein weiterer Anwendungsfall gezeigt, in dem eine Methode parametrisiert wird. Die Idee stammt aus [5], sie wurde hier angepasst und korrigiert. Listing 11 implementiert eine generische Funktion mit der sogenannten Type-Variable T (Schreibweise ), die einen asynchronen Aufruf an ein API tätigt. Als Rückgabetyp wird ein Promise definiert, das am Ende JSON ausgeben soll. Da Objekte in JavaScript praktisch im JSON-Format sind – daher auch der Name JavaScript Object Notation –, kann der Rückgabewert wieder auf den übergebenen Typ gecastet werden. Das geht mit dem Diamantoperator:

<Promise<T>>response.json()

Listing 11: Generische Funktion mit der ­sogenannten „Type“-Variable „T“ ­implementieren

­function getAsync<T>(arg: T): Promise<T> {
  // ERROR: Property 'id' does not exist on type 'T'
  return fetch(`/api/${arg.id}`)
    .then((response: Response) => <Promise<T>>response.json());
}

Leider gibt es hier ein Problem, denn der Typ des übergebenen Parameters arg ist nicht eingeschränkt. Es kann also vom Compiler nicht ermittelt werden, ob arg tatsächlich eine Property-ID besitzt. Der Typ wird nach any aufgelöst. Hier kommen Generic Constraints [6] zum Einsatz. Dafür legen wir ein neues Interface Identity an, das eine Property id enthält:

interface Identity { id: string; }

Die Signatur von getAsync() wird erweitert und sieht anschließend so aus:

function getAsync<T extends Identity>(arg: T): Promise<T>

Wie hilft das insgesamt dem Entwickler? Da die Methode getAsync() über die Type-Variable parametrisiert werden kann, ist eine beliebige Anfrage an das API möglich, das ein Objekt vom übergebenen Typ zurückliefert. Das komplette Beispiel ist in Listing 12 zu finden. Die Klasse Movie implementiert das zuvor besprochene Interface Identity und kann damit als Type-Variable von getAsync() verwendet werden. Der Aufruf von get­Async() mit Movie als Type-Variable und einem Parameter Movie-Objekt hat zur Folge, dass im then-Block später die Typinformation verfügbar ist. Der Entwickler hat vollen Zugriff auf IntelliSense, da movie nicht vom Typ any ist, sondern vom Typ Movie.

Listing 12: Methode „getAsync()“

interface Identity { id: string; }
 
class Movie implements Identity {
  id: string;
}
 
function getAsync<T extends Identity>(arg: T): Promise<T> {
  return fetch(`/api/${arg.id}`)
    .then((response: Response) => <Promise<T>>response.json());
}
 
let movieToFind: Movie = { 'id' : '42' };
getAsync<Movie>(movieToFind)
  .then(movie => {
    console.log(movie.id);
  });

TypeScript Advanced Tutorial

Die große interaktive TypeScript-Tutorialreihe

typescript_advanced_tutorial.png Der Kurs behandelt weiterführende Themen der Bereiche Typisierung, objektorientierte Programmierung, funktionale Programmierung und Tooling in TypeScript.

Auf die notwendigen Grundlagen wird jeweils zu Beginn der Lesson eingegangen. Der Kurs führt zunächst theoretisch in das Thema ein und erläutert dann die Theorie anhand praktischer Beispiele. Dafür wird teils auf RxJS und Angular als Anwendungsfälle zurückgegriffen. Vorwissen in diesen Bereichen ist gut, aber nicht zwingend notwendig.

TypeScript Advanced: Die große interaktive TypeScript-Tutorialreihe auf entwickler.de ansehen.

ECMAScript 20XX

TypeScript wurde neben der optionalen statischen Typisierung auch entwickelt, um zukünftige JavaScript-Sprachfeatures früh zugänglich zu machen, indem sie zu ECMAScript 3/5 transpiliert werden. Der Artikel stellt nur eine Auswahl der am meisten erwarteten ECMA­Script-2016/17-Sprachmittel vor, die schon heute in TypeScript verfügbar sind. Wer sich ein genaueres Bild machen will, kann sich eine detaillierte Übersicht unter [7] anzeigen lassen. Den Start machen Dekoratoren, die wahrscheinlich in ECMAScript 2018 volle Unterstützung bekommen. Dekoratoren sind einfache Funktionen, die als Parameter target, name und descriptor erhalten. Listing 13 zeigt einen einfachen Decorator log, mit dem Methoden in einer Klasse dekoriert werden können. Funktionen außerhalb von Klassen sind nicht dekorierbar. Erzeugt man nun ein Objekt vom Typ Greeter mit

var greeter = new Greeter();

dann bekommt man in der Konsole folgende Ausgabe:

Greeter {}
greet
Object {
  value: [Function: greet],
    writable: true,
    enumerable: false,
    configurable: true }

Das eröffnet viele Möglichkeiten, auf die Ausführung der Methode Einfluss zu nehmen. Für Querschnittsaspekte ist diese Vorgehensweise empfehlenswert. Neben Methoden können auch Klassen dekoriert werden. Dort können dann zum Beispiel einzelne Properties readonly gesetzt werden, indem im Descriptor writable auf false gestellt wird. Die Möglichkeiten sind enorm. Für eine detailliertere Einführung siehe [8].

Listing 13: Decorators

function log(target: Object, name: string, descriptor: PropertyDescriptor) {
  console.log(target);
  console.log(name);
  console.log(descriptor); 
}
 
class Greeter {
 
  @log
  greet(x, y) {
    return x + y;
  }
}

Ein weiteres heiß erwartetes Feature ist Async/Await, das die Arbeit mit asynchronen Funktionen vereinfacht, insbesondere mit Promises. Der Sinn dahinter ist, dass das Programmiermodell wie bei synchronem Code aufgebaut wird, aber im Hintergrund tatsächlich asynchron abläuft. Ab TypeScript 1.7 ist es verfügbar, wenn zu ECMAScript 2015 transpiliert wird [9], seit TypeScript 2.1 ist es ebenfalls für ECMA­Script 5 verfügbar. Sehen wir uns dazu das Beispiel aus Listing 14 an. Die Funktion afunc wird mit dem Schlüsselwort async versehen. Das ist nötig, damit innerhalb der Funktion ein await benutzt werden kann. Ruft man afunc nun auf mit let res = afunc(); wird auf der Konsole Folgendes ausgegeben:

Hello Zoe and Amelie

Würde man ohne die Schlüsselworte async/await arbeiten, wäre die Ausgabe auf der Konsole undefined, da nicht auf den Rückgabewert des Promises gewartet wird.

Listing 14: Async/Await

function getPromise(): Promise<string> {
  return new Promise<string>((resolve, reject) => {
    resolve("Hello Zoe and Amelie");
  });
}
 
async function afunc() {
  let res = await getPromise();
  console.log(res);
}

Fazit

TypeScript hat sich über die letzten Jahre zu einer echten Alternative für JavaScript entwickelt. Das JavaScript für Backend-Entwickler punktet mit optionaler statischer Typisierung und einem schnellen intelligenten Compiler. Zusätzlich ist mit Interfaces und Modifiern echte objekt­orientierte Entwicklung möglich. Das alles prädestiniert es für den Einsatz in großen Projekten. Abgerundet wird es durch die Bereitstellung von Features aus zukünftigen JavaScript-Versionen, die größtenteils zu ECMAScript 3/5 transpiliert werden können und damit voll abwärtskompatibel zum JavaScript-Sprachstandard sind.

Links & Literatur

[1] TypeScript-Homepage: https://www.typescriptlang.org/

[2] „never“-Typ: https://basarat.gitbooks.io/typescript/content/docs/types/never.html

[3] Discriminated Union: https://basarat.gitbooks.io/typescript/content/docs/types/discriminated-unions.html

[4] Typenkompatibilität in TypeScript: https://www.typescriptlang.org/docs/handbook/type-compatibility.html

[5] Praktische Anwendung von Generics: https://templecoding.com/blog/2016/03/17/leveraging-the-power-of-generics-with-typescript/

[6] Generics im TypeScript-Handbuch: https://www.typescriptlang.org/docs/handbook/generics.html

[7] ES6/ECMAScript-2015-Kompatibilität: http://kangax.github.io/compat-table/es6/

[8] Dekoratoren: http://www.typescriptlang.org/docs/handbook/decorators.html

[9] Async/Await: https://www.typescriptlang.org/docs/handbook/release-notes/typescript-1-7.html