はじめに
作るもの
その1で作成したコードの入力を中間層(Presenter)を用いてすっきりと記述する
Modelの担当範囲
実践編その1で書いたように、単純にスコア計算だけをするstaticクラスのモデルを使います
Presenterの担当範囲
「既に選択されていたらもう選択しない」であったり「全部選択されたらスコア計算」であったり、Unityや画面とは無関係な部分で、条件判断をする部分を担当します。
public class NineNumberPresenter
{
Subject<int> _numberSelected;
public IObserver<int> NumberSelectedObserver => _numberSelected;
List<int> selectedNumbers;
NumberStatus[] numberStatuses;
public NineNumberPresenter()
{
_numberSelected = new Subject<int>();
selectedNumbers = new List<int>();
numberStatuses = new NumberStatus[9];
for(int i = 0;i < numberStatuses.Length; i++)
{
numberStatuses[i] = NumberStatus.notSelected;
}
_numberSelected.Skip(1).Subscribe(SelectNumber);
}
void SelectNumber(int number)
{
if(number <= 0 || number >= 10) { return; }
if (selectedNumbers.Contains(number)) { return; }
selectedNumbers.Add(number);
if(selectedNumbers.Count == number)
{
numberStatuses[number - 1] = NumberStatus.AccordSelected;
}
else
{
numberStatuses[number - 1] = NumberStatus.DiscordSelected;
}
if (selectedNumbers.Count == 9)
{
int accordCount = numberStatuses.Count(x => x == NumberStatus.AccordSelected);
int discordCount = numberStatuses.Count(x => x == NumberStatus.DiscordSelected);
NineNumberModel.CalculateScore(accordCount, discordCount);
}
}
}
public enum NumberStatus
{
notSelected, AccordSelected, DiscordSelected
}
Viewの担当範囲
実際の座標を用いた処理はViewで行います。
実践編その1では、「この処理はどちらに書いても辛い処理」と書きました。しかし、今回はViewに書いています。
Presenterの存在によって何が変わったのでしょうか。
それは単調な変換処理以外を全部Presenterに追い出したことにあります。
その1で解説した内容は「Viewにデカい処理を書いたら、そのテストが辛くなっちゃうし...うーん」ということでした。
つまり、座標処理そのものの問題というよりかはModelを純粋に保つと、ややこしい座標処理のテストがしにくいから、Viewに大きい処理を入れるのが厳しいという問題だったわけです。
要するに、ややこしい処理をModel以外の場所に追い出せばそれで全部が解決するという種類の問題だったんです。
座標処理は絶対にViewが要りますから、ここがViewにあるのは仕方がないですね。
public class NineNumberView : MonoBehaviour
{
private NineNumberPresenter presenter;
void Start()
{
presenter = new NineNumberPresenter();
}
void Update()
{
if (Input.GetMouseButton(0))
{
Vector2 mousePosition = Input.mousePosition;
if (mousePosition.x <= 760 && mousePosition.x >= 560 && mousePosition.y <= 940 && mousePosition.y >= 740)
{
presenter.NumberSelectedObserver.OnNext(1);
}
if (mousePosition.x <= 1060 && mousePosition.x >= 860 && mousePosition.y <= 940 && mousePosition.y >= 740)
{
presenter.NumberSelectedObserver.OnNext(2);
}
if (mousePosition.x <= 1360 && mousePosition.x >= 1160 && mousePosition.y <= 940 && mousePosition.y >= 740)
{
presenter.NumberSelectedObserver.OnNext(3);
}
if (mousePosition.x <= 760 && mousePosition.x >= 560 && mousePosition.y <= 640 && mousePosition.y >= 440)
{
presenter.NumberSelectedObserver.OnNext(4);
}
if (mousePosition.x <= 1060 && mousePosition.x >= 860 && mousePosition.y <= 640 && mousePosition.y >= 440)
{
presenter.NumberSelectedObserver.OnNext(5);
}
if (mousePosition.x <= 1360 && mousePosition.x >= 1160 && mousePosition.y <= 640 && mousePosition.y >= 440)
{
presenter.NumberSelectedObserver.OnNext(6);
}
if (mousePosition.x <= 760 && mousePosition.x >= 560 && mousePosition.y <= 340 && mousePosition.y >= 140)
{
presenter.NumberSelectedObserver.OnNext(7);
}
if (mousePosition.x <= 1060 && mousePosition.x >= 860 && mousePosition.y <= 340 && mousePosition.y >= 140)
{
presenter.NumberSelectedObserver.OnNext(8);
}
if (mousePosition.x <= 1360 && mousePosition.x >= 1160 && mousePosition.y <= 340 && mousePosition.y >= 140)
{
presenter.NumberSelectedObserver.OnNext(9);
}
}
}
}
PresenterとViewの接続
PresenterとViewをUniRxを用いて接続します。
PresenterがIObservableを公開し、それを参照するViewが通知を送るという仕組みです。
これにより、Presenterは不安定なViewに依存せずに済みます。なぜなら、Viewがコードのどこでイベント通知を送ってもPresenterはそもそもどこでイベントが発行されているかなど知らないので、問題にならないからです。
Presenterの動作確認
UniRxを用いて分離したおかげで、実際にUnityを起動せずとも、コードでサンプルの数値を送って動作を確認できます。
Presenterは小さいので、より限定された範囲からバグを探すことが出来るので、良い設計です。
画面の論理的な状態のテストが十分にされた状態で、Viewと結合することが出来れば、Viewのミスも場所を絞り込むことが出来ます。
試してみましょう。
次のスクリプトは、Presenterに対して特定の数値パターンを送って、その結果を期待するものと比較しています。
(Unity Test Runnerで動かすコードを想定しています)
var presenter = new NineNumberPresenter();
// 1,1,5,1,3,5,6,4,9,8,8,7,2の順で通知する。
// 実質的には1,5,3,6,4,9,8,7,2の順であるので、一致しているのが1と3の2個、不一致なのが5,6,4,9,8,7,2の7個です。
presenter.NumberSelectedObserver.OnNext(1);
presenter.NumberSelectedObserver.OnNext(1);
presenter.NumberSelectedObserver.OnNext(5);
presenter.NumberSelectedObserver.OnNext(1);
presenter.NumberSelectedObserver.OnNext(3);
presenter.NumberSelectedObserver.OnNext(5);
presenter.NumberSelectedObserver.OnNext(6);
presenter.NumberSelectedObserver.OnNext(4);
presenter.NumberSelectedObserver.OnNext(9);
presenter.NumberSelectedObserver.OnNext(8);
presenter.NumberSelectedObserver.OnNext(8);
presenter.NumberSelectedObserver.OnNext(7);
presenter.NumberSelectedObserver.OnNext(2);
Assert.That(presenter.numberStatuses[0] == NumberStatus.AccordSelected);
Assert.That(presenter.numberStatuses[1] == NumberStatus.DiscordSelected);
Assert.That(presenter.numberStatuses[2] == NumberStatus.AccordSelected);
Assert.That(presenter.numberStatuses[3] == NumberStatus.DiscordSelected);
Assert.That(presenter.numberStatuses[4] == NumberStatus.DiscordSelected);
Assert.That(presenter.numberStatuses[5] == NumberStatus.DiscordSelected);
Assert.That(presenter.numberStatuses[6] == NumberStatus.DiscordSelected);
Assert.That(presenter.numberStatuses[7] == NumberStatus.DiscordSelected);
Assert.That(presenter.numberStatuses[8] == NumberStatus.DiscordSelected);
Viewの動作確認
Unityを起動して動かすのは最小限で済ますことが出来ました。これもよい設計です。
Viewのテストを簡易的にやるには、購読先を偽のPresenterにして、そこでDebug.Log
を呼ぶといいです。
public class ViewTester
{
Subject<int> _numberSelected;
public IObserver<int> NumberSelectedObserver => _numberSelected;
public ViewTester()
{
_numberSelected = new Subject<int>();
_numberSelected.Subscribe(SelectNumber);
}
SelecteNumber(int number)
{
Debug.Log(number);
}
}
これをViewに適用すれば(簡単にですが)Viewの挙動をテスト出来ます。
おわりに
今回は具体的なコードをもとに、Presenterの入力における威力を見ることが出来ました。
今回のコードはあくまで入力部分のみです。したがって、計算したスコアをどのように表示するかについてのもっともよい設計は検討段階にあるので注意してください。第3章では、監視型View、Passive View、データバインディング(単方向)のそれぞれを検討していくことで、Unityにおける出力に適した設計を考えていきます。