以前の記事、UnityのScript構成を考えてみる まとめの構成に関し、改善案を2点ほど追加します。
PresenterとViewはクラスを分ける
前回はPresenterクラスの中にViewも混ぜて書いていたのですが、別件でその書き方をしていたところ、酷くPresenterが太りました。
資料だと割と一緒になっていたりするのですが、リッチなViewを使う場合はPresenterから分離したほうがよさそうです。
using System;
using UnityEngine;
using UnityEngine.UI;
using UniRx;
public class View : MonoBehaviour
{
public InputField InputForm;
public Text OutputText;
public Button PowButton;
public Button IncrementButton;
public Button DecrementButton;
public void Render(int number)
{
OutputText.text = number.ToString();
// ※ このあたりでややこしい描画処理が行われる
// そんな仕様でViewとPresenterを一緒にしていたら酷いことになった。
}
public void RenderResult(int number)
{
OutputText.text = "処理結果 " + number.ToString();
}
}
制御系のロジックもPresenterから分離してControllerに。
「MV(R)PなのにController?」と思われそうですが、別件で各オブジェクトの制御をPresenterに任せると、これまたPresenterが太りました。そこで制御部分をControllerに分けたところ、一気にすっきりしました。Dependency Injection パターンも使いやすくなるというおまけつきです。
Controllerのコードはおおむね次のような流れになるかなと(サンプルコードです)。
using UnityEngine;
// MV(R)Pの呼び出しや交通整理。
public class Controller : MonoBehaviour
{
void Start()
{
// 1)初期データを作る
var mediator = new NumberMediator();
mediator.Initialize();
// 2)作ったデータを描画する
var view = GameObject.Find("Canvas").GetComponent<View>();
view.Render(mediator.ReactNum.Value);
// 3)データと描画の連動を開始する
var presenter = gameObject.AddComponent<Presenter>();
presenter.SetUp(mediator, view);
}
}
ちなみにダイエット後のPresenterはこんな感じ。
using System;
using UnityEngine;
using UniRx;
public class Presenter : MonoBehaviour
{
NumberMediator NumberMediator;
View UiView;
public void SetUp(NumberMediator mediator, View view)
{
// ※ Dependency Injectionが使えるようになったので地味ですがFactoryが不要になっています。
NumberMediator = mediator;
UiView = view;
// Modelの監視
NumberMediator.ReactNum
.Skip(1)
.Subscribe(number => UiView.RenderResult(number));
// 値のチェック(とボタンの有効/無効登録3点。)
var stream = UiView.InputForm.OnValueChangedAsObservable().Select(x => Validate(x)).Publish();
stream.SubscribeToInteractable(UiView.PowButton);
stream.SubscribeToInteractable(UiView.IncrementButton);
stream.SubscribeToInteractable(UiView.DecrementButton);
stream.Connect();
// ユーザーアクションとロジックの紐づけ
UiView.PowButton.OnClickAsObservable()
.Where(_ => Validate(UiView.InputForm.text))
.Subscribe(_ => NumberMediator.Pow(Int32.Parse(UiView.InputForm.text)));
UiView.IncrementButton.OnClickAsObservable()
.Where(_ => Validate(UiView.InputForm.text))
.Subscribe(_ => NumberMediator.Increment(Int32.Parse(UiView.InputForm.text)));
UiView.DecrementButton.OnClickAsObservable()
.Where(_ => Validate(UiView.InputForm.text))
.Subscribe(_ => NumberMediator.Decrement(Int32.Parse(UiView.InputForm.text)));
}
bool Validate(string input)
{
int tmp;
if (!Int32.TryParse(input, out tmp)) return false;
return true;
}
}
参考: MV(R)P構成で綺麗に設計するコツとは?
どうもMV(R)P構成をとる場合、次のような感じで設計すると綺麗に設計しやすいようです。
- Model
- アプリで使っている全データの原本となる。
- そのデータに対する処理のすべてを受け持つ。
- Presenter
- データの原本であるModelの値を監視し、適宜その値をViewに反映させる。
- ユーザーの入力をViewから受け取ってModelに伝える。
- Modelのインスタンスを保持することでデータの具体的な置き場所を提供する。
- View
- Presenterから受け取ったデータを画面への出力や音声等で表現する。
- ユーザーからの入力受付とユーザーへの出力表示を行う。
Modelがデータの原本、という点が特に重要です。MV(R)PはPresenterが、Modelのデータを、Viewに反映させるものなので、そもそもModelにデータがないと困ってしまいます。
ところが実際に作っていると、処理に使うデータがUnityにしかない、あるいはModelとUnityの両方にデータがあるけど正しいのはUnityのほう、というUnity(View)が原本の状態になっていることがあります。positionとかありがちです。こうなるとModel → Presenter → View(Unity) となるはずのデータの流れが、View(Unity) → Presenter → View(Unity)となったりもして、かなり構成が崩れます。
Unityはゲームエンジンなので自前のデータや処理を持つのはある意味当然ですし、また任せられるところは任せた方が実際楽なのですが、本質的にその状態はMV(R)P構成と相性があまりよくないという点には注意した方がよさそうです。