はじめに
今回の発表資料です!!
— とりすーぷ (@toRisouP) February 19, 2021
Unityにおける設計パターン https://t.co/PaWoANk42I #CA_Unity
先日、CA.Unity
という勉強会において「Unityにおける設計レベル」というものを定義してみました。
この内容を改めてブログ記事という形でまとめておきます。
Unityにおける設計レベル
概要
Unityにおいてはいろいろな開発スタイルがあり、作るプロダクトの性質や開発組織によってやり方がバラバラです。
同様にプログラムの設計の度合いも状況によってバラバラでした。
そのため設計の議論をしようと思ってもお互いに考えている設計の度合いが食い違ってしまい、うまくまとまらないパターンが多々ありました。また「今の設計に問題がある」とは薄々感じていながらもそれを改善する方法がわからないという人も多くいると思います。
今回「設計レベル」という度合いを定義してみたので、自分たちがいまどの設計レベルで開発をしているのか、改善するには何をしていけばいいのかの参考になればと思います。
異論・反論は大歓迎です。
前提
設計レベルは高ければ高いほどよいわけではないというのを前提においてください。
そもそも設計の目的は「プロダクトの開発・保守・運用のコストを最低限にする」です。
今の設計レベルでこの目的が達成できていると感じているのであればそれで問題はありません。
設計レベルの定義
設計レベル | 状態 |
---|---|
-1 | C#の文法がわかってない |
0 | 設計なし |
1 | 制御フローは整理されているが密結合な状態 |
2 | 制御フローと依存関係が分離した状態 |
3 | モジュール化の意識が現れた状態 |
4 | MonoBehaviourへの依存をやめた状態 |
5 | 「アーキテクチャ」が意識された状態 |
レベル-1 : C#の文法がわかってない
まず根本として「C#の文法がわかっていない」がレベル-1として存在すると考えます。
つまり「UnityのAPI」と「C#の言語機能」の区別がついていない状態です。
この状態では設計の話をするよりも、まずC#の学習をするのが先になります。
レベル-1を脱出するためには
- まずC#の言語機能を抑えよう
- MonoBehaviourを使わないクラス定義のやり方を覚える
- 今使っている機能がUnityAPIなのかどうかの区別をつける
レベル0 : 設計なし
レベル0は「設計なし」という状態です。
後先考えず、MonoBehaviour
を継承した上で手当たり次第に実装を行っている状態です。
「プロトタイピングレベルの超小規模・超短期間な開発」であればこのやり方でも上手くいくかもしれませんが、「ユーザリリースを前提とした開発」では推奨しません。
この状態で1週間も開発を続ければ見事なスパゲッティコードが誕生することになります。オブジェクトごとの責務が不明瞭となり、大量の責務を抱えたオブジェクトがお互いにがんじがらめになりつつかろうじで動作する、という状況になります。
レベル1を目指すには
- まず何を作ろうとしているのかを明確にしよう
- そもそも何をしたいのかが明確でないとキレイにコードは書けない
- 概念を洗い出し、過不足無くオブジェクトとマッピングしよう
- オブジェクトの責務をハッキリさせよう
- 1つのオブジェクトに機能を詰め込まないようにしよう
- オブジェクト同士の関係性をハッキリさせよう
- オブジェクトの主従関係を決めて依存関係を整理しよう
- 制御フローをキレイにしよう
- オブジェクト同士のつながりは一方通行になるように整理しよう
- 相互参照や循環参照でがんじがらめになるのを避けよう
レベル1 : 制御フローは整理されているが密結合な状態
レベル1は「制御フローが整理されている」という状態です。
オブジェクトの概念やそれぞれの関係性を明確化してコードに反映されている状態です。この時点でもMonoBehaviour
は継承したままです。
レベル1が維持できていれば多少の問題はあるものの全体が破綻するようなことは避けることができます。
(オブジェクトの関係がぐちゃぐちゃで大量のネストしたメソッドコールの果てに意味不明な処理がある、みたいな絶望的な状況だけは避けることができる)
ただしオブジェクト同士は密結合な状態のため、機能拡張や挙動の差し替えといった場面で難が出てきます。
レベル2を目指すには
- 「疎結合化する」ということを意識しよう
- インタフェースの使い方を覚える
- SOLID原則を覚えよう
- オブジェクトの作り方や、オブジェクト同士の関係をよりキレイに整えるための原則集
- 依存関係の逆転というワザがめちゃくちゃ便利なので絶対に覚えよう
レベル2 : 制御フローと依存関係が分離した状態
レベル2は疎結合化が進んだ状態です。
オブジェクト同士の関係性がより整理され、制御フローと依存関係が分離して定義された状態です。この時点でもMonoBehaviour
は継承したままです。
おそらくこのあたりが一番実用的なラインです。
MonoBehaviour
を継承していることでUnityの機能を最大限引き出しながら、コード上では柔軟性を出している状態です。
本来、疎結合化した状況においてはDIをどうするのかが問題となってきます。
ですが、Unityの場合はインタフェース指定でGetComponent
しておけばとりあえず対象のインスタンスが取得できてしまいます。
そのため基本的にMonoBehaviour
を使っている限りにおいてはGetComponent
しておけば解決するので、DIが問題になることはあまりありません。
なので「設計何もわからない」という人はこのレベル2を目指してみるのがよいでしょう。
レベル3を目指すには
-
モジュール化に意識を向ける
- 機能をパッケージにまとめていく
-
Assembly Definition Files
によるdll分割を視野にいれる -
internal
などのアクセスレベルも活用するとよい
-
コンポーネントの原則を覚えよう
- コンポーネントの凝縮性の原則
- コンポーネントの結合性の原則
レベル3 : モジュール化の意識が現れた状態
レベル3はモジュール化の意識が現れた状態です。
このレベルになると「大局的な視点でのコード設計」に意識が向いている状態になります。
- コードを機能単位でまとめてモジュール化して管理し始める
- モジュールの再利用性を考え始める
- モジュールの安定性を考えてモジュール間の依存関係を整理し始める
-
Assembly Definition Files
の検討を行い、適宜dll分割が行われる - コンポーネントの原則が意識された状態
- コンポーネントの凝縮性の原則
- コンポーネントの結合性の原則
詳細な設計ではなく、「モジュールとモジュールがどう関係しあってプロダクトを構築しているか」を見ている状態です。たとえば、「通信処理周りだけを別のパッケージに切り出して、複数箇所から呼べるようにする」など。
このレベルになってくると「大規模開発」を見据えた状態ともいえます。
それなりの人数で、開発期間も長期化しており、コード量も増えてきた。
そういった状況において大量に記述されたコードをどう整理してまとめあげていくか、となったときにこのレベル3の状況になります。
逆にいうと、小規模開発ではレベル3を達成する必要性は薄いともいえます。
そもそもモジュール化を意識するほど規模が大きくないのであればレベル2で留めておいても十分です。
レベル4を目指すには
-
モジュールからUnity依存を消していく
- MonoBehaviourを使わない実装を考える
-
より抽象的なモジュールに分離できないか考えていく
- インタフェースや構造体のみが定義された安定度の高いモジュールをまず考えるようにする
- それに肉付けする形で別モジュールを実装する
- そのときにUnityを使う/使わないの判断ができるようになる
レベル4 : MonoBehaviourへの依存をやめた状態
(もしかしたらレベル3とレベル4は反対かもしれない?)
レベル4はMonoBehaviour
への依存をやめた状態です。
ピュアなC#の比率が増え、MonoBehaviour
を継承したクラスの数のほうが少なくなってくる状態です。
-
MonoBehaviour
を使わずにロジックが記述されている箇所のほうが増えてくる -
GameObject
を使う必要がある部分でのみMonoBehaviour
が使われる
このレベルでは「Unity」と「Unityが必要ない部分」の切り分けがハッキリしてきます。
Unityを使わない部分で基本的に処理を実装して最後のViewとしてUnityを使う、といった実装が増えてきます。
そしてこのレベルになると「DIをどうするか」が深刻な問題となります。
GetComponent
によるゴリ押し解決ができないので、DIコンテナなど何らかの解決手段を用意する必要がでてきます。
レベル5を目指すには
- アーキテクチャの考え方を勉強する
- おすすめは「クリーンアーキテクチャ」
レベル5 : 「アーキテクチャ」が意識された状態
レベル5はアーキテクチャが意識された状態です。
つまり「プロダクト全体の構成を頭に入れ、どのモジュールとモジュールがどのような関係で成り立っているか」のルールを詳細に決定した状態です。
モジュールの切り方にルールが生まれ、「上位レイヤ」「下位レイヤ」といったレイヤ分けの意識がされた状態です。
ここまでくると完全に大規模開発を想定した状態であり、趣味プロのような小規模プロジェクトでここまでやるのは冗長すぎるかもしれません。
まとめ
設計レベルを上げることにはデメリットもあります。
設計レベルが高いほど「長期開発」においてはメリットが増えます。
ですが小規模・短期開発においては高すぎる設計レベルは逆に手間を増やすだけでメリットが享受できない場合もあります。また開発者に求められるスキルレベルも高くなるため、チームメンバーがついていけないということも起きうることがあります。
また設計レベルを上げすぎるとパフォーマンスチューニングが難しくなります。
「パフォーマンスチューニングのためにはUnityに密結合したコードを書かないといけない」といった状況は当然ながら発生します。そのため「設計レベルを落としたコードを書かざるを得ない」という状況は絶対に発生します。
大事なこと
プロダクト内でいろんな設計レベルが混在した状態になっていてよいです。
そもそもプロダクト内で統一した設計レベルは不可能です。
というのも、1つのプロダクトであっても要所要所で要求されるものが異なるからです。
- パフォーマンスチューニングしたい部分
- 外部ライブラリに処理を投げたい部分
- 今後の機能拡張を前提として柔軟に作っておきたい部分
- 他のプロジェクトと共有した実装が必要となる部分
- 課金が絡む部分なので絶対にミスなく作りたい部分
- 経験の浅いエンジニアのみが担当して開発することになった部分
これらをすべて1つの「設計レベル」のコードで解消することはできません。
あるものは「局所最適解」です。
場面にあわせて最適な設計レベルを適用し、それの組み合わせで1つのプロダクトを構成するのが妥当な方法でしょう。
最後に
今回、「設計レベル」というものを定義してみました。
設計について体系的にどう勉強していくのかを考えた結果、「マイルストーン」を設定するのが適切ではないかと考え、今回このようなレベル分けをしてみました。
「設計」を学ぼうとしたときに指標として次に何を勉強し、次にどういうコードの書き方を目指せばよいのか、の参考になればと思います。
なお、何度も言いますが設計レベルが高いプロダクトほどよいというわけではありません。
プロダクトの規模感や作る内容に応じて最適な設計レベルというものは変わります。
また1つのプロダクト内で複数の設計レベルが混在していても問題ありません。
いくらコードを疎結合化してDIしようが、発生するGCアロケーションは消すことができません。パフォーマンスチューニングのためには設計レベルを落とし、ベタ書きコードを書かざるを得ない場合も当然発生します。
そのため「異なる設計レベルのコードが混在していても破綻しないようにつくる」というのも重要になってきます(それって結局レベル5では…?という気持ちがしないでもないが…)
おまけ:クリーンアーキテクチャ
「クリーンアーキテクチャ」という単語を耳にするかもしれません。
これがどういうアーキテクチャなのか調べてもよくわからず、結局よくわからなくなって挫折してしまうという人もいるかもしれません。
というのも、クリーンアーキテクチャは具体的な手法ではなく「考え方」を述べたものだからです。
なのでニュアンスとしては「クリーンアーキテクチャ指向」と呼んだほうが適切かもしれません。
そしてこの「クリーンアーキテクチャ指向」を理解するためには設計原則(SOLID原則、コンポーネントの凝縮性/結合性の原則)をしっかり抑えておく必要があります。
というのも、「これら設計原則によって導き出される結論を改めてまとめたものがクリーンアーキテクチャ」だからです。
いきなりクリーンアーキテクチャから勉強するのではなく、基礎となる設計原則をしっかり抑えておくことがなによりも重要であると考えます。
メモ
- あとレベルごとにコード例を追記するかも
- そもそも1次元で設計の複雑さを定義すること自体無理があった可能性はある
- レベル3とレベル4は逆かもしれない?
- レベル4とレベル5の境界が薄い気がする