Kotlin Tutorial

Kotlin: Ein Tutorial für Einsteiger, Teil 3

Alexander Hanschke

Die statisch typisierte Sprache Kotlin erfreut sich zunehmender Beliebtheit. Kotlin läuft sowohl auf der JVM als auch in JavaScript-Umgebungen. Durch die Kompatibilität zu Java 6 findet Kotlin zudem im Android-Umfeld Anklang, da die Sprache prägnanteren Code ermöglicht und zusätzlich die funktionale Programmierung unterstützt. In diesem mehrteiligen Tutorial betrachten wir die wichtigsten Sprachfeatures anhand einer Beispielanwendung.

JVM-Sprache Kotlin

In den ersten beiden Teilen dieser Serie haben wir damit begonnen, die Kotlin-Bindings für das Pocket API zu entwickeln. Wir ergänzen in diesem Teil die verbleibende Funktionalität für das Archivieren und Löschen von Artikeln und schließen die Implementierung mit geeigneten Tests sowie entsprechender Dokumentation ab. Der Sourcecode kann direkt von GitHub bezogen werden. Als Erstes beginnen wir damit, die verbleibenden Operationen für das Modifizieren von Artikeln zu implementieren. Für unseren Anwendungsfall reicht das Löschen und Archivieren vorerst aus. Um die Aktionen zu modellieren, erstellen wir eine Superklasse Action und die Subklassen Archive und Delete. Das Pocket API erwartet jeweils den zu ändernden Artikel sowie die Angabe, welche Aktion ausgeführt werden soll. Das stellen wir im Konstrukor der Superklasse wie folgt dar:

pocket-api/src/main/kotlin/de/techdev/pocket/Action.kt
class Action(name: String, item: Item)

Versiegelte Klassen

Kotlin unterstützt das Konzept von Enums. Ein Enum begrenzt die Anzahl der möglichen Typen. So hat das Enum Details nur die zwei Typen SIMPLE und FULL, von denen es jeweils nur eine Instanz gibt. Im Gegensatz dazu geben für die Vererbung vorgesehene Klassen die Anzahl möglicher Subklassen nicht vor. Es kann beliebig viele Subklassen und von diesen beliebig viele Instanzen geben.

In Kotlin gibt es noch ein Sprachfeature, das zwischen beiden Ansätzen steht: die versiegelten Klassen (Sealed Classes). Diese Klassen begrenzen die Vererbungshierarchie auf eine feste Anzahl von Typen, die allesamt zur Compile-Zeit feststehen. Allerdings kann es beliebig viele Instanzen dieser Typen geben. Wir nutzen das für unsere Action-Klasse und müssen dafür nur das Schlüsselwort sealed angeben.

pocket-api/src/main/kotlin/de/techdev/pocket/Action.kt
internal sealed class Action(name: String, item: Item) {

    @JsonProperty("action") var name: String = name
    @JsonProperty("item_id") var id: Long = item.id

    class Archive(item: Item) : Action("archive", item)

    class Delete(item: Item)  : Action("delete", item)

}

Die beiden zulässigen Subklassen Archive und Delete müssen wir innerhalb von Action definieren. Durch die Kennzeichnung mit sealed ist die Klasse implizit für die Vererbung geöffnet. Anderenfalls müssten wir das Schlüsselwort open verwenden. Wie im zweiten Teil beschrieben, nutzen wir die Jackson-Annotationen, um die auszuführende Aktion sowie die Artikel ID zu mappen.

Pattern Matching

Ein Szenario, in dem versiegelte Klassen verwendet werden können, ist im Zusammenspiel mit dem Pattern Matching. Basierend auf bestimmten Konditionen werden unterschiedliche Verzweigungen im Code durchlaufen. Das Pattern Matching ist vergleichbar mit if-else-Abfragen oder switch-Statements in Java, aber in der Regel prägnanter und lesbarer. In Kotlin existiert dafür der Ausdruck when, wie das folgende Beispiel zeigt:

fun check(param: Any): Any {
    return when(param) {
        is Int -> param in 0..100
        is String -> param.reversed()
        else -> {
            println("param unknown")
            return "unknown"
        }
    }
}

In dem Beispiel verwendet die Funktion check einen Parameter der vom Typ Any ist. Wir nutzen when, um auf diesem Parameter bestimmte Konditionen zu prüfen, beispielsweise, ob der Parameter vom Typ Int ist (erster Fall). Ist das der Fall, wird überprüft, ob der Wert in dem Bereich 0 bis 100 enthalten ist. Diese Überprüfung nutzt erneut den Smart Cast – der Compiler erkennt, dass der Typ innerhalb der Bereichsüberprüfung vom Typ Int ist und castet ihn automatisch.

