●はじめに
前:【Unity】公式プロジェクトから、デザインパターンを学んでみよう(Part 3:リスコフの置換原則)
●インターフェース分離の原理とは
「インターフェースっていうのを使って、クライアント側に必要なパーツだけ渡そうね」という原則です。
インターフェースは、下で説明します。
クライアントってのは、サーバー関係でも使われる用語ですが、「欲しい機能を要求して、それを使うヤツ」です。
インターフェースを要求して、それを使うのがクライアントです。
僕の説明は少々適当なので、気になったら各自でググってください!
●プロジェクト内容
- EnemyUnit
- ExplodingBarrel
- IDamageable
- IExplodable
- IMovable
- IUnitStats
●コード内容
〇EnemyUnit
敵のクラスです。敵にはインターフェースでHPとか力とか、移動速度とかが割り当てられてます。
RPGの敵というよりも、マイクラとかテラリアとかの敵っぽいです。
クリックしてコードを展開
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace DesignPatterns.ISP
{
// Using Interface Segregation, we only implement what interfaces apply.
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; }
// implement logic here
public void TakeDamage() { }
public void Die() { }
public void RestoreHealth() { }
public void MoveForward() { }
public void Reverse() { }
public void TurnLeft() { }
public void TurnRight() { }
}
}
一番上のコメントを翻訳してみます。
インターフェイス分離を使用して、適用されるインターフェイスのみを実装します。
「適用されるインターフェースのみを実装する」ってのがインターフェース分離の原理の全てです。用語が難しく感じちゃうだけで、内容はこんだけです。
インターフェースってのは、
public class EnemyUnit : MonoBehaviour, IDamageable, IMovable, IUnitStats
↑ここのMonoBehavior
以外の4つのヤツです。
インターフェースは、名前の一番初めに"I"をつけることが定着しています。親クラスとの見分けがつけにくいので、Interfaceの頭文字である"I"を付けた方が分かりやすいからです。
また、親クラスからの継承は1つのクラスからしか参照できませんが、インターフェースを使うと、複数のインターフェースから関数とプロパティを参照することができます。というか、インターフェースをつけたら、必ずインターフェース内の処理をクライアント側のコードに記載しないと警告出ます。
今回は、Damageble
, Mobale
, UnitStats
を取り入れてるので、それぞれのインターフェースの関数やプロパティを実装しないと警告が出ます。(これらインターフェースの詳細は下で説明します)
しかし、インターフェースは継承と違って、共通の処理を書いたり変数をprotectとかで参照させることはできません。
継承とインターフェースの使い分けをすればいい感じのコードになると思います。
〇ExplodingBarrel
ドンキーコングとかでよくある、爆発する樽です。
樽は生物じゃないので、移動しません。また、力などもないです。
クリックしてコードを展開
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace DesignPatterns.ISP
{
// Using Interface Segregation, we only implement what interfaces apply.
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; }
// implement logic here
public void Die() { }
public void Explode() { }
public void RestoreHealth() { }
public void TakeDamage() { }
}
}
翻訳はさっきのと同じなので省略
インターフェースを見てみると、
public class ExplodingBarrel : MonoBehaviour, IExplodable, IDamageable
Explodable
とDamageable
だけです。爆発可能で、ダメージを与えることができるのがこの爆弾樽の機能なので、こんな感じになってます。
先ほど記述したように、樽は移動しないし、力とかないので、Movable
とUnitStats
のインターフェースはついてません。
こんな感じで、生物と無生物とで分別したりできちゃうのがインターフェースっていう機能です。
別になくても動作は問題なく実装できますが、あった方が付け忘れとかチーム制作で役立つ感じです。
〇IUnitStats
Statsっていうのは、ステータスっていう意味らしいです。
Unitは一単位みたいな感じらしい。
ここでは、主に生物関係につけるようなステータスが割り振られています。
コメントアウトされる前のコードでは、他のインターフェースを分離せずにここに全部載せちゃってました。
そうしちゃうと、無生物である爆弾樽に不要なステータスが付与されて、混乱に陥ることになります。
現在のコードでは、力、器用さ、耐久力?がステータスとして定義されています。
このインターフェースは、EnemyUnit
にのみ適用されています。樽には力も器用さもありません。(耐久力はありそうだけど…)
クリックしてコードを展開
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace DesignPatterns.ISP
{
// Interface Segregation tells us to use smaller interfaces. Rather than
// incorporate all of these Methods/Properties in one interface, use several.
public interface IUnitStats
{
public int Strength { get; set; }
public int Dexterity { get; set; }
public int Endurance { get; set; }
}
}
上部のコメントを翻訳します。
インターフェイスの分離は、より小さなインターフェイスを使用するように指示します。これらすべてのメソッド/プロパティを 1 つのインターフェイスに組み込むのではなく、いくつか使用してください。。
「プロパティとかメソッドが多すぎたら、一部のクライアントにとって不要なプロパティとかメソッドが出てきちゃうから、いくつかのインターフェースを利用して、しっかり分離しよう」 ってことを言ってます。
〇IDamageable
ダメージを与えられるものにつけるインターフェースです。
プロパティには、HPと防御力があり、関数は死ぬときの処理と、ダメージ付与時の処理と、回復時の処理が定義されています。
敵にも樽にもダメージは付与できるらしいので、どっちもこのインターフェースを適用してます。
クリックしてコードを展開
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace DesignPatterns.ISP
{
// Here we have segregated part of the original interface into IDamageable.
public interface IDamageable
{
public float Health { get; set; }
public int Defense { get; set; }
public void Die();
public void TakeDamage();
public void RestoreHealth();
}
}
〇IExplodable
爆発するものにつけるインターフェースです。
爆発に関するプロパティと、爆発処理に関する関数が定義されています。
このインターフェースは、爆弾樽にのみ適用されています。
クリックしてコードを展開
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace DesignPatterns.ISP
{
// Here we have segregated part of the original interface into IExplodable.
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();
}
}
〇IMovable
移動可能なものにつけるインターフェースです。
移動速度や加速度のプロパティ、移動に関する関数が定義されています。
樽は移動できないので、EnemyUnitのみに適用されています。
クリックしてコードを展開
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace DesignPatterns.ISP
{
// Here we have segregated part of the original interface into IMovable.
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();
}
}
●さいごに
抽象クラスで管理するのもいいですが、インターフェースを利用してより細分化することで、よりよいコードが作成できると思います。
☆整理
あまりインターフェースを利用せず理解していない部分が多いので、個人的理解のために、既存のゲームでインターフェースを考えてみます。
スプラトゥーンを例とすると、
被弾するオブジェクトにインターフェースを継承させるとします。
被弾するオブジェクトは、プレイヤー、エネミー(たこ)、木箱、シャケなどがあります。
インターフェースには、被弾した時の処理や、指定ダメージを受けた時の処理、被弾した際の音声を格納するフィールドなどを定義します。
これでいい感じに管理できます。
そこそこの規模のゲームに使う感じかも?
小規模な場合は、抽象クラスなどで補えそうだと感じました。