Closed26

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(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に依存しているか議論するのはナンセンスだ」ということが言われており撃沈している

https://qiita.com/toRisouP/items/5365936fc14c7e7eabf9
https://shikaku-sh.hatenablog.com/entry/unity-ui-design-using-the-mvp-pattern

にー兄さんにー兄さん

でもかなりなるほどとなった
自分はそもそもMonoBehaviourを実装している→いろいろ辛いじゃん!という思いだったのだが
それよりもGUI周りの整備のためにそこまでするのは冗長であるという判断になるっぽい
MonoBehaviourに依存していようが、GUI周りがすっきりかけているのであればMVPの目的は達成されているからである
そうか、自分の興味はナンセンスだったんだな

にー兄さんにー兄さん

とりあえずネットで感じた肌感は知見ではあるけどそれはそれとして

PをMonoBehaviourではないクラスにしてみたらどうなるのかを考えてみたこと自体は悪いことではないはず、はず、、、
やってみてといわれたのだからやっただけで
だから真正面から新提案みたいな体で記事を公開しなければ燃えないはず……
そう、それこそこれは現場では生きない知見であることを公言する

にー兄さんにー兄さん

いやぁここまでの現状を知ったうえで質問すればよかったなぁ......
(もともとこれのキッカケはGCCでCA社員さんに「PresenterもMonoBehaviour非依存にできるのでは?」と質問したことである)

にー兄さんにー兄さん

実装までできたということで振り返っていく
振り返りの内容は以下

  1. 何がしたいのか・何をやったのか
  2. なぜやろうと思ったのか
  3. どのように実現したのか
  4. 結果どう思ったか・考えたか
にー兄さんにー兄さん

何がしたいのか、何をやったのか

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される

にー兄さんにー兄さん

結果どうだったのか

実際にやってみて、そしてこれを踏まえてネットの記事などを見て見たところ
当初の目的は達成したものの、実践に向いているかは微妙だったので、
どう微妙だったのかを振り返りたい

微妙だった点として、冗長であるという部分がある
とりすーぷさんのスライドのまさにここの部分
https://speakerdeck.com/torisoup/unityniokerushe-ji-patan?slide=73

そしてまたとりすーぷさんの言葉を拝借すると、

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コンテナが結構キレイにできるので、魅力だった

このスクラップは2023/02/19にクローズされました