DSL in Kotlin: Wie Sie an einem Abend Ihre eigene Programmiersprache erstellen

Online Python-Trainer für Anfänger

Lernen Sie Python einfach ohne theoretische Überlastung. Lösen Sie praktische Aufgaben mit automatischer Überprüfung und schreiben Sie Code direkt im Browser.

Kurs starten

Was ist ein DSL und warum brauchen Sie es?

\n\n

Stellen Sie sich vor, Sie schreiben keinen Code, sondern eine Anleitung in verständlicher Sprache, die sich selbst in ein funktionierendes Programm verwandelt. Genau so funktionieren domänenspezifische Sprachen (DSL – Domain Specific Language). Im Gegensatz zu universellen Sprachen (Java, Python, C#) wird ein DSL zur Lösung einer bestimmten Aufgabe entwickelt – sei es die Serverkonfiguration, die Beschreibung von Geschäftsregeln oder die Erstellung komplexer SQL-Abfragen.

\n\n

Kotlin ist der ideale Kandidat für die Erstellung von DSLs. Dank Lambdas mit Empfänger, Erweiterungen und type-safe Buildern können Sie Code schreiben, der wie native Syntax aussieht. In diesem Artikel behandeln wir nicht nur die Theorie, sondern erstellen einen echten DSL für ein Test-Framework – und Sie werden sehen, wie einfach und spannend das ist.

\n\n

Warum ist Kotlin der König der DSLs?

\n\n

Kotlin hat die Liebe der Entwickler nicht nur wegen seiner Kompatibilität mit Java gewonnen. Seine syntaktischen Möglichkeiten ermöglichen die Erstellung interner DSLs – Sprachen, die innerhalb der Host-Sprache leben und deren Syntax verwenden. Hier sind die drei Säulen, auf denen jedes Kotlin-DSL basiert:

\n\n
    \n
  • Lambdas mit Empfänger – ermöglichen den Zugriff auf Methoden eines Objekts ohne explizite Angabe des Namens. Dies ist die Grundlage des Builder-Patterns.
  • \n
  • Erweiterungen – fügen jeder Klasse neue Funktionen hinzu, auch wenn Sie sie nicht ändern können.
  • \n
  • Infix-Funktionen – ermöglichen das Schreiben von Code im Stil natürlicher Sprache: should be equalTo 5.
  • \n
\n\n

Diese Möglichkeiten verwandeln das Schreiben von DSLs in einen Lego-Baukasten: Sie nehmen vorgefertigte Blöcke – Lambdas, Funktionen, Eigenschaften – und bauen daraus eine verständliche und sichere Sprache.

\n\n

Erstellen eines einfachen DSLs für Tests: Schritt für Schritt

\n\n

Lassen Sie uns einen DSL zum Schreiben von Tests erstellen. Das Ziel ist, dass Tests wie eine lesbare Beschreibung aussehen und nicht wie eine Reihe von assert-Aufrufen:

\n\n
test("Der Rechner sollte Zahlen addieren") {\n    val result = Calculator.add(2, 3)\n    result should beEqualTo 5\n    result shouldNot beNull\n}
\n\n

Geben Sie zu, das ist fast Englisch. Lassen Sie uns nun analysieren, wie das funktioniert.

\n\n

Schritt 1. Erstellen des Testkontexts

\n\n

Wir benötigen eine Funktion test, die einen Namen und einen Codeblock akzeptiert. Der Block wird in einem Kontext ausgeführt, in dem should und shouldNot verfügbar sind:

\n\n
class TestContext {\n    fun Any.should(matcher: Matcher) {\n        if (!matcher.matches(this)) {\n            throw AssertionError("Erwartet ${matcher.expected}, aber $this erhalten")\n        }\n    }\n\n    fun Any.shouldNot(matcher: Matcher) {\n        if (matcher.matches(this)) {\n            throw AssertionError("Erwartet nicht ${matcher.expected}, aber $this erhalten")\n        }\n    }\n}\n\nfun test(name: String, block: TestContext.() -> Unit) {\n    println("Führe Test aus: $name")\n    try {\n        TestContext().block()\n        println("✓ Bestanden")\n    } catch (e: AssertionError) {\n        println("✗ Fehlgeschlagen: ${e.message}")\n    }\n}
\n\n

Der entscheidende Punkt: TestContext.() -> Unit ist ein Lambda mit Empfänger. Innerhalb des Blocks verweist this auf das Objekt TestContext, sodass die Methoden should und shouldNot implizit verfügbar werden.

\n\n

Schritt 2. Implementieren der Matcher

\n\n

Jetzt erstellen wir einfache Matcher. beEqualTo und beNull sind Funktionen, die Matcher-Objekte zurückgeben:

\n\n
interface Matcher {\n    val expected: String\n    fun matches(value: Any?): Boolean\n}\n\nfun beEqualTo(expected: Any) = object : Matcher {\n    override val expected = "gleich $expected"\n    override fun matches(value: Any?) = value == expected\n}\n\nval beNull = object : Matcher {\n    override val expected = "null"\n    override fun matches(value: Any?) = value == null\n}
\n\n

Blogs

Buchempfehlungen