Автор статьи: Сергей Прощаев (@sproshchaev)
Руководитель направления Java-разработки в FinTech

Введение

Domain Specific Language (DSL) — это язык, ориентированный на конкретную предметную область, который позволяет выражать решения в терминах этой области. В отличие от языков общего назначения вроде Java или Kotlin, DSL фокусируется на узкой задаче, делая код более читаемым и выразительным. 

Kotlin благодаря своему синтаксису и возможностям предоставляет отличные инструменты для создания внутренних DSL. В этой статье мы рассмотрим, как создавать собственные предметно-ориентированные языки в Kotlin, какие языковые конструкции для этого используются и как это применяется в реальных проектах.

Чтобы статья была практико-ориентированной, мы сосредоточимся на одной области — создании DSL для конфигурации приложений и разберем несколько компактных примеров.

Что такое DSL и зачем он нужен?

DSL (Domain Specific Language) — это язык программирования, специализированный для конкретной предметной области. В отличие от языков общего назначения, DSL решает узкий круг задач, но делает это максимально эффективно и понятно для экспертов в этой области. 

Преимущества DSL: 

  • Выразительность — код читается как естественный язык 

  • Безопасность — компилятор проверяет корректность 

  • Производительность — разработчики пишут код быстрее

Базовые конструкции Kotlin для создания DSL

Лямбды с получателем (Lambda with Receiver)

Самая важная возможность для создания DSL. Позволяет вызвать лямбду в контексте определенного объекта.

class Config {
    var host: String = "localhost"
    var port: Int = 8080
}

fun config(block: Config.() -> Unit): Config {
    return Config().apply(block)
}

Использование:

val appConfig = config {
    host = "api.example.com"
    port = 9000
}

Этот пример демонстрирует базовый DSL для конфигурации. Функция config принимает лямбду с получателем типа Config, что позволяет внутри блока обращаться к свойствам класса Config напрямую, без явного упоминания объекта. Конструкция .apply(block) применяет все операции из лямбды к созданному объекту Config, делая код чистым и читаемым.

Инфиксные функции (Infix Functions)

Позволяют вызывать функции в более естественном стиле.

class DatabaseConfig {
    infix fun url(value: String) { println("URL: $value") }
    infix fun user(value: String) { println("User: $value") }
}

Использование:

val dbConfig = DatabaseConfig()
dbConfig url "jdbc:postgresql://localhost/db"
dbConfig user "admin"

Этот пример показывает использование инфиксных функций для создания DSL с более естественным синтаксисом. Ключевое слово infix позволяет вызывать функции без точки и скобок, что делает код похожим на естественный язык.

Создание DSL для конфигурации приложения

Давайте создадим простой, но практичный DSL для настройки приложения.

class AppConfig {
    var name: String = ""
    var version: String = "1.0"
    val database = DatabaseConfig()
    val server = ServerConfig()
    
    fun database(block: DatabaseConfig.() -> Unit) {
        database.apply(block)
    }
    
    fun server(block: ServerConfig.() -> Unit) {
        server.apply(block)
    }
}

class DatabaseConfig {
    var url: String = ""
    var username: String = ""
    var password: String = ""
}

class ServerConfig {
    var port: Int = 8080
    var host: String = "localhost"
}

fun appConfig(block: AppConfig.() -> Unit): AppConfig {
    return AppConfig().apply(block)
}

Использование DSL:

val config = appConfig {
    name = "MyApp"
    version = "2.1"
    
    database {
        url = "jdbc:postgresql://localhost/mydb"
        username = "admin"
        password = "secret"
    }
    
    server {
        port = 9000
        host = "0.0.0.0"
    }
}

Этот пример демонстрирует вложенные DSL для сложной конфигурации приложения. Основной DSL appConfig содержит вложенные блоки database и server, каждый из которых имеет свой собственный контекст настроек.

DSL для настройки зависимостей

Создадим мини-DSL для конфигурации зависимостей в стиле DI-контейнеров.

class DIContainer {
    private val dependencies = mutableMapOf<String, Any>()
    
    fun <T> single(name: String, creator: () -> T) {
        dependencies[name] = creator()
    }
    
    fun <T> get(name: String): T = dependencies[name] as T
}

fun dependencies(block: DIContainer.() -> Unit): DIContainer {
    return DIContainer().apply(block)
}

Использование

val di = dependencies {
    single("userService") { UserService() }
    single("authService") { AuthService() }
}

val userService: UserService = di.get("userService")

Этот пример показывает создание DSL для dependency injection (внедрения зависимостей). DSL позволяет регистрировать и получать сервисы в стиле, похожем на популярные DI-фреймворки.

Практический пример: DSL для кэширования

Реализуем простой DSL для настройки политик кэширования.

class CacheConfig {
    var ttl: Long = 3600
    var maxSize: Int = 1000
    var evictionPolicy: String = "LRU"
    
    infix fun ttl(seconds: Long) { this.ttl = seconds }
    infix fun maxSize(size: Int) { this.maxSize = size }
}

fun cache(block: CacheConfig.() -> Unit): CacheConfig {
    return CacheConfig().apply(block)
}

Использование:

val userCache = cache {
    ttl = 1800
    maxSize = 500
    evictionPolicy = "FIFO"
}

val sessionCache = cache {
    ttl(900) maxSize 200
}

Этот пример демонстрирует гибридный подход в DSL, сочетающий обычное присваивание и инфиксные функции для настройки кэша. Показывает гибкость Kotlin в создании различных стилей конфигурации.

Лучшие практики создания DSL в Kotlin

Что можно добавить к вышесказанному:

  1. Начинайте с простого — не перегружайте DSL функциональностью 

  2. Используйте осмысленные имена близкие к предметной области 

  3. Обеспечивайте типобезопасность — ошибки должны обнаруживаться на этапе компиляции 

  4. Документируйте DSL — он должен быть интуитивно понятен

Заключение

Создание DSL в Kotlin — это мощный инструмент для повышения читаемости и выразительности кода, особенно в области конфигурации приложений. Благодаря лямбдам с получателем и другим возможностям языка, мы можем создавать элегантные предметно-ориентированные языки, которые делают код более понятным. 

DSL особенно полезны для настройки приложений, конфигурации зависимостей и описания бизнес-правил. Однако важно помнить, что DSL — это не серебряная пуля. Их следует использовать там, где они действительно упрощают понимание кода и снижают вероятность ошибок. 

Для новых проектов начинайте с простых DSL и постепенно расширяйте их функциональность, ориентируясь на реальные потребности команды.


Если тема DSL заходит, то в современном Kotlin-бэкенде это лишь часть более широкой картины: корутины, асинхронные пайплайны, проектирование API, работа с базами и продакшн-инфраструктура переплетаются куда теснее. Курс Kotlin Backend Developer. Professional помогает собраться в этой экосистеме целостно: от архитектуры и микросервисов до практики с Ktor, безопасностью и развёртыванием.

20 ноября в рамках курса пройдет демо-урок на тему «DSL в Kotlin: от теории к практике». Поучаствовать можно бесплатно, присоединяйтесь.

Комментарии (0)