はじめに
最近ではClaudeというチャットAIが来ているらしいです。
なんとこいつ、Chatgpt-4を超えるらしく相当優秀らしい。
ClaudeをC#で利用できるようにClaudiaというライブラリが開発されたとのことで、実際に利用してみようと思います。
単純に使ってみるだけだと面白くないので、最近リリース版になったAppUIを使ってMVVM設計でアプリとしてUnityで開発してみましょう。
現状AppUIの解説記事が全くないのでなるべく詳細に記事にすることとします。
筆者は設計初心者です。
間違いや独自のとらえ方も多いためなるべく疑って読んでください。
記事がかなり長くなってしまったので4章に分けることとしました。
(追記)
Middleware編を追加したため全5章になります。
1章 設計説明編
2章 AppUI編
3章 MVVM実装編
4章 アプリ実装編
5章 Middleware編
できるもの
以下のようなアプリを作成できます。
UIはClaudiaのBlazorのサンプルを参考にしています。
動画版もあるのでよかったら見てみてください。
リポジトリもあがっています。
対象読者
- ある程度Unity/C#がわかるひと
- 設計を学びはじめのひと
各設計パターンの解説
内容も簡単なため、さっそく導入方法に移ってもよいのですが、本記事で扱うMVVMやReduxはUnityで扱わない単語であるため最初はこれらの解説から入っていこうと思います。
本記事で作成するチャットアプリは最終的に以下のような設計を目指すこととします。
MVVM
GUI構造をModel, View, ViewModelの3つに分けたパターンです。
それぞれ以下の役割を持ちます。
- Model アプリケーションのデータを持つ。ドメインロジックが含まれる
- View ユーザー操作を受け付け、ViewModelの状態変化にたいして画面を更新する
- ViewModel ViewとModelをつなぐ Viewに使うデータを保持する
図で表すと以下のようになります。
上の矢印は依存関係を表します。
下の矢印はデータの方向を表します。
http://yujiro15.net/YKSoftware/MVVM_Introduction.html
MVVMではViewとViewModelがデータバインディングされているため、Modelの変更がViewに自動的に反映されます。逆にViewからの変更もViewModelを介してModelに伝わります。このようにMVVMはView層とModel層を明確に分離することで、UI部分とロジック部分の疎結合を実現します。結果としてコードの可読性と保守性が向上します。
また、ViewはスクリプトではなくUXMLやHTMLに記述されることが多いようです。
MVPとの比較
これだけでは分かりづらいため、より親しみやすいUnityと比較して説明します。
MVPを知らないという方にもわかりやすいようにMVPの説明から始めます。
MVPはGUIを3つに分けたModel、View、Presenterからなり図で表すと以下のようになります。
https://support.touchgfx.com/ja/docs/development/ui-development/software-architecture/model-view-presenter-design-pattern
パット見た感じ ViewModel ⇔ Presenter となっているだけで全く違いがないように思えます。
明確に違う点として挙げるならば、PresenterがViewを参照しないことです。MVVMの用語でいうとViewがViewModelの値をバインドします。こうすることにより、ViewModelはViewを意識しなくてよくなるため、MVPの時よりもテストが容易になります。
それぞれのコードは以下のようになると思います。(あくまで一例です。)
MVP コード例
class Model: IModel {
Model(){}
}
class Presenter {
private IModel _model;
private IView _view;
// ModelとViewを持つ
Presenter(IModel model,IView view){
_model = model;
_view = view;
}
}
class View: IView {
View()
}
PresenterはModelとViewをもつ必要があるためテストの際に面倒です。
ロジックをPresenterでもつかModelで持つかにもよりますが、比較的Presenterが肥大化しがちです。
MVVM コード例
class Model: IModel {
Model(){}
}
class ViewModel: IViewModel {
private IModel _model;
ViewModel(IModel model){
_model = model;
}
}
// ViewはUXMLでもって、ViewModelの値をバインドする。
大体こんな感じになると思います。場合によりますが、ViewModelはバインドする用の値であったりICommandであったりを持つことになります。
この設計の場合、ViewModelはModelのみを意識すればよいためさらにシンプルなものになるでしょう。
Model、ViewModel、Viewのテストを比較的簡単に書くことができます。
MVVMのまとめ
さて、ここまでで簡単にですがMVVM設計パターンの説明をしました。ここまででもかなり保守性が上がると思いますが、もっと良くしたいポイントがあります。
例えば、
- ロジック部分が複雑になる
- ViewModelの肥大化
等々...
これらの問題を解決しようと登場したのがReduxというアーキテクチャです。
Redux
Reduxは、もともとは状態管理を効率的に行うための作られたJavascriptのライブラリです。Reactなどのフロントエンドフレームワークやライブラリと組み合わせて使用されることが多いです。
Reduxの主な特徴は以下の通りです:
- 単一責任の原則:アプリケーションの全状態を一つのオブジェクトツリー内に格納します。これにより、状態の予測が容易になり、デバッグやアプリケーションの開発がしやすくなります
- Stateは読み取り専用:状態を変更する唯一の方法は、Actionと呼ばれるオブジェクトを発行することです。これにより、Viewから状態を直接変更することができなくなります
- 変更はReducerで書かれる:アクションが状態ツリーにどのように影響を与えるかを指定するために、Reducerと呼ばれる関数を作成し使用します。これにより、アプリケーションのロジックが予測可能でテストしやすくなります
このようにReduxは、大規模なアプリケーションにおいて、データの流れを管理しやすくするために役立ちます。
大量の値を一元管理できる倉庫のように思うと理解しやすいかもしれません。
(もしくは設計が徹底されたシングルトン...?)
実際に値が変更されるときは以下の図のようになります。
UIはViewと置き換えてください。
- Viewの入力をもとにActionを作成
- ActionをStoreへDispatchする
- ActionをもとにReducerがStateを更新
- StateをもとにViewを更新
より詳細な説明をしていきます。
ユーザーがViewのDepositボタンを押したと仮定してデータの流れを追ってみましょう。
Actionが生成される
ここでいうActionはUnityやC#のActionではないことに注意してください。
Actionは次の二つから成り立ちます。
- どんなイベントが起きたかを判別する文字列
- イベントに関する追加情報 (値など)
今回の場合、Deposit $10
というボタンが押されたため
string type = "app/deposit";
float payload = 10f
というデータ構造をもつActionが生成されたということにしましょう。
ActionをStoreへDispatchする
生成されたActionをStoreに送信します。
Store
Storeはアプリケーションの状態(State)を一元管理する役割を持ちます。アプリケーション内のさまざまな状態を一つのオブジェクトツリー内に格納し、これを通して状態管理を行います。
前述したActionのtypeに"app"という文字列があるのは"app"というStateを作成したかったためです。
Actionのtypeには、「{state名}/{action名}」という形式で命名することを推奨します。これにより、どの状態に対するどのようなアクションかが明確になり、コードの可読性が向上します。
必ず {state名}/{action名} といった命名にしましょう。
Dispatch
Storeを更新する唯一の関数です。Actionを引数にとります。Storeにイベントが発生したことを知らせる役割を担っています。
コードでいうと以下のように表現できると思います。
Action action = 先述したもの;
Store store = new Store();
store.Dispatch(action);
---
// AppUIではActionを内部的に生成してくれるので以下で十分です。
store.Dispatch("app/deposit",10f);
こうすることで、Store内の値を更新することができます。
Dispatch()はStoreを更新する唯一の方法です。
ReducerがStateを更新
Actionと現在のStateが、Store内のReducerに渡され、新しいStateを作成します。
Reducer
ReducerはStateとActionを受け取り、新しいStateを返します。これにより、Actionのtypeに基づいたイベント処理が可能になります。
Reducerを作成するときは、以下の点に気を付ける必要があります。
- 既存のStateに変更を加えることはできません。(Stateは不変であるため)その代わり、現在保持しているStateをコピーし、コピーされた値に変更を加えることによって、更新を行うことができます
- 非同期ロジックを実行するといった「副作用」を引き起こしてはいけません
非同期処理を書きたい場合については3章でやります。
実際に書く場合は以下のようになるのでしょうか...?
action.typeを条件分けする必要がある等、かなりイケていないですが、AppUIではもっといい感じに書けるつくりとなっています。
record State {
float account;
}
void DepositReducer(State state, Action action){
if (action.type == "deposit"){
return state with {account = state.account - action.payload}
}
}
StateをもとにViewを更新
最後に更新されたStateをもとにViewを更新します。
Reduxのまとめ
以上がReduxの簡単なまとめになります。
ReduxはReactやVueといったフロントエンドのフレームワークで使用されているためネット上に情報がたくさんあります。気になった方は自分で調べてみてください。より正確で質の良い情報が手に入るはずです。
MVVM×Redux
前述したようにMVVMを用いるとViewModelが肥大化しがちです。
そこで値の更新責務をReduxに押し付けようと、そういう魂胆です。
Modelが丸々Reduxに置き換わります。
実際には以下の図のようなものになります。
ViewModelはReduxにaction typeと値を渡せばよいので1行で済みます。
また、Stateの更新後の値を保持しておくだけでよいので肥大化は抑えられるはずです。
store.Dispatch("app/deposit",10f);
実際の開発ではView-ViewModelは増大していくことになります。それに伴って、Modelも増やしていって...とするとそれぞれのつながりが明確にわかっていないと苦しくなってくるでしょう。(設計なしで開発するよりははるかに良い)
そんなときにデータを一元管理したStoreがあると便利かと。
ロジックはどこ
MVVM設計パターンでは、アプリケーションのロジックは主にModelに配置されます。
しかし、ModelをReduxに置き換えてしまった場合どうすればよいのでしょうか。
大きく分けて次の2通りがあります。
- Redux Middlewareに実装する
- ViewModelに実装する
Redux Middlewareに実装する
ReduxにはDispatchの前後に処理を挿入できるMiddlewareという機能が存在します。
このMiddlewareにロジックを実装することでModelの代わりになります。
また、非同期処理もここに実装可能です。
ViewModelに実装する
ViewModelに実装します。
具体的にはDispatch()の前後に挟みたいロジック処理を入れます。
しかし、この方法は本来Modelにあるべき処理の一部をViewModelに移動しただけにすぎません。
できれば避けたいところです。
結果的にロジックはMiddlewareに実装したい...となりそうです。
しかしAppUIでは現状、Middlewareに相当する実装が提供されていません。そのため、ロジックはViewModelに実装する必要があります。
3章ではロジックをViewModelに実装しています。
1章 おわりに
チャットアプリをつくる下準備として各設計パターンを簡単にですが説明しました。間違っている点があれば教えていただけると幸いです。
次章はAppUIに関する説明を行います。