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á
- Elementos de design DSL: receptores lambda
Type.() -> Unit - Construtores type-safe:
apply/run/with @DslMarker: prevenir vazamento de escopo- Gradle Kotlin DSL como exemplo real
- Charlie em ação:
OrderBuilder+OrderItemBuilderDSL aninhada
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
// 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
// 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
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
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
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
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
@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
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:
// 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
// ============================================
// 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:
=== 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
// 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)
// 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
// @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
// 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)
// 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
// 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
// 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 viait(explícito). Receptores são adequados para DSLs (thisimplícito parece mais natural); parâmetros são adequados para callbacks (itexplí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/fore outros fluxos de controle dentro dos blocos. Esta é uma grande vantagem sobre arquivos de configuração — configuração + lógica em um.
📖 Resumo
- Receptores lambda
Type.() -> Unitsão o coração das DSLs — usethispara acessar implicitamente o receptor dentro do bloco applyretorna o receptor (configuração encadeável);run/with/letretornam o resultado do bloco- Padrão Builder + receptores lambda = DSL type-safe
@DslMarkerprevine vazamento de escopo DSL aninhado, garantido em tempo de compilação- DSL = flexibilidade de configuração + segurança de tipo de código
- Gradle Kotlin DSL é a aplicação industrial mais bem-sucedida de DSL
📝 Exercícios
- Iniciante (⭐): Use
applypara configurar um objetoOrder, definindo id, total e status. Dica:order.apply { id = "ORD-001"; total = 299.99 } - 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 - Desafio (⭐⭐⭐): Use
@DslMarkerpara 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