0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【リファクタリング】Encapsulate Collection(コレクションのカプセル化)

Posted at

1. 概要(Overview)

Encapsulate Collection は、クラスが持つコレクション(List, Set, Map など)を
直接公開せず、読み取り専用ビューを返し、追加・削除は明確なメソッド に限定するリファクタリングです。

目的

  • コレクションへの変更経路を制御し、不変条件を保つ
  • 外部からの勝手な add/remove/clear を防ぐ
  • 変更時の副作用(イベント発火、検証、整合性維持)を一元化

2. 適用シーン(When to Use)

  • フィールドに MutableList / MutableSet をそのまま公開している
  • クラス外で自由に add/remove され、整合性バグ が生じている
  • 変更時に 検証/ログ/イベント を差し込みたい
  • 取得側がコレクションを 直接変更してしまう恐れ がある

よくある匂い:

  • Mutable Data(無制限な可変データ)
  • Inappropriate Intimacy(過度な親密さ)

3. 手順(Mechanics / Steps)

  1. 既存の公開コレクションを private バッキング に変更
  2. パブリックには 読み取り専用ビュー を返す(List, Set, Map
  3. 変更操作は addXxx/removeXxx/clearXxx 等の 明示メソッド を用意
  4. 既存呼び出しを置き換え、外部の直接変更を禁止
  5. 必要に応じて変更時の 検証/通知/双方向整合性 を実装

4. Kotlin 例(Before → After)

Before:コレクションを丸出し公開

class Order {
    val items: MutableList<String> = mutableListOf()
}

fun main() {
    val order = Order()
    order.items.add("A")     // 外部から自由に変更
    order.items.clear()      // クリティカルな変更も可能
}
  • どこからでも変更でき、不変条件を守れない
  • 将来、追加時に検証や在庫確認を入れたくても制御できない。

After①:読み取り専用ビュー + 明示メソッド

class Order {
    private val _items = mutableListOf<String>()
    val items: List<String> get() = _items  // 読み取り専用

    fun addItem(name: String) {
        require(name.isNotBlank()) { "item name is required" }
        _items.add(name)
        // 例:在庫確認・イベント発火などをここに集約
    }

    fun removeItem(name: String) {
        _items.remove(name)
    }

    fun clearItems() {
        _items.clear()
    }
}

fun main() {
    val order = Order()
    // order.items.add("X") // コンパイル不可(List には add がない)
    order.addItem("A")
    println(order.items) // [A]
}

変更の出入口が Order 内に限定され、検証や副作用を一元管理できる。


After②:双方向整合性(所有側でのみ変更)

class Customer(val name: String) {
    private val _orders = mutableListOf<Order>()
    val orders: List<Order> get() = _orders

    fun addOrder(order: Order) {
        if (order.customer !== this) order.setCustomer(this)
        if (!_orders.contains(order)) _orders.add(order)
    }

    internal fun removeOrder(order: Order) {
        _orders.remove(order)
    }
}

class Order(val id: Int) {
    var customer: Customer? = null
        private set

    fun setCustomer(newCustomer: Customer) {
        customer?.removeOrder(this)    // 旧所有者から除去
        customer = newCustomer
        newCustomer.addOrder(this)     // 新所有者へ登録(片側のみ公開)
    }
}

→ 追加・削除を 所有側 API に集約し、整合性が崩れない ようにする。


5. 効果(Benefits)

  • コレクションの不変条件(重複禁止、サイズ制限、整合性)を 一か所で強制
  • 変更イベント・ログ・監査を 中央集約 で差し込める
  • 外部からの予期せぬ変更がなくなり、バグを大幅に減少
  • API が「読めば意図がわかる」自己文書化 に近づく

6. 注意点(Pitfalls)

  • パフォーマンス要件が厳しい場合、ビュー生成やコピーに注意
  • 読み取り専用ビューを返しても、内部参照を漏らすと意味がない
    • 例:return _items(Mutable をそのまま返す)は厳禁
  • 変更 API は ドメイン操作に即した名前 を付与(addItem ではなく addLineItem(product, qty) など)
  • 並行アクセスでは スレッドセーフティ(同期・不変コレクション)を検討

まとめ

  • Encapsulate Collection は「コレクションを外に晒さず、変更は意図のあるメソッドだけで行わせる」リファクタリング。
  • 判断基準:このコレクションは外から勝手に変えられてよいか?
  • 基本思想:読み取りは公開・変更は統制。整合性と副作用の起点をひとつに集める。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?