課題6: 依存性の逆転を取り入れて BasketController の肥大化を防ぐ

2024年9月28日

依存性の逆転の原則を適用し、BasketControllerがアイテムの種類に依存しない設計に変更することで、コードの拡張性と保守性を向上させる。

ステップ:

アイテムごとの共通の動作を定義するインターフェースを作成します。これにより、BasketControllerは具体的なアイテムクラスに依存せず、インターフェースを介してアイテムとやり取りします。

インターフェースの定義:

Collectible: 収集可能

// ICollectible.cs
using UnityEngine;

public interface ICollectible
{
    void Collect(); // アイテム収集時の処理
}

ItemControllerの修正:

ICollectibleインターフェースを実装し、サウンド再生とスコア変更を行うメソッドを定義します。
GameDirectorオブジェクトのGameDirectorコンポーネントのpoint(得点)に加算します
(これまでは、GetApple()メソッドなどで処理していました)

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class ItemController : MonoBehaviour, ICollectible
{
    public float dropSpeed = -0.03f;
    public AudioClip itemSE;  // 各アイテムが持つサウンド
    public int scoreChange = 0; // 各アイテムが持つポイント

    GameObject director;

    void Awake()
    {
        this.director = GameObject.Find("GameDirector");
    }

    protected virtual void Start()
    {
        // 共通の初期化処理をここに記述
    }

    void Update()
    {
        // アイテムの移動
        transform.Translate(0, this.dropSpeed, 0);

        // アイテムが画面外に出たら破壊
        if (transform.position.y < -1.0f)
        {
            Destroy(gameObject);
        }
    }

    public virtual void Collect()
    {
        // SoundManagerを使用してサウンドを再生
        SoundManager.instance.PlaySound(this.itemSE, transform.position);
        director.GetComponent<GameDirector>().point += scoreChange;
        Destroy(gameObject);
    }
}

説明:

  • ICollectible インターフェースを実装することで、BasketController がアイテムの具体的なクラスに依存せずに Collect メソッドを呼び出せるようになります。
  • directorが持つ(管理する)得点に加減算されるようにします

3. アイテムごとのクラス作成

  • 各アイテムごとにクラスを作成し、ICollectibleインターフェースを実装します。これにより、各アイテムの固有の動作を定義できます。

例1: 爆弾アイテム

public class BombController : ItemController
{
    protected override void Start()
    {
        base.Start(); // 基底クラスのStartを呼び出す
        this.scoreChange = -50; // 爆弾はスコアを減少させる
    }

    public override void Collect()
    {
        base.Collect();
        // 爆弾特有の追加処理があればここに記述
    }
}

例2: ゴールドアップルアイテム

using UnityEngine;

public class GoldAppleController : ItemController
{
    protected override void Start()
    {
        base.Start(); // 基底クラスのStartを呼び出す
        this.scoreChange = 500; // ゴールドアップルはスコアを大幅に増加させる
    }

    public override void Collect()
    {
        base.Collect();
        // ゴールドアップル特有の追加処理があればここに記述
    }
}

残りも同様に作成します

爆弾アイテムの登録サンプル

得点が半分になるのではなく、50点の減点になるように仕様を変えています

4. BasketController の修正

  • BasketControllerでは、アイテムの種類に依存せず、ICollectibleインターフェースを介してアイテムとやり取りします。

目的:

  • BasketController が具体的なアイテムクラスに依存せず、ICollectible インターフェースを介してアイテムとやり取りできるようにする。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class BasketController : MonoBehaviour
{
    void Start()
    {
        Application.targetFrameRate = 60;
    }

    void OnTriggerEnter(Collider other)
    {
        // ICollectibleインターフェースを実装しているか確認
        ICollectible collectible = other.gameObject.GetComponent<ICollectible>();
        if (collectible != null)
        {
            collectible.Collect();
        }
    }

    void Update()
    {
        float moveDistance = 1.0f; // 移動する距離(1マス)
        Vector3 newPosition = transform.position;

        if (Input.GetKeyDown(KeyCode.LeftArrow))
        {
            newPosition += Vector3.left * moveDistance; // 左に1マス移動
        }
        if (Input.GetKeyDown(KeyCode.RightArrow))
        {
            newPosition += Vector3.right * moveDistance; // 右に1マス移動
        }
        if (Input.GetKeyDown(KeyCode.UpArrow))
        {
            newPosition += Vector3.forward * moveDistance; // 前(上)に1マス移動
        }
        if (Input.GetKeyDown(KeyCode.DownArrow))
        {
            newPosition += Vector3.back * moveDistance; // 後ろ(下)に1マス移動
        }

        // グリッド範囲内に制限(例: x, z が -1, 0, 1 の範囲)
        newPosition.x = Mathf.Clamp(Mathf.Round(newPosition.x), -1.0f, 1.0f);
        newPosition.z = Mathf.Clamp(Mathf.Round(newPosition.z), -1.0f, 1.0f);

        // バスケットの位置を更新
        transform.position = new Vector3(
            Mathf.Round(newPosition.x),
            transform.position.y,
            Mathf.Round(newPosition.z)
        );
    }
}

コードの調整:

  • BasketControllerがアイテムの種類に依存しないため、GetApple()GetBomb()といった具体的なメソッド呼び出しを排除し、イベント駆動設計へと移行します。

テストと確認:

  • ゲームをプレイし、バスケットが各アイテムに衝突した際に、各アイテムが正しくサウンドを再生し、スコアが適切に変更されることを確認する。

学習ポイント:

  • オブジェクト指向設計の基本概念。
  • インターフェースの定義と実装方法。
  • 依存性の逆転の原則(Dependency Inversion Principle)の理解と適用。

