extra: design pattern - dsuz/csharp GitHub Wiki
今回のテーマ
- いくつかのデザインパターンを比較・検討する
準備
- UniRx が入っていない場合は、アセットストアからインポートするか、もしくは OpenUPM から Package Manager を通してインポートする
- CSharp4-5.unitypackage をプロジェクトにインポートする
サンプルの説明
/4-5 Design Patterns/ の中にはフォルダごとにサンプルシーンがあり、それらは以下の機能を持っている。
- AD またはカーソルキーの左右でプレイヤーを動かせる
- スペースキーでジャンプする
- ステージ内の黄色いコインを取ると、画面左上の Coin カウンターが増える
- コインを3つ取ると、画面に CLEAR!! と表示される
シーンは3つあり、上記の機能はまったく同じであるが作り方が異なる。
これらの作り方の違いを比較し、メリット・デメリットを議論する。
1. リファレンス パターン
「リファレンス パターン」という言葉は特にないが、Unity で最初に学ぶような、特に工夫していない普通のパターンである。これは以下のような構成になっている。

- Coin -> GameManager の参照は、Start() 内で参照を獲得し、保持している
- GameManager -> Text の参照は、SerializeField により設定している
2. シングルトン + イベント パターン
このパターンでは GameManager をシングルトン パターンで作り、コイン獲得時・ゲームクリア時にイベントを発行している。

- Coin -> GameManager の参照は、シングルトンパターンであるためその場でインスタンスを呼び出している。
- GameManager ではコイン獲得時とクリア時のイベントをそれぞれ定義している。コイン獲得時のイベントでは「現在獲得したコインの数」という情報がイベントの引数として渡される。
2つのパターンを比較する
前述の2つのパターンでは以下のような特徴がある。
- シングルトンパターンを使っていない場合は、GameManager が見つからない場合がある。使っている場合は、見つからなかった場合インスタンスが生成されて呼び出される。
このことから複数人開発時に以下のようなケースが想定できる。
- 前者では開発中に GameManager がない状態で動かした場合、見つからずに NRE で処理が期待通りに動かない懸念がある。ただし、これは null チェックを適切に行うことで軽減できる。
- 後者では開発中に GameManager がない状態で動かした場合、GameManager の初期設定によっては期待通りに動作しない可能性がある。
- どちらのケースでも最低限必要なオブジェクト・コンポーネントをセットアップしたテンプレートシーンを作っておくことで、新規シーンの開発が円滑に進むことが想定できる。
参照を使ったパターンと、イベントを使ったパターンとでは以下のようなケースが想定できる。
- 前者では分業時に密なコミュニケーションが必要である。例えば表示を凝りたくなったために新しいコンポーネントを作った場合は、GameManager を修正して参照先を変えるなど、GameManager の修正が延々と発生する可能性がある。
- 後者では GameManager 側でイベント発行タイミングとデリゲートを決めておけば、CoinCount, ClearText を開発する側はそれを購読すればよい。やり取りするデータやタイミングに変更がなければ、購読者側が仕様を変更したい場合でも GameManager 側は変更する必要がない。
- ただし、イベント発行側・購読側共にプログラミングの負担は増える。また、イベントやデリゲートに対する理解が双方に必要である。特に GameManager 側では、デリゲートの発行タイミング・引数の意味をドキュメント化(ドキュメントコメントを記述)しておく必要があるだろう。なぜならコードだけ見てもデリゲートの用途や引数の意味はわからない懸念がある。
- 前者ではコイン獲得時の画面表示更新処理において GameManager は Text コンポーネントに依存している。後者は GameManager は CoinCount コンポーネントに依存していない。このように依存しないように作ることで、各機能を開発する際の他への影響をなくすことができる。
3. Observer パターン
3つめは Observer パターンを使って以下のような構成で作っている。

- Observer パターンで作るために UniRx を使っている。C# の機能のみで Observer パターンを作ろうとするとある程度コードを書く必要があるが、UniRx を使うことで大幅に記述量を減らすことができる。
- ClearText を表示する条件は「コインを3つ獲得した時」であるが、このケースではその条件を ClearText 側に記述している。
さらに比較する
UniRx を使った Observer パターンでは以下のような特徴がある。
- GameManager を作る側は監視対象をプロパティとして公開しておくだけでよく、負担がかなり減る。
- 監視側を作る者の方が UniRx やデリゲートの知識を必要とする。
- ClearText のように、監視側で動作条件(コインを3つ以上獲得した時)を設定することができる。
分担作業をする場合には以下のようなケースが想定される。
- GameManager のようなシステムを作る者は作業が多くなりがちであるが、その時の負担をかなり軽減できる。
- ただし、監視側を作る者がある程度の技能を持っている必要がある。
- 全体的な記述量を減らしつつ、依存関係を排することができる。
まとめ
今回は3つのパターンを比較したが、開発の際は状況に応じて適切な手法を選択しなければならない。例えば以下のようなファクターによって適切な手法は変わる。
- 期間
- 作るものの内容・規模
- 作るものの目的
- メンバーの人数
- メンバーの技能
また、それぞれのパターンを使うには習熟が必要である。各パターンについて十分に練習しておくこと。短期開発(ゲームジャム等)では練習していないパターンは使わないこと。
パターンを選択する際、短期間のゲームジャムなどの場合は、各メンバーの技能を素早く把握することが求められる。作るものの目的によっては、パターンを混在させるという選択もありうる。例えばゲームジャムの場合はメンバーの技能を考慮してパターンを混在させることもあるでしょう。
就職用の作品(ポートフォリオ)として会社に提出する場合は、「自分は会社に所属してチームで開発することができる」とアピールするためにイベントや Observer パターンを使ったものを提出するのもよいでしょう。その際は実際に複数人で開発しているものを送るのもよいでしょうし、前述の図のように構成を示した図、どのクラスでどんなパターンを使っているのかを説明する資料があるとよいでしょう。(プロジェクトをただ渡してそこから見つけてもらうのを期待するのは不親切・期待しすぎだと思います)
参考資料
UniRx の書き方そのものはさほど難しくないと思いますが、以下の資料のように「どんな時に使うか」という実例を学ぶアプローチもあった方がよいと思います。