はじめに
現在Unityでブロックを持ったり置いたりしてギミックを解いていくゲームを作っています。
操作するキャラクターの処理に対してICharacterというインターフェイスを使っているのですが、そのインターフェースが見事にInterface Soup アンチパターンになっていたので、次からミスをしないようにここに記録しておきます。
作成にあたって「Adaptive Code C#実践開発手法 第2版」を参考にさせていただきました。
問題となっているコード
public interface ICharacter:
IMoveExecutor,
IHoldActionExecutor,
IPassActionExecutor,
{
}
// 実装クラス
public class Character : ICharacter
{
// ... 省略
}
// ICharacterを使用するMonobehaviourのクラス
public class CharacterController : MonoBehaviour
{
ICharacter _character;
public void Construct(ICharacter character)
{
_character = character;
}
}
このコードはSOLID原則のうちの一つである、「インターフェース分離の原則(Interface Segregation Principle)」に反しています。
Adaptive Codeでは以下のようにInterface Soup アンチパターンについて以下のように説明されています。
分割されたインターフェイスを何らかの理由で1つのインターフェイスに集約するのは、よくある間違いです。
実装時に全ての操作の実装を再び提供しなければならなくなるため、ターゲットを絞ったデコレーターを作成する余地はありません。
解決方法
ICharacterを受け取っているところ(CharacterControllerのConstruct())で、それぞれのインターフェースで受け取るように変更します。
このことによって、同一のインスタンスを複数回実引数として渡すことになりますが、これで良いとされています。Adaptive Codeではこのことがインターフェース分離の原則における一般的な副作用であると説明されています。
public class Character :
IMoveExecutor,
IHoldActionExecutor,
IPassActionExecutor
{
public void Move(Vector3 input)
{
// ...
}
public void Hold()
{
// ...
}
public void Pass()
{
// ...
}
}
public class CharacterController : MonoBehaviour
{
IMoveExecutor _move;
IHoldActionExecutor _holdAction;
IPassActionExecutor _passAction;
// それぞれのインターフェースを受け取るようにする
public void Construct(IMoveExecutor move, IHoldActionExecutor holdAction, IPassActionExecutor passAction)
{
_move = move;
_holdAction = holdAction;
_passAction = passAction;
}
}
public class CharacterBuilder : MonoBehaviour
{
[SerializeField] CharacterController characterPrefab;
public void Create()
{
var characterController = Instantiate(characterPrefab, transform.position, Quaternion.identity);
var character = new Character();
characterController.Construct(character, character, character); // 同一のインスタンスを複数渡すことになるが間違いではない
}
}
参考記事
書籍「Adaptive Code C#実践開発手法 第2版」