UnityでMVPと、UniRxを使ったMV(R)Pを実装してみました。
また、調べたらいろんな実装方法があったのでそのあたりの考察も加えています。
- まずは全部Viewに書く
- MVP
- MV(R)P
- いろんな実装がある
- ViewとPresenterは1:1にするべき?
- PresenterはMonoBehaviourにするべき?
- Viewはインタフェースにするべき?
- ViewはModelを知るべき?
- 参考
まずは全部Viewに書く
まず、アーキテクチャを使わない状態、
すなわち全ての処理を一つのクラスに書いたものを作ってみます。
using UnityEngine; using UnityEngine.UI; public class CountingButton : MonoBehaviour { [SerializeField] private Button _button; [SerializeField] private Text _buttonText; [SerializeField] private string _targetId = "Gem"; private void Start () { _button.onClick.AddListener(OnClick); // 現在の数をボタンに表示 SetButtonText(GetCount()); } private void OnDestroy() { _button.onClick.AddListener(OnClick); } /// <summary> /// クリックされた時の処理 /// </summary> private void OnClick() { // 数をインクリメントして保存 var count = GetCount(); count++; SetCount(count); // ボタンのテキストに数を表示 SetButtonText(count); } private void SetCount(int count) { PlayerPrefs.SetInt(_targetId, count); } private int GetCount() { return PlayerPrefs.HasKey(_targetId) ? PlayerPrefs.GetInt(_targetId) : 0; } private void SetButtonText(int count) { _buttonText.text = _targetId + " : " + count; } }
押すたびにGemの数がインクリメントされて保存され、ボタンに表示されます。
今回は処理が簡単なのであまり気になりませんが、
全ての処理をこのクラスに書いているので、
処理が増えるにつれて複雑なクラスが出来上がっていきます。
MVP
つぎにMVPの設計方針に従い責務を分離してみます。
Viewの責務でない部分をPresenterに移します。
今回の例におけるViewの責務は、テキストの更新とクリックイベントの通知なので、これをPresenterに移します。
using UnityEngine; using UnityEngine.UI; // Viewの更新メソッドはインタフェースを定義する public interface ICountingButton { void SetButtonText(int count); } public class CountingButton : MonoBehaviour, ICountingButton { [SerializeField] private Button _button; [SerializeField] private Text _buttonText; [SerializeField] private string _targetId = "Gem"; private CountingButtonPresenter _presenter; private void Start () { _button.onClick.AddListener(OnClick); // Presenterを生成 _presenter = new CountingButtonPresenter(_targetId, this); } private void OnDestroy() { _button.onClick.AddListener(OnClick); } private void OnClick() { // Presenterに通知をするだけ _presenter.OnClick(); } // インタフェースを実装 public void SetButtonText(int count) { _buttonText.text = _targetId + " : " + count; } }
クリックされた時のイベントはPresenterに通知しているだけで、実際の処理は行いません。
またViewの更新は、更新用のメソッドがインタフェース経由で公開されているだけです。
実際にこのメソッドを読んでViewを更新するのはPresenterに任せます。
というわけでPresenterを書いていきます。
public class CountingButtonPresenter { private string _targetId; private ICountingButton _view; public CountingButtonPresenter(string targetId, ICountingButton view) { _targetId = targetId; _view = view; SetCount(GetCount()); } // Viewがクリックされた時の処理 public void OnClick() { var count = GetCount(); count++; SetCount(count); // Viewを更新 _view.SetButtonText(count); } private void SetCount(int count) { PlayerPrefs.SetInt(_targetId, count); } private int GetCount() { return PlayerPrefs.HasKey(_targetId) ? PlayerPrefs.GetInt(_targetId) : 0; } }
Viewからのクリック通知を受けて実際にGemの数を更新し、Viewを更新しています。
これでViewとPresenterを分けることができました。
ちなみにPlayerPrefsをいじっている部分などはいわゆるModelですが、
ひとまずはViewとPresenterを分離することが重要なので本記事では深くは触れないことにします。
MV(R)P
次にMV(R)Pを使ってみます。
MV(R)PはMVPとUniRxを組み合わせたアーキテクチャです。
neue cc - UniRx 4.8 - 軽量イベントフックとuGUI連携によるデータバインディング
まずViewは次のようになります。
using UnityEngine; using UnityEngine.UI; using UniRx; public class CountingButton : MonoBehaviour { [SerializeField] private Button _button; [SerializeField] private Text _buttonText; // イベント監視用のObservableを公開する public IObservable<Unit> OnClickAsObservable() { return _button.OnClickAsObservable(); } public void SetButtonText(string id, int count) { _buttonText.text = id + " : " + count; } }
Viewにはイベントを監視するためのObservableを定義するだけでPresenterの参照は持ちません。
また、MVPと違ってViewのインタフェース定義をしていません。
using UnityEngine; using UniRx; public class CountingButtonPresenter : MonoBehaviour // PresenterはMonoBehaviourにする { [SerializeField] private string _targetId = "Gem"; private CountingButton _view; public void Start() { // ViewはGetComponentで取得 _view = GetComponent<CountingButton>(); _view.SetButtonText(_targetId, GetCount()); // クリックを監視してViewを更新 _view .OnClickAsObservable() .Subscribe(_ => { var count = GetCount(); count++; SetCount(count); _view.SetButtonText(_targetId, count); }); } private void SetCount(int count) { PlayerPrefs.SetInt(_targetId, count); } private int GetCount() { return PlayerPrefs.HasKey(_targetId) ? PlayerPrefs.GetInt(_targetId) : 0; } }
PresenterはViewのイベントを監視して、Viewを更新します。
今回は書いていませんが、同じようにModelを監視することでModelの変更を受け取ります。
一度監視してしまえばあとは勝手にModelに紐づいてViewが更新されるのでいい感じです。
いろんな実装がある
MVPとMV(R)Pについて、標準的だと思われる実装を前節までで紹介しました。
しかしこれらのアーキテクチャについて調べると、実装や考え方が微妙に異なっていることがわかります。
- ViewとPresenterが1:1のものと多:1のものがある
- PresenterがMonoBehaviourを継承しているものとしていないものがある
- Viewのインタフェースを定義する場合と直接参照している場合がある
- View用のModelを定義しているものとしていないものがある
以下ではこれらについて見ていきます。
ViewとPresenterは1:1にするべき?
MVPやMV(R)Pの実装を見ていると、1つのViewに対して1つのPresenterを定義しているものと、
1つのPresenterが複数のViewを管理しているものがあります。
上述のMVPの実装のようにViewがPresenterを生成する場合は1:1にならざるを得ないのかなと思いますが、
上述のMV(R)PのようにPresenterからViewを取得する場合には複数のViewを持つ実装にもできます。
それぞれのメリット・デメリットに関しては下記のスライドにまとめられています。
個人的にはViewとPresenterを1:1にするのは冗長すぎるように感じました。
PresenterはMonoBehaviourにするべき?
上述のMVPの実装ではPresenterが純粋なクラスであるのに対して、
MV(R)PのPresenterはMonoBehaviourを継承しています。
MonoBehaviourにしない場合にはnewできるのでテストがしやすくなるメリットがあります。
ただしViewがPresenterを生成することになるので、ViewとPresenterが自然と1:1になります。
また、どんなに簡単なViewでもViewクラスとPresenterをいちいち定義しなければなりません。
開発方針にはよりますが、UnityではMonoBehaviourにするメリットは大きいと思います。
Viewはインタフェースにするべき?
上述のMVPの実装ではViewのインタフェースを定義しているのに対して、MV(R)Pでは定義していません。
Viewのインタフェースを定義するとViewをテスト用のものに入れ替えられるのでテストしやすくなります。
ただ、前節の議論でPresenterをMonoBehaviourにした場合には結局テストはしづらくなるので、
Viewのインタフェースを定義するメリットも自然と無くなりそうです。
ViewはModelを知るべき?
Viewに対応したModelを定義している実装とそうでない実装があります。
後者は実装が簡潔になりますが、ViewがModelを知るため依存度は高まります。
これに対して前者は実装は比較的冗長になりますが、疎結合になります。
この辺りはかなり方針が分かれそうなところです。
疎結合にする場合にはClean Architectureのような考え方を適用するといい感じになりそうですが、これはまた別記事で。