【C#設計】SOLID原則をUnity公式サンプルで学ぼう~I:インターフェイス分離の原則~

はじめに

講座トップに戻る

この講座ではプログラミングの設計を勉強する際に避けては通れない「SOLID原則」について学ぶことができます。

SOLID原則は有名ですので名前を知っている人も多いかもしれませんが、全然知らない人でも大丈夫です!

初心者でも分かりやすいようにできるだけ丁寧に解説します。

「Unityの使い方は分かってきたけど、コードが綺麗に書く方法が分からない」

「設計を勉強するといいらしいけど、どうやって勉強したらいいか分からない」

こんな悩みを持っているあなたは、この記事で解決できるかもしれません!

この記事ではUnity公式のサンプルをもとに解説していきます。

公式サンプルのダウンロード方法はこちらの記事を参考にしてください。

SOLID原則とは?

OLID原則とはオブジェクト指向において、拡張性・保守性が高く保つために守る原則のことです。

これは、ソフトウェアエンジニアのRobert C. Martinが提唱した多くの設計原則を5つにまとめたものです。

それぞれの頭文字をとってSOLID原則と呼ばれています。

  • S … Single Responsibility Principle: 単一責任の原則
  • O … Open-Closed Principle: 開放閉鎖の原則
  • L … Liskov Substitution Principle: リスコフの置換原則
  • I … Interface Segregation Principle: インターフェイス分離の原則
  • D … Dependency Inversion Principle: 依存性逆転の原則

何も考えずにプログラムを書いていると、段々と拡張性・保守性が下がってきます。

  • 機能の追加、変更に時間がかかる
  • コードに再利用性がない
  • 膨大な機能を持ったクラスが出来上がる(こういうクラスを神クラスと言ったりします笑)
  • 依存関係が複雑でクラス同士の関係性がハッキリしていない

SOLID原則を学ぶことで、上記のような悩みを解決できる可能性があります。

今回はSOLID原則のI「インターフェイス分離の原則」を解説していきます。

SOLID原則などの設計に関する学習をしていると「モジュール」という言葉が良く出てきます。

モジュールというと分かりにくいかもですが、C#の場合クラスに置き換えて考えてください。

インターフェイス分離の原則とは?

これは「クライアント(インターフェースを使う側)は自身が使わないメソッドへの依存を強制させない」という原則です。

分かりやすく言い直すと、「1つのインターフェースに色々なメソッドを入れずに細かく分離する」ということです。

インターフェースで定義されたメソッドを実装する際に不要なものまであると、その実装までしなくてはなりません。

もしインターフェース側に変更があった場合、実際にはそのメソッドを使っていないクラスでの修正も必要になります。

それを避けるためにインターフェースは小さい単位で分けるべきであるという事です。

Unity公式サンプルを見てみよう

Assets/4 InterfaceSegregation/Scriptsというフォルダの中に6つのスクリプトがあります。

これらのスクリプトはそれぞれ以下のような内容になっています。

  • EnemyUnit.cs・・・敵クラス
  • ExplodingBarrel.cs・・・爆発樽クラス
  • IDamageable.cs・・・ダメージ可能インターフェース
  • IExplodable.cs・・・爆発可能インターフェース
  • IMovable.cs・・・移動可能インターフェース
  • IUnitState.cs・・・キャラクターのパラメータなどのプロパティが定義されているインターフェース

敵と爆発する樽のクラスを作る時を例にサンプルコードが書かれています。

まずは良くない例から見ていきましょう。

改善前のコード

・オブジェクトが持つ特性をまとめたインターフェース

public interface IUnitStats
{
    //体力
    public float Health { get; set; }
    //防御力
    public int Defense { get; set; }

    //死亡処理
    public void Die();
    //ダメージ処理
    public void TakeDamage();
    //回復処理
    public void RestoreHealth();

    //移動スピード
    public float MoveSpeed { get; set; }
    //加速度
    public float Acceleration { get; set; }

    //前進
    public void MoveForward();
    //後退
    public void Reverse();
    //左に進む
    public void TurnLeft();
    //右に進む
    public void TurnRight();

