はじめに
理論編では、ModelとViewを分離することの利点について解説しました。実践編では以下のようなアプリケーションを作って、ModelとViewが分離できていない状態の欠点を確認していきます。
作成するもの
- 1から9の数字を好きな順番で選択することが出来ます。
- n番目にnの数字を選択すると、背景が赤くなります。
- n番目にn以外の数字を選択すると、背景が青くなります。
- n個の数字を選択すると、「赤色の数字の数*青色の数字の数」の値が左上にスコアとして表示されます。
ヒエラルキービュー
ゲームオブジェクト1~9は単なるuGUIのImageとTextです。また、Scoreは単なるuGUIのTextです。
それぞれの数字の大きさは200px200px、数字と数字の間隔は100px、画面全体の大きさは1920px1080pxです。
ViewとModelが密結合しているコード
さて、上の仕様を実現するために以下のようなコードを書いたとしましょう。このコードがCanvasにアタッチされます。
そこまで詳しく読み込まなくても記事の理解に差し支えないので、軽く流れを理解する程度で読み進めてください。
public class NineNumberManager : MonoBehaviour
{
Image[] buttons;
Text score;
List<int> selectedList;
void Start()
{
var one = transform.Find("1").GetComponent<Image>();
var two = transform.Find("2").GetComponent<Image>();
var three = transform.Find("3").GetComponent<Image>();
var four = transform.Find("4").GetComponent<Image>();
var five = transform.Find("5").GetComponent<Image>();
var six = transform.Find("6").GetComponent<Image>();
var seven = transform.Find("7").GetComponent<Image>();
var eight = transform.Find("8").GetComponent<Image>();
var nine = transform.Find("9").GetComponent<Image>();
buttons = new Image[] { one, two, three, four, five, six, seven, eight, nine };
score = transform.Find("Score").GetComponent<Text>();
selectedList = new List<int>();
}
private void Update()
{
if (Input.GetMouseButton(0))
{
Vector2 mousePosition = Input.mousePosition;
if (mousePosition.x <= 760 && mousePosition.x >= 560 && mousePosition.y <= 940 && mousePosition.y >= 740)
{
OnClick(1);
}
if (mousePosition.x <= 1060 && mousePosition.x >= 860 && mousePosition.y <= 940 && mousePosition.y >= 740)
{
OnClick(2);
}
if (mousePosition.x <= 1360 && mousePosition.x >= 1160 && mousePosition.y <= 940 && mousePosition.y >= 740)
{
OnClick(3);
}
if (mousePosition.x <= 760 && mousePosition.x >= 560 && mousePosition.y <= 640 && mousePosition.y >= 440)
{
OnClick(4);
}
if (mousePosition.x <= 1060 && mousePosition.x >= 860 && mousePosition.y <= 640 && mousePosition.y >= 440)
{
OnClick(5);
}
if (mousePosition.x <= 1360 && mousePosition.x >= 1160 && mousePosition.y <= 640 && mousePosition.y >= 440)
{
OnClick(6);
}
if (mousePosition.x <= 760 && mousePosition.x >= 560 && mousePosition.y <= 340 && mousePosition.y >= 140)
{
OnClick(7);
}
if (mousePosition.x <= 1060 && mousePosition.x >= 860 && mousePosition.y <= 340 && mousePosition.y >= 140)
{
OnClick(8);
}
if (mousePosition.x <= 1360 && mousePosition.x >= 1160 && mousePosition.y <= 340 && mousePosition.y >= 140)
{
OnClick(9);
}
}
}
void OnClick(int index)
{
if (selectedList.Contains(index)) { return; }
if ((selectedList.Count + 1) == index)
{
buttons[index - 1].color = Color.red;
}
else
{
buttons[index - 1].color = Color.blue;
}
selectedList.Add(index);
if (selectedList.Count == 9)
{
WriteScore();
}
}
void WriteScore()
{
score.text = $"{CountAccord() * CountDiscord()}";
}
int CountAccord()
{
return buttons.Count( numberPanel => numberPanel.color == Color.red );
}
int CountDiscord()
{
return buttons.Count(numberPanel => numberPanel.color == Color.blue );
}
}
それでは、このコードを例に、ModelとViewの密結合がいかに悪影響であるかを見ていきます。
シーン間でデータが共有できない
「スコア」や「どのが選択されたのか」という、いかにも重要でほかのシーンでも流用できそうなデータが、Monobehaviourのフィールドに保存されています。これによって、シーンのアンロードとともに重要データが消えてしまうために、複数画面を作ろうとした時に困ってしまうのです。
また、スコアに関しては、保存先がただの文字列になっています。別のコンポーネントがスコアを使いたくなったときにはわざわざどこのコンポーネントに保存されているのかを探して、GameObject.Find
を呼んで、そこからint.Parse
を呼ばなければなりません。ViewはUnityの仕組みに従ってたくさんのゲームオブジェクトやMonobehaviourに分散するので、ViewがModelのデータを持つということは、データが分散して保存されて一覧性や統一性、実装の柔軟性が下がるということなのです。
デザインが変わったら連鎖的に不具合が発生する。その1
例えば、ボタンのサイズを変更したとします。この場合、ボタンの大きさに依存したOnClickを呼び出している以下の部分は動かなくなります。
if (mousePosition.x <= 1060 && mousePosition.x >= 860 && mousePosition.y <= 340 && mousePosition.y >= 140)
{
OnClick(8);
}
つまり、デザインを変更するときには常に全部が壊れることに怯えないといけないのです。これは、デザイナーが気軽にデザインを変えることが出来ず、作業分担が難しくなるということを意味します。
デザインが変わったら連鎖的に不具合が発生する。その2
このコードにおいて「n番目にnが選択された」という情報は、Viewに記録されています。なぜなら、「同じなら赤、違ったら青」というデザイン上の仕様に基づいて、色がColor.Red
であるなら、n番目にnが選択されていて、Color.blue
ならn番目にn以外が選択された、という判定をしているからです。
int CountAccord()
{
return buttons.Count( numberPanel => numberPanel.color == Color.red );
}
int CountDiscord()
{
return buttons.Count(numberPanel => numberPanel.color == Color.blue );
}
これもまた、デザインが変わると動かなくなるコードですね。色の設定部分を赤から黄色に変えたらそれだけで動かなくなります。つまり、デザインを変えることに怯えないといけません。
メインロジックを検証できない その1
「スコア計算」というメイン部分が以下のように、文字列埋め込みの内部に入っている状況です。
つまり、スコア計算の動作確認をするには、プログラムをほとんど完成させなければなりません。
なぜなら、CountAccord()
やCountDiscord()
はViewに属するそれぞれの数字の背景色を数えているからです。つまり、Viewが完成するまでModelが検証できないということです。一般に、テストが遅くなればなるほどバグの特定は難しくなります。したがって、これは悪い設計です。
score.text = $"{CountAccord() * CountDiscord()}";
メインロジックが検証できない その2
キー入力からOnClick
関数を呼び出す部分では、Inputに依存した処理を書いています。
if (Input.GetMouseButton(0))
{
Vector2 mousePosition = Input.mousePosition;
if (mousePosition.x <= 760 && mousePosition.x >= 560 && mousePosition.y <= 940 && mousePosition.y >= 740)
{
OnClick(1);
}
この処理は、どの場合にどの関数を呼び分けるかという、本来Viewには関係ない(すなわち、Unityがなくても成立する)ような処理であるのにもかかわらず、Inputがないとテストが出来ません。これがどういうことかと言うと、いちいちUnityを起動してボタンを押さないとテストできません。非常に使いにくい設計です。
おわりに
理論的にも実践的にも、ModelとViewの分離の必要性について、実感を持ってもらえると嬉しいです。
第1章でわかったこととしては、ModelとViewが分離できると嬉しい!ということだけであり、現実的にどのように分離していくのかについては第2章以降で具体的に議論を進めていきます。