ルーグの備忘録

主にC#についてまとめてます。

Zenjectを使ってSingletonを撲滅する

前置き

Singletonなんてそんな野蛮な…ここは穏便にDIで…。

Zenjectとは

Unityで使えるDIフレームワーク
assetstore.unity.com

DIとは

Dependency Injectionの略。日本語訳すると「依存性の注入」だけど…直訳すぎてよく分からない。
だからか「機能の注入」とか「オブジェクト(インスタンス)の注入」ってよく言い換えられてる。こっちの方がなんとなく分かりやすいけど、言葉の意味を追うよりも実際に使ってみた方が理解が早かったので先に進む。

DIでSingletonを撲滅する

例えばキー入力を購読できるイベントを公開してるこんな感じのシングルトンがあったとする。

public class InputManager : Singleton<InputManager>
{
    /// <summary>
    /// 入力イベント
    /// </summary>
    private Subject<InputInfo> m_InputEvent = new Subject<InputInfo>();
    public IObservable<InputInfo> InputEvent => m_InputEvent;

..... 以下省略

入力を購読したいと思っている他のクラスはInputManagerに依存することになる。

public class PlayerInput : ActorComponentBase, IPlayerInput
{
    protected override void Initialize()
    {
        // 入力イベント購読
        InputManager.Instance.InputEvent.Subscribe(input =>
        {
            DetectInput(input.KeyCodeFlag);
        }).AddTo(CompositeDisposable);

..... 以下省略

これを解決する。

まず、イベント公開機能をインターフェイスとして切り出す。

public interface IInputManager
{
    IObservable<InputInfo> InputEvent { get; }
}
public class InputManager : MonoBehaviour, IInputManager
{
    /// <summary>
    /// 入力イベント
    /// </summary>
    private Subject<InputInfo> m_InputEvent = new Subject<InputInfo>();
    IObservable<InputInfo> IInputManager.InputEvent => m_InputEvent;

..... 以下省略

次に依存関係をInstallerクラスに定義する

public class CommonSystemInstaller : MonoInstaller
{
    [SerializeField]
    private GameObject m_CommonSystem;

    public override void InstallBindings()
    {
        Container.Bind<IInputManager>()
            .To<InputManager>()
            .FromComponentOn(m_CommonSystem)
            .AsSingle()
            .NonLazy();

..... 以下省略

Inject属性を使うと、Zenjectが勝手にIInputManagerを提供してくれるようになる。便利。

public class PlayerInput : ActorComponentBase, IPlayerInput
{
    [Inject]
    private IInputManager m_InputManager;

    protected override void Initialize()
    {
        // 入力イベント購読
        m_InputManager.InputEvent.Subscribe(input =>
        {
            DetectInput(input.KeyCodeFlag);
        }).AddTo(CompositeDisposable);

..... 以下省略

結局、DIとはなんだったのか

シングルトンパターンだと、入力イベントを購読したいクラス(PlayerInput)が入力イベントを持っているクラス(InputManager)を絶対に知っておく必要がある。
しかし、イベント公開機能をインターフェイスとして切り出して、さらにInstallerクラスが第三者の立場となって依存関係を定義することで、イベントを購読する側はインターフェイスさえ外から提供されれば、イベントの購読ができるようになる。

これの何が嬉しいの?

InputManagerで実装の中身の変更をしても、PlayerInputは(インターフェイスの変更がない限り)その影響を受けない。
インターフェイスの実装を別のクラスで置き換えたいときは、Installerクラスだけを変更すれば良い。
→依存する側は依存先のことを考える必要がなく、依存先の変更もしやすい。つまり疎結合ってこと。

まとめ

Zenjectを使ってSingletonとさようならしたお話でした。

参考

依存関係の設計理念をゴリゴリに解説してる。
www.nuits.jp