Kotlin: Kotlinの委譲とプロパティ委譲

最終更新:2026-08-26

委譲はKotlinの最大の特徴的な機能です。スレッドセーフな遅延初期化も、プロパティの変更監視も、たった1行で実現できます。Charlieは、クラス委譲を使ってデコレータパターンの定型コードを置き換え、プロパティ委譲を使ってOrderProcessorの設定プロパティを自動的に永続化しています。

1. 学習内容


2. 本物の建築家の物語

(1) 課題:デコレーターパターンの定型コード

Charlieは、Javaのデコレータパターンを用いてOrderRepositoryにロギングとキャッシュ機能を追加しました。各レイヤーには20行以上の委譲メソッドが必要でした。インターフェースには8つのメソッドがあり、3つのデコレータレイヤーがあったため、純粋な委譲コードだけで480行にもなりました。

(2) Kotlinにおけるクラス委譲の解決策

KOTLIN
// Java: 20+ lines per method per decorator
class LoggedRepository implements OrderRepository {
    private final OrderRepository delegate;
    @Override public Order findById(String id) { log(); return delegate.findById(id); }
    // ... repeat for all 8 methods
}

// Kotlin: 1 line delegates ALL methods
class LoggedRepository(
    private val delegate: OrderRepository
) : OrderRepository by delegate  // Auto-delegates all methods!

たった1行で20行以上のメソッド委譲を置き換えられます。拡張したいメソッドのみをオーバーライドすればよいのです。


3. クラスの委譲 (by)

(1) 基本的な構文

KOTLIN
interface OrderRepository {
    fun findById(id: String): Order?
    fun save(order: Order)
    fun findAll(): List<Order>
}

class InMemoryRepository : OrderRepository {
    private val storage = mutableMapOf<String, Order>()
    override fun findById(id: String) = storage[id]
    override fun save(order: Order) { storage[order.id] = order }
    override fun findAll() = storage.values.toList()
}

// Class delegation: auto-delegates all interface methods
class LoggedRepository(
    private val inner: OrderRepository
) : OrderRepository by inner {  // All methods delegated!
    // Override only what you want to enhance
    override fun save(order: Order) {
        println("Saving order: ${order.id}")
        inner.save(order)  // Call delegate explicitly
    }
}

(2) 多層的な権限委譲

KOTLIN
// Layer 1: Caching
class CachedRepository(
    private val delegate: OrderRepository
) : OrderRepository by delegate {
    private val cache = mutableMapOf<String, Order>()

    override fun findById(id: String): Order? {
        return cache.getOrPut(id) { delegate.findById(id)!! }
    }
}

// Layer 2: Logging
class LoggedRepository(
    private val delegate: OrderRepository
) : OrderRepository by delegate {
    override fun save(order: Order) {
        println("Saving: ${order.id}")
        delegate.save(order)
    }
}

// Compose: logging + caching + in-memory
val repo = LoggedRepository(CachedRepository(InMemoryRepository()))

(3) クラス委譲 vs 継承 vs デコレータ

次元 継承 デコレータ (Java) クラス委譲 (Kotlin)
コード量 高(定型文) 低(by)
柔軟性 単一継承 多層構成 多層構成
カップリング
実行時 親クラスのバインディング デリゲートオブジェクトのバインディング デリゲートオブジェクトのバインディング

4. プロパティの委譲:遅延評価による

(1) 基本的な使い方

KOTLIN
class OrderProcessor {
    // Lazy: computed on first access, thread-safe by default
    val cache: OrderCache by lazy {
        println("Initializing cache...")
        OrderCache(maxSize = 10_000)
    }

    // Lazy with custom thread safety
    val heavyConfig by lazy(LazyThreadSafetyMode.PUBLICATION) {
        loadConfig()  // May be called multiple times, but only first result used
    }
}

(2) 遅延スレッドセーフモード

モード 動作 パフォーマンス ユースケース
SYNCHRONIZED (デフォルト) シングルイニシャライズ、ダブルチェックロック 若干処理が遅い マルチスレッド環境
PUBLICATION 複数のスレッドで計算が行われる場合があるが、最初の結果のみが使用される 高速 副作用のない初期化
NONE 同期なし 最速 既知のシングルスレッド環境

5. プロパティの委譲:オブザーバブル/拒否可能

(1) 観測可能 — 変更通知

KOTLIN
import kotlin.properties.Delegates

class Order(var id: String) {
    // observable: notify on every change
    var status: String by Delegates.observable("PENDING") { _, old, new ->
        println("Status changed: $old -> $new")
    }
}

