Kotlin: Kotlin DSLの構築

最終更新:2026-08-26

DSLはKotlinの最も表現力豊かな機能です。見た目は設定ファイルのようですが、実際には型安全なKotlinコードです。Charlieはこの機能を利用して注文設定用DSLを構築し、技術に詳しくないユーザーでも注文ルールを定義できるようにしています。

1. 学習内容


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のビルドフロー

100%
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などの制御構造を使用できます。 これは設定ファイルに比べて大きな利点であり、設定とロジックが一体となっているのです。

📖 まとめ


📝 練習問題

  1. 初心者 (⭐): apply を使用して Order オブジェクトを設定し、id、total、ステータス を指定します。ヒント: order.apply { id = "ORD-001"; total = 299.99 }
  2. 中級 (⭐⭐): EmailBuilder DSL: email { from("a@b.com"); to("c@d.com"); subject("Hi") } を構築してください。ヒント: fun email(block: EmailBuilder.() -> Unit) + ラムダレシーバー
  3. 課題 (⭐⭐⭐): @DslMarker を使用して、ネストされた DSL order { customer { name("Alice") }; item { sku("ABC") } } を実装し、内側のスコープから外側のメンバーにアクセスできないようにしてください。ヒント: @DslMarker +多層ビルダー

← 前へ | 次へ →

Web-Tutorial.com

Web-Tutorial 技術チーム

複数の開発者によって共同維持されているプログラミングチュートリアルプラットフォーム。各チュートリアルは専門分野の開発者が執筆・レビューしています。正確で信頼性の高いコンテンツを目指しています — 問題を見つけた場合はお知らせください。

100%