Dasselbe gilt für die zweite Abfrage, nach der Überprüfung auf den Typen, kann anschließend direkt darauf zugegriffen werden, als handele es sich um einen String. Trifft keine der Bedingungen zu, so muss zwingend ein Default-Zweig angegeben werden, der alle Evantualitäten abfängt. Innerhalb der Zweige können neben einfachen Ausdrücken auch ganze Blöcke angegeben werden.

Da when ein Ausdruck ist, ist dessen Ergebnis das Ergebnis des ausgeführten Zweiges. Im ersten Zweig wird ein Boolean zurückgegeben und in den darauffolgenden jeweils ein String. Aus diesem Grund muss der Rückgabetyp der Funktion auch Any sein, da das der kleinste gemeinsame Nenner ist. Bei der Überprüfung der Konditionen wählt Kotlin den ersten passenden Zweig aus; die anderen werden nicht ausgeführt. Daher ist hier auch kein break wie in Java erforderlich.

Ersetzen wir das Beispiel mit unseren Aktionen, sieht dies wie folgt aus.

fun check(action: Action) {
    when (action) {
        is Action.Archive -> "archiving.."
        is Action.Delete  -> "deleting.."
    }
}

Die Besonderheit hier ist, dass alle möglichen Typen zur Compile-Zeit bereits feststehen. Daher ist in diesem Beispiel kein else-Zweig nötig. Zusätzlich kann when gänzlich ohne Parameter verwendet werden, was sich nachteilig auf die Lesbarkeit auswirken kann. Das folgende Beispiel ist semantisch identisch zu dem vorherigen.

fun check(action: Action) {
    when {
        action is Action.Archive -> "archiving.."
        action is Action.Delete  -> "deleting.."
    }
}

Nachdem wir die Aktionen definiert haben, legen wir die Klassen für das Mapping von Request und Response für das Archivieren und Löschen an. In der Datei Requests.kt kommt eine weitere Klasse ModifyRequest hinzu, welche im Konstruktor eine Collection von Action-Objekten erwartet.

pocket-api/src/main/kotlin/de/techdev/pocket/Requests.kt
internal class ModifyRequest(val actions: Collection<Action>) : PocketRequest()

Analog dazu definieren wir die Klasse ModifyResponse in der Datei Responses.kt.

pocket-api/src/main/kotlin/de/techdev/pocket/Requests.kt
internal class ModifyResponse(@JsonProperty("status") val status: Int)

Da wir die Operationen zum Löschen und Archivieren von Artikeln unterstützen, müssen wir entsprechende Funktionen in den ModifyOperations bereitstellen. Pocket erlaubt das Verarbeiten von mehreren Artikeln auf einmal, was weniger HTTP-Aufrufe benötigt. Um dennoch auch einzelne Artikel zu bearbeiten, ohne dafür eine Collection erzeugen zu müssen, bieten wir jeweils eine entsprechende Funktion an.

pocket-api/src/main/kotlin/de/techdev/pocket/api/ModifyOperations.kt
interface ModifyOperations {

    fun archive(item: Item) = archive(listOf(item))

    fun archive(items: Collection<Item>)

    fun delete(item: Item) = delete(listOf(item))

    fun delete(items: Collection<Item>)
}

Wir zuvor nutzen wir die Defaultimplementierung, um das Erstellen der Liste sowie das Aufrufen der passenden Funktion zu übernehmen. Daher müssen wir nur die Funktionen archive(items: Collection) und delete(items: Collection) innerhalb des ModifyTemplate implementieren. Da alle Aktionen identisch gehandhabt werden, erstellen wir uns zuerst eine Hilfsfunktion, die basierend auf Action-Elementen arbeitet.

pocket-api/src/main/kotlin/de/techdev/pocket/ModifyTemplate.kt
private fun execute(actions: Collection<Action>) {
    transport.post<ModifyResponse>(ModifyRequest(actions), "https://getpocket.com/v3/send")
}

Die Implementierung der eigentlichen Funktionen fällt wieder recht knapp aus.

pocket-api/src/main/kotlin/de/techdev/pocket/ModifyTemplate.kt
override fun archive(items: Collection<Item>) {
    execute(items.map { Action.Archive(it) })
}

override fun delete(items: Collection<Item>) {
    execute(items.map { Action.Delete(it) })
}

