imog

主にUnityとかの事を書いています

Zenject Decorator Binding

ZenjectにはDecoratorパターンをサポートするDecorator Bindingなる機能があったりします。あんまりメインで使うような機能ではありませんが覚えておくと便利かもしれません。

github.com

Decorator パターン

DecoratorパターンはGoFで定義されてるデザインパターンの一つです。既存のオブジェクトに対して動的な機能追加を行う場合などに効果を発揮します。 例えば超絶シンプルなRPGを作るときに、攻撃力とHPを持つクラスがあるとします。ステータス部分は共通のインタフェースとして、戦士と魔法使いの二種類を作ります。

public interface IStatus
{
    int Life { get; }
    int Attack { get; }
}

public class Knight : IStatus
{
    public int Life => 20;
    public int Attack => 10;
}

public class Wizard : IStatus
{
    public int Life => 15;
    public int Attack => 2;
}

これらはIStatusで抽象化できるので、攻撃の数値部分などは同じように扱えます。ところでRPGにはやはり攻撃力アップなどバフ効果も欲しいですね。その場合はAttackの補正処理をかけることになるのですがどこに実装しましょう。例えばIStatusを持った抽象クラスのBaseCharacterなどが生まれて、そこにバフクラスを渡すような処理にするのもありだと思います。

public abstruct class BaseCharacter : IStatus
{
    public virtual int Life { get; }
    public virtual int Attack { get; }

    public void SetBuffStatus(IStatusBuffer buff) {
        // 内部パラメータをあれこれする処理
    }
}

var knight = new Knight();
knight.SetBuffStatus(new AttackUpBuffer()); // バフをかける

この場合、バフの種類が増えていくとちょっと大変そうですね。メソッド内に変更の実装を書くならSetBuffStatusが肥大化します。じゃあBuff側に実装を書いてそのメソッドを呼ぶだけ、という形にすると今度はBuff側から更新できるようにBaseCharacterがパラメータ更新メソッドを外部に公開することになります。どちらにせよ、既存への影響を与えがちです。気合でなんとかしましょう。

Decoratorパターンは、上からインスタンスを被せることで変更を少なくします。

// 攻撃力を高めるバフ
public class AttackUpDecorator : IStatus
{
    private readonly IStatus _decorator;

    public int Life => _decorator.Life;
    public int Attack => _decorator.Attack * 2; // 強い!

    public AttackUpDecorator(IStatus decorator)
    {
        _decorator = decorator;
    }
}

var knight = new Knight();
var buffKnight = new AttackUpDecorator(knight); // バフをかける

同じinterfaceを持ったDecoratorクラスを用意することで、同じ振る舞いを持たせたまま拡張ができるようになります。

この実装方法のメリットは以下です。

  • 既存のオブジェクトへの変更が少ない
  • 呼び出し側が変化を気にしなくてよい

すでに動いているプロジェクトに対して効果的だったりします。

Zenjectでの実装方法

Zenjectでバインドしているインスタンスに同様のDecorateを行いたい場合は以下で実現できます。

public class StatusInstaller : MonoInstaller
{
    public override void InstallBindings()
    {
        // いつものバインド
        Container.Bind<IStatus>().To<Knight>().AsCached();
        // IStatusをAttackUpDecoratorでDecorateする
        Container.Decorate<IStatus>().With<AttackUpDecorator>().AsCached();
    }
}

Decorate<T>().With<InheritedT>といった感じで一行で実装できます。便利。この場合Resolveして返ってくるのはAttackUpDecoratorです。

どう使うの?

上記のようなRPGはかなり極端な例なので実際使えるかという推敲すべきですが、デバッグ、プロファイリングの処理を書くには有効ではないかなと思います。あとゲームで言うとローディング中の表示をDecorateで切り出すのももしかしたらありかもしれないですね。

public interface IAsyncLoadable<T> {
    Task<T> Load();
}

public class FooLoader : IAsyncLoadable<Foo> {
    public async Task<Foo> Load() {
        // なんか読み込みロジック
    }
}

public class LoadingIconDecorator : IAsyncLoadable<T> {
    private readonly IAsyncLoadable<T> _decorator;
    public LoadingIconDecorator(IAsyncLoadable<T> decorator) {
        _decorator = decorator;
    }
    public async Task<T> Load() {
        ShowLoadIcon(); // ローディングアイコン出すなどの処理
        await _decorator.Load();
        CloseLoadIcon(); // 閉じるなど
    }
}

Container.BindInterfacesAndSelfTo<FooLoader>().AsCached();
Container.Decorate<IAsyncLoadable<Foo>>()
         .With<LoadingIconDecorator>()  // FooLoader実行時にローディングアイコンを出すようにDecorate
         .AsCached();

実際便利かはわからないので誰かやってみて!

性質上、DecoratorContextとの相性はよいのでこちらも併せて使っていくといい感じに分離できておススメです。

https://github.com/svermeulen/Zenject#scene-decorators

ただ、Bind終わった後に動的なDecorateしたいなあという気持ちが個人的には強かったりする。シーン開始時は何もないけど、ゲーム中に動的にFactoryから生成されたものにDecorateされたりとか。その辺うまくやれないか考えてみよ。