val order = Order("ORD-001")
order.status = "CONFIRMED"  // Prints: Status changed: PENDING -> CONFIRMED
order.status = "SHIPPED"    // Prints: Status changed: CONFIRMED -> SHIPPED

(2) 拒否可能 — 変更のインターセプト

KOTLIN
class Order(var id: String) {
    // vetoable: can reject the change
    var total: Double by Delegates.vetoable(0.0) { _, old, new ->
        if (new < 0) {
            println("Rejecting negative total: $new")
            false  // Veto: keep old value
        } else {
            true  // Accept the change
        }
    }
}

val order = Order("ORD-001")
order.total = 299.99  // Accepted
order.total = -50.0   // Rejected: prints warning, keeps 299.99

(3) 「観察可能」と「拒否可能」の比較

デリゲート 動作 戻り値 ユースケース
observable 変更を通知し、インターセプトしない ユニット ロギング/監査
vetoable 変更を検知・拒否可能 ブール値 検証/制約

6. カスタムプロパティの委譲

(1) ReadWriteProperty インターフェース

KOTLIN
import kotlin.reflect.KProperty

class PersistentProperty<T>(
    private val key: String,
    private val defaultValue: T
) : ReadWriteProperty<Any?, T> {

    override fun getValue(thisRef: Any?, property: KProperty<*>): T {
        // Load from persistence (simplified)
        return loadFromStore(key) ?: defaultValue
    }

    override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {
        // Save to persistence
        saveToStore(key, value)
    }
}

// Simulated persistence
private val store = mutableMapOf<String, Any?>()
private fun <T> loadFromStore(key: String): T? = store[key] as T?
private fun <T> saveToStore(key: String, value: T) { store[key] = value }

(2) カスタムデリゲートの使用

KOTLIN
class OrderConfig {
    var maxRetries: Int by PersistentProperty("order.max_retries", 3)
    var timeout: Long by PersistentProperty("order.timeout", 5_000L)
    var taxRate: Double by PersistentProperty("order.tax_rate", 0.08)
}

val config = OrderConfig()
println(config.maxRetries)  // 3 (default)
config.maxRetries = 5
println(config.maxRetries)  // 5 (persisted)

(3) デリゲートクラス図

100%
classDiagram
    class ReadOnlyProperty {
        <<interface>>
        +getValue(thisRef, property): T
    }
    class ReadWriteProperty {
        <<interface>>
        +getValue(thisRef, property): T
        +setValue(thisRef, property, value: T)
    }
    class LazyDelegate {
        +getValue(): T
    }
    class ObservableDelegate {
        +setValue()
    }
    class VetoableDelegate {
        +setValue()
    }
    class CustomDelegate {
        +getValue()
        +setValue()
    }
    ReadOnlyProperty <|-- ReadWriteProperty
    ReadOnlyProperty <|-- LazyDelegate
    ReadWriteProperty <|-- ObservableDelegate
    ReadWriteProperty <|-- VetoableDelegate
    ReadWriteProperty <|-- CustomDelegate

7. 完全な例:OrderProcessor 委譲アーキテクチャ

▶ サンプル:委譲を使ったアーキテクチャ

KOTLIN
// ============================================
// OrderProcessor - Delegation Architecture
// Feature: Class delegation + property delegation
// ============================================

import kotlin.properties.Delegates
import kotlin.reflect.KProperty

// --- Class Delegation ---

interface OrderRepository {
    fun findById(id: String): Order?
    fun save(order: Order)
    fun findAll(): List<Order>
}

data class Order(val id: String, var total: Double, var status: String, val customer: String)

class InMemoryRepository : OrderRepository {
    private val storage = mutableMapOf<String, Order>()
    override fun findById(id: String) = storage[id]
    override fun save(order: Order) { storage[order.id] = order }
    override fun findAll() = storage.values.toList()
}

class LoggedRepository(private val inner: OrderRepository) : OrderRepository by inner {
    override fun save(order: Order) {
        println("  [LOG] Saving order: ${order.id} (\$${order.total} USD)")
        inner.save(order)
    }
}

class ValidatedRepository(private val inner: OrderRepository) : OrderRepository by inner {
    override fun save(order: Order) {
        require(order.total >= 0) { "Total must be non-negative" }
        require(order.id.startsWith("ORD-")) { "Invalid order ID format" }
        inner.save(order)
    }
}

// --- Property Delegation ---

private val configStore = mutableMapOf<String, Any?>()

class ConfigProperty<T>(private val key: String, private val default: T) {
    operator fun getValue(thisRef: Any?, property: KProperty<*>): T {
        @Suppress("UNCHECKED_CAST")
        return configStore[key] as? T ?: default
    }
    operator fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {
        configStore[key] = value
        println("  [CONFIG] $key = $value")
    }
}