Auf den übergebenen Artikeln rufen wir die Funktion map auf, die Teil der Kotlin-Bibliothek ist. Diese bildet jedes Element der Collection auf ein neues Element ab, basierend auf einer übergebenen Abbildfunktion. In unserem Fall nehmen wir jeden Artikel und bilden ihn auf eine Action ab. Dafür verwenden wir einen Lambda-Ausdruck, der an den geschweiften Klammern erkennbar ist. Innerhalb des Lambda-Ausdrucks wird der jeweilige Artikel unter der Referenz it verfügbar gemacht, was eine allgemeine Konvention in Kotlin ist. Durch das Mappen erhalten wir eine neue Collection aus Action-Elementen, die wir dann an unsere Hilfsfunktion weitergeben und die Aufrufe gegen das Pocket-API absetzen.

Tests

Nachdem wir die Implementierung des API-Moduls vorerst abgeschlossen haben, kümmern wir uns nun um das Erstellen von Tests. Bevor wir die Tests schreiben können, benötigen wir ein paar zusätzliche Abhängigkeiten. Für das Erstellen der Tests benötigen wir Kotlin Test JUnit. Um Request und Responses des Pocket-API zu simulieren, nutzen wir den OkHttp Mock Webserver, und für das Mocken der Pocket-Operationen verwenden wir Mockito 2. Daher fügen wir die folgenden Abhängigkeiten mit dem testCompile Scope der Build-Datei hinzu.

pocket-api/build.gradle
...
dependencies {
    testCompile "org.jetbrains.kotlin:kotlin-test-junit:$kotlin_version"
    testCompile "com.squareup.okhttp3:mockwebserver:3.4.1"
    testCompile "org.mockito:mockito-core:2.2.0"
}

Im Testverzeichnis bilden wir die Paketstruktur de.techdev.pocket.api nach, obwohl das nicht zwingend erforderlich ist, und erstellen die Klasse PocketTest mit einem ersten Testfall.

pocket-api/src/test/kotlin/de/techdev/pocket/api/PocketTest.kt
class PocketTest {

    @Test
    fun testsRun() {
        fail("nothing to test here..")
    }

}

Hier gibt es keine große Überraschung. Die Definition von Testfällen erfolgt mittels Annotation, genauso wie man das aus Java kennt. Für das Testen des Pocket Interfaces, samt der Operationen, nutzen wir Mockito. Wie zuvor erwähnt, sind Klassen und Funktionen in Kotlin per Default final, es sei denn, sie werden mittels open für die Vererbung geöffnet. Das würde das Mocken kaum möglich machen. Allerdings unterstützt die jüngste Mockito-Version auch das Mocken von finalen Klassen und damit von Kotlin-Code, wie es Hadi Hariri (JetBrains) in seinem Blog beschreibt.

Um das zu nutzen, müssen wir eine neue Datei org.mockito.plugins.MockMaker unter dem Pfad pocket-api/src/test/resources/mockito-extensions anlegen und mock-maker-inline  einfügen. Jetzt können wir die benötigten Mocks wie folgt erstellen.

pocket-api/src/test/kotlin/de/techdev/pocket/api/PocketTest.kt
private fun pocket(): Pocket {
    val pocket = mock(Pocket::class.java)
    val transport = Components.transport("consumer", "access")
    `when`(pocket.modifyOperations()).thenReturn(ModifyTemplate(transport))
    `when`(pocket.retrieveOperations()).thenReturn(RetrieveTemplate(transport))

    return pocket
}

Wir beginnen mit dem Mocken des Pocket Interfaces, durch den Aufruf der mock-Methode. Diese erwartet einen Java-Typen. Auf diesen können wir mir der dargestellten Syntax zugreifen. Pocket::class gibt uns Zugriff auf die Laufzeitreferenz einer Kotlin-Klasse. Mit der Property java bekommen wir Zugriff auf die Referenz einer Java-Klasse. Danach erzeugen wir wie gewohnt eine Transportinstanz und verwenden für Consumer Key und Access Token jeweils Dummy-Daten.

Nun konfigurieren wir das Verhalten der Pocket-Instanz beim Zugriff auf die Operationen. Wird eine der Operationen aufgerufen, so geben wir eine eigens erstellte Instanz zurück. In Mockito wird dafür die Methode when verwendet. Das ist ebenfalls ein Schlüsselwort und muss daher per Backticks escapt werden.

