VContainerを使ったViewコンポーネント以外がPure-C#なMVPアーキテクチャ考
UnityでMVPアーキテクチャを実現するときに、PとVがMonoBehaviourになるのってどうなの?って思ったことがあり
実際にMVP全部Pure C#で書いたらどうなるのかやってみたくなった
例えば
- 単体テストがしにくい(もしくはできない)
- コンストラクタによる依存関係が明確にならない
- 末端のViewコンポーネントが処理の起点になる
MonoBehaviourにはなくなったほうが幸せになれるものがたくさんあります。
それらをVContainerとPure C#の力ですべて消し去ります。
単体テストがしにくいのは単純にnewできないからだと思う
コンストラクタによる依存関係が明確にならないのは、上のと同じくコンストラクタが使えないので
外部に何が依存しているのかコンストラクタ制約で明確化できないところがある
依存関係の解決が、SerializeFieldなのかGetComponentなのかAddComponentなのか、いろいろ選択肢があってしまう
末端のView~~はVContainerから言葉を拝借している
MonoBehaviourは実質のエントリポイントみたいなところがあるんだけど、
本来はただのViewコンポーネントであるべき存在が処理の起点になり、クラス同士の制御を担当することになる
ちょっといやだ
簡単なシーンをVContainer+Pure-MVPで作ってみる
- ボタンを押すとダメージが入る
- ダメージはダメージバーと数字のテキストで書かれている
これはUnityが出しているゲームプログラミングパターンのMVPのサンプルを参考にしている
と思ったら普通にVContainerにそういうことが書かれていた
つまり自分の勘はお門違いではなかったということか
実際、今回には最悪VContainer(DIコンテナ)は必要がない
手動DIを行うための単一のMonoBehaviourがあればよいので
Unityのゲームプログラミングパターンのサンプルでは、HealthというモデルとHealthPresenterというプレゼンターがあったViewはuGUIのコンポーネントを直接変更していた
MVP全部がMonoBehaviourで、HealthPresenterのSerializeFieldで参照しているという感じ
んー実際直感的でよいのか?自分はあまりそう思わない
クラス図を作って全体のイメージを膨らませる
あとで編集できるようにPlantUMLも載せておく
UML
@startuml
interface IHealthModel{
+ int CurrentHealth
+ Action<void> OnChangeHelth
+ void Decrement(int amount)
+ void Increament(int amount)
+ void Reset()
}
interface IHealthPresenter{
+ void Decrement(int amount)
+ void Increament(int amount)
+ void Reset()
}
interface IHealthView{
+ void SetHealthValue(int health)
}
class HealthModel{
+ int CurrentHealth
+ Action<void> OnChangeHelth
+ void Decrement(int amount)
+ void Increament(int amount)
+ void Reset()
+ HealthModel()
- int _currentHealth
}
HealthModel-up-|>IHealthModel
class HealthView{
+ HealthView(Slider healthSlider, Text healthLabel)
+ void SetHealthValue(int health)
- Slider _healthSlider
- Text _healthLabel
}
HealthView-up-|>IHealthView
class HeadlessHealthView{
+ void SetHealthValue(int health)s
}
HeadlessHealthView-up-|>IHealthView
class HealthPresenter{
+ HealthPresenter(IHealthModel model, IHealthView view)
- IHealthModel _model
- IHealthView _view
+ void Decrement(int amount)
+ void Increament(int amount)
+ void Reset()
+ void Start()
}
HealthPresenter-up-|>IHealthPresenter
HealthPresenter-up-|>IStartable
HealthPresenter::_view--->IHealthView
HealthPresenter::_model--->IHealthModel
class HealthLifetimeScope{
+ void Configure(IContainerBuilder builder)
- [SerializeField] Slider _healthSlider
- [SerializeField] Text _healthLabel
}
HealthLifetimeScope-up-|>LifetimeScope
@enduml
HealthViewは今のことろPure C#で記述する予定だが、MonoBehaviourでもよさそうな雰囲気がある
MonoBehaviourとしてRegisterしたほうが自然だし、テスタビリティに関してもHeadlessHealthViewはMonoBehaviourにはならないので問題はない
なのでこだわる必要はないかなぁと思っている
あと、SliderやTextなどをregisterするとなると、ちょっとどうしようか迷うのである
SerializeFieldなのかRegisterInHierarchyなのか
あと、Presenterの階層構造とLifetimeScopeの階層構造をどうしようか迷っている
Presenterの階層構造は、一番大きなGamePresenterがあって、それが小さなPresenterを参照するというような階層構造になるのだが
LifetimeScopeは逆で、子のLifetimeScopeのコンテナにregisterされていなかったら親のコンテナを探しに行く構造になっている
どちらかを逆にしないといけない
色々考えてみている
今回はUnityのゲームプログライングパターンを参考にしたサンプルを作ろうと思っていて、
- スライダーとテキストでライフが表示されている
- ボタンを押したらライフが減ったり回復したりする
というものにしようとしていて、それをうまくMVPに落とし込もうと思ってクラス図を更新している
そこでここまで書いて一旦他の人の実装が気になったので「Unity MVP」で検索して色々見ていたのでけど
かなりその、なんというか「どれがMonoBehaviourに依存しているか議論するのはナンセンスだ」ということが言われており撃沈している
でもかなりなるほどとなった
自分はそもそもMonoBehaviourを実装している→いろいろ辛いじゃん!という思いだったのだが
それよりもGUI周りの整備のためにそこまでするのは冗長であるという判断になるっぽい
MonoBehaviourに依存していようが、GUI周りがすっきりかけているのであればMVPの目的は達成されているからである
そうか、自分の興味はナンセンスだったんだな
とりあえずネットで感じた肌感は知見ではあるけどそれはそれとして
PをMonoBehaviourではないクラスにしてみたらどうなるのかを考えてみたこと自体は悪いことではないはず、はず、、、
やってみてといわれたのだからやっただけで
だから真正面から新提案みたいな体で記事を公開しなければ燃えないはず……
そう、それこそこれは現場では生きない知見であることを公言する
自分と同じような考えをしている人を発見した
本当に同じこと考えている気がするな
いやぁここまでの現状を知ったうえで質問すればよかったなぁ......
(もともとこれのキッカケはGCCでCA社員さんに「PresenterもMonoBehaviour非依存にできるのでは?」と質問したことである)
最終的なクラス構成は次の通りになった
実装したサンプルがこちら
実装までできたということで振り返っていく
振り返りの内容は以下
- 何がしたいのか・何をやったのか
- なぜやろうと思ったのか
- どのように実現したのか
- 結果どう思ったか・考えたか
何がしたいのか、何をやったのか
UnityにおけるMV(R)Pアーキテクチャの実装で、Presenter-ModelをPure C#クラスで実装し、ViewコンポーネントのみMonoBehaviourに依存した状態が作れないかを試してみたい
そしてそれが有用かどうかを見極めたい
なぜやろうと思ったのか
MonoBehaviourはとても手軽で便利だが、何も考えずに多用すると問題が発生する
それは以下の特徴によるものである
- コンストラクタが使えない
- コンポーネントがエントリポイントになる
コンストラクタが使えない
MonoBehaviourはUnityコンテキストに強く結びついた仕組みであり、
普通のC#クラスのようにコンストラクタが使えない
コンストラクタが使えないとはつまりオブジェクトの生成をUnityコンテキスト上ではないと行えないため、単体テストのような場で扱えない
また、コンストラクタは依存関係の記述においてとても有効なので
コンストラクタが使えないことのデメリットが大きい
コンポーネントがエントリポイントになる
Unityにおいてロジックを実行するためにはシーンにMonoBehaviourを配置し、そこを起点とする必要がある
しかしコンポーネントとは通常末端のロジックになるため、そこが起点となって全体のフロー制御行うような構造になってしまう
これらの問題解決のために、自分は極力コアなロジックにはPure C#クラス、
ViewコンポーネントとしてのみMonoBehaviourを使うのが、最適であると考えてきた
MVPの実装例を見ていると、View-PresenterがMonoBehaviour、もしくはMVP全部MonoBehaviourである例が散見されており、MVをつなぐ薄い層であるはずのPresenterがMonoBehaviourな必要があるのか疑問だった
また、ちょうどGame Client Collegeの設計編でMVPを扱っており、そこでメンターさんにPresenterも非MonoBehaviourにできないかという提案をしたところ、やってみればいいのではないかという回答をいただいた
というモチベーション
どのように実現したか
今回実現したプロジェクトでは、MVPのうちViewコンポ―ントのみがMonoBehaviourクラスになっている。
Presenterはコンストラクタでモデル・ビューのインタフェースを受け取り、
イベントの橋渡しをしている
また今回はPresenterが階層構造になっており、一番上の階層のGamePresenterがエントリポイントになる
それぞれのクラスはMonoBehaviourかどうかにかかわらずインターフェースで分けられており、VContainerによってDIされる
結果どうだったのか
実際にやってみて、そしてこれを踏まえてネットの記事などを見て見たところ
当初の目的は達成したものの、実践に向いているかは微妙だったので、
どう微妙だったのかを振り返りたい
微妙だった点として、冗長であるという部分がある
とりすーぷさんのスライドのまさにここの部分
そしてまたとりすーぷさんの言葉を拝借すると、
MV(R)Pパターンは「MonoBehaviourの有無について語っていません」。
そもそもMV(R)Pパターンは「GUIをキレイに実装すること」を目的にした設計パターンです。そのため「GUIさえキレイに実装できるなら細かい部分は割とどうでもいい」です。
実際やってみてそうだと思った。
GUIの描画に関して、単一のコンポーネントで全部処理をするのはやりにくいけど、MVPを導入すると描画とロジックが分かれていいよね
ここまででいい気がしている。これでうまく運用できているのであれば問題はない
MVPが関心があるのはここまでであるということ
Viewコンポーネントというものについて考えていたのですが、
MVPでもViewって言葉があるから混乱するけど、MVPで構成しているGUI周りの処理はそれ全体がViewコンポーネントとして見れるのではないかと思いました
実際GUI周りの処理って言ってるし、プロジェクトのコアなロジックと切り離されていても違和感はなさそう
ただ、全体をMVPパターンで記述しているようなゲームも存在すると思うので、それは場合に寄るのかなぁ
おまけ、なぜDIコンテナを使おうと思ったのか
別に手動DIでもよかったよなぁと思いつつ、DIコンテナを使ってよかったと思っています
今回の試行ではViewコンポーネントとロジックの分離を目的にしていたのもあり、
なるべくViewコンポーネントに振り回されない設計を追い求めていた
そこでIoCを行うことで、ロジックの制御をViewコンポーネントに頼らないようにしたくて、
そのためにDI用のフレームワークに乗っかりたかったのかもしれない
けっこう自然とVContainerを使う方針になっていた
あとMonoBehaviourのDIをするのにDIコンテナが結構キレイにできるので、魅力だった