Blog

Domänenspezifische Sprachen in Kotlin: das Type-Safe Builder-Muster

Bjorn van der Laan

Aktualisiert Oktober 16, 2025
6 Minuten

Domänenspezifische Sprachen sind eine der Supermächte von Kotlin. Wenn Sie sie richtig aufbauen, können Sie den Benutzern eine fließende und elegante Syntax bieten, die lesbar ist und weniger Textbausteine enthält. Domänenspezifische Sprachen oder DSLs sind Miniprogrammiersprachen innerhalb Ihrer Kotlin-Codebasis, die sich auf bestimmte Anliegen konzentrieren. Alles, was eine DSL kann, ist auch mit allgemeinen, "unspezifischen" Sprachfunktionen wie Methoden und Lambdas möglich. Aber diese Minisprachen können sauberer und einfacher zu lesen sein. Sehen wir uns zwei Beispiele aus Spring Boot an, in denen domänenspezifische Sprachen glänzen.

Das erste Beispiel ist die Bean Definition DSL, die Beans mit Lambdas anstelle von Annotationen registriert. Ein Beispiel aus der Spring-Dokumentation:

val myBeans = beans {
    bean<Foo>()
    bean<Bar>()
    bean("bazBean") {
        Baz().apply {
            message = "Hello world"
        }
    }
    profile("foobar") {
        bean { FooBar(ref("bazBean")) }
    }
    bean(::myRouter)
}

Dieser Ansatz ist effizienter, da er weder Reflection noch Proxies verwendet. Außerdem gefällt mir sehr, dass Sie if-Ausdrücke und andere Konstrukte verwenden können. Schließlich ist es ja nur Kotlin!

Ein weiteres Beispiel ist die von Spring angebotene Router-DSL, mit der Sie die Routen in unserer Anwendung definieren können. Werfen Sie einen Blick auf eine meiner eigenen Kreationen:

fun apiRouter() = coRouter {
    "/api/cats".nest {
        accept(APPLICATION_JSON).nest {
            GET("", catHandler::getAll)


            contentType(APPLICATION_JSON).nest {
                POST("", catHandler::add)
            }


            "/{id}".nest {
                GET("", catHandler::getById)
                DELETE("", catHandler::delete)


                contentType(APPLICATION_JSON).nest {
                    PUT("", catHandler::update)
                }
            }
        }
    }
}

Alle Wörter in dieser DSL, außer nest, verraten Ihnen etwas darüber, wie wir die Routen konfiguriert haben: ein Zeichen für wenig Boilerplate. Die Router-DSL verwendet Verschachtelungen, damit Sie Routen auf der Grundlage gemeinsamer Eigenschaften wie Pfadsegmente oder Inhaltstypen gruppieren können.

Die verschachtelte Natur von DSLs

Diese verschachtelte Struktur ist das Hauptmerkmal der domänenspezifischen Sprachen in Kotlin. Wenn wir eine DSL verwenden, konstruieren wir tatsächlich ein komplexes, hierarchisches Objekt. Eine geschriebene DSL sollte Ihnen einen unmittelbaren Einblick geben, wie das resultierende Objekt aussehen wird. Um dies zu sehen, werfen Sie einen Blick auf diese domänenspezifische Sprache zur Erstellung von HTML-Dokumenten und das Objekt, das sie konstruiert:

Die Struktur der DSL ähnelt dem Objekt, das Sie konstruieren

Die DSL besteht aus Builder-Konstrukten, die beschreiben, wie ein Objekt aussehen wird. Die Builder-Konstrukte in unserer HTML-DSL sind html, body, head, h1 und p, und innerhalb der geschweiften Klammern definieren wir, wie diese Objekte aussehen sollen. In der Kotlin-Welt nennen wir sie typsichere Builder. Typsicher deshalb, weil alle Funktionen und Werte Typen haben, so dass der Compiler Ihre Eingaben überprüfen kann. Das ist hilfreich, denn Sie erhalten eine Fehlermeldung, wenn Sie etwas vergessen, einen ungültigen Wert eingeben oder einfach einen Tippfehler machen. Wenn der Compiler sagt, dass alles in Ordnung ist, sollte das Ergebnis gültig sein, vorausgesetzt, Ihre DSL ist korrekt.

Diese typsicheren Konstrukteure können auf den ersten Blick recht magisch aussehen. Einfach ein Wort, gefolgt von geschweiften Klammern, innerhalb derer die DSL fortgesetzt wird.

Wir können domänenspezifische Sprachen für viele Zwecke verwenden, von der Routing-Konfiguration bis zur Erstellung von HTML. Das Muster ist jedoch immer das gleiche. Typsichere Builder folgen immer drei Schritten:

  1. Erstellen Sie eine neue Instanz des Typs, den Sie erstellen möchten.
  2. Initialisieren Sie die Objektinstanz auf der Grundlage dessen, was zwischen den geschweiften Klammern steht.
  3. Gibt die initialisierte Instanz zurück (wenn Root Builder) oder fügt die Instanz dem Parent Builder hinzu (wenn Child Builder).

