【C#】依存性逆転の原則をクソゲーを使って簡単に説明【Unity】

この記事は約7分で読めます。

このページに辿り着いた方は、SOLID原則に足を踏み入れ、キレイなコードを書きたい!と思っている中級者なのでしょう。

そして辿り着いた「依存性逆転の原則」という謎の原則!?漢字多すぎてもはや無理!!って感じですか?

わかります。私もそうです。

でも大丈夫。実は意外と簡単なんですよ。

今回は依存性逆転の原則を、実際にUnityに落とし込んでみたものを備忘的に残していきます。

ゲームの全体像

こんな感じのゲームを作りたいと思います。

弾丸でスライムを倒したり。弾丸で大きいボスを倒したり。そんなクソゲー。

弾丸オブジェクト

Bulletオブジェクトにはこんなコンポーネントが付いてます

  • Box Collider 2D  当たり判定用に付与
  • Rigidbody 2D   当たり判定用に付与
  • Bullet Script    弾丸オブジェクトに関するコード

Slimeオブジェクト

Slimeオブジェクトにはこんなコンポーネントが付いています。

  • Box Collider 2D  当たり判定用に付与
  • Slime Script    スライムオブジェクトに関するコード

Bossオブジェクト

Bossオブジェクトにはこんなコンポーネントが付いています。

  • Box Collider 2D  当たり判定用に付与
  • Boss Script    ボスオブジェクトに関するコード

良くない例

ここから「依存性逆転の原則」に迫っていきたいと思います。

弾丸が接触した時の処理(良くない例)

Unity入門書に書いてある処理方法。これは大抵良くないものです。

  • if文を使って SlimeBoss で条件分岐させる
  • 接触したオブジェクトのTagによって処理を変える
  • Enum(列挙型)で処理を変える
// Bulletが当たった時の処理判定イメージ(簡単に)

if (Tag == Slime)
  {
    // なんかの処理
  }
else if(Tag == Boss)
  {
    // なんかスゴイ処理
  }

イメージとしてはこんな感じのコードですね。

このやり方が完全悪だとは言いません。

「Unity1Week」など限られた期間に単発のゲームを作るような場合は最適でしょう。

しかし、作ったアプリ。リリースしたアプリを少しずつアップデートしてどんどん育てていきたい。

そんな場合は、やはり後々困る事になるでしょう。

どこが良くないのか?

弾丸の処理コードがSlimeBossのTagに依存している事が良くないですね。

上記のような場合、「そうだ!レッドスライムも実装しよう」となった時どうしますか?

  • if文の分岐を増やす
  • その他、依存関係があるコードを全て直す

これがレッドスライム1匹なら良いですけど「大型アップデート!100種追加!」なんて企画が通ったら地獄の始まりですよね。

良い例

インターフェイスを実装し、そのインターフェイスを介して接触相手の処理を呼び出しましょう。

先ほどの依存関係の図と比べると、矢印の向き、つまり依存関係が逆転している事がわかるかと思います。

そう!インターフェイスを介して処理を行う事こそが「依存関係逆転の原則」なのです。

「う〜ん…もはや着いていけない。」という方も諦めずに実際にコードを見てみましょう。

Bullet Script

弾丸のコードはこうです。

  • 毎秒少し右に弾丸が移動する。
  • もしコライダーに接触した場合、接触したオブジェクトのインターフェイスを取得する。
  • インターフェイスに実装されている ApplyDamageメソッドを呼び出す。
  • おしまい
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Bullet : MonoBehaviour
{
    Rigidbody2D rb;

    void Start()
    {
        rb = this.GetComponent<Rigidbody2D>();
    }


    float speed_x = 2;

    void Update()
    {
        rb.velocity = new Vector2(speed_x, 0);
    }


    private void OnCollisionEnter2D(Collision2D collision)
    {
        GameObject hitObj = collision.gameObject;
        IApplicableDamage d = hitObj.GetComponent<IApplicableDamage>();

        if(d != null)
        {
            d.ApplyDamage();
        }
    }
}

どこが良いのか?

弾丸に実装したコードのどこが良いのでしょう?

それは、弾丸が接触した相手オブジェクトに指定したインターフェイスが実装されていれば処理が行われる点です。

弾丸としては何にぶつかったかわからないけど、接触相手に「IApplicableDamage」というインターフェイスが実装されていれば処理が行われるという事です。

  • SlimeオブジェクトとBossオブジェクトは「IApplicableDamage」というインターフェイスを実装している
  • IApplicableDamage を実装しているクラスは必ず「ApplyDamage」という名前のメソッドを実装している。
  • ApplyDamage メソッドの中身は、SlimeスクリプトとBossスクリプトでそれぞれ別の処理を書いても良い

Slime Script

じゃあ実際 Slime 側はどうなってんの?という部分です。

内容は「弾丸に当たった事を表示して自身のHPを減らす」というメソッドです。

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

public class Slime : MonoBehaviour , IApplicableDamage
{
    int hp = 100;
    int damage = 10;

    public void ApplyDamage()
    {
        Debug.Log("弾がスライムに当たった");

        hp -= damage;
        Debug.Log("hpは " + hp);
    }
}

それでは実行してみましょう。

弾丸オブジェクトがぶつかったタイミングでSlimeスクリプトのApplyDamageメソッドが処理されました。

Boss Script

では続きまして Boss側も見てみましょう。

処理内容は

  • 弾丸と接触したことを表示
  • 「ガハハ!そんな弾効かぬわ」と表示
  • BossのHPが回復しちゃう
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Boss : MonoBehaviour , IApplicableDamage
{
    int hp = 500;
    int damage = 10;

    public void ApplyDamage()
    {
        Debug.Log("弾はボスに当たった");

        Debug.Log("ガハハ!そんな弾効かぬわ");
        hp += damage;

        Debug.Log("hpは " + hp);
        Debug.Log("回復しちゃった...");
    }
}

それでは実行してみましょう。

成功です。

Bossに接触すると、Bulletスクリプトを書き替えずともSlime接触時と違う処理をする事ができました。

まとめ

今後、敵キャラが増えた場合でも Bulletスクリプトを修正する必要はなくなりました。

このように「依存性逆転の原則」を使うことでアップデートに強い設計を行う事ができました。

そして、これはあくまで「原則」であるという事に注意しましょう。

この原則に違反した方が良い場合には、違反しても良いという事です。

コメント

タイトルとURLをコピーしました