Kotlin: Kotlinの委譲とプロパティ委譲
最終更新:2026-08-26
委譲はKotlinの最大の特徴的な機能です。スレッドセーフな遅延初期化も、プロパティの変更監視も、たった1行で実現できます。Charlieは、クラス委譲を使ってデコレータパターンの定型コードを置き換え、プロパティ委譲を使ってOrderProcessorの設定プロパティを自動的に永続化しています。
1. 学習内容
- クラス委譲:定型コードが一切不要なデコレータパターン
by lazy: スレッドセーフな遅延評価Delegates.observable/Delegates.vetoable: プロパティの変更監視およびインターセプト- カスタムプロパティの委譲:
ReadWriteProperty/getValue/setValue - Charlieの実践:
ValidatedRepository+ConfigPropertyカスタム委任
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) デリゲートクラス図
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は単なる便宜上のインターフェースです。📖 まとめ
byによるクラス委譲では、すべてのインターフェースメソッドが自動的に委譲される — デコレータパターンの定型コードが一切不要by lazyはスレッドセーフな遅延初期化機能を提供しており、負荷の高いオブジェクトをオンデマンドで生成するのに最適ですDelegates.observableはプロパティの変更を監視し、Delegates.vetoableはそれらをインターセプトして拒否することができます- カスタムプロパティのデリゲートには、
getValue/setValue演算子関数のみが必要です - 継承よりも委譲を優先する — コンポジションはサブクラス化よりも柔軟性が高い
- 多層的な委譲の構成により、関心事の分離を実現:ロギング+キャッシュ+バリデーション
📝 練習問題
- 初心者 (⭐):
by lazyを使用して、最初のアクセス時に「Connecting...」と表示される遅延初期化型のデータベース接続を実装してください。ヒント:val connection by lazy { ... } - 中級 (⭐⭐): クラス委譲を使用して
LoggedOrderRepoを実装し、saveメソッドのみをオーバーライドしてロギングを追加してください。ヒント:class LoggedOrderRepo(val inner: OrderRepository) : OrderRepository by inner { ... } - 課題 (⭐⭐⭐): 読み取りおよび書き込みのたびにプロパティ名と値を出力する、カスタム
TraceDelegateプロパティデリゲートを実装してください。ヒント:getValueおよびsetValueの演算子関数