Die Implementierungen ModifyTemplate und RetrieveTemplate enthalten aktuell noch die Pocket API Endpoints als private Property. Wir wollen aber verhindern, dass das echte API aufgerufen wird und die Anfragen stattdessen an den Mock Webserver geleitet werden. Wir erweitern beide Templates jeweils um einen sekundären Konstruktor, der neben dem Transport auch einen Endpoint entgegennimmt und den vordefinierten Wert ersetzt.

pocket-api/src/main/kotlin/de/techdev/pocket/RetrieveTemplate.kt
internal class RetrieveTemplate(private val transport: Transport) : RetrieveOperations {

    private var endpoint = "https://getpocket.com/v3/get"

    constructor(transport: Transport, endpoint: String) : this(transport) {
        this.endpoint = endpoint
    }
...
}

Für den Sekundärkonstruktor existiert kein init-Block. Stattdessen kann Initialisierungslogik direkt in einem Block ausgeführt werden. Das nutzen wir, um den Endpoint zu überschreiben. Zusätzlich muss der Konstruktor an den primären Konstruktor delegieren und die erwartete Referenz auf den Transport übergeben. Das wird mittels : this(transport) erledigt. Nun können wir den Test weiter ausbauen und erstellen den Mock-Webserver, an den die Aufrufe geschickt werden.

pocket-api/src/test/kotlin/de/techdev/pocket/api/PocketTest.kt
var server = MockWebServer()

Wir benötigen den URL und Port des Servers, um beides an die Implementierung der Pocket-Operationen zu übergeben. Diese Informationen können wir direkt vom Server abfragen.

pocket-api/src/test/kotlin/de/techdev/pocket/api/PocketTest.kt
`when`(pocket.modifyOperations()).thenReturn(ModifyTemplate(transport, server.url("/v3/send").toString()))
`when`(pocket.retrieveOperations()).thenReturn(RetrieveTemplate(transport, server.url("/v3/get").toString()))

Zuletzt müssen wir noch sicherstellen, dass der Server vor jedem Test gestartet und anschließend wieder gestoppt wird. Dazu können wir eine JUnit-Regel verwenden.

pocket-api/src/test/kotlin/de/techdev/pocket/api/PocketTest.kt
@Rule @JvmField
val resource = object : ExternalResource() {
    override fun before() = server.start()
    override fun after()  = server.shutdown()
}

Für unseren Anwendungsfall existiert die Klasse ExternalResource, die verwendet wird um Server oder beispielsweise Datenbanken vor einem Test aufzusetzen. Ein weiteres Mal nutzen wir eine Object Expression, um eine anonyme Implementierung zu erstellen. In dieser überschreiben wir die Funktionen before und after, um dem Server zu starten oder zu stoppen. Standardmäßig erzeugt der Kotlin-Compiler Properties (keine Felder). Das bedeutet, dass für resource nur ein Getter auf dem Java-Typen verfügbar ist. JUnit erwartet aber ein Feld.

Daher instruieren wir den Compiler das mittels @JvmField zu generieren. Unser erster richtiger Test soll verifizieren, dass bei einem Fehler beim Aufruf des Pocket API eine PocketException ausgelöst wird. Dazu erstellen wir noch eine Hilfsfunktion, welche die entsprechende Response des APIs mockt.

pocket-api/src/test/kotlin/de/techdev/pocket/api/PocketTest.kt
private fun error(code: Int, message: String): MockResponse {
    val response = MockResponse()
    response.setResponseCode(code)
    response.setHeader("X-Error", message)
    return response
}

Jetzt können wir den Test wie folgt umschreiben:

pocket-api/src/test/kotlin/de/techdev/pocket/api/PocketTest.kt
@Test
fun `given a bad request, PocketException is thrown`() {
    server.enqueue(error(400, "Missing API Key"))

    assertFailsWith<PocketException> {
        pocket().retrieveOperations().items()
    }
}

Initial fügen wir dem Webserver die gemockte Antwort hinzu, sodass diese bei der nächsten Anfrage gegen den Server zurückgeliefert wird. Anschließend nutzen wir die in Kotlin Test verfügbare Funktion assertFailsWith um zu verifizieren, dass beim Aufruf des API eine Exception ausgelöst wird. Unter Verwendung von Backticks können wir die Namen von Typen oder Funktionen lesbarer gestalten. Das ist bei der Auswertung vieler Tests hilfreicher, als die Camel-Case-Syntax.

Dokumentation

Zum Abschluss dieses Teils beginnen wir mit der Dokumentation des Codes. Analog zu JavaDoc gibt es in Kotlin KDoc. Sinnvoll ist es, alle Typen innerhalb des api Packages zu dokumentieren, da Clients diese direkt verwenden. Um exemplarisch die RetrieveOperations zu dokumentieren, können wir das wie folgt tun.

