Kotlin: Kotlin DSLの構築
最終更新:2026-08-26
DSLはKotlinの最も表現力豊かな機能です。見た目は設定ファイルのようですが、実際には型安全なKotlinコードです。Charlieはこの機能を利用して注文設定用DSLを構築し、技術に詳しくないユーザーでも注文ルールを定義できるようにしています。
1. 学習内容
- DSLの設計要素:ラムダレシーバー
Type.() -> Unit - 型安全なビルダー:
apply/run/with @DslMarker: スコープのリーク防止- 実用例としてのGradle Kotlin DSL
- Charlieの実践:
OrderBuilder+OrderItemBuilderのネストされたDSL
2. 本物の建築家の物語
(1) 課題:安全でない設定形式
Charlieのオーダー管理ルールではYAML設定が使用されていましたが、YAMLには型チェック機能がないため、入力ミスやフィールドの欠落は実行時にしか判明しませんでした。このため、設定ミスによる本番環境でのインシデントが月に2~3件発生していました。
(2) Kotlin DSLによる解決策
KOTLIN
// YAML: no type safety, errors at runtime
order:
customer: Alice
items:
- sku: ABC
qty: three // TYPO! Should be 3
// Kotlin DSL: type-safe, errors at compile time
order {
customer = "Alice"
items {
item { sku = "ABC"; qty = 3 } // Compile-time type check!
}
}
Kotlin DSL = 設定の柔軟性 + コードの型安全性。コンパイラがコンパイル時に設定エラーを検出します。
3. ラムダ受信機
(1) レシーバーを持つラムダ式
KOTLIN
// Regular lambda: parameter
val greet: (String) -> Unit = { name -> println("Hello, $name") }
// Lambda with receiver: 'this' refers to receiver
val greet2: String.() -> Unit = { println("Hello, $this") }
// Usage
greet2("Alice") // Hello, Alice
"Alice".greet2() // Hello, Alice (extension-like call)
(2) / apply / run を使用する
KOTLIN
val order = Order()
// with: execute block with receiver, return block result
val summary = with(order) {
id = "ORD-001"
total = 299.99
"Order $id created" // Return value
}
// apply: execute block with receiver, return receiver itself
val configured = order.apply {
id = "ORD-001" // 'this' is order
total = 299.99
} // Returns order
// run: execute block with receiver, return block result
val result = order.run {
id = "ORD-002"
"Processed $id"
}
(3) スコープ関数の比較
| 関数 | 参照 | 戻り値 | 使用例 |
|---|---|---|---|
apply |
this |
受信機 | オブジェクト構成(チェーン) |
run |
this |
ブロックの結果 | 実行 + 結果の返却 |
with |
this |
ブロックの結果 | 非拡張の用途 |
let |
it |
ブロックの結果 | null安全な変換 |
also |
it |
レシーバー | 副作用(ロギング/検証) |
4. 型安全なビルダー
(1) ベーシック・ビルダー
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)
}
// DSL entry point
fun order(block: OrderBuilder.() -> Unit): Order {
return OrderBuilder().apply(block).build()
}
(2) ビルダーDSLの使用
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
(1) 問題:暗黙的な受信者の曖昧性
KOTLIN
order {
id = "ORD-001"
item {
sku = "ABC"
// DANGER: 'id' here could refer to outer OrderBuilder.id!
id = "ITEM-001" // Which id? Order or Item?
}
}
(2) @DslMarker の解決策
KOTLIN
@DslMarker
annotation class OrderDsl
@OrderDsl
class OrderBuilder {
var id: String = ""
fun item(block: OrderItemBuilder.() -> Unit) { ... }
}
@OrderDsl
class OrderItemBuilder {
var sku: String = ""
// Now: cannot access outer OrderBuilder.id from here!
}
order {
id = "ORD-001" // OK: OrderBuilder.id
item {
sku = "ABC" // OK: OrderItemBuilder.sku
// id = "X" // ERROR: cannot access outer scope!
}
}
(3) DSLのビルドフロー
flowchart TD
A[order block] --> B[OrderBuilder<br/>id, total, customer]
B --> C[item block]
C --> D[OrderItemBuilder<br/>sku, qty, price]
D --> E[build OrderItem]
E --> F[add to items list]
B --> G[build Order]
G --> H[Return Order object]
6. DSLデザインパターンの比較
| パターン | 実装 | 可読性 | 型安全性 | ユースケース |
|---|---|---|---|---|
| ビルダー + ラムダレシーバー | fun order(block: Builder.() -> Unit) |
★★★★★ | ✅ コンパイル時 | 複雑なネストされた設定 |
| インフィックス関数 | infix fun A.to(b: B) |
★★★★ | ✅ コンパイル時 | 簡単な連鎖 |
| 演算子のオーバーロード | operator fun plus() |
★★★ | ✅ コンパイル時 | 数学/コレクション演算 |
| アノテーション処理 | @Annotation class X |
★★★ | ✅ 生成時間 | 定型コードの削減 |
| ダイナミックプロキシ | dynamic スタイル |
★★ | ❌ 実行時 | 柔軟な設定/スクリプト |
7. Gradle Kotlin DSL の例
Kotlin DSL の最も成功した活用例の一つ――Gradle ビルドスクリプト:
KOTLIN
// build.gradle.kts - this IS a 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. 完全な例:OrderProcessor の注文設定 DSL
▶ サンプル:注文設定DSL
KOTLIN
// ============================================
// OrderProcessor - Order Configuration DSL
// Feature: Type-safe order builder DSL
// ============================================
@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()) { "Order ID is required" }
require(customer.isNotBlank()) { "Customer is required" }
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 is required" }
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)
}
// DSL entry point
fun order(block: OrderBuilder.() -> Unit): Order =
OrderBuilder().apply(block).build()
fun main() {
// Type-safe DSL: reads like configuration, verified by compiler
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 = "Rush delivery requested"
}
println("=== Order Summary ===")
println("ID: ${myOrder.id}")
println("Customer: ${myOrder.customer}")
println("Priority: ${myOrder.priority}")
println("\nItems:")
myOrder.items.forEach { item ->
println(" ${item.sku} x${item.qty} @ \$${item.unitPrice} = \$${item.subtotal} USD")
}
println("\nTotal: \$${myOrder.total} USD")
println("Ship to: ${myOrder.shippingAddress}")
println("Notes: ${myOrder.notes}")
// Validation: compile-time type safety
// sku = 123 // ERROR: String expected, not Int
// qty = "three" // ERROR: Int expected, not String
}
出力:
TEXT
📖 参照専用
=== Order Summary ===
ID: ORD-001
Customer: Alice
Priority: HIGH
Items:
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
Ship to: Address(street=123 Main St, city=New York, country=US)
Notes: Rush delivery requested
❓ よくある質問
Q DSLと通常のAPIの違いは何ですか?
A DSLは「自然言語や設定ファイルのように読みやすい」ことを目指しており、ラムダレシーバー+拡張関数+中置呼び出しによってこれを実現しています。一方、通常のAPIは可読性よりも機能性を優先しています。
Q @DslMarker は必須ですか?
A 必須ではありませんが、強く推奨されます。これがないと、ネストされた DSL スコープが誤って外側のスコープのメンバにアクセスしてしまい、バグの原因となる可能性があります。@DslMarker を使用することで、コンパイル時にこれを防ぐことができます。
Q ラムダ受信者とラムダパラメータの違いは何ですか?
A 受信者は
this (暗黙的)を介してアクセスされ、パラメータは it (明示的)を介してアクセスされます。 レシーバーはDSLに適しています(暗黙的な this の方が自然です);パラメータはコールバックに適しています(明示的な it の方が明確です)。Q DSLのパフォーマンスはどうですか?
A DSLはBuilderオブジェクトを生成するため、わずかなメモリ割り当てのオーバーヘッドが発生します。しかし、設定処理(ホットパス以外)においては、これは無視できる程度です。1秒あたり数百万回の呼び出しが行われる内部ループでは、DSLの使用を避けてください。
Q Kotlin DSLとGroovy DSLの違いは何ですか?
A Kotlin DSLには、コンパイル時の型チェック、IDEの自動補完、リファクタリングのサポートがあります。Groovy DSLは動的な柔軟性がありますが、コンパイル時のチェック機能がありません。GradleのKotlin DSLは、徐々にGroovy DSLに取って代わりつつあります。
Q DSLに条件分岐ロジックを追加するにはどうすればよいですか?
A DSL自体はKotlinコードであるため、ブロック内で
if/when/forなどの制御構造を使用できます。 これは設定ファイルに比べて大きな利点であり、設定とロジックが一体となっているのです。📖 まとめ
- ラムダレシーバー
Type.() -> Unitは DSL の核心です。ブロック内でthisを使用すると、レシーバーに暗黙的にアクセスできます。 applyはレシーバー(チェーン可能な設定)を返します。run/with/letはブロックの結果を返します。- ビルダーパターン + ラムダレシーバー = 型安全なDSL
@DslMarkerは、ネストされた DSL のスコープ漏れを防止し、コンパイル時に強制されます- DSL = 設定の柔軟性 + コードの型安全性
- Gradle Kotlin DSL は、DSL の実用分野における最も成功した事例である
📝 練習問題
- 初心者 (⭐):
applyを使用してOrderオブジェクトを設定し、id、total、ステータス を指定します。ヒント:order.apply { id = "ORD-001"; total = 299.99 } - 中級 (⭐⭐):
EmailBuilderDSL:email { from("a@b.com"); to("c@d.com"); subject("Hi") }を構築してください。ヒント:fun email(block: EmailBuilder.() -> Unit)+ ラムダレシーバー - 課題 (⭐⭐⭐):
@DslMarkerを使用して、ネストされた DSLorder { customer { name("Alice") }; item { sku("ABC") } }を実装し、内側のスコープから外側のメンバーにアクセスできないようにしてください。ヒント:@DslMarker+多層ビルダー