From 13dd6bfa904232150ea098d0e5f3d9c12798c29b Mon Sep 17 00:00:00 2001
From: Johannes Teklote
Welche Möglichkeiten haben wir, wenn neben einer REST-API auch eine SOAP-Schnittstelle zur Verfügung stehen soll? +In diesem Artikel wird eine Methode vorgestellt, wie man diese Anforderung mit Hilfe von Kong umsetzen kann.
+ +]]>Welche Möglichkeiten haben wir, wenn neben einer REST-API auch eine SOAP-Schnittstelle zur Verfügung stehen soll? +In diesem Artikel wird eine Methode vorgestellt, wie man diese Anforderung mit Hilfe von Kong umsetzen kann.
+ +Wie bereist beschrieben, kann es manchmal gewollt sein, dass eine Schnittstelle über REST und SOAP erreichbar ist. +Hier stellt sich nun die Frage wie man diese Anforderung realisiert.
+ +Eine intuitive Herangehensweise wäre es, für den Service die zwei Schnittstellen getrennt zu implementieren. +Diese Lösung hat allerdings den Nachteil, dass die Schnittstellen sich durch die getrennte Implementierung unterschiedlich verhalten könnten. +Außerdem müssen bei einer Änderung des Service beide Schnittstellen verändert werden, womit zusätzlicher Arbeitsaufwand verbunden wäre.
+ +Um diese Probleme zu vermeiden, könnten wir für den Service nur eine Schnittstelle implementieren, zum Beispiel eine REST-API, da diese heutzutage häufig verwendet wird. +Die SOAP-Schnittstelle hingegen könnten wir anschließend aus der REST-API generieren.
+ +Für die Realisierung beider Schnittstellen wird ein Kong API Gateway mit dem Kong Plugin soap2rest sowie ein REST-Service benötigt.
+ +Die Abbildung zeigt, wie die einzelnen Komponenten miteinander verknüpft sind. +Das Kong API Gateway verwaltet den Zugriff auf den REST-Service. +Für den Fall, dass wir eine Anfrage über die REST-Schnittstelle stellen, wird diese Anfrage an den Service weitergeleitet und bearbeitet.
+ +Für die Verarbeitung von SOAP-Anfragen wird das Kong Plugin soap2rest verwendet. +Das Plugin benötigt zur Konfiguration zwei Dateien. +Damit das Plugin die SOAP-Anfragen richtig verarbeiten kann, benötigt es die WSDL der SOAP-Schnittstelle. +Und um die Konvertierung von SOAP zu REST Anfragen korrekt durchzuführen, wird zusätzlich die OpenAPI-Spezifikation der REST-API benötigt.
+ +Diese Abbildung zeigt den Fall, dass eine Anfrage über die SOAP-Schnittstelle gestellt wird. +Sobald Kong eine Anfrage über die Route der SOAP-Schnittstelle registriert, wird das Plugin soap2rest ausgeführt. +Das Plugin konvertiert die Anfrage in eine gültige REST-Anfrage und sendet diese an den REST-Service. +Nachdem der REST-Service geantwortet hat, wird die Antwort in eine gültige SOAP-Antwort übersetzt und zurückgegeben.
+ +Anhand des folgenden Beispiels lässt sich der Ablauf näher verdeutlichen. +Angenommen, folgende Anfrage wird an die SOAP-Schnittstelle gestellt.
+ +<?xml version="1.0" encoding="utf-8"?>
+<soap:Envelope
+ xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"
+ xmlns:tns="http://www.w3.org/2001/XMLSchema">
+ <soap:Body>
+ <tns:GetPetByPetid_InputMessage>
+ <tns:petId>1</tns:petId>
+ </tns:GetPetByPetid_InputMessage>
+ </soap:Body>
+</soap:Envelope>
+
+
+Aus dieser Anfrage lassen sich verschiedene Informationen ableiten.
+Zum einen wird mit dieser Anfrage die SOAP-Action GetPetByPetid
ausgeführt.
+Zusätzlich beinhaltet die Anfrage den Parameter petId
mit dem Wert 1
.
+Anhand dieser Informationen kann die Anfrage der passenden REST Anfrage zugeordnet werden.
+In diesem Fall entspricht die Anfrage dem Pfad /pet/1
.
Nachdem das Plugin die generierte Anfrage an die REST-API gestellt hat, bekommt es folgende Antwort:
+ +{
+ "id": 1,
+ "name": "doggie",
+ "photoUrls": [],
+ "tags": [],
+ "status": "available"
+}
+
+
+Anschließend wird diese Antwort vom Plugin in gültiges XML umgewandelt und in eine SOAP-Antwort eingesetzt, welche anschließend zurückgegeben wird.
+ +<?xml version="1.0" encoding="UTF-8"?>
+<soap:Envelope
+ xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"
+ xmlns:tns="http://www.w3.org/2001/XMLSchema">
+ <soap:Body>
+ <tns:GetPetByPetid_OutputMessage>
+ <tns:Pet>
+ <tns:id>1</tns:id>
+ <tns:name>doggie</tns:name>
+ <tns:photoUrls></tns:photoUrls>
+ <tns:tags></tns:tags>
+ <tns:status>available</tns:status>
+ </tns:Pet>
+ </tns:GetPetByPetid_OutputMessage>
+ </soap:Body>
+</soap:Envelope>
+
+
+Die Analyse der WSDL und der OpenAPI ist ein wichtiger Bestandteil des Plugins. +Sie wird nur einmal vor der ersten Anfrage an die SOAP-Schnittstelle ausgeführt, um die Schnittstelle zu konfigurieren. +Dadurch verzögert sich die Antwort der ersten Anfrage im Durchschnitt um 200 Millisekunden. +Anschließend wird diese Konfiguration für die Verarbeitung aller folgenden Anfragen verwendet.
+ +Bei der Analyse der WSDL werden wichtige Merkmale der SOAP-Schnittstelle ausgelesen. +Dazu gehören zum Beispiel das Auslesen der verschiedenen Operationen und ihrer zugehörigen Rückgabetypen und Faults. +Es wird besonders die Struktur der Rückgabetypen betrachtet, damit das Plugin später gültige SOAP-Antworten generieren kann.
+ +Nachdem die WSDL analysiert wurde, wird die Konfiguration des Plugins mit der OpenAPI-Spezifikation der REST-API vervollständigt. +Dabei werden den SOAP-Operationen die passenden Pfade der REST-API automatisiert zugeordnet. +Außerdem werden jeder Operation die passenden Content-Types zugeordnet, damit die HTTP-Header bei der Weiterleitung der Anfragen an die REST-API korrekt gesetzt werden können.
+ +Bei der Konvertierung von eingehenden SOAP-Anfragen, werden diese in den meisten Fällen direkt von XML in JSON überführt. +Manchmal müssen davor aber noch andere Verarbeitungsschritte durchgeführt werden. +Zum Beispiel werden sämtliche SOAP-Header in HTTP-Header überführt. +Außerdem werden Dateiuploads in Multipart Bodys überführt und den Dateien wird mit Hilfe einer Mime Type Analyse der richtige Content-Type zugeordnet.
+ +Auch bei der Konvertierung der REST-Antworten werden verschiedene Zwischenschritte benötigt. +Wenn die REST-API nicht den Statuscode 200 zurückgibt, wird die Antwort in ein gültiges SOAP-Fault umgewandelt.
+ +Bei der Umwandlung der Antwort in XML wird besonders darauf geachtet, dass die Reihenfolge der Attribute des Rückgabetyps mit der Reihenfolge in der WSDL übereinstimmen. +Dafür wird die automatisch generierte Konfiguration des Plugins verwendet.
+ +Damit der Einsatz des Plugins sich lohnt, sollte die Verfügbarkeit der Schnittstelle nicht unter dem Einsatz des Plugins leiden. +Um die Auswirkungen des Plugins auf die Verfügbarkeit der Schnittstelle zu testen, wurde ein Performance- und ein Lasttest vorgenommen.
+ +Der Performancetest besteht aus vier verschiedenen Anfragen, welche jeweils 10-mal wiederholt wurden. +Zwei der vier Anfragen liefern nur einen HTTP Status Code von 200 und 300 zurück. +Die dritte Anfrage sendet mit einem HTTP POST ein kleines JSON Objekt an die Schnittstelle. +Und die vierte Anfrage versucht die Grenzen der Schnittstelle auszuloten, indem eine Datei an die Schnittstelle gesendet wird.
+ +Die nachfolgende Grafik zeigt links die Ergebnisse des Performancetests auf die REST-API und rechts die Ergebnisse der SOAP-Schnittstelle.
+ +Im direkten Vergleich fällt auf, dass die Performance nur beim Senden von Dateien leidet. +Die Performance aller andern Anfragen verändert sich nur geringfügig.
+ +Der Anstieg der Antwortzeit ist darauf zurückzuführen, dass bei SOAP-Anfragen deutlich mehr Daten an die Schnittstelle gesendet werden müssen.
+ +Im Gegensatz zum Performancetest wurden die vier Anfragen 250-mal parallel ausgeführt. +Um ein aussagekräftiges Ergebnis zu erhalten wurde dieser 10-mal wiederholt.
+ +Die folgende Grafik zeigt die Ergebnisse des Lasttests.
+ +Auf den ersten Blick fällt auf, dass die SOAP-Schnittstelle bei vielen parallelen Anfragen längere Antwortzeiten aufweist, als die REST-API. +Vor allem das Senden von Dateien führt zu einem deutlichen Anstieg der Antwortzeit. +Der Kommunikationsaufwand in Verbindung mit den vielen gleichzeitigen Anfragen auf die Schnittstelle ist für die langsamere Antwortzeit verantwortlich.
+ +Zusammenfassend lässt sich sagen, dass die Nutzung dieses Kong Plugins den Vorteil hat, dass nicht beide Schnittstellen implementiert werden müssen. +Ein Nachteil ist die zusätzliche Wartezeit auf die Antwort der Schnittstelle, da sich hinter jeder SOAP-Anfrage eine Anfrage auf die REST-API verbirgt. +Diese Verzögerung kann allerdings vernachlässigt werden, da erst bei vielen parallelen Anfragen ein deutlich längere Wartezeit entsteht. +Im Großen und Ganzen überwiegen die Vorteile des Plugins die Nachteile.
+ +In diesem Blogeintrag widmen wir uns der Programmiersprache Kotlin. +Wir werfen einen kurzen Blick auf die Ursprünge der Sprache, wie sie aufgebaut ist und mit welchen Designprinzipien im Hinterkopf sie entworfen wurde. +An Beispielen betrachten wir die Best Practices und stellen uns dabei die Frage: Was hat das mit funktionaler Programmierung zu tun?
+ +]]>In diesem Blogeintrag widmen wir uns der Programmiersprache Kotlin. +Wir werfen einen kurzen Blick auf die Ursprünge der Sprache, wie sie aufgebaut ist und mit welchen Designprinzipien im Hinterkopf sie entworfen wurde. +An Beispielen betrachten wir die Best Practices und stellen uns dabei die Frage: Was hat das mit funktionaler Programmierung zu tun?
+ +Vor zehn Jahren (2011) stellte JetBrains erstmals die Open-Source Programmiersprache Kotlin auf dem JVM Language Summit vor – “Eine Sprache einfach genug für den gewöhnlichen Entwickler und produktiv genug für moderne Anforderungen an Projekte”. +Die Sprache setzt auf der Java Virtual Machine (JVM) auf und erschien 2016 in der ersten Release-Version. +Seit 2017 wird sie von Google offiziell zur Entwicklung von Android-Apps unterstützt und ist seit 2019 Googles bevorzugte Sprache für diese Plattform.
+ +Im TIOBE-Index1 rangiert Kotlin aktuell (Juli 2021) auf Platz 38 der beliebtesten Programmiersprachen. +Betrachtet man ausschließlich die JVM-spezifischen Sprachen steht Kotlin dort auf Platz 4 (übertrumpft von #36 Scala, #15 Groovy und #2 Java).
+ +Im PYPL-Index2 belegt Kotlin den 11. Platz.
+ +Kotlin, eine statisch typisierte Programmiersprache, ist voll interoperabel zu Java-Programmen und -Bibliotheken und kann ohne aufwändige Integration in bereits bestehende Projekte eingepflegt werden.
+ +Jeder Einstieg in eine Programmiersprache fängt gleich an. +Man muss sich an die Syntax gewöhnen. +Folgendes Beispiel zeigt ein kleines, in Kotlin geschriebenes Programm, welches zuerst das Ergebnis einer Instanzmethode und dann das einer statischen Methode ausgibt:
+fun main(){
+ val mainClassInstance = MainClass("instanceString")
+ println(mainClassInstance.instanceMethod())
+ println(MainClass.staticMethod(3))
+}
+
+class MainClass(private var member: String){
+ companion object{
+ fun staticMethod(parameter:Int):Int{
+ return parameter*2
+ }
+ }
+
+ public fun instanceMethod():String{
+ return "A"
+ }
+}
+
+
+Das companion object
ist, wie der Name es andeutet, ein Begleiterobjekt zu dieser Klasse und verhält sich ähnlich der statischen Initialisierung in Java (auch wenn das Begleiterobjekt noch einiges mehr kann, auf das ich hier nicht eingehen werde).
+Anders als in Java werden die Rückgabewerte von Methoden am Ende des Methodenkopfs plaziert und auch für Wertdefinitionen und Parameter wird der jeweilige Typ durch einen “:” getrennt auf die rechte Seite gestellt.
+Kotlin unterstützt Typinferenz, weswegen die Typdefinitionen in den meisten Fällen auch weggelassen werden können.
+Was – anders als bei Java – hier auch auffällt, ist, dass ich die Properties der MainClass
direkt hinter den Klassennamen in “( )” definieren kann und sie nicht im Codeblock schreiben muss (, aber auch das könnte ich).
+Kotlin generiert für die Variablen Getter- und Setter-Methoden und für die Values nur Getter-Methoden.
+Auf den Unterschied komme ich im Abschnitt Immutabilität zu sprechen.
+Wie das Beispiel oben auch zeigt, habe ich einen primären Konstruktor für die MainClass
geschrieben, der sich direkt im Header befindet.
+Die in den Klammern des Konstruktors angegebenen Properties (hier member) entsprechen direkt einer Deklaration dieser als Teil der Klasse.
+Auch die Semikolons können wir in den meisten Fällen weglassen.
Eine Ärgerlichkeit, mit der wir uns im Entwicklungsalltag häufig auseinandersetzen müssen, ist das Behandeln von Nullpointer-Exceptions, also dem Fehlen von Daten an Stellen, an denen das Programm welche erwartet hat. +Tony Hoare, der Erfinder der Null-Referenz, hielt 2009 einen Vortrag und nannte als Grund für dessen Einführung die Einfachheit, mit der sie zu implementieren gewesen sei. +Er bezeichnet seine Entscheidung inzwischen als “Milliarde-Dollar-Fehler”: +(My billion-dollar mistake) +Kotlin behandelt dieses Problem aus meiner Sicht pragmatisch (Null-Safety), indem, wenn nicht anders angegeben, Werte einfach nicht null sein dürfen. +Betrachten wir folgendes kleines Beispiel eines Produkts, für das ein Preis mit Steuer berechnet werden soll.
+fun main() {
+ val product = Product(4.99)
+ val vat: Double = null
+ product.getConsumerPrice(vat)
+}
+
+class Product(private val price: Double) {
+ fun getConsumerPrice(vat: Double): Double {
+ val tempValue = helperFunction(vat)
+ return tempValue * this.price
+ }
+ ...
+}
+
+Was tendenziell in Java und vielen anderen Sprachen funktioniert wird hier vom Compiler mit einer Fehlermeldung quittiert, da der Wert val vat: Double
als Double mit Wert definiert und null dort nicht erlaubt ist.
+So werden wir bei der Entwicklung immer informiert, wenn Daten potentiell undefinierte Zustände annehmen könnten:
+"Kotlin: Null can not be a value of a non-null type Double"
+Manchmal lässt es sich allerdings auch nicht vermeiden oder ist erwünscht, dass ein null Wert übernommen wird;
+Das kann zum Beispiel an Schnittstellen der Fall sein, an denen ein Standardwert keinen Sinn ergibt (auch wenn sich hier wieder darüber streiten lässt, ob ein Standardwert wirklich nicht die bessere Entscheidung ist).
+Wir können die Werte mit einem ?
markieren, um kenntlich zu machen, dass sie null (nullable) sein dürfen:
+val vat: Double? = null
+Hier kommt die Arbeit zum Vorschein, die uns der Compiler durch diese kleine Änderung abnimmt: product.getConsumerPrice(vat)
wird mit dem Double?
aufgerufen, aber getConsumerPrice(vat: Double)
erwartet einen Wert, der nicht null ist.
+Auch das erkennt der Compiler und gibt Type mismatch: inferred type is Double? but Double was expected
zurück.
+So sind wir gezwungen, uns um diesen Fall zu kümmern und entweder vorher sicherzustellen, dass vat
nicht null sein kann, oder einen Nullwert als Eingabeparameter zu erlauben, wodurch sich die Fehlermeldung auf nachfolgende Aufrufe von vat
verbreitet.
Ein anderes Beispiel zeigt, wie wir uns beim Programmieren mit Nullwerten in Kotlin viel Boilerplate-Code sparen können. +Dafür schauen wir uns zunächst ein Problem in Java und anschließend eine Lösung in Kotlin an:
+class Head {
+ public Node next;
+}
+class Node {
+ public Node next;
+ String value = null;
+}
+...
+head.next.next.value;
+
+Die Suche von value
in dem Beispiel kann, wenn einer der Zwischenaufrufe null ist, zu einer NullpointerException
führen.
+Um dieses Problem zu umgehen, müssen wir zwischen den Aufrufen null-Checks einführen.
+Um Platz zu sparen schreibe ich die Prüfungen direkt als ternäre Operationen:
Head head = new Head();
+Node nodeA = head.next != null? head.next :null;
+Node nodeB = nodeA.next != null? nodeA.next :null;
+String value = nodeB != null? nodeB.value: null;
+
+In Kotlin kann bei potentiellen Nullwerten ?
eingesetzt werden, um diese zu erlauben:
class Head(val next: Node?)
+class Node(val next: Node?, val value: String)
+...
+val head = Head(null) // Bei der Initialisierung muss ich den Wert für 'next' direkt angeben und kann ihn nicht unbestimmt lassen
+val string = head.next?.next?.value
+
+Dieser Aufruf führt zu keiner NullpointerException
, sondern weist string
null zu, da bereits der Aufruf von head.next?
null zurückgibt.
+Der Wert ist dabei implizit vom Typ String?
, wodurch auch alle folgenden Aufrufe vom Compiler wieder geprüft werden.
Alternativ kann der Elvis-Opeator ?:
genutzt werden, um in solchen Fällen direkt einen Standardwert zuzuweisen, sodass statt String?
der Typ String
inferiert wird.
val string = head.next?.next?.value?:"default"
+
+
+So kann sichergestellt werden, dass Nullwerte innerhalb der Anwendung angemessen behandelt werden können.
+ +In den beiden Beispielen der vorherigenen Sektionen habe ich das val
- und das var
-Schlüsselwort zur Definition von Werten genutzt.
+val
wird genutzt um einen zur Laufzeit unveränderlichen Wert zu definieren (anders noch als const
, welches für unveränderliche, zur Kompilierzeit bekannte, Werte steht).
+Es ist vergleichbar mit final
aus Java.
+Auf der anderen Seite steht das var
-Schlüsselwort mit dem herkömmliche Variablen beschrieben werden können.
+Immutabilität hilft während der Entwicklung Nebeneffekte im Code auf ein Minimum zu reduzieren und schafft so Sicherheit vor allem für Parallelität.
+Betrachten wir folgendes Beispiel in Java:
class SideEffect {
+ public int member = 0;
+
+ public int someCalculation(int input) {
+ int aux = member + 2;
+ int result = member + aux + input;
+ member++;
+ return result;
+ }
+}
+
+someCalculation
nutzt die Variable member
für einige Berechnungen.
+In einer synchronen Umgebung ist dies problemlos möglich.
+Soll die Methode allerdings parallel ausgeführt werden, kann es zu inkonsistentem Verhalten kommen, da member
zu verschiedenen Zeitpunkten innerhalb der Ausführung der Methode unterschiedliche Werte annehmen kann.
+Besser ist hier eine Lösung, die someCalculation
weitestgehend unabhängig vom aktuellen Wert von member
macht.
+Denkbar ist:
public int someCalculation(int input, int memberVal) {
+ int aux = memberVal + 2;
+ int result = memberVal + aux + input;
+ member++;
+ return result;
+}
+
+Durch das Verlagern des für die Berechnung genutzten Wertes ist sichergestellt, dass die Methode, selbst wenn sich member
zur Laufzeit ändert, innerhalb ihres Ausführungskontextes einen konsistenten Zustand einhält.
+An dieser Stelle bediene ich mich zusätzlich an einigen Punkten, auf die man zum Thema Immutabilität im Internet immer wieder trifft:
Diese Auflistung zeigt, dass der Aufwand für Immutabilität im Verältnis zu den Vorteilen in den meisten Fällen gering ausfällt.
+ +An dieser Stelle macht es Sinn, die funktionalen Programmierung ins Spiel zu bringen. +Was ist funktionale Programmierung und wie kann sie uns bei unserer Arbeit helfen? +Die funktionale Programmierung ist ein Ansatz der Programmierung, die Verarbeitung von Daten nicht anweisungsgetrieben (imperativ, wie z.B. in Java) zu konzipieren, sondern aus einer mathematischen Perspektive heraus – funktional – zu betrachten. +Also statt dass wir ein Problem aus der Perspektive betrachten, jeden Schritt einzeln durchzugehen, arbeiten wir mit einer Menge von Daten, auf die Operationen angewandt werden und die mitunter eine neue Menge von Daten erzeugt. +Die funktionale Programmierung ist etwas, was in vielen großen Programmiersprachen immer mehr Einzug hält, auch weil die Rechenleistung heutiger Computer so hoch ist, dass die schlechtere Performance, die durch diesen Ansatz erreicht wird, nicht mehr ins Gewicht fällt. +Theoretische Grundlage der funktionalen Programmierung ist das Lambda-Kalkül, welches in den 30er Jahren von Church und Kleene zur Beschreibung von Funktionen eingeführt wurde. +Ein einfacher Lambda-Ausdruck sieht dabei wie folgt aus und beschreibt hier f(x)=x+2:
+λx.x+2
+
+Lambda-Ausdrücke kennen wir aus der Entwicklung im Java-Kontext hauptsächlich in Form von Lambda-Ausdrücken (ab Java 8 in 2014). +Collections müssen dazu erst in einen stream konvertiert, transformiert und dann dann wieder zurück konvertiert werden:
+strings
+ .stream()
+ .filter(s -> s.length() == 5)
+ .collect(Collectors.toList());
+
+Lambda-Ausdrücke gibt es auch in Kotlin und werden dort viel häufiger verwendet. +Gibt es nur einen Wert im Lambda-Ausdruck der gebunden werden muss, kann der implizite Name it benutzt werden, statt dem Laufwert einen konkreten Namen geben zu müssen.
+strings.filter { it.length == 5 }
+
+Auch wenn beide Beispiele hier nur einfache sind, empfinde ich persönlich die Kotlin-seitigen Lösungen häufig intuitiver und kürzer als das bei Java der Fall ist. +Allein der Wegfall der Konvertierungen reduziert den Boilerplate-Code und erleichtert damit die Wartung der Software.
+ +Die filter
-Methode haben wir gerade eben kennengelernt.
+Wie der Name beschreibt, kann sie genutzt werden, um Elemente aus einer Menge an Daten herauszufiltern.
+Die zwei wichtigen anderen Methoden, die häufig eingesetzt werden, sind die map
- und die reduce
-Methode.
+map
iteriert über jedes Element einer Menge von Daten und wendet eine Funktion auf dieses an.
+Heraus kommt dabei eine neue Menge von Daten, die möglicherweise geändert wurden.
+(Ich sage möglicherweise, weil die identische Abbildung f(x)=x existiert)
productList.map { product -> product.getConsumerPrice(0.19) }
+
+Obiges Beispiel zeigt, wie eine Liste von Produkten in eine Liste von Preisen konvertiert wird, indem von jedem Produkt-Element der Konsumentenpreis geholt wird.
+Die reduce
-Methode verhält sich ähnlich zur map
-Methode, mit dem Unterschied, dass das Ergebnis ein einzelnes Element ist.
+Auch hier wird auf jedes Element der Menge eine Funktion angewandt.
+Das folgende Beispiel zeigt, wie aus unserer Preisliste eine Summe über alle Preise gebildet wird.
+sum
definiert dabei das Akkumulator-Element im ersten Parameter.
+price
ist die Laufvariable (eher Laufwert) für die einzelnen Preise, über die iteriert wird.
priceList.reduce { sum, price -> sum+price }
+
+Statt uns mit der Iteration beschäftigen zu müssen, erlaubt diese Heransgehensweise uns das eigentliche Problem behandeln zu können.
+map
, filter
und reduce
sind Beispiele für sogenannte Funktionen höherer Ordnung, denn sie nehmen nicht nur einfache Werte als Parameter entgegen, sondern erwarten Funktionen, die sie während ihrer Ausführung aufrufen können.
+Ihre Flexibilität im Kern, während sie einen klaren Rahmen für die Verarbeitung von Daten in einer bestimmten Art und Weise schaffen, machen sie zu mächtigen Werkzeugen.
inline fun <S, T : S> Iterable<T>.reduce(
+ operation: (acc: S, T) -> S
+): S
+
+Definition der reduce Extensionfunktion mit Generics
+ +Zwei Dinge, die an dem obigen Beispiel auffallen, sind der Einsatz von Generics zur Verallgemeinerung der Anwendbarkeit der Funktion; +Und dass es sich hierbei um eine sogenannte Extension-Function handelt, die – in diesem Fall – Iterable um eine Methode erweitert. +Extension-Functions können genutzt werden um Klassen zu erweitern, ohne neue Klassen oder Interfaces definieren zu müssen, die von der Grundklasse erben. +Hier gibt es mehr Informationen zu inline-Funktionen.
+ +Hier ist ein Beispiel aus einem Projekt in dem ich gearbeitet habe. +Dort haben wir Extension-Functions häufig genutzt, um vor allem die Lesbarkeit unseres Codes zu erhöhen.
+data class Partner(
+ val name: String,
+ ...
+)
+
+fun Partner.toDto(): PartnerDto = PartnerDto(
+ name,
+ ...
+)
+...
+partner01.toDto()
+
+Natürlich lässt sich die Konvertierung in ein DTO auch klassisch lösen:
+fun createPartnerDto(partner:Partner):PartnerDto {
+ return PartnerDto(
+ partner.name,
+ ...
+ )
+}
+
+createPartnerDto(partner01)
+
+Allerdings erhöht die erste Variante die Lesbarkeit des Codes, wenn es um Methodenverkettung geht (wie oben zu sehen, gibt es dieses Konzept auch in Java).
+ +Klassische Funktionen:
+prepareSend(enrichWithData(createFromPartnerDto(partner01), data), destination)
+
+Extension-Functions:
+partner01.toDTO().enrichWithData(data).prepareSend(destination)
+
+Ein anderes Beispiel zeigt, dass wir auch Klassen erweitern können, die wir nicht selber geschrieben haben.
+(Das Schlüsselwort suspend
kann ignoriert werden.
+Bei Interesse empfehle ich die Einführung in Coroutines.)
+Hier haben wir String um eine domänenspezifische Funktion erweitert, um zu diesem eine zugehörige Klasse zu finden:
private suspend fun String.getCategoryByCategoryId(): Category?
+...
+val category = item.category?.getCategoryByCategoryId()
+
+
+Nehmen wir das, was wir bis jetzt betrachtet haben, ergeben sich daraus auch neue Möglichkeiten Softwarequalität sicherzustellen. +Die Standardvorgehensweise für das Schreiben von Tests auf unterster Ebene sind die Unit-Tests. +Kleine Blöcke, die die Funktionalität einzelner, isolierter Code-Ausschnitte überprüfen sollen, indem wir feste Vorgabewerte definieren und an die jeweilige Funktion übergeben. +An dieser Stelle tritt das Property based testing aus der funktionalen Programmierung auf den Plan. +Dahinter verbirgt sich die Idee, statt einige feste Werte auf bestimmte Ergebnisse zu überprüfen (und damit Fehlerräume an den Stellen zu lassen, die man nicht testet), gemeinsame Eigenschaften in Gruppen von Eigabeparametern zu finden, die anschließend randomisiert überprüft werden können. +Schauen wir uns für ein besseres Verständnis ein einfaches Beispiel an. +Die folgende Funktion konkateniert zwei Strings miteinander (Man beachte, dass die geschweiften Klammern weggelassen werden können, wenn es sich bei der Funktion um eine einzelne Operation handelt):
+public fun concatenate(string1: String, string2: String): String = string1 + string2
+
+In klassischer Herangehensweise würden wir beim Testen neben den Grenzfällen (leerer String, Nullstring), einen “normalen” Methodenaufruf testen. +Eine andere Art und Weise an den Test heranszugehen ist, sich zu überlegen, welche Eigenschaft die Ergebnisse des Methodenaufrufs gemein haben. +Eigenschaften lassen sich dabei nach folgender Form beschreiben:
+ +Für Werte … gilt, wenn … zutrifft, dass … wahr/falsch ist
+ +In diesem Fall können wir also sagen:
+ +Für alle Strings string1 und string2 gilt, dass die Konkatenation von string1 und string2 mit string1 anfängt und mit string2 endet
+ +In Kotest könnte der Test dann so aussehen:
+class StringConcatTest: StringSpec({
+ "Alle konkatenierten Strings starten mit string1 und enden mit string2" {
+ forAll<String, String> { string1, string2 -> {
+ val concat = concatenate(string1, string2)
+ return concat.startsWith(string1) && concat.endsWith(string2)
+ }
+ }
+ }
+})
+
+Kotest würde mit seinem Standardgenerator diesen Test für 1000 Werte durchspielen und die Ergebnisse prüfen. +Selbstverständlich können auch eigene Generatoren geschrieben werden, die an die eigenen Bedürfnisse angepasst sind. +In diesem Beispiel mag der Test trivial sein, da auch die Fachlichkeit sehr simpel ist. +Je komplexer allerdings die Methode, desto schwieriger kann es sein herauszufinden, welche Menge von Daten eigentlich welche Eigenschaften erfüllen soll. +Ein netter Nebeneffekt: +Tests so zu schreiben zwingt uns damit nochmal auf eine andere Art und Weise über die Korrektheit einer Methode nachzudenken.
+ +Für mich persönlich, mit Java-Erfahrung seit 2012, war mein erster praktischer Kontakt mit Kotlin in 2019 wie eine kleine Offenbarung. +Möglicherweise ist es der Gewöhnungseffekt, dass Kotlin für mich in vielen Punkten durchdachter als Java scheint. +Sicherlich wird dabei auch das noch recht junge Alter der Sprache und die Erfahrungen, die in sie hineingeflossen sind, eine Rolle spielen. +Das funktionale Paradigma und Kotlin in seiner Ausprägung, ich habe es schon vorher geschrieben, kommen mir häufig leichtgängiger vor und es bereitet mir viel Freude, so zu programmieren. +Wir sollten uns dabei aber auch immer bewusst sein, dass dies zu einem gewissen Preis geschieht. +Der Preis, den wir hier zahlen, sind Effizienzeinbußen (die in vielen Projekten allerdings vernachlässigbar sein werden) gegenüber zum Beispiel einer optimierten Programmierlösung in C, sowie der Aufwand, den es für uns mit sich bringt, sich an diese neue Art zu denken zu gewöhnen. +Es hat einen Grund, dass sich viele Sprachen heutzutage an funktionaler Programmierung orientieren und diese sehr populär ist. +Ich hoffe, dass ich euch einen kleinen Einblick in Kotlin und die funktionale Welt geben konnte.
+ +Wenn ihr an dieser Stelle neugierig geworden seid und euch weiter mit der Sprache beschäftigen möchtet, dann empfehle ich ausdrücklich die Kotlin Koans. +Kotlin Koans sind sehr gute offizielle Tutorial-Reihe, die sich unter anderem mit den Inhalten beschäftigt, die wir hier nur oberflächlich betrachten konnten, und auch noch viel weiter in die Details der Sprache eintaucht.
+ +Im Allgemeinen empfehle ich auch den Kotlin Playground zum schnellen und unkomplizierten herumprobieren und programmieren im Webbrowser eurer Wahl, wenn ihr Kotlin nicht lokal ausführen wollt.
+ +Ein anderer spannender Blogeintrag zum Thema Kotlin bei adesso zum direkt Weiterlesen: +Kotlin Multiplattform Mobile +oder direkt an der Quelle: +The Kotlin Blog
+ +Quellen:
+ + + +