はじめに
Unityで機能を分ける際、interfaceを使うことが推奨されます。
しかし、interfaceはデフォルト処理は書けるようになったものの、ローカルメンバに値を保持しておくことなどができず、基本的に継承先のクラスに必ず処理を記述する必要があります。
正味この処理はたいてい同じ内容になるので、抽象クラスの継承のようにできるだけ共通化したいです。
つまり、C++におけるクラスの多重継承を再現したいということになります。
ベストプラクティスが分からなかったので、個人的に考えた解決策の備忘録がこの記事の内容になります。
結論
クラスは多重継承できない、インターフェースは多重継承できる。
それなら、インターフェースをクラスに変身させてしてしまおうという思想です。
インターフェースを継承したら、絶対にメンバに持たなくてはいけない専用クラスを作り、すべての呼び出しをそこにリダイレクトさせることで基底クラスのごとく振る舞うようにします。
- interface「
IHoge
」と一対一で対応するクラス(以下、「義親クラス」とします)「HogeBase
」を用意する - interface「
IHoge
」に継承先「HogeBehaviour
」が義親クラス「HogeBase
」必ず持つように指定する - 「
HogeBehaviour
」のプロパティ、メソッドへのアクセスをすべてメンバの「HogeBase
」の処理へリダイレクトするよう、インターフェースにデフォルト実装する - 「
IHoge
」の継承先「HogeBehaviour
」クラスでは「HogeBase
」の処理を使うか、上書きするかを選ぶ
以上で、インターフェースIHoge
を継承するともれなく義親クラスHogeBase
もついてくるようになります。
メリット
- 同じく「
IHoge
」を継承している「FugaBehaviour
」と処理が共通化できる(コピペが不要になる) - 同じ設計で「
IPiyo
」を作成すれば、多重継承ができる
具体例
適当なのでコンパイルできないかもしれません。
インターフェース
namespace Test
{
public interface IHoge
{
// 対応するクラス.
public HogeBase Hoge
{
get;
}
// プロパティ(継承先で書き換える気がなければvirtualじゃなくてもいい).
public virtual float HP
{
get { return Hoge.HP; }
}
// メソッド.
public virtual bool IsBoss()
{
return Hoge.IsBoss();
}
}
}
IHogeの義親クラス
namespace Test
{
// Inspectorで指定させることもできる.
// 委譲先として同じ関数を必ず持つようにIHogeを継承しておく.
[System.Serializable]
public class HogeBase : IHoge
{
// ないといけないが実際はアクセスしない.
// 一応自分自身を返すようにするがアクセスしたら例外を出してもいいと思う.
public HogeBase Hoge
{
get { return this; }
}
private MonoBehaviour MB { get; set; }
private IHoge Hoge { get; set; }
private GameObject GO { get { return MB.gameObject; } }
private CancellationToken token { get { return MB.destroyCancellationToken; } }
private int EnemyID { get; set; }
public void Init(MonoBehaviour mb, IHoge hoge)
{
this.MB = mb;
this.Interface = hoge;
}
// プロパティ.
[field: SerializeField]
public float HP
{
get; set;
} = 5.0f;
// メソッド.
public bool IsBoss()
{
return this.HP > 8.0f && this.EnemyID > 20;
}
}
}
IHogeの継承先クラス(同じ設計のIFugaも継承している)
namespace Test
{
public class HogeBehaviour : MonoBehaviour, IHoge, IFuga
{
[field: SerializeField]
public HogeBase Hoge { get; set; }
[field: SerializeField]
public FugaBase Fuga { get; set; }
private void Awake()
{
this.Hoge.Init(this, this);
this.Fuga.Init(this, this);
this.Hoge.EnemyID = 30;
}
// メソッドとプロパティはすべてデフォルト実装が呼ばれ、HogeBase,FugaBase内の処理が走る.
}
}
補足
つまりは、C++で言うところの多重継承元クラスのかわりに、interface用クラスを作ってしまい、base.Method();
と同じようなノリでinterface用クラスの関数へ飛ばすということになりますね。
もしかしたらC#の思想に反した方法とかなのかもしれませんが、自分的にはやりたいことができたので満足です。