依存性逆転原則学習:BasketControllerを使ったダメージ処理設計ガイド

この資料では、Unityで落下してくるりんご(Apple)や爆弾(Bomb)をバスケットで受け止めた際にHPを減少させる処理を、まずはシンプルなタグ判定による実装例で紹介し、その後「依存性逆転の法則(DIP: Dependency Inversion Principle)」を適用したリファクタリング例へとステップアップしていきます。最後に、初学者にも分かりやすいようにDIPの有効性を解説し、学習のポイントを整理します。


1. シンプルなタグ判定による HP デクリメント

BasketController.cs

using UnityEngine;

public class BasketController : MonoBehaviour
{
    private void OnTriggerEnter(Collider other)
    {
        // 「Apple」タグのオブジェクトに当たったら HP をデクリメント
        if (other.CompareTag("Apple"))
        {
            var apple = other.GetComponent<Apple>();
            if (apple != null)
            {
                apple.HP--;
            }
        }
        // 「Bomb」タグのオブジェクトに当たったら HP をデクリメント
        else if (other.CompareTag("Bomb"))
        {
            var bomb = other.GetComponent<Bomb>();
            if (bomb != null)
            {
                bomb.HP--;
            }
        }
    }
}

Apple.cs

using UnityEngine;

public class Apple : MonoBehaviour
{
    public int HP = 3;
}

Bomb.cs

using UnityEngine;

public class Bomb : MonoBehaviour
{
    public int HP = 1;
}
  • ポイント
    1. CompareTag でタグを判定し、それぞれのクラスの HP フィールドを直接操作している。
    2. 新たにダメージ対象を増やすたびに if–else を追加する必要があるため、クラスが増えるほど可読性・保守性が低下する。
    3. 高レベルモジュール(BasketController)が低レベルモジュール(Apple/Bomb)に直接依存している。

2. IDamageable インターフェースの定義

/// <summary>
/// ダメージを与える抽象を表すインターフェース
/// </summary>
public interface IDamageable
{
    /// <summary>
    /// ダメージを与える処理。amount ぶん HP を減らす。
    /// </summary>
    void TakeDamage(int amount);
}
  • ポイント
    1. 「HPを持つものは何でもダメージを与えられる」という共通の契約(抽象)を定義。
    2. 高レベルモジュールはこの抽象にのみ依存し、具体実装には依存しない設計を目指す。

3. 依存性逆転の法則を適用した実装

Apple.cs

using UnityEngine;

public class Apple : MonoBehaviour, IDamageable
{
    public int HP = 3;

    public void TakeDamage(int amount)
    {
        HP -= amount;
        if (HP <= 0)
            Destroy(gameObject);
    }
}

Bomb.cs

using UnityEngine;

public class Bomb : MonoBehaviour, IDamageable
{
    public int HP = 1;

    public void TakeDamage(int amount)
    {
        HP -= amount;
        if (HP <= 0)
            Explode();
    }

    private void Explode()
    {
        // 爆発エフェクト再生など
        Destroy(gameObject);
    }
}

BasketController.cs

using UnityEngine;

public class BasketController : MonoBehaviour
{
    private void OnTriggerEnter(Collider other)
    {
        // IDamageable を実装しているものを探し、一律にダメージを与える
        var damageable = other.GetComponent<IDamageable>();
        if (damageable != null)
        {
            damageable.TakeDamage(1);
        }
    }
}
  • ポイント
    1. BasketController は具体クラス(Apple/Bomb)を参照せず、IDamageable インターフェースだけに依存。
    2. 各ダメージ対象(Apple/Bomb)は IDamageable を実装しており、TakeDamage メソッドで自身の HP を管理する。
    3. 新規ダメージ対象(例:Box や Missile)を追加する際は、IDamageable を実装したクラスを作成するだけでよく、コントローラー側に変更を加える必要がない。

4. 依存性逆転の法則の有効性

  1. 高レベル/低レベルの分離
    • 高レベルモジュール(BasketController)は抽象(IDamageable)にのみ依存し、低レベルモジュール(Apple/Bomb)はその抽象を実装するだけ。
    • これにより、クラス同士の結びつきが弱くなり、システム全体の柔軟性が向上する。
  2. 拡張性の向上
    • 例:新しく「Box」や「Missile」といったダメージ対象を追加する場合、以下の流れで対応可能。
      1. Box クラスを作成し、IDamageable を実装
      2. Box の TakeDamage メソッド内で HP 管理や破壊処理を記述
      3. Unity エディタ上で Box オブジェクトにタグやコライダーを設定
      4. 既存の BasketController はそのままで、動作できる
    • 既存コードを変更しないため、修正コストを大幅に削減できる。
  3. 単体テストが容易
    • BasketController の単体テストでは、モック実装の IDamageable を用意して、意図した回数だけ TakeDamage が呼ばれるかを検証できる。
    • 具体クラス(Apple/Bomb)を差し替えずにテストできるため、テストコードがシンプルになる。
  4. 可読性・保守性の向上
    • 従来の if–else 分岐がなくなり、OnTriggerEnter の処理は「衝突した相手がダメージ可能かを判定し、ダメージを与える」という1行のロジックで書ける。
    • コードから意図が直感的にわかるため、後から読む人にも理解しやすい。
  5. 学習上のポイント
    • 抽象レイヤーを作る意味:抽象(インターフェース)を介在させることで、高レベルと低レベルの依存を切り離せる。
    • 依存性逆転の考え方:上位のモジュールが下位のモジュールに直接依存するのではなく、両方が同じ抽象(インターフェース)に依存することで結合度を下げる。
    • 実践への応用:ダメージ処理以外でも、例として「武器切り替え」「スコア管理」「エフェクト再生」など、複数の実装が考えられる機能で同様に インターフェース+DIP のパターンを適用すると、拡張性やテスト容易性が向上する。

まとめ

  1. タグ判定による実装 は手軽だが、対象が増えるほど if–else 分岐が増え、保守性・可読性が低下する。
  2. 依存性逆転の法則(DIP) を適用すると、上位モジュールはインターフェースに依存し、下位モジュールはそのインターフェースを実装するだけ。
  3. 拡張時やテスト時に変更が少なく、コード品質が向上する。
  4. 実際の開発では、ダメージ処理以外にもこの考え方を積極的に取り入れてみることで、より柔軟で管理しやすいアーキテクチャを学習できる。

以上が「依存性逆転原則学習」を意識した設計ガイドです。いきなりすべてを理解するのは難しいかもしれませんが、「インターフェースを使って、高レベルな処理と低レベルな処理を切り離す」という考え方を意識して実装してみてください。

訪問数 27 回, 今日の訪問数 1回