pocket-api/src/main/kotlin/de/techdev/pocket/api/RetrieveOperations.kt
/**
 * Operations used to retrieve [Item]s from Pocket
 *
 * @author Alexander Hanschke
 */
interface RetrieveOperations {

    /**
     * Retrieves existing [Item]s from Pocket
     * @param state only retrieve [Item]s with this [State]
     * @param sort sort [Item]s based on the sort direction
     * @param details specify whether all [Item] details shall be returned or only a minimum
     *
     * @return all existing [Item]s matching the specified parameters
     */
    fun items(state: State = State.UNREAD, sort: Sort = Sort.OLDEST, details: Details = Details.SIMPLE): Collection<Item> = emptyList()

}

Der @author-Tag ist identisch mit der Java-Version. Um einen Link auf einen anderen Typen oder eine andere Funktion zu erstellen, werden die eckigen Klammern verwendet (z. B. [Item]). Wie oben gezeigt, verwendet man @param, um die Parameter einer Funktion zu beschreiben. Eine vollständige Übersicht der Syntax-unterstützten Tags kann hier gefunden werden. Wie generieren wir nun die fertige Dokumentation? Mit Dokka.

Dokka ist ein Generator für die fertige Dokumentation. Damit wir ihn verwenden können, müssen wir ihn zuerst als Abhängigkeit in unserem Buildscript definieren.

pocket-api/build.gradle
buildscript {
    ext.kotlin_version = '1.0.4'
    ext.dokka_version = '0.9.9'

    repositories {
        jcenter()
    }

    dependencies {
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
        classpath "org.jetbrains.dokka:dokka-gradle-plugin:${dokka_version}"
    }
}

apply plugin: 'kotlin'
apply plugin: 'org.jetbrains.dokka'

Das Gradle-Plug-in erzeugt einen Task documentation > dokka, den wir direkt ausführen können, um die Dokumentation zu generieren.

gradle dokka

Anschließend finden wir die generierte Dokumentation innerhalb des API-Moduls unter build | dokka. Die komplette Dokumentation kann auf GitHub gefunden werden. Neben der Dokumentation der Typen können wir auch Packages und ganze Module dokumentieren. Dazu legen wir eine neue Datei docs.md direkt im API-Modul an. Wie der Name bereits verrät, handelt es sich um eine Markdown-Datei. Entsprechend können wir die Dokumentation wie folgt vornehmen:

# Module pocket-api

This module encapsulates the communication with the Pocket API.

# Package de.techdev.pocket.api

This package contains the Kotlin bindings for the Pocket API.

Damit Dokka diese Datei ebenfalls für die Generierung der Dokumentation berücksichtigt, müssen wir den Gradle Task etwas anpassen.

dokka {
    includes = ["docs.md"]
}

Führen wir den Task nun erneut aus, ist auch die Package- und Modul-Dokumentation verfügbar. Damit schließen wir die Implementierung des API-Moduls ab. Im nächsten Schritt bauen wir Janitor zu einer Spring-Anwendung aus, die uns erlaubt, auf Artikel zuzugreifen und regelmäßig veraltete Artikel zu archivieren.

Fazit

Im dritten Teil des Tutorials haben wir die Operationen für das Archivieren und Löschen von Artikeln in Pocket mittels Sealed-Klassen umgesetzt. Diese lassen sich innerhalb von when-Blöcken einfach nutzen. Generell ist mit when das Pattern Matching in Kotlin möglich. Unter Zuhilfenahme von Mockito haben wir den ersten Test entwickelt und gezeigt, wie man selbst die finalen Funktionen von Kotlin mocken kann. Um die Templates testbar zu machen, haben wir sekundäre Konstruktoren verwendet. Mittels der Annotation @JvmField lässt sich der Compiler instruieren, statt Properties-Felder zu verwenden. Am Ende haben wir den Code in KDoc dokumentiert und mittels Dokka die Dokumentation generiert.

Verwandte Themen:

Geschrieben von
Alexander Hanschke
Alexander Hanschke
Alexander Hanschke arbeitet seit 2013 bei der techdev Solutions GmbH in Karlsruhe, wo er Kunden bei der erfolgreichen Umsetzung von IT-Projekten unterstützt.
Kommentare

Hinterlasse einen Kommentar

Hinterlasse den ersten Kommentar!

avatar
400
  Subscribe  
Benachrichtige mich zu: