Kotlin: Construção de DSL do Kotlin Explicada

Última atualização: 2026-08-26

DSL é o recurso mais expressivo do Kotlin — lê como um arquivo de configuração, mas é na verdade código Kotlin type-safe. Charlie a usa para construir uma DSL de configuração de pedidos, permitindo que usuários não-técnicos definam regras de pedidos.

1. O que Você Aprenderá


2. A História Real de um Arquiteto

(1) Ponto de Dor: Formatos de Configuração Inseguros

As regras de pedidos de Charlie usavam configuração YAML, mas YAML não tem verificação de tipo — erros de digitação e campos ausentes só apareciam em tempo de execução. Isso causava 2-3 incidentes de produção por mês devido a erros de configuração.

(2) Solução com Kotlin DSL

KOTLIN
// YAML: sem segurança de tipo, erros em tempo de execução
order:
  customer: Alice
  items:
    - sku: ABC
      qty: three  // ERRO DE DIGITAÇÃO! Deveria ser 3

// Kotlin DSL: type-safe, erros em tempo de compilação
order {
    customer = "Alice"
    items {
        item { sku = "ABC"; qty = 3 }  // Verificação de tipo em tempo de compilação!
    }
}

Kotlin DSL = flexibilidade de configuração + segurança de tipo de código. O compilador captura erros de configuração em tempo de compilação.


3. Receptores Lambda

(1) Lambdas com Receptor

KOTLIN
// Lambda regular: parâmetro
val greet: (String) -> Unit = { name -> println("Olá, $name") }

// Lambda com receptor: 'this' refere-se ao receptor
val greet2: String.() -> Unit = { println("Olá, $this") }

// Uso
greet2("Alice")          // Olá, Alice
"Alice".greet2()         // Olá, Alice (chamada tipo extensão)

(2) with / apply / run

KOTLIN
val order = Order()

// with: executar bloco com receptor, retornar resultado do bloco
val summary = with(order) {
    id = "ORD-001"
    total = 299.99
    "Pedido $id criado"  // Valor de retorno
}

// apply: executar bloco com receptor, retornar o próprio receptor
val configured = order.apply {
    id = "ORD-001"       // 'this' é order
    total = 299.99
}  // Retorna order

// run: executar bloco com receptor, retornar resultado do bloco
val result = order.run {
    id = "ORD-002"
    "Processado $id"
}

(3) Comparação de Funções de Escopo

Função Referência Valor de Retorno Caso de Uso
apply this Receptor Configuração de objeto (encadeamento)
run this Resultado do bloco Executar + retornar resultado
with this Resultado do bloco Uso não-extensão
let it Resultado do bloco Transformações null-safe
also it Receptor Efeitos colaterais (logging / validação)

4. Construtores Type-Safe

(1) Construtor Básico

KOTLIN
class OrderBuilder {
    var id: String = ""
    var total: Double = 0.0
    var status: String = "PENDING"
    var customer: String = ""
    private val items = mutableListOf<OrderItem>()

    fun item(block: OrderItemBuilder.() -> Unit) {
        items.add(OrderItemBuilder().apply(block).build())
    }

    fun build() = Order(id, total, status, customer, items.toList())
}

class OrderItemBuilder {
    var sku: String = ""
    var qty: Int = 1
    var unitPrice: Double = 0.0
    fun build() = OrderItem(sku, qty, unitPrice)
}

// Ponto de entrada da DSL
fun order(block: OrderBuilder.() -> Unit): Order {
    return OrderBuilder().apply(block).build()
}

(2) Usando a DSL Builder

KOTLIN
val myOrder = order {
    id = "ORD-001"
    total = 299.99
    customer = "Alice"
    item {
        sku = "SKU-WIDGET"
        qty = 3
        unitPrice = 9.99
    }
    item {
        sku = "SKU-GADGET"
        qty = 1
        unitPrice = 149.99
    }
}

5. @DslMarker para Prevenir Vazamento de Escopo

(1) Problema: Ambiguidade de Receptor Implícito

KOTLIN
order {
    id = "ORD-001"
    item {
        sku = "ABC"
        // PERIGO: 'id' aqui poderia referir-se ao OrderBuilder.id externo!
        id = "ITEM-001"  // Qual id? Order ou Item?
    }
}

(2) Solução com @DslMarker

KOTLIN
@DslMarker
annotation class OrderDsl

@OrderDsl
class OrderBuilder {
    var id: String = ""
    fun item(block: OrderItemBuilder.() -> Unit) { ... }
}

@OrderDsl
class OrderItemBuilder {
    var sku: String = ""
    // Agora: não pode acessar OrderBuilder.id externo daqui!
}

order {
    id = "ORD-001"  // OK: OrderBuilder.id
    item {
        sku = "ABC" // OK: OrderItemBuilder.sku
        // id = "X"  // ERRO: não pode acessar escopo externo!
    }
}

(3) Fluxo de Construção da DSL

100%
flowchart TD
    A[bloco order] --> B[OrderBuilder<br/>id, total, customer]
    B --> C[bloco item]
    C --> D[OrderItemBuilder<br/>sku, qty, price]
    D --> E[build OrderItem]
    E --> F[adicionar à lista de items]
    B --> G[build Order]
    G --> H[Retornar objeto Order]

6. Comparação de Padrões de Design DSL

Padrão Implementação Legibilidade Segurança de Tipo Caso de Uso
Builder + Receptor Lambda fun order(block: Builder.() -> Unit) ★★★★★ ✅ Tempo de compilação Configuração aninhada complexa
Funções Infix infix fun A.to(b: B) ★★★★ ✅ Tempo de compilação Encadeamento simples
Sobrecarga de Operadores operator fun plus() ★★★ ✅ Tempo de compilação Matemática / operações de coleção
Processamento de Anotações @Annotation class X ★★★ ✅ Tempo de geração Reduzir boilerplate
Proxy Dinâmico estilo dynamic ★★ ❌ Tempo de execução Config flexível / scripts

7. Exemplo do Gradle Kotlin DSL

Uma das aplicações mais bem-sucedidas do Kotlin DSL — scripts de build do Gradle:

KOTLIN
// build.gradle.kts - isto É uma Kotlin DSL!
plugins {
    kotlin("jvm") version "1.9.22"
    application
}

dependencies {
    implementation(kotlin("stdlib"))
    implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3")
    testImplementation(kotlin("test"))
}

kotlin {
    jvmToolchain(17)
}

application {
    mainClass.set("com.order.MainKt")
}

7. Exemplo Completo: OrderProcessor DSL de Configuração de Pedidos

KOTLIN
// ============================================
// OrderProcessor - DSL de Configuração de Pedidos
// Recurso: DSL builder type-safe para pedidos
// ============================================

@DslMarker
annotation class OrderDsl

data class OrderItem(val sku: String, val qty: Int, val unitPrice: Double) {
    val subtotal: Double get() = qty * unitPrice
}

data class Address(val street: String, val city: String, val country: String)

data class Order(
    val id: String,
    val customer: String,
    val items: List<OrderItem>,
    val shippingAddress: Address?,
    val priority: String,
    val notes: String?
) {
    val total: Double get() = items.sumOf { it.subtotal }
}

@OrderDsl
class OrderBuilder {
    var id: String = ""
    var customer: String = ""
    var priority: String = "STANDARD"
    var notes: String? = null
    private val items = mutableListOf<OrderItem>()
    private var shippingAddress: Address? = null

    fun item(block: OrderItemBuilder.() -> Unit) {
        items.add(OrderItemBuilder().apply(block).build())
    }

    fun items(block: ItemsBuilder.() -> Unit) {
        items.addAll(ItemsBuilder().apply(block).build())
    }

    fun shipTo(block: AddressBuilder.() -> Unit) {
        shippingAddress = AddressBuilder().apply(block).build()
    }

    fun build(): Order {
        require(id.isNotBlank()) { "ID do pedido é obrigatório" }
        require(customer.isNotBlank()) { "Cliente é obrigatório" }
        return Order(id, customer, items.toList(), shippingAddress, priority, notes)
    }
}

@OrderDsl
class OrderItemBuilder {
    var sku: String = ""
    var qty: Int = 1
    var unitPrice: Double = 0.0

    fun build(): OrderItem {
        require(sku.isNotBlank()) { "SKU é obrigatório" }
        return OrderItem(sku, qty, unitPrice)
    }
}

@OrderDsl
class ItemsBuilder {
    private val items = mutableListOf<OrderItem>()
    fun item(block: OrderItemBuilder.() -> Unit) {
        items.add(OrderItemBuilder().apply(block).build())
    }
    fun build() = items.toList()
}

@OrderDsl
class AddressBuilder {
    var street: String = ""
    var city: String = ""
    var country: String = ""
    fun build() = Address(street, city, country)
}

// Ponto de entrada da DSL
fun order(block: OrderBuilder.() -> Unit): Order =
    OrderBuilder().apply(block).build()

fun main() {
    // DSL type-safe: lê como configuração, verificada pelo compilador
    val myOrder = order {
        id = "ORD-001"
        customer = "Alice"
        priority = "HIGH"

        items {
            item { sku = "SKU-WIDGET"; qty = 3; unitPrice = 9.99 }
            item { sku = "SKU-GADGET"; qty = 1; unitPrice = 149.99 }
            item { sku = "SKU-DOOHICKEY"; qty = 5; unitPrice = 4.50 }
        }

        shipTo {
            street = "123 Main St"
            city = "New York"
            country = "US"
        }

        notes = "Entrega urgente solicitada"
    }

    println("=== Resumo do Pedido ===")
    println("ID: ${myOrder.id}")
    println("Cliente: ${myOrder.customer}")
    println("Prioridade: ${myOrder.priority}")
    println("\nItens:")
    myOrder.items.forEach { item ->
        println("  ${item.sku} x${item.qty} @ \$${item.unitPrice} = \$${item.subtotal} USD")
    }
    println("\nTotal: \$${myOrder.total} USD")
    println("Enviar para: ${myOrder.shippingAddress}")
    println("Notas: ${myOrder.notes}")

    // Validação: segurança de tipo em tempo de compilação
    // sku = 123       // ERRO: String esperado, não Int
    // qty = "three"   // ERRO: Int esperado, não String
}

Saída:

TEXT 📖 Somente leitura
=== Resumo do Pedido ===
ID: ORD-001
Cliente: Alice
Prioridade: HIGH

Itens:
  SKU-WIDGET x3 @ $9.99 = $29.97 USD
  SKU-GADGET x1 @ $149.99 = $149.99 USD
  SKU-DOOHICKEY x5 @ $4.5 = $22.5 USD

Total: $202.46 USD
Enviar para: Address(street=123 Main St, city=New York, country=US)
Notas: Entrega urgente solicitada

9. Exemplos práticos rápidos

▶ Exemplo: TipoBuilder básico

KOTLIN
// DSL para configurar conexões de banco de dados
class ConnectionBuilder {
    var host: String = "localhost"
    var port: Int = 5432
    var database: String = "default"
    private val properties = mutableMapOf<String, String>()

    fun property(key: String, value: String) {
        properties[key] = value
    }

    fun build() = "jdbc:postgresql://$host:$port/$database?${properties.entries.joinToString("&") { "${it.key}=${it.value}" }}"
}

// Função DSL - usa receiver lambda (this: ConnectionBuilder)
fun connection(block: ConnectionBuilder.() -> Unit): String {
    return ConnectionBuilder().apply(block).build()
}

