CA.unity #1 2021/02/19 https://meetup.unity3d.jp/jp/events/1271
Unityにおける設計パターン2021/2/19とりすーぷ
View Slide
自己紹介•とりすーぷ• @toRisouP• VR系の開発してる• Microsoft MVP 2018~• Developer Technologies• 最近はVRChatしてますillustrations by kota(@kt_kkz)さん
本を出しました•「 UniRx/UniTask完全理解より高度なUnity C#プログラミング」
今回の話•設計・アーキテクチャとは•Unityにおける設計のレベル•設計レベルをあげるためには•クリーンアーキテクチャとは•Unityでクリーンアーキテクチャ
発表はショート版です•調子に乗って100枚超のスライド作っちゃった•10分に絶対に収まらないのでかなり端折ったショート版で発表します!•公開する資料はフル版です
先に言っておくと•この本読んでください
設計・アーキテクチャとは
設計とアーキテクチャ•設計• クラス設計、コンポーネントの構成、全体の構成• 「何をどうやって作っていくか?」という指針
設計とアーキテクチャ•設計• クラス設計、コンポーネントの構成、全体の構成• 「何をどうやって作っていくか?」という指針俯瞰して見た設計をアーキテクチャ と呼ぶ
例:家の建築•設計• 壁の位置、形状、配管、配線…•アーキテクチャ• 家全体の形、間取り、部屋のレイアウト…
設計とアーキテクチャ•設計とアーキテクチャは地続きである• 詳細な設計の連続がアーキテクチャを構築する
「設計・アーキテクチャ」の目的•「システムの開発・保守・運用にかかるコストを最小限にする」ことが目的
コストを最小限にする•クソコードに振り回される時間を減らす•後の仕様変更にも柔軟(ソフト)に対応する
よくある誤解:その1「設計を捨てて、開発メンバーが思い思いのやり方で開発した方が効率いいんだけど」
解•「設計を捨てる」で得られた開発速度は、後に訪れる莫大な保守コストで帳消しになる• 超短期的には速度が出るが、後に必ず失速する• 長期的に見るなら「ちゃんと設計する」以外に開発コストを下げる方法はない
ダメなコードの例•スパゲッティコード• オブジェクトとオブジェクトがどう関係するのかがわからない状態• 影響範囲が謎でどこを触っていいかわからない状態• ちょっとした機能変更にもすごく時間がかかる状態•外部モジュールと一蓮托生した状態• 「生殺与奪の権を他人に握らせるな」状態• 外部モジュール(ライブラリやフレームワーク)の機能に強く依存しすぎた結果、外部モジュールの仕様変更に振り回されてしまうような状態
コードは腐る•少しの油断でコードはすぐにダメになる• 割れ窓理論• どんな状況においても「設計を守る」を強く意識して開発に臨む必要がある
よくある誤解:その2•「巷にある○○アーキテクチャっていうのを使えば万事OKなんでしょ?」
解•NOです• そもそもアーキテクチャは自分で考えてDIYするものであり、万能なテンプレートがあるわけではない• プロジェクトに応じて柔軟な設計が求められる
よくある誤解:その3•「設計とかアーキテクチャって、要件がガチガチに固まっている、ブレがないプロジェクトでしかまともに使えないでしょ?」
解•認識が逆です• 「変化に柔軟に対応できるようにアーキテクチャを作っていく」が正解• 機能の変更・追加・破棄があるのはソフトウェアの宿命• 変化を受け入れられるアーキテクチャを作る、が理想(現実はメテオフォールなどの根本的なちゃぶ台返しもあり上手くいかないこともあるが、それは組織体制の問題であって開発者だけで改善できる話ではない)
言い方を変えると•「変更にかかるコストを最低限にする」を達成していればそれでアーキテクチャとして正解• 巷にある「○○アーキテクチャ」から外れてたり、アーキテクチャに汚い部分があってもそれで上手く開発が回っているならそれでOK(設計しなくていい、と言っているわけではない!)
よくある誤解:その4•「そもそもUnityって特殊なフレームワーク(FW)だと思うんだけど、設計ってやって意味あるの?」
解•あります。• FWに振り回されるのは別にUnityに限った話ではない• Unityだけが特別なわけではない• 「(様々な)FWとの上手な付き合い方」はソフトウェア開発の一般的な問題• その対策として「クリーンアーキテクチャ」などの○○アーキテクチャが考案されてきた
設計・アーキテクチャ まとめ•設計・アーキテクチャを使う目的• 開発・保守・運用にかかるコストを最低限にすること• プロダクトの価値を高め続けられる状態を維持する•アーキテクチャを死守するのが開発者の使命• 安易に「設計を捨てる」という選択肢を取らない
Unityにおける設計のレベル
レベルわけ•Unityにおける「設計のレベル」を自分が勝手に決めてみました• 異論・反論は受け付けます(むしろ議論したい)
図の意味GameObject(with MonoBehaviour)C#Pure C#Package(Assembly Definition Files)
レベル0:設計が存在しない• 好き勝手作られた状態• GameObjectベタ置き• 神クラスがたくさん• 相互参照、循環参照、密結合でぐちゃぐちゃな状態
レベル1:制御フローが整理された状態• 制御フローが整理された状態• オブジェクトの役割が明確になった• 参照関係が一方向に• 機能ごとに関係性が整理されている• 密結合なのはそのまま
レベル2:制御フローと依存関係の分離• 疎結合化が進んだ状態• SOLID原則が意識されている状態• 依存関係逆転を考えてインタフェースが要所要所に利用されるC# C#C#
レベル 3:モジュール化が意識される• 機能ごとにモジュール化される• モジュールの安定性が考慮される• 機能の再利用性が考慮される• asmdefが切られたりする(コンポーネント、という呼び方もあるがUnityのComponentと被るのでここではモジュールと呼びます)C# C#C#
レベル 4:ピュアなC#を活用する• ピュアなC#がメインロジック化• MonoBehaviourを使わない実装が増えるC# C#C# C#C#←Unity依存はここだけ
レベル 5:アーキテクチャが意識される• 全体のアーキテクチャを考慮して一貫した作りになる• 詳細設計のみではなく、全体を俯瞰してみた設計(アーキテクチャ)が強く意識される状態• レイヤの概念が入ってくるなどC# C#C#C#↓Unity依存はここだけC# C# C#C#
レベル分け5:アーキテクチャが意識される4:ピュアなC#が活用される3:モジュール化が意識される2:制御フローと依存関係が分離する1:フローが整理される0 : 設計なし
レベル分け5:アーキテクチャが意識される4:ピュアなC#が活用される3:モジュール化が意識される2:制御フローと依存関係が分離する1:フローが整理される0 : 設計なし ハイリスクローリターンどんな状況でも最低限達成したいできてればよりGoodコストは高いがメリットも大きい
設計レベルをあげるメリット•長期開発における機能追加・保守・運用のコストが下がる• 機能追加や仕様変更に柔軟に対応できるようになる• 大人数で開発しても破綻しにくい体制が作れる
設計レベルをあげるデメリット•開発者に対する負担も上がる• 開発規模に見合わない設計レベルは逆に手間を増やす• 開発者に求められる設計スキルも高くなる•動作パフォーマンスが落ちる場合もある• 「DIが遅い」「GCアロケートが増える」• チューニングのためベタ書きコードがどうしても必要になる
トレードオフ⇔• 理想を語るなら「長期的な運用のしやすさ」に振りたいが…• 現実は様々な理由で妥協しなくてはいけないことが多い• 場面によってはレベル1~4の範囲で収まってても全然よい短期的な開発のしやすさ極限を求めるチューニング長期的な運用のしやすさ
大事なこと•チームがやりやすいレベルで止めてもよい• 難しいと感じたらレベルを下げるのもあり• ただし最低でも「レベル1」は維持したい•設計レベルをハイブリッドにしても良い• チューニングしたいところはUnityと密にする• Unityと関係ない部分はできるだけピュアC#で書く
ハイブリッドなやり方C# C#C#C# C# C# C#C#C#C#レベル2で構成された領域レベル5で構成された領域依存
設計レベル まとめ•Unityにおける設計をレベルわけしてみた• 勝手に決めたレベルなので異論反論は受け付ける•どのレベルで開発するかバランスをみるの大事• 理想としてはレベル5を目指していきたいが、現実はそこまでいけることは稀• どこかでバランスを取る必要がある
設計レベルをあげるためには
設計レベルの壁を超えるためには?5:アーキテクチャが意識される4:ピュアなC#が活用される3:モジュール化が意識される2:制御フローと依存関係が分離する1:フローが整理される0 : 設計なしここの壁を超えるためには?
レベル0→レベル1•制御フローの整理を意識しよう• 制御フローをキレイにすることが何よりも大事• フローさえ整っていれば最低限スパゲッティコードは回避ができる
制御フロー•「誰が」「誰に」命令をするのかの流れ• オブジェクトの責務、メッセージの流れぐちゃぐちゃなフローの例 整理されたフローの例
制御フローの整理1. オブジェクトのもつ役割をハッキリさせる2. オブジェクトごとの主従関係をハッキリさせる3. 相互参照、循環参照を避けてデータの流れをわかりやすくするこれで レベル0 → レベル1 に上がる
レベル2以上を目指すなら•制御フローに実装が振り回されない対策が必要になる• オブジェクトの制御フローをそのままコードに落とし込んでいるだけではダメ(密結合化してしまい柔軟性がなくなる)
学ぶべきもの•「設計原則」を学ぶべし• 設計原則は「制御フローを変えずにどうキレイに実装するか?」というやり方をまとめたもの
設計原則•SOLID原則• SRP、OCP、LSP、ISP、DIP•コンポーネントの凝縮性の原則• REP、CCP、CRP•コンポーネントの結合性の原則• ADP、SDP、SAP
設計原則•SOLID原則• SRP、OCP、LSP、ISP、DIP•コンポーネントの凝縮性の原則• REP、CCP、CRP•コンポーネントの結合性の原則• ADP、SDP、SAP←制御フローと実装の分離に必須(レベル2に必須)
設計原則•SOLID原則• SRP、OCP、LSP、ISP、DIP•コンポーネントの凝縮性の原則• REP、CCP、CRP•コンポーネントの結合性の原則• ADP、SDP、SAP←より大局的な視点での設計の話(レベル3以上に必須)←より大局的な視点での設計の話(レベル3以上に必須)
詳しくは•この本読んでください
必要なこと5:アーキテクチャが意識される4:ピュアなC#が活用される3:モジュール化が意識される2:制御フローと依存関係が分離する1:フローが整理される0 : 設計なし 制御フローを意識しようSOLID原則を学ぼうコンポーネントの凝縮性・結合性の原則を学ぼう
一番大事なテクニック•「依存関係の逆転」• 依存関係逆転の原則(DIP)を行うのに必須• これができるだけでもかなり違うので絶対覚えたい
依存関係の逆転•制御フローはそのままに、モジュールの依存関係を逆転させるテクニック• インタフェースを活用するワザ• これができると疎結合化が進む
uGUI.TextUnityEngine.UI例:MVPパターン•Model – View – Presenterパターン• PresenterがModelとViewの連結を行うデザインパターン• (Unityの場合は View = UnityEngine.UI)PresenterModel
uGUI.TextUnityEngine.UI制御フローと依存関係が一致• PresenterがModelとViewの2つを参照するPresenterModel依存する 依存する
コード例
コード例PresenterがViewとModelを参照している
uGUI.TextUnityEngine.UI依存関係ModelPresenterパッケージPresenter.cs依存する依存する
依存関係を逆転させたい•PresenterがViewを操作するのは変わらないが、依存関係は逆にしたいPresenterModel制御フロー 制御フロー依存する 依存するuGUI.TextUnityEngine.UI
やり方1. Presentersパッケージ内にインタフェースを定義する
依存関係ModelPresenterパッケージPresenter.cs依存するITextPrinter.csuGUI.TextUnityEngine.UI
やり方2. Viewsパッケージを定義して、そこでITextPrinterを実装する
依存関係ModelPresenterパッケージPresenter.cs依存するITextPrinter.cs TextView.cs依存するViewsパッケージuGUI.TextUnityEngine.UI
やり方3. PresenterがITextPrinterを使う
依存関係の逆転完了ModelPresenterパッケージPresenter.cs依存するITextPrinter.cs TextView.csViewsパッケージuGUI.TextUnityEngine.UI依存する
依存関係の逆転完了ModelPresenterパッケージPresenter.cs依存するITextPrinter.cs TextView.csViewsパッケージuGUI.TextUnityEngine.UIPresenterとViewの依存が逆になった!依存する
制御フローは変わってない!ModelPresenterパッケージPresenter.cs依存するITextPrinter.cs TextView.csViewsパッケージuGUI.TextUnityEngine.UI更新R/W依存する
依存関係の逆転•どんなものでも依存関係は逆転できる• 制御フローはそのまま維持できる!•これを上手く活用してねっていうのが「依存関係逆転の原則(DIP)」
設計レベルをあげるためには まとめ•制御フローを意識しよう• オブジェクト間の関係をハッキリさせる•制御フローに振り回されない対策をしよう• 設計原則をしっかり抑える• 依存関係の逆転がかなり大事
(補足)• UnityのMVPパターンでここまでやるのは「冗長」• 最初のPresenter→Viewの参照の形で困ってないなら、無理にこの形に変形する必要はないです(あくまでサンプルとして挙げただけ)ModelPresenterパッケージPresenter.cs依存するITextPrinter.cs TextView.cs依存するViewsパッケージuGUI.TextUnityEngine.UI
クリーンアーキテクチャ(CA)
すっごい雑にいうと「君だけの最強のアーキテクチャをつくろう!」(そのための指標に“クリーンアーキテクチャ” を使うべし!)
アーキテクチャの考え•そのソフトウェアに最適なアーキテクチャは、自分たちで模索して育てるしかない• 万能な「アーキテクチャ」は存在しない!• 自分たちのニーズに応じて自分たちでアーキテクチャを考えて作るしかない
クリーンアーキテクチャとは様々なアーキテクチャに共通するルールをまとめたものこういうルールでアーキテクチャを作ると上手くいくことが多いよね、という指標を提示するもの
ざっくり言うと過去に提唱されてきた「オニオンアーキテクチャ」「ヘキサゴナルアーキテクチャ」とかって何かどれも同じこと言ってね?↓つまり基本的な考えは同じでやり方が異なるだけでは?↓共通部分を抜き出してみよう↓クリーンアーキテクチャ
クリーンアーキテクチャのルール「依存関係は上位レベルに向け一方通行にしろ」「制御フローと依存関係を分離しろ」
クリーンアーキテクチャでは•具体的に「こうしろ!」とは言ってない• レイヤ数やコンポーネント名は自由に決めていい• レイヤ/コンポーネント構成を後から変更しても良い• 小さいアーキテクチャから始めてだんだん大きくしても良い
この図は何なの?
「レイヤを4つ用意するんだな…?」「実装をこの図に上手く配置しろってことだな?」よくある誤解
この図でいいたいところ• 依存性は常に上位レベル(内側)にのみ向かってなくてはいけない!この小さい矢印が一番重要
• この図はただの「たとえ」• 「Webを例にするならこういう作り方もあるよね」くらいのノリ• レイヤ数やコンポーネントは自由に定義してよいこの図に振り回されてはいけない
ここの部分•「依存関係の逆転」を使って依存関係と制御フローを分離しろ
クリーンアーキテクチャ まとめ•クリーンアーキテクチャは「指標」• DIYするときの指針をまとめたもの、くらいの認識• 具体的なレイヤ定義やコンポーネント定義は一切してない•設計原則を学ぶのが一番大事• 設計原則を抑えておかないとうまくアーキテクチャは作れない• CA本でも第22章までCAの話が出てこない(それだけ前提知識が必要)
Unityでクリーンアーキテクチャ
Q. UnityでCAってできるの?•A.「アーキテクチャの基本的な考え方」に則ればUnityと共存できます
とある考え方•「Unity」という土台の上にクリーンアーキテクチャで世界を作ろうとしているUnityクリーンアーキテクチャ
とある考え方•「Unity」という土台の上にクリーンアーキテクチャで世界を作ろうとしている→ この考え方だと失敗するUnityクリーンアーキテクチャ
アーキテクチャの考え方•「フレームワークと結婚するな!」• アーキテクチャの上位レイヤにフレームワークを持ってくるな!• フレームワークの都合でアーキテクチャが振り回されるべきではない!
何と結婚するのかよく考えること•ライブラリ系もよく考えよう• UniRxとかそういうやつ• 上位レイヤで使ったほうが話が早いなら結婚してOK• ただし後から切り離しはできなくなるので覚悟が必要
Unityでクリーンアーキテクチャ•Unity側がアーキテクチャ側に依存するべき• Unityとは独立してCAでコンポーネントを構成し、それをUnity側が利用する形にするべきCAな世界Unity依存
実際どうするのか?「レベル間の違う領域のハイブリッド」をやればOK
2つの世界の両立C# C#C#C# C# C# C#C#C#C#UnityEngineにべったりな世界(レベル2)CAで構築された世界(レベル5)依存
使い分け•Unity非依存でロジックが構築できる部分→ クリーンアーキテクチャが適用可能•UnityEngineを使わざるを得ない部分→ MonoBehaviourベタ書きを多用で対処する
両極端に振れず、ハイブリッドにするという択があることを忘れてはいけない
UnityでCA まとめ•UnityでCAは利用可能• Unityが関係無い部分はCAで作ることができる• ただしUnityEngineの機能に強く依存する部分は相性悪い•UnityとCAの関係性には注意• UnityのアーキテクチャにCA側が振り回されてはいけない
(補足)CAFU•CAFU: Clean Architecture for Unity• そっくりそのまま利用はオススメしません(断言)• 黎明期に提案されたCAのテンプレート• コンポーネント定義がちょっとやりすぎで煩雑• 作者本人も「やりすぎた」と反省している• テンプレートそのまま使うのではなく、開発規模にあったサイズのアーキテクチャを自分で考えるべき
最後のまとめ
最後に•最初から完璧なアーキテクチャは存在しない• 「アーキテクチャ」もまた開発過程で成長していく• 「プロダクトの開発・保守・運用ににかかるコストを最低限にする」が目的であり、そこが達成できるなら多少キタナイ部分があってもOK
クリーンアーキテクチャになってなくても、設計原則に違反する部分があっても、Unityと密結合してる部分があっても、それがやりやすいならそれで良い(設計しなくていいと言ってるわけではない!「バランス感を持て!」という話)
参考資料• 世界一わかりやすいClean Architecture• https://www.nuits.jp/entry/easiest-clean-architecture-2019-09• Adaptive Code ~ C#実践開発手法 第2版• https://www.amazon.co.jp/dp/B07DJ2BL4Y/• Clean Architecture 達人に学ぶソフトウェアの構造と設計• https://asciidwango.jp/post/176293765750/clean-architecture