開発方針についての文書を書いていた
色々決めごとをするときは社内にissueなりwikiなり残すのですが、これは外に公開してもいいんじゃない?と思ったので公開します。 今回は「開発方針について」です。
目的
- クライアントアウトゲーム全体の開発方法、指針を把握する
- あとから参加した人もスムーズに着手できるようにする
お品書き
- アーキテクチャのお話
- Rxのお話
- DIのお話
- テストのお話
アーキテクチャ
なぜソフトウェアアーキテクチャが必要か
- 大規模開発、運用にはスケールを見通した設計が必要
- 秩序なき開発は人間が犠牲になる
- 設計ルールをその都度作っていくのは大変
- すでに先人たちが積み上げてきた効率の良い設計が存在する
- それがソフトウェアアーキテクチャ
- 一般化されている設計ルールは独自設計より学習しやすい
- 後から入った人も理解しやすい、むしろそれを前提とした採用もできる
プロジェクトの設計(アウトゲーム)
- MV(R)Pアーキテクチャを採用している
- MVPアーキテクチャの派生
- ViewとPresenterをRxで繋ぐ
- 経緯 ここにissueのリンクが入る
MV(R)P アーキテクチャ
Model
- いわゆるロジックと呼ばれる部分
- PureClassであり、MonoBehaviorではない
- 自身を更新したり、処理結果を返すインタフェースを公開している
- 自身のパラメータをReactivePropertyで公開もする
- Modelと一口に言っても内部で分類が色々ある。
- Presenterを知らない
- Viewを知らない
- ユニットテストが書けるようにする
View
- ButtonやImageなど、画面に表示されるもの
- MonoBehavior
- 複数の要素をまとめてViewクラスとするのも可
- Viewはイベントストリームを持っている
- クリックされた、選択されたetc..
- ストリームを公開して、外部から購読できるようにする
- Viewは外部から情報を更新できるインタフェースを公開している
- 任意のキャラの情報を表示など
- Presenterを知らない
- 自身の情報更新のため、Modelは知っている
- メンバ変数で持つのは推奨しない
Presenter
- ViewとModelの間に位置するもの
- MonoBehavior
- ViewとModelを知っている
- Viewのストリームを購読し、Modelに更新をかける
- Modelの変更ストリームを購読し、Viewに更新をかける
- Presenterは複数のViewを購読してもよい
- 大きくなりすぎたら別のPresenterに切り出す
Modelの詳細
Modelは多種多様なので、役割ごとに適切に層を分けないと開発に支障が出る。
- どこにファイル置けばいいの?と考える時間は少ないほうがいい
逆に層を分けすぎてもそれは分割の手間や思考の時間を奪う
- スパゲティコードに対してラザニアコードと呼ばれる
MV(R)Pにおいて、モデル下は規約は特に定まっていない。 が、スタンダードな考え方はいくつかある
プロジェクトで考えてるモデルの分け方
- Model
- Model/Entity
- Model/UseCase
- Model/Repository
Model
- いわゆるロジックと呼ばれるもの
- 現状はここに大半いる
- 色々な役割のクラスが混ざっているので適切な分割は必要
Entity
- ロジックをほぼ持たないデータのみのクラス
- キャラクタのパラメータとか
- 通信のレスポンスオブジェクトとか
Repository
- リソースを処理する存在
- リソースのGET,PUT,CREATE,DELETEのイメージ
- どうやって処理するのかを書く
- 通信?ローカル?etc...
- 外部からはリソースがどこに存在するのかは見えない
- キャッシュ機構を持つのもあり
UseCase
- ビジネスロジックと呼ばれるもの
- 「やりたいこと」単位でクラスを作る
UserNameChangeUseCase
TitleUserLoginUseCase
など
- Presenterは基本UseCaseを使ってモデルを触る
名前変更処理の一例(現在こうなっているわけではない)
インゲームのアーキテクチャについて
- 現状定めていない
- パフォーマンスチューニングなどが必要になるので、レイヤーの分割が適切に行えないことがある
- ViewとModelに分割はできるかも?くらいの認識
Rx
ReactiveExtentionsとは
- こちらをご参照ください https://www.slideshare.net/torisoup/unity-unirx
MV(R)PにおけるRxの役割
- Viewのイベントを購読する、モデルの更新を購読する
- Presenterがイベントを検知するために使う
- https://www.slideshare.net/torisoup/unirxmvrp
ここの Subscribe
OnNext
の関係を実装するのがRx
async/await とObservableの使い分け
- 単純に非同期を待ち合わせたいだけならasync/awaitが簡単
- それだけじゃない複雑なことをするならObservable
- 特にイベント処理はObservableのが楽
- とりさんのスライドで大体かいてる https://niconare.nicovideo.jp/watch/kn3081
DI
Dependency Injection
DIはもはやもうこれ見てもらったほうが早い・・
https://qiita.com/toRisouP/items/b3d3c43db40857ca4ad4
プロジェクトにおける主なZenjectの使い方
- staticなオブジェクトをなくす
- 環境によるモジュールの差し替え
staticなオブジェクトをなくす
- 実体に強く依存するため、差し替えづらい
- isTestみたいなフラグは持ちたくない
- ゲームはシングルトンによるstaticが生まれやすいイメージ
- マスタデータ、サウンドマネージャetc...
これらをDIContainerに管理させることで、疎結合なシングルトンに変更する
環境によるモジュールの差し替え
- テスト時には通信したくない
- ローカルと本番でリソース取得先を差し替えるなど
通常だとこれらのフラグ管理などが必要 or 手でDIしなくてはならないが、これらをDIContainerに任せられる
何をBindすべきか
- あらゆるメンバをBindするとかえって面倒になる
- 普通にコンストラクタで渡せるならそれのが楽
- 基本的にはシングルトンだったものが対象
- Presenter、Viewの層まではやってしまってよい感覚
Model層にContainerを渡さない
- 上記の通り、コンストラクタで渡せるものは
- Containerそのものに依存してしまうのはそもそも設計としてよくない
- Presenterから適切なオブジェクトだけをモデルに渡すのが良い
- ここは努力目標で・・
public class BadUseCase { [Inject] DiContainer container; // UseCaseがcontainerまで気にするのはおかしい public void Foo() { var foo = new BadFoo(container.Resolve<Bar>()); } }
public class BadUseCase { private Bar bar; // 素直に受け取る public BadUseCase(Bar b) { bar = b; } public void Foo() { var foo = new BadFoo(bar); } }
テスト
テストいろいろ
プロジェクトで書けるテストは2種類 - ユニットテスト - UIテスト
なぜ書くのか?
- 大体issueにまとめている
- 以前発表したスライドも参照
ユニットテストはどのくらい書く?
- 基本的にモデルはすべてユニットテストが書けると考えている
- 作成したメソッドのテストは書いてほしい
- タスクの完了条件に「テストを書いている」を足したい
- 現場の状況で書かない判断はしましょう
テストでよくある質問
Q.仕様変更はしょっちゅう起こるし、その都度落ちたテストを書き換えるのは手間では?
A.確かに手間です。が、どう落ちたのかが可視化されるのは大きなメリットです。影響範囲がある程度わかれば修正もやりやすくなるので結果的には開発速度は上がると考えます。
テストでよくある質問
Q.テスト書く時間をかけすぎて辛い。
A.テストが書けない理由を掘り下げることが大事です。
- 何をテストしたらいいかわからない => そのクラスに何をしてほしいのか明確になっていない?
- テストのための準備が多くて書きづらい => 1クラスの依存関係が多すぎる?
- テストの構文がわかってない => ググるぞ!