class OrderProcessorConfig {
    var maxOrderValue: Double by ConfigProperty("max_order_value", 100_000.0)
    var defaultTaxRate: Double by ConfigProperty("default_tax_rate", 0.08)
    var enableAuditing: Boolean by Delegates.observable(true) { _, _, new ->
        println("  [CONFIG] Auditing ${if (new) "enabled" else "disabled"}")
    }
    var processingTimeout: Long by Delegates.vetoable(30_000L) { _, _, new ->
        if (new < 1_000) { println("  [CONFIG] Rejecting timeout < 1000ms"); false }
        else true
    }
}

fun main() {
    // Compose: validation + logging + in-memory
    val repo: OrderRepository = ValidatedRepository(LoggedRepository(InMemoryRepository()))

    println("=== Saving Orders ===")
    repo.save(Order("ORD-001", 299.99, "CONFIRMED", "Alice"))
    repo.save(Order("ORD-002", 15_000.00, "PENDING", "Bob"))

    println("\n=== Find Order ===")
    println(repo.findById("ORD-001"))

    // Property delegation
    println("\n=== Configuration ===")
    val config = OrderProcessorConfig()
    println("Max order value: \$${config.maxOrderValue} USD")
    config.defaultTaxRate = 0.10
    config.enableAuditing = false
    config.processingTimeout = 500  // Rejected
    config.processingTimeout = 60_000  // Accepted
    println("Timeout: ${config.processingTimeout}ms")
}

出力:

TEXT 📖 参照専用
=== Saving Orders ===
  [LOG] Saving order: ORD-001 ($299.99 USD)
  [LOG] Saving order: ORD-002 ($15000.0 USD)

=== Find Order ===
Order(id=ORD-001, total=299.99, status=CONFIRMED, customer=Alice)

=== Configuration ===
Max order value: $100000.0 USD
  [CONFIG] default_tax_rate = 0.1
  [CONFIG] Auditing disabled
  [CONFIG] Rejecting timeout < 1000ms
  [CONFIG] processingTimeout = 60000
Timeout: 60000ms

❓ よくある質問

Q クラスの委譲と継承の違いは何ですか?
A クラスの委譲はコンポジション(has-a)であり、継承はサブタイピング(is-a)です。委譲の方が柔軟性が高く、実行時にデリゲートオブジェクトを置き換えることができ、継承に伴う強固な結合を回避できます。
Q by lazy の初期化は常に実行されますか?
A 必ずしもそうとは限りません。そのプロパティに一度もアクセスされない場合、初期化は実行されません。これが lazy の最大の利点であり、不要な初期化のオーバーヘッドを回避できる点です。
Q プロパティの委譲のパフォーマンスはどのようになっていますか?
A プロパティの読み書きごとに追加の関数呼び出し(getValue/setValue)が発生しますが、JVMのインライン化最適化によって、通常はこのオーバーヘッドは解消されます。ホットパスについては、決定する前に測定を行ってください。
Q クラスで複数の by 委譲を使用することはできますか?
A クラス委譲は 1 つのインターフェースをデリゲートしますが(複数のインターフェースによる多重継承)、プロパティ委譲は各プロパティを個別にデリゲートします。
Q observable はリアクティブプログラミングに使用できますか?
A その適用範囲は限定的です。同じオブジェクト内でのみ通知が行われます。コンポーネント間のリアクティブ処理には、StateFlow/SharedFlow を使用してください。 observableは、監査ログや単純な通知に適しています。
Q カスタムデリゲートは ReadWriteProperty を実装する必要がありますか?
A 必須ではありません。getValue(および var の場合は setValue)を指定するだけで十分です。 ReadWritePropertyは単なる便宜上のインターフェースです。

📖 まとめ


📝 練習問題

  1. 初心者 (⭐): by lazy を使用して、最初のアクセス時に「Connecting...」と表示される遅延初期化型のデータベース接続を実装してください。ヒント: val connection by lazy { ... }
  2. 中級 (⭐⭐): クラス委譲を使用して LoggedOrderRepo を実装し、save メソッドのみをオーバーライドしてロギングを追加してください。ヒント: class LoggedOrderRepo(val inner: OrderRepository) : OrderRepository by inner { ... }
  3. 課題 (⭐⭐⭐): 読み取りおよび書き込みのたびにプロパティ名と値を出力する、カスタム TraceDelegate プロパティデリゲートを実装してください。ヒント: getValue および setValue の演算子関数

← 前へ | 次へ →

Web-Tutorial.com

Web-Tutorial 技術チーム

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

100%