val config = connection {
    host = "prod-db.com"
    port = 5433
    database = "orders"
    property("ssl", "true")
    property("user", "app")
}

println(config)

▶ Exemplo: DSL com BuilderContext (apply)

KOTLIN
// DSL com apply para configuração fluente
class HttpRequestBuilder {
    var url: String = ""
    var method: String = "GET"
    private val headers = mutableMapOf<String, String>()
    private val params = mutableMapOf<String, String>()

    fun header(name: String, value: String) {
        headers[name] = value
    }

    fun param(name: String, value: String) {
        params[name] = value
    }
}

fun httpRequest(url: String, block: HttpRequestBuilder.() -> Unit) {
    HttpRequestBuilder().apply { url = url }.apply(block).also {
        println("URL: ${it.url}")
        println("Method: ${it.method}")
    }
}

httpRequest("https://api.example.com/orders") {
    method = "POST"
    header("Content-Type", "application/json")
    header("Authorization", "Bearer xxx")
    param("limit", "50")
}

▶ Exemplo: Type-safe builders com @DslMarker

KOTLIN
// @DslMarker impede chamadas acidentais entre scopes aninhados
@DslMarker
annotation class HtmlDsl

@HtmlDsl
class HtmlBuilder {
    private val children = mutableListOf<String>()
    fun body(init: BodyBuilder.() -> Unit) = BodyBuilder().apply(init).render().also { children.add(it) }
    fun render() = "<html>\n${children.joinToString("\n")}\n</html>"
}

@HtmlDsl
class BodyBuilder {
    private val content = mutableListOf<String>()
    fun h1(text: String) { content.add("<h1>$text</h1>") }
    fun p(text: String) { content.add("<p>$text</p>") }
    fun render() = "<body>\n${content.joinToString("\n")}\n</body>"
}

fun html(init: HtmlBuilder.() -> Unit) = HtmlBuilder().apply(init).render()

val page = html {
    body {
        h1("Bem-vindo")
        p("Página construída com DSL Kotlin")
        p("Type-safe e fluente")
    }
}

println(page)

▶ Exemplo: infix functions em DSL

KOTLIN
// Operador infix para sintaxe fluente
infix fun String.should(expected: String) {
    if (this != expected) error("'$this' != '$expected'")
    else println("OK: '$this' == '$expected'")
}

infix fun Int.shouldBe(expected: Int) {
    if (this != expected) error("$this != $expected")
    else println("OK: $this")
}

// DSL-style assertions
"alice".should("alice")
"server-01".should("server-01")
42.shouldBe(42)

▶ Exemplo: Operadores em DSL (invoke)

KOTLIN
// operator fun invoke para DSL com notação funcional
class ScopeBuilder(val name: String) {
    val elements = mutableListOf<String>()

    operator fun String.unaryPlus() {
        elements.add(this)
    }

    operator fun invoke(action: ScopeBuilder.() -> Unit) {
        action(this)
    }

    fun render() = "Scope '$name': ${elements.joinToString(", ")}"
}

fun buildScope(name: String, action: ScopeBuilder.() -> Unit) =
    ScopeBuilder(name).action().render()

val result = buildScope("orders") {
    +"customer"
    +"address"
    +"payment"
}

println(result)

▶ Exemplo: Lambda with receiver em collection

KOTLIN
// use with/apply em DSL
fun buildOrder(block: Order.() -> Unit): Order = Order().apply(block)

class Order {
    val items = mutableListOf<String>()
    val metadata = mutableMapOf<String, String>()

    fun addItem(sku: String) { items.add(sku) }
    fun setMeta(key: String, value: String) { metadata[key] = value }
    fun summary() = "Itens: $items, Meta: $metadata"
}

val order = buildOrder {
    addItem("SKU-001")
    addItem("SKU-002")
    setMeta("customer", "Alice")
    setMeta("priority", "high")
}

