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?

【リファクタリング】Change Bidirectional Association to Unidirectional(双方向の関連を一方向に変更)

Posted at

1. 概要(Overview)

Change Bidirectional Association to Unidirectional は、
互いを参照している2つのクラスの関連を、必要な方向だけ 残してシンプルにするリファクタリングです。

目的は以下の通りです:

  • 不要な結合を解いて、設計を簡素化
  • 同期や循環参照による バグ・複雑性・メモリリーク のリスクを低減
  • データ取得や遷移の経路を明確化

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

  • 片側の参照が ほとんど使われていない/価値が薄い
  • 双方向同期(add/remove、set)が煩雑で、整合性バグ が起こる
  • ORマッパーやシリアライズ時に 循環参照 が問題になる
  • 集約境界(DDD)を見直した結果、所有方向が明確 になった

よくある匂い:

  • Inappropriate Intimacy(不適切な親密さ)
  • Shotgun Surgery(散弾銃のような変更)
  • Cyclic Dependencies(循環依存)

3. 手順(Mechanics / Steps)

  1. 使われていない(価値が低い)方向を特定
  2. その参照/コレクションを 非推奨(@Deprecated にして呼び出し箇所を置き換え
  3. 同期メソッド(add/remove/set で相互更新している箇所)を片方向用に簡素化
  4. 置き換えが完了したら、不要な参照・同期コードを削除
  5. テスト&静的解析で影響を確認(循環が消えたこと、取得経路が妥当なこと)

4. Kotlin 例(Before → After)

Before:双方向の関連(Customer ↔ Order

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

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

    fun removeOrder(order: Order) {
        if (_orders.remove(order)) {
            order.clearCustomer(this)
        }
    }
}

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

    fun setCustomer(customer: Customer) {
        if (_customer != customer) {
            _customer = customer
            customer.addOrder(this)
        }
    }

    fun clearCustomer(customer: Customer) {
        if (_customer == customer) {
            _customer = null
        }
    }
}
  • 双方向同期のためのガードや相互呼び出しが複雑。
  • シリアライズ/ORM で循環が問題になることがある。

After:一方向(Order → Customer のみに簡素化)

class Customer(val name: String)
// 顧客は注文を持たない(必要ならリポジトリ/クエリで取得)

class Order(val id: Int, var customer: Customer)
  • Customer.orders と相互同期ロジックを削除

  • 「注文から顧客へ」という 業務で自然な所有方向 だけ維持

  • もし「顧客の注文一覧」が必要なら、リポジトリやクエリ で取得する:

    interface OrderRepository {
        fun findByCustomer(customer: Customer): List<Order>
    }
    

5. 効果(Benefits)

  • 相互同期の排除で コードが大幅に簡潔、バグ要因が減る
  • 循環参照が消え、シリアライズ/キャッシュ/ORM マッピング が安定
  • 集約の所有方向が明確になり、結合度が低下
  • テスト容易性・変更容易性の向上

6. 注意点(Pitfalls)

  • 本当に片方向で足りるかを事前に確認(アクセスパターン・集約境界)
  • 既存コードが customer.orders に依存しているなら、段階的移行@Deprecated と置換)を行う
  • 取得コストが増える場合は、リポジトリ/読み取り用クエリ、必要に応じて 読み取りモデル(CQRS 的分離) を検討
  • ORマッパー使用時は マッピング定義の見直し(外部キーの所有側)を忘れない

まとめ

  • Change Bidirectional Association to Unidirectional は「不要な片側参照を捨てて、必要な方向だけ残す」リファクタリング。
  • 判断基準:どちらの方向が業務的な所有・遷移として自然か?
  • 基本思想:結合を削り、取得はクエリで解決——モデルはシンプルに、読み取りは賢く。

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?