$30 off During Our Annual Pro Sale. View Details »

Zenject Example ~さよならManager編~

いも
August 26, 2019

Zenject Example ~さよならManager編~

Roppongi.unity #04 で発表した資料です

いも

August 26, 2019
Tweet

More Decks by いも

Other Decks in Programming

Transcript

  1. Zenject Example
    ~さよならManager

    いも(@adarapata)
    2019/08/26 Roppongi.unity #04
    1

    View Slide

  2. 今日話すこと
    Manager
    をZenject
    使って滅ぼす話
    Zenject
    を多少知ってる人向け
    2019/08/26 Roppongi.unity #04
    2

    View Slide

  3. どこのご家庭にもあるMasterDataManager
    public class MasterDataManager : SingletonMonoBehaviour
    {
    public List Players { get; private set; }
    public List Enemies { get; private set; }
    public List Items { get; private set; }
    private CsvLoader _provider;
    public PlayerData FindById(int id) => Players.Find(p => p.id == id);
    void Start()
    {
    Players = _provider.LoadAll("path/to/players");
    Enemies = _provider.LoadAll("path/to/enemies");
    Items = _provider.LoadAll("path/to/items");
    }
    }
    2019/08/26 Roppongi.unity #04
    3

    View Slide

  4. MasterDataManager
    の気になるポイント
    責務が多い
    インスタンスを持つだけでなく便利メソッド生え始めた
    今後あらゆるデータが追加されて肥大する香りがする
    テスタビリティが低い
    1
    メソッドテストするのに複数の要素が必要になる
    static
    なのでMasterDataManager
    に依存しているコード側もテス
    トしにくい
    2019/08/26 Roppongi.unity #04
    4

    View Slide

  5. MasterDataManaer
    の立ち位置を考える
    なぜMasterDataManager
    という名前がついてたのか?
    複数の種類のデータクラスを纏めて持っているから
    なぜ複数の種類のデータを持ってたのか?
    全部ユニークなインスタンスなのでSingleton
    なインスタンスに持
    たせた方が楽
    なぜSingleton
    なのか
    いろんなところから簡単にアクセスできるようにしたい
    各依存先に渡すのが大変だから
    これらを解決できるならばManager
    は不要になる
    2019/08/26 Roppongi.unity #04
    5

    View Slide

  6. やっていくぞ
    2019/08/26 Roppongi.unity #04
    6

    View Slide

  7. まずはテスト
    namespace Tests
    {
    public class MasterDataManagerTest
    {
    private MasterDataManager _manager;
    [UnityTest]
    public IEnumerator FindById_Success()
    {
    _manager = new GameObject().AddComponent();
    yield return new WaitForEndOfFrame();
    var playerData = _manager.FindById(1);
    Assert.AreEqual("Foo", playerData.name);
    }
    }
    }
    2019/08/26 Roppongi.unity #04
    7

    View Slide

  8. コードのお気持ちを探る
    なぜSingleton
    なの? ←
    イマココ
    なぜMonoBehaviour
    なの?
    2019/08/26 Roppongi.unity #04
    8

    View Slide

  9. なぜSingleton
    なの?
    いろんなところから簡単にアクセスできるようにしたい
    各依存先に渡すのが大変だから
    これを解決できるならばSingleton
    である必要はない。
    じゃあDI
    しましょう。
    2019/08/26 Roppongi.unity #04
    9

    View Slide

  10. MasterDataManager
    をBind
    する
    まだMonoBehaviour
    なのでZenject Binding
    でサッとやる
    2019/08/26 Roppongi.unity #04
    10

    View Slide

  11. Project Context
    を作って子にする
    2019/08/26 Roppongi.unity #04
    11

    View Slide

  12. 呼ぶ側が書き直せる
    public class FooBehaviour {
    public void Foo(){
    var player = MasterDataManager.Instance.FindById(1);
    }
    }

    public class FooBehaviour {
    [Inject]
    private MasterDataManager manager;
    public void Foo(){
    var player = manager.FindById(1);
    }
    }
    2019/08/26 Roppongi.unity #04
    12

    View Slide

  13. 全箇所移行完了したらMonoBehaviour
    に書き直す
    public class MasterDataManager : MonoBehaviour
    {
    public List Players { get; private set; }
    public List Enemies { get; private set; }
    public List Items { get; private set; }
    private CsvLoader _provider;
    public PlayerData FindById(int id) => Players.Find(p => p.id == id);
    void Start()
    {
    Players = _provider.LoadAll("path/to/players");
    Enemies = _provider.LoadAll("path/to/enemies");
    Items = _provider.LoadAll("path/to/items");
    }
    }
    2019/08/26 Roppongi.unity #04
    13

    View Slide

  14. コードのお気持ちを探る
    なぜSingleton
    なの? :done:
    なぜMonoBehaviour
    なの? ←
    イマココ
    2019/08/26 Roppongi.unity #04
    14

    View Slide

  15. なぜMonoBehaviour
    なの?
    初期化処理(
    ロード部分)
    を呼びたい
    誰も初期化を呼ぶ人がいないのでStart()
    を使いたい
    これを解決できるならばMonoBehaviour
    である必要はない。
    2019/08/26 Roppongi.unity #04
    15

    View Slide

  16. IInitializable
    を実装してみよう
    public class MasterDataManager : MonoBehaviour, IInitializable
    {
    public List Players { get; private set; }
    public List Enemies { get; private set; }
    public List Items { get; private set; }
    private CsvLoader _provider;
    public PlayerData FindById(int id) => Players.Find(p => p.id == id);
    public void Initialize(){
    Players = _provider.LoadAll("path/to/players");
    Enemies = _provider.LoadAll("path/to/enemies");
    Items = _provider.LoadAll("path/to/items");
    }
    }
    2019/08/26 Roppongi.unity #04
    16

    View Slide

  17. IInitializable
    Zenject
    が提供するinterface
    やることは初期化のみ
    実装すると、依存関係解決後にInitialize()
    をコールしてくれる
    裏側でInitializableManager
    が動いてる。
    2019/08/26 Roppongi.unity #04
    17

    View Slide

  18. 不要になったのでMonoBehaviour
    をやめよう
    public class MasterDataManager : IInitializable
    {
    public List Players { get; private set; }
    public List Enemies { get; private set; }
    public List Items { get; private set; }
    private CsvLoader _provider;
    public PlayerData FindById(int id) => Players.Find(p => p.id == id);
    public void Initialize(){
    Players = _provider.LoadAll("path/to/players");
    Enemies = _provider.LoadAll("path/to/enemies");
    Items = _provider.LoadAll("path/to/items");
    }
    }
    2019/08/26 Roppongi.unity #04
    18

    View Slide

  19. MonoBehaviour
    じゃなくなったのでInstaller
    を書こう
    public class MasterDataInstaller : MonoInstaller
    {
    public override void InstallBindings()
    {
    Container.Bind().AsSingle();
    }
    }
    2019/08/26 Roppongi.unity #04
    19

    View Slide

  20. テストも少し書き直そう
    namespace Tests
    {
    public class MasterDataManagerTest
    {
    private MasterDataManager _manager;
    // MonoBehaviour
    じゃないのでEditTest
    で十分
    [Test]
    public void FindById_Success()
    {
    _manager = new MasterDataManager();
    _manager.Initialize();
    var playerData = _manager.FindById(1);
    Assert.AreEqual("Foo", playerData.name);
    }
    }
    }
    2019/08/26 Roppongi.unity #04
    20

    View Slide

  21. 今一度MasterDataManaer
    の立ち位置を考える
    なぜMasterDataManager
    という名前がついてたのか?
    複数の種類のデータクラスを纏めて持っているから
    なぜ複数の種類のデータを持ってたのか?
    全部ユニークなインスタンスなのでSingleton
    なインスタンスに持
    たせた方が楽
    なぜSingleton
    なのか
    いろんなところから簡単にアクセスできるようにしたい
    各依存先に渡すのが大変だから
    2019/08/26 Roppongi.unity #04
    21

    View Slide

  22. 今一度MasterDataManaer
    の立ち位置を考える
    なぜSingleton
    なのか => DI
    でOK
    なぜ複数の種類のデータを持ってたのか? =>
    直接Inject
    できるなら
    纏める必要はない
    なぜMasterDataManager
    という名前がついてたのか? =>
    纏める必
    要ないなら不要では?
    2019/08/26 Roppongi.unity #04
    22

    View Slide

  23. データ持ってるクラスを独立
    public class PlayerDataRepository
    {
    private readonly List _resources;
    public PlayerDataRepository(IResourceLoadProvider _provider)
    {
    _resources = _provider.LoadAll("path/to/players");
    }
    public PlayerData FindById(int id) => _resources.Find(p => p.id == id);
    }
    public class MasterDataInstaller : MonoInstaller
    {
    public override void InstallBindings()
    {
    Container.BindInterfacesAndSelfTo().AsSingle();
    Container.BindInterfacesAndSelfTo().AsSingle();
    Container.Bind().To().AsSingle();
    }
    }
    2019/08/26 Roppongi.unity #04
    23

    View Slide

  24. PlayerDataRepository
    だけ差し替えて動作は変えない
    public class MasterDataManager : IInitializable
    {
    public List Enemies { get; private set; }
    public List Items { get; private set; }
    [Inject]
    private IResourceLoadProvider _provider;
    [Inject]
    private PlayerDataRepository _playerDataRepository;
    public PlayerData FindById(int id) => _playerDataRepository.FindById(id);
    public void Initialize()
    {
    Enemies = _provider.LoadAll("path/to/enemies");
    Items = _provider.LoadAll("path/to/items");
    }
    }
    2019/08/26 Roppongi.unity #04
    24

    View Slide

  25. 新規に書くときは直接Repository
    を渡してあげればよい
    public class FooBehaviour {
    [Inject]
    private PlayerDataRepository _repository;
    public void Foo(){
    var player = repository.FindById(1);
    }
    }
    2019/08/26 Roppongi.unity #04
    25

    View Slide

  26. Test
    もZenject
    対応
    public class MasterDataManagerTest : ZenjectUnitTestFixture {
    public class MockLoader : IResourceLoadProvider {
    public List LoadAll(string path)
    => new List(/*
    なんかいい感じ生成 */);
    }
    private MasterDataManager _manager;
    [Test]
    public void FindById_Success() {
    Container.Bind().AsSingle();
    Container.BindInterfacesTo().AsSingle();
    Container.Bind().AsSingle();
    _manager = Container.Resolve();
    _manager.Initialize();
    var playerData = _manager.FindById(1);
    Assert.AreEqual("Foo", playerData.name);
    }
    }
    2019/08/26 Roppongi.unity #04
    26

    View Slide

  27. さよならManager
    2019/08/26 Roppongi.unity #04
    27

    View Slide

  28. まとめ
    Zenject
    使えばインスタンスを集約するだけのクラスは不要になる
    Manager
    と呼ばれてるものの役割を見直してみよう
    もしかしたらインスタンスを集めてるだけのクラスかもしれない
    漸進的に変更できる方法を選ぼう
    旧コードと新コードが一時は同居できるようにする
    テストを書くと安心感ある
    テストを書くと安心感ある
    テストを書くと安心感ある
    2019/08/26 Roppongi.unity #04
    28

    View Slide

  29. 宣伝 Zenject
    本書いてます
    技術書典7
    こ20D
    前後編になりました
    〆切と印刷費に負けた
    ごめん
    今回は前編を出します
    DI
    の話~Scene
    跨ぎのBind
    くらいまで
    2019/08/26 Roppongi.unity #04
    29

    View Slide