println(order.summary())

▶ Exemplo: DSL aninhado com escopo fechado

KOTLIN
// DSL para configurar rotas
class RouterConfig {
    val routes = mutableListOf<Route>()

    fun route(path: String, block: RouteBuilder.() -> Unit) {
        routes.add(RouteBuilder(path).apply(block).build())
    }

    fun dump() = routes.forEach { println(it) }
}

data class Route(val path: String, val method: String, val handler: String)

class RouteBuilder(val path: String) {
    var method = "GET"
    var handler = "default"

    fun handler(fn: () -> Unit) {
        handler = fn.toString().lineSequence().first().take(40)
    }

    fun build() = Route(path, method, handler)
}

fun router(block: RouterConfig.() -> Unit) = RouterConfig().apply(block)

router {
    route("/orders") {
        method = "POST"
        handler = "createOrder()"
    }
    route("/orders/{id}") {
        method = "GET"
        handler = "getOrder()"
    }
    route("/orders/{id}/cancel") {
        method = "POST"
        handler = "cancelOrder()"
    }
    dump()
}

❓ Perguntas Frequentes

P: Qual a diferença entre uma DSL e uma API regular? R: Uma DSL visa "ler como linguagem natural ou configuração", alcançado através de receptores lambda + funções de extensão + chamadas infix. APIs regulares priorizam funcionalidade sobre legibilidade.

P: @DslMarker é obrigatório? R: Não obrigatório, mas fortemente recomendado. Sem ele, escopos DSL aninhados podem acessar acidentalmente membros de escopos externos, levando a bugs. @DslMarker previne isso em tempo de compilação.

P: Qual a diferença entre receptores lambda e parâmetros lambda? R: Receptores são acessados via this (implícito); parâmetros via it (explícito). Receptores são adequados para DSLs (this implícito parece mais natural); parâmetros são adequados para callbacks (it explícito é mais claro).

P: Qual o desempenho de DSLs? R: DSLs criam objetos Builder, que incorrem em leve sobrecarga de alocação. Mas em cenários de configuração (caminhos não-quentes), isso é negligenciável. Evite usar DSLs em loops internos com milhões de chamadas por segundo.

P: Qual a diferença entre Kotlin DSL e Groovy DSL? R: Kotlin DSL tem verificação de tipo em tempo de compilação, autocompletar IDE e suporte a refatoração. Groovy DSL é dinamicamente flexível, mas carece de verificações em tempo de compilação. Gradle Kotlin DSL está gradualmente substituindo Groovy DSL.

P: Como adiciono lógica condicional a uma DSL? R: A DSL é ela mesma código Kotlin — você pode usar if/when/for e outros fluxos de controle dentro dos blocos. Esta é uma grande vantagem sobre arquivos de configuração — configuração + lógica em um.


📖 Resumo


📝 Exercícios

  1. Iniciante (⭐): Use apply para configurar um objeto Order, definindo id, total e status. Dica: order.apply { id = "ORD-001"; total = 299.99 }
  2. Intermediário (⭐⭐): Construa uma DSL EmailBuilder: email { from("a@b.com"); to("c@d.com"); subject("Olá") }. Dica: fun email(block: EmailBuilder.() -> Unit) + receptor lambda
  3. Desafio (⭐⭐⭐): Use @DslMarker para implementar uma DSL aninhada: order { customer { name("Alice") }; item { sku("ABC") } }, garantindo que escopos internos não possam acessar membros externos. Dica: @DslMarker + Builder multi-camada

← Anterior | Próximo →

Web-Tutorial.com

Equipe Técnica Web-Tutorial

Uma plataforma de tutoriais mantida por diversos desenvolvedores. Cada tutorial é escrito e revisado por profissionais da área correspondente. Trabalhamos para manter nosso conteúdo preciso e confiável — se encontrar algum problema, avise-nos.

100%