Der dritte Punkt mag auf den ersten Blick verwirrend klingen. In dem obigen HTML-Beispiel sind die Builder head und body verschachtelte Child-Builder innerhalb des Builders html, der die Wurzel darstellt. Anstatt zurückzukehren, sollten diese untergeordneten Builder ihre erstellten Instanzen an den übergeordneten html Builder weitergeben, um die Gesamtinstanz HTML zu erstellen.

Der html builder. Wie implementieren wir das?

Implementierung von typsicheren Buildern

Wir haben jetzt also das Muster gesehen, aber wie implementieren wir es? Der erste Schritt besteht darin, das Modell unserer Domäne zu definieren. Nehmen wir an, wir möchten die domänenspezifische Sprache für HTML implementieren, die wir oben gesehen haben. In diesem Fall müssen wir HTML-Tags modellieren. Unten sehen wir ein mögliches Modell.

Domänenmodell: HTML-Tags

Neben den Tags <html>, <head> und <body> unterstützen wir auch <h1> und <p>. Beachten Sie, dass die beiden letztgenannten eine andere Basisklasse BodyElement haben, da wir diese Tags nur innerhalb des Body verwenden können.

Unser erster Versuch, den html builder zu erstellen, besteht darin, eine Funktion zu definieren, die ein lambda aufnimmt und eine Html-Instanz zurückgibt. Die Funktion gibt einfach die Instanz zurück, die durch das Lambda geliefert wird. Wenn in Kotlin das Lambda der letzte Parameter einer Funktion ist, können wir die Klammern weglassen. Damit erhalten wir die folgende Syntax:

Das obige Beispiel sieht dem Builder sehr ähnlich. Alle drei Schritte des typsicheren Builder-Musters (erstellen, initialisieren, zurückgeben) werden innerhalb der geschweiften Klammern ausgeführt. Es ist jedoch nicht sinnvoll, den Benutzer unserer DSL aufzufordern, die Instanz im Builder manuell zu erstellen und zurückzugeben. Wir wollen das erleichtern. Der erste Schritt besteht darin, den Lambda-Parameter so zu ändern, dass er einen Empfänger hat:

Diese Syntax mag auf den ersten Blick verwirrend erscheinen. Ich stelle sie mir gerne so vor, dass eine Erweiterungsfunktion als Parameter übergeben wird. Im Rahmen dieser Funktion hat die HTML-Klasse nun eine zusätzliche Methode namens init, die der Aufrufer der Funktion definiert. Wenn sich unser Cursor innerhalb der Funktion befindet und wir unsere IDE nach allen Methoden der HTML-Klasse fragen, zeigt sie uns die neue Superkraft der Klasse:

Dieses 'Funktionsliteral mit Empfänger' ermöglicht es uns, die anfängliche HTML-Instanz in der Funktion html zu erstellen, und der Benutzer unserer DSL kann auf die Instanz innerhalb des Lambdas über das Schlüsselwort `this` zugreifen. Unser Builder erleichtert nun also Schritt 1 des Musters.

Um auch den dritten Schritt zu erledigen, müssen wir unseren Lambda-Parameter erneut leicht verändern. Diesmal wird der Rückgabetyp in Unit geändert, was bedeutet, dass der Lambda-Parameter nichts zurückgeben wird.

Und das ist wirklich alles, was wir tun müssen. Der Lambda ändert den Zustand des HTML-Objekts, während unser Builder die Instanz erstellt und zurückgibt.

Hinzufügen der Child-Builder

So bauen wir also den Stamm-Builder auf, aber wir wollen natürlich auch verschachtelte Builder haben. Wie macht man das?

Wir wiederholen das gleiche Muster wie zuvor, aber jetzt sind diese Funktionen Methoden der übergeordneten Builder-Klasse. Da wir mit Child-Buildern arbeiten, fügt der dritte Schritt die erstellte Instanz zur übergeordneten Klasse hinzu, anstatt sie zurückzugeben.

Mit diesen Methoden können wir unser DSL jetzt voll funktionsfähig machen:

Nächste Schritte

Wir haben in diesem Artikel gesehen, wie das typsichere Builder-Muster funktioniert und wie wir es implementieren können. Diese Builder sind das Rückgrat jeder DSL, und Sie können jetzt Ihre eigenen erstellen. Doch das ist nur der Anfang Ihrer Reise. Das Leben eines DSL-Builders beginnt zwischen den geschweiften Klammern, und in meinem nächsten Artikel werde ich Ihnen meine Lieblingsmuster zur Erstellung einer schönen DSL vorstellen. Bleiben Sie dran!

Verfasst von

Bjorn van der Laan

Software engineer at Xebia Software Development

Contact

Let’s discuss how we can support your journey.