UnityのScriptの構成をどのようにすべきか、手を動かしながら考えてみることにした。
サンプルプログラム概要
考察のために用意したサンプルプログラムがこちら。
画面
挙動
1)入力フォームに整数を入れ
2)3つあるボタンのいずれかを押下すると
3)それぞれのボタンに対応した処理(2乗、インクリメント、デクリメント)を行った結果がファイルに保存され、かつ画面上にも出力される。
……というシンプルなプログラム。
1. View のみ、共通化なし
まずはということで、原始的に3つの処理をすべて1ファイルに書いてみる。関数の共通化もあえて行わない。
using System;
using System.IO;
using UnityEngine;
using UnityEngine.UI;
public class GodView : MonoBehaviour
{
public void OnClickPow()
{
// in
var input = GameObject.Find("InputForm").GetComponent<InputField>().text;
// check
int number;
bool canParse = Int32.TryParse(input, out number);
if (!canParse)
{
GameObject.Find("OutputText").GetComponent<Text>().text = "整数のみ入力可";
return;
}
// logic
var output = Math.Pow(number, 2);
using (var fileStream = new FileStream(Application.persistentDataPath + "/pow.txt", System.IO.FileMode.Create, System.IO.FileAccess.Write, System.IO.FileShare.None))
{
using (var writer = new StreamWriter(fileStream, System.Text.Encoding.UTF8))
{
writer.Write(output.ToString());
}
}
// out
GameObject.Find("OutputText").GetComponent<Text>().text = "処理結果 " + output.ToString();
}
public void OnClickIncrement()
{
// in
var input = GameObject.Find("InputForm").GetComponent<InputField>().text;
// check
int number;
bool canParse = Int32.TryParse(input, out number);
if (!canParse)
{
GameObject.Find("OutputText").GetComponent<Text>().text = "整数のみ入力可";
return;
}
// logic
var output = number + 1;
using (var fileStream = new FileStream(Application.persistentDataPath + "/increment.txt", System.IO.FileMode.Create, System.IO.FileAccess.Write, System.IO.FileShare.None))
{
using (var writer = new StreamWriter(fileStream, System.Text.Encoding.UTF8))
{
writer.Write(output.ToString());
}
}
// out
GameObject.Find("OutputText").GetComponent<Text>().text = "処理完了" + output.ToString();
}
public void OnClickDecrement()
{
// in
var input = GameObject.Find("InputForm").GetComponent<InputField>().text;
// check
int number;
bool canParse = Int32.TryParse(input, out number);
if (!canParse)
{
GameObject.Find("OutputText").GetComponent<Text>().text = "整数のみ入力可";
return;
}
// logic
var output = number - 1;
using (var fileStream = new FileStream(Application.persistentDataPath + "/decrement.txt", System.IO.FileMode.Create, System.IO.FileAccess.Write, System.IO.FileShare.None))
{
using (var writer = new StreamWriter(fileStream, System.Text.Encoding.UTF8))
{
writer.Write(output.ToString());
}
}
// out
GameObject.Find("OutputText").GetComponent<Text>().text = "処理完了" + output.ToString();
}
}
この程度の短いサンプルプログラムでありながら、すでに保守性の低さがチリチリと異臭を漂わせている。
やはり共通化は行うべきである。また、共通化を容易にするために表示とロジックを分離すべきだ。
2. LogicとViewの分離
というわけでViewからLogicを抜き出して共通化を進めてみた。
using System;
using UnityEngine;
using UnityEngine.UI;
public class View : MonoBehaviour
{
private string Input()
{
var input = GameObject.Find("InputForm").GetComponent<InputField>().text;
return input;
}
private void Output(string message)
{
GameObject.Find("OutputText").GetComponent<Text>().text = message;
}
public void OnClickPow()
{
// in
var input = Input();
// check
var logic = new Logic();
if (! logic.Check(input))
{
Output("整数のみ入力可");
return;
}
// logic
var output = logic.Pow(Int32.Parse(input));
// out
Output("処理結果 " + output.ToString());
}
public void OnClickIncrement()
{
// in
var input = Input();
// check
var logic = new Logic();
if (!logic.Check(input))
{
Output("整数のみ入力可");
return;
}
// logic
var output = logic.Increment(Int32.Parse(input));
// out
Output("処理結果 " + output.ToString());
}
public void OnClickDecrement()
{
// in
var input = Input();
// check
var logic = new Logic();
if (!logic.Check(input))
{
Output("整数のみ入力可");
return;
}
// logic
var output = logic.Decrement(Int32.Parse(input));
// out
Output("処理結果 " + output.ToString());
}
}
using UnityEngine;
public class Config
{
public static readonly string PersistentDataPath = Application.persistentDataPath;
}
using System;
using System.IO;
public class Logic
{
public bool Check(string input)
{
int number;
bool canParse = Int32.TryParse(input, out number);
return canParse;
}
public double Pow(int number)
{
var result = Math.Pow(number, 2);
Save(Config.PersistentDataPath + "/pow.txt", result.ToString());
return result;
}
public int Increment(int number)
{
var result = number + 1;
Save(Config.PersistentDataPath + "/increment.txt", result.ToString());
return result;
}
public int Decrement(int number)
{
var result = number - 1;
Save(Config.PersistentDataPath + "/decrement.txt", result.ToString());
return result;
}
private void Save(string filePath, string contents)
{
using (var fileStream = new FileStream(filePath, System.IO.FileMode.OpenOrCreate, System.IO.FileAccess.Write, System.IO.FileShare.None))
{
using (var writer = new StreamWriter(fileStream))
{
writer.Write(contents);
}
}
}
}
分離するついでにロジック内の共通化できる箇所(ファイル保存)を共通化。また、UnityEngine.Application.persistentDataPathを直接ロジック内で利用しているとUnity依存が残るのでこれもConfigクラスとして分離した。それだけなのだが、だいぶマシに見える。
とはいえ改善できそうな点もまだまだ目に付く。
- Viewに似たような処理が多い。
- Logic内のメソッドが単一責任の原則違反。
- ロジッククラス内でデータ層の操作とビジネス層の操作とが密接に繋がっていてテスタビリティに欠けるし変更にも弱そう。
というわけで、
- View側になんらかの工夫があったほうがよさそうだ。
- ロジックももう少しリファクタが必要だろう。数字のインクリメントを行うメソッドにファイル保存がくっついていたりしてまだまだダサい。
- データ層部分はレイヤを分けたいところだ。
といったとっかかりを元に、さらにどのような構成にすべきなのかを探っていきたい。
長くなったので別ページで。