    //強さ
    public int Strength { get; set; }
    //賢さ
    public int Dexterity { get; set; }
    //耐久力
    public int Endurance { get; set; }

    //重さ
    public float Mass { get; set; }
    //爆発力
    public float ExplosiveForce { get; set; }
    //発火時間
    public float FuseDelay { get; set; }
    //タイムアウト
    public float Timeout { get; set; }

    //爆発処理
    public void Explode();
}

・敵クラス

public class EnemyUnit : MonoBehaviour, IUnitStats
{
    //IUnitStatsにある関数などを実装
}

・爆発樽クラス

public class ExplodingBarrel : MonoBehaviour, IUnitStats
{
    //IUnitStatsにある関数などを実装
}

現状の悪い点

全てのオブジェクトに存在する機能がIUnitStateに定義されています。

現状では敵に必要ないはずのExplode関数(爆発処理)なども実装しないといけません。

これは「クライアント(インターフェースを使う側)は自身が使わないメソッドへの依存を強制させない」というインターフェイス分離の原則に反しています。

改善後のコード

・ダメージ可能インターフェース

public interface IDamageable
{
    public float Health { get; set; }
    public int Defense { get; set; }

    public void Die();
    public void TakeDamage();
    public void RestoreHealth();
}

・移動可能インターフェース

public interface IMovable
{
    public float MoveSpeed { get; set; }
    public float Acceleration { get; set; }

    public void MoveForward();
    public void Reverse();
    public void TurnLeft();
    public void TurnRight();
}

・キャラなどのステータス用のインターフェース

public interface IUnitStats
{
    public int Strength { get; set; }
    public int Dexterity { get; set; }
    public int Endurance { get; set; }
}

・爆発するオブジェクト用のインターフェース

public interface IExplodable
{
    public float Mass { get; set; }
    public float ExplosiveForce { get; set; }
    public float FuseDelay { get; set; }
    public float Timeout { get; set; }

    public void Explode();
}

・敵クラス

public class EnemyUnit : MonoBehaviour, IDamageable, IMovable, IUnitStats
{
    public float Health { get; set; }
    public int Defense { get; set; }
    public int Strength { get; set; }
    public int Dexterity { get; set; }
    public int Endurance { get; set; }
    public float MoveSpeed { get; set; }
    public float Acceleration { get; set; }

    //インターフェースの関数実装
    public void TakeDamage(){}
    public void Die(){}
    public void RestoreHealth()
{}
    public void MoveForward()
{}
    public void Reverse()
{}
    public void TurnLeft(){}
    public void TurnRight(){}
}

・爆発樽クラス

public class ExplodingBarrel : MonoBehaviour, IExplodable, IDamageable
{
    public float Mass { get; set; }
    public float ExplosiveForce { get; set; }
    public float FuseDelay { get; set; }
    public float Timeout { get; set; }
    public float Health { get; set; }
    public int Defense { get; set; }

    //インターフェースの関数実装
    public void Die(){}
    public void Explode(){}
    public void RestoreHealth(){}
    public void TakeDamage(){}
}

改善後の良い点

改善後はインターフェースが機能ごとに分けられています。

これで不要な関数を実装する必要がなくなりました。

これは「クライアント(インターフェースを使う側)は自身が使わないメソッドへの依存を強制させない」というインターフェイス分離の原則を守ることができています。

もしも新しく動かいない敵を作りたくなったらIDamageableとIUnitStatsのみを継承したクラスを作成してやれば無駄なく実装することができます。

まとめ

以上が「インターフェース分離の原則」でした。

1つに機能を詰め込むのはやめた方が色々と実装しやすくなることが分かったと思います。

これはクラスにも言えることですので、あまり気にしていなかった人はこれから気をつけましょう!

👇その他の原則を学ぶ

S:単一責任の原則

O:開放閉鎖の原則

L:リスコフの置換原則

I:インターフェイス分離の原則←今ここ

D:依存性逆転の原則

👇あなたにお勧めの書籍

デザインパターンが分かりやすくまとめられていて、設計を学ぶ際におすすめです!

コメント