依存性逆転の原則(DIP:Dependency Inversion Principle)とは?

依存性逆転の原則(DIP)は、SOLID原則の一つであり、以下の2つのルールから構成されます:

  1. 高レベルモジュールは低レベルモジュールに依存すべきではない。 両者は抽象(インターフェースや抽象クラス)に依存すべきである。
  2. 抽象は詳細に依存すべきではない。 詳細は抽象に依存すべきである。

この原則を適用することで、システムの柔軟性、拡張性、保守性が向上します。

リファクタリング前のクラス図

リファクタリング前のクラス図では、BasketController が GameDirector を介して ItemController に依存しています。具体的なアイテム処理が高レベルモジュール(BasketController)から低レベルモジュール(ItemController)に直接依存しているため、DIPに違反しています。

リファクタリング前のクラス図の特徴

  • BasketController
    • OnTriggerEnter メソッド内でアイテムのタグをチェックし、GameDirector の GetApple() や GetBomb() を呼び出しています。
  • DIPへの影響
    • 高レベルモジュール(BasketController)が低レベルモジュール(ItemController)に依存しています。
    • 具体的なアイテム処理が BasketController に組み込まれているため、新しいアイテムを追加する際に BasketController を変更する必要があります。
    • これはDIPに違反し、システムの柔軟性と拡張性を低下させます。

リファクタリング後のクラス図(DIP適用)

リファクタリング後のクラス図では、ICollectible インターフェースを導入し、BasketController が具体的なアイテムクラスに依存するのではなく、抽象に依存するように設計を変更しました。これにより、DIPが遵守され、システムの柔軟性と拡張性が向上しています。

リファクタリング後のクラス図の特徴

  • ICollectible インターフェース
    • Collect() メソッドを定義し、アイテム収集時の共通処理を標準化しています。
    • 高レベルモジュール(BasketController)と低レベルモジュール(ItemController およびその派生クラス)がこのインターフェースに依存します。
  • ItemController クラスと派生クラス
    • ItemController が ICollectible を実装し、基本的なアイテムの挙動(Collect() メソッド)を提供します。
    • BombController や GoldAppleController は ItemController を継承し、Collect() メソッドをオーバーライドして各アイテム特有の処理を実装します。
  • BasketController クラス
    • ICollectible インターフェースに依存するようになり、具体的なアイテムクラスには依存しません。
    • これにより、新しいアイテムタイプを追加する際に BasketController を変更する必要がなくなります。
  • ItemGenerator クラス
    • 生成するアイテムの型として ICollectible を使用し、具体的なアイテムクラスに依存しない設計に変更しました。
    • 新しいアイテムタイプを追加する際にも、ItemGenerator の変更は最小限に抑えられます。
  • DIPへの影響
    • 高レベルモジュール(BasketController)が低レベルモジュール(具体的なアイテムクラス)に依存しなくなり、抽象(ICollectible)に依存するようになりました。
    • 低レベルモジュール(具体的なアイテムクラス)が抽象(ICollectible)に依存するようになりました。

リファクタリング前後のクラス図比較による学習ポイント

リファクタリング前

  • 高レベルモジュールが低レベルモジュールに依存
    • BasketController が ItemController に依存しているため、具体的なアイテム処理に強く結合しています。
    • 新しいアイテムを追加する際に BasketController や GameDirector の変更が必要になる可能性があります。
  • 拡張性の低さ
    • 新しいアイテムタイプを追加する際に既存の高レベルモジュールに影響を与えるため、システム全体の拡張が困難です。

リファクタリング後

  • 高レベルモジュールが抽象に依存
    • BasketController が ICollectible インターフェースに依存することで、具体的なアイテムクラスには依存しなくなりました。
    • 新しいアイテムを追加する際に BasketController や GameDirector の変更が不要です。
  • 低レベルモジュールが抽象に依存
    • 具体的なアイテムクラス(BombControllerGoldAppleController)が ICollectible インターフェースを実装することで、抽象に依存しています。
    • これにより、具体的な実装の変更が高レベルモジュールに影響を与えません。
  • 拡張性と保守性の向上
    • 新しいアイテムタイプを追加する際に、ICollectible を実装する新しいクラスを作成するだけで済み、システムの拡張が容易になります。
    • モジュール間の結合度が低くなるため、保守性が向上します。

学習ポイント

  • 依存性逆転の原則(DIP)の重要性
    • 高レベルモジュールが低レベルモジュールに直接依存する設計は避け、抽象に依存することでシステムの柔軟性と拡張性を高める。
  • インターフェースの活用
    • インターフェースを用いることで、具体的な実装に依存せずにモジュール間の相互作用を設計できる。
  • ポリモーフィズムの利点
    • 派生クラスが基底クラスやインターフェースを実装することで、異なるアイテムタイプ間の共通の動作を統一的に扱える。

8. まとめ

このリファクタリングを通じて、依存性逆転の原則(DIP)を適用することで、BasketController と ItemController 間の依存関係が抽象に基づくものへと変わり、システム全体の柔軟性と拡張性が向上しました。具体的には以下の点が改善されました:

  • 高レベルモジュールが低レベルモジュールに直接依存しなくなった。
  • 新しいアイテムタイプの追加が容易になり、既存のコードに影響を与えない。
  • モジュール間の結合度が低くなり、保守性とテスト容易性が向上。

オブジェクト指向設計の原則を適用することで、コードベースの品質と可読性を高め、長期的なプロジェクトの成功に寄与します。今後もSOLID原則を念頭に置いた設計を心がけることで、より堅牢で拡張性の高いソフトウェア開発が可能となるでしょう。


Unity

Posted by hidepon