Slide 1

Slide 1 text

Zenject Example ~さよならManager ~ いも(@adarapata) 2019/08/26 Roppongi.unity #04 1

Slide 2

Slide 2 text

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

Slide 3

Slide 3 text

どこのご家庭にもある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

Slide 4

Slide 4 text

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

Slide 5

Slide 5 text

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

Slide 6

Slide 6 text

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

Slide 7

Slide 7 text

まずはテスト 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

Slide 8

Slide 8 text

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

Slide 9

Slide 9 text

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

Slide 10

Slide 10 text

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

Slide 11

Slide 11 text

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

Slide 12

Slide 12 text

呼ぶ側が書き直せる 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

Slide 13

Slide 13 text

全箇所移行完了したら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

Slide 14

Slide 14 text

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

Slide 15

Slide 15 text

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

Slide 16

Slide 16 text

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

Slide 17

Slide 17 text

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

Slide 18

Slide 18 text

不要になったので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

Slide 19

Slide 19 text

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

Slide 20

Slide 20 text

テストも少し書き直そう 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

Slide 21

Slide 21 text

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

Slide 22

Slide 22 text

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

Slide 23

Slide 23 text

データ持ってるクラスを独立 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

Slide 24

Slide 24 text

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

Slide 25

Slide 25 text

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

Slide 26

Slide 26 text

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

Slide 27

Slide 27 text

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

Slide 28

Slide 28 text

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

Slide 29

Slide 29 text

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