らんどなテックブログ

エンジニアのいろいろ

設計について本気出して考えてみた

こんちは、じぬです。

本日のメニューはこちらです。

はじめに

お仕事で扱っている SDKドッグフーディングとして、趣味でミニゲームらしきものを作ってます。 せっかくなので UniRx や Extenject など使ってみたかったライブラリもふんだんに盛り込んで好き放題に書いています。

が、好き放題やり過ぎてとっちらかってきたので、リファクタリングしたくなりました。 あわせてクリーンアーキテクチャなどの設計にも興味が湧き、そのあたりを学んでみました。

※ 正直まだ理解し切れていないので、あくまで試した内容の紹介としてご覧ください

ゲームの現状

f:id:ryu_rand:20211008111455g:plain:w360f:id:ryu_rand:20211011122256g:plain:w360

f:id:ryu_rand:20211012110014g:plain:w360f:id:ryu_rand:20211012110919g:plain:w360

概要

このゲームは操作キャラ(ユニティちゃん)が赤白のボールに触れることでポイントが貯まります。 操作キャラはジャンプやスライディングなどのアクションも可能です。

黄色のボールはポイント獲得の代わりに操作キャラをパワーアップさせます。 パワーアップ中は障害物を破壊できるなどの変化があります。

ほかにも SE、タップエフェクト、3D オブジェクト生成中のプログレスバーSDK の機能を使ったボールのランダム配置、などの処理が存在します。

当初の実装概要

名称は一部省略してます。 はじめはサンプルを無理やり拡張したので Main が二つあったりしました。(笑(笑えない

class role
Main ステートマシン
建物などの GameObject 生成
ミニマップカメラの生成
ロード中のプログレスバー管理
GameMain ゲーム開始のカウントダウン
ゲーム中と終了の管理
ロード中のテキスト更新
タップエフェクト
Walker ボールに衝突した際の処理
パワーアップ処理の反映
パワーアップ中の障害物破壊
CharacterControl 入力を操作キャラに反映(移動、ジャンプ、スライディング)
アニメーションの制御
Ball ポイント獲得処理を発生
パワーアップ処理を発生
自らを Destroy
SDK にランダム配置されるための処理
BallMaker ボールの生成と配置
PointManager ポイント加算処理
ポイント表示の更新
獲得エフェクト
ボール破壊エフェクト

まず感じたのは個々の役割が多いことです。 Main はつなぎ役なのでまだしも、Ball は複数の役割が入り乱れています。

また、依存の向きに統一感がありませんでした。 例えばキャラがボールを獲得した際、ポイント計算、サウンド、獲得エフェクト、ボール消滅、などが発生します。 しかしそれぞれの処理を「誰が誰に対して」起こすのかが統一されておらず、拡張する際にどこを直すべきかが曖昧でした。

リファクタリング

ここでは、リファクタリング前に考えたことを一通り説明していきます。 最後の節で実際に行った修正をまとめます。

仮想の追加仕様

目的も無くリファクタリングすると迷走しそうだったので、仮想の追加仕様を考えそれに向けて実装を整理することにしました。 考えたネタは以下のものです。

追加仕様 修正方法とその意図
別キャラを使いたい キャラ操作を抽象化して差し替え可能にする、パワーアップ時のパラメータ調整を可能にする
ボールの種類を増やしたい パワーダウンや速度アップなど新たな効果を想定し、操作キャラとボールが相互に行う処理を抽出することで差し替え可能にする
壁破壊時にポイント獲得したい 操作キャラに対するポイント処理を抽象化して、発生元を差し替え可能にする

これらの実現を目標として、既存クラスを役割ごとに分解していきました。

クリーンアーキテクチャについて

ある程度分解が進むと、次はそれらをどう整理するか考えます。 せっかくなら原則にそって見直そうと考え、クリーンアーキテクチャを調べました。

クリーンアーキテクチャ自体の説明は著名な本なり記事なり多数あるので割愛します。 少なくとも自分にとっては難しい部分もあったので、複数の情報を取り込んで自分なりに噛み砕く必要はあるかと思います。

クリーンアーキテクチャと聞いて有名なのは次の図ですが、重要なのは「図に遵守したレイヤー分け」ではなく「依存方向の向き先」であると耳にします。 とは言え初めてでピンと来ない部分もあったため、感覚をつかむまでとりあえず図に倣った分類を考えてみます。

f:id:ryu_rand:20210908104053p:plain 出典:Clean Coder Blog

クリーンアーキテクチャによる分類は以下です。(一部の名称を略してます)

layer role
Business Rules ビジネスルール
App Rules ビジネスルール同士のつながり、関係
Interface Adapters 上位レイヤーの具現化、下位レイヤーとの接続
Frameworks DB やデバイスなど環境に応じた具現

これを元に整理した結果が次の図です。

f:id:ryu_rand:20211001112023p:plain
リファクタリング後の依存関係

整理前に考えたこと

まず、UI(特に UnityEngine.UI)に依存している部分を切り離すことは比較的分かりやすいです。 Frameworks 層に UI に直接依存する処理を置き、Adapters 層の IPresenter を通してそれらを利用します。 こうすれば UI 以外のテストが容易になります。 例えば、UI として表示する部分だけを切り離すことでポイント計算処理がテスト可能になる、という感じです。

Frameworks 層に依存しない具体実装も Adapters 層に置くことを想定しました。 抽象の実体化に伴う複雑さを引き受けるのが Adapters 層という印象だったため、このレイヤーは必然的に厚くなります。 とは言え散らかり過ぎな印象はあり、本来はこの内部をもっと役割ごとに分類するべきかと思います。

Business Rules 層と App Rules 層を実際に分けていくのは混乱がありました。 参考記事をいくつか見る限り、User などのモデルや純粋なロジックなどのドメイン知識が Business Rules 層、それらの使い方を定義したり関連付けるのが App Rules 層という分け方のようです。 いまのところ、純粋な知識の層とそれらを関連付ける層、という理解が感覚としてはしっくり来ています。 この点はなにをドメインとするかという話が重要なため、プロダクトの思想が出る部分かと思います。

整理後に考えたこと

ひとまず依存方向の統一はできていそうです。

Business Rules には依存される抽象クラスを、App Rules にはそれらをやや具体化したクラスを置いています。 App Rules が少ないですが、おそらく通信や DI などドメイン外のやるべきことが増えると合わせて拡大するのではと思います。 もしくは、ドメインの定義が甘いために Business Rules に要素を乗せすぎているという可能性もありそうです。 上位のルール層は、正直見直す余地があるだろうなと思っています。

Others は整理すれば Frameworks 層ぽい役割ですが、各所から気軽に呼び出せた方が使いやすいため独立した整理をしています。

実際にやったこと

実際に行った修正は以下のような内容です。

Main の責務を分割

まず、記述量が多い GameObject 関連の処理を別クラスに切り出しました。 建物や地形、ボールの生成と配置です。(MapObjectCreator

プログレスバーの内部挙動がベタ書きされていました。 管理を別クラスに分け、初期化時に必要なパラメータを渡し、Main からは次に進むタイミングだけを伝えるように疎結合化しました。 (ProgressManager

そもそもステートマシン自体イマイチな気もしましたが、やめるのは少々大がかりでした。 そのためこのクラスからはステート変更時の呼び出しだけを記述し、具体的な処理は他クラスに委譲する形で整理しました。

Ball の整理

Ball は生成時に SDK の機能でランダム配置され、ポイント獲得やパワーアップが発生するという役割があります。

まず形状が同じというだけでポイント獲得アイテムとパワーアップアイテムという役割を持っていますが、両者のコアロジックは無関係です。 また分かりにくいですが、自動ランダム配置されるための処理も他の機能と無関係です。 そこで役割を分け、フィールド上にある獲得できるアイテム(FieldIem)、ポイント獲得できるアイテム(IPointItem)、自動配置されるアイテム(OrnamentItem)という役割を必要に応じて Add する形にしました。

細かい依存関係の修正

例えば Walker クラスがゲームのアクティブ判定を持っている、BallDestroy を実行している、CharacterControl と密結合である、などが気になりました。 このあたりは依存性逆転の原則を意識し、interface で抽象化して依存方向を整えました。

Manager の分割

例えばポイント獲得イベントが発生すると、現在のポイントととの足し合わせ、複数ボールを獲得した場合の調整、その数値に向けてカウントアップしていく表現、など一連の処理を一つのクラスが管理していました。 結果としてクラスの役割が広くなり Manager という無難な名前がつけられていました。 対処としてまず、UI(UnityEngine.UI)と触れる部分を IPresenter として抽出し、Manager との接続は interface 経由で行うようにしました。 残った処理はほぼ計算だったので、責務を限定的にするため ManagerLogic というクラス名にしました。

感想

基本的にはやって良かった(面白かった、設計を考慮する有益さが理解できた)と思いますが、良し悪しをまとめておきます。

Pros

interface を切り出すことで強制的に役割の整理が行われるので、図示したことと合わせて制御の流れが分かりやすくなりました。 特に UI と切り離すことでテストがしやすくなる、というより「テストしたいところに注目して切り出す重要性」を意識できるようになりました。 今回は大した物量がないため効果は薄いかも知れませんが、ある程度大きなアプリケーションではもっと有益かと思います。(その分整理し続ける負荷はあると思いますが...)

また今回はリファクタリングを一部省略しましたが、プログレスバーやローディング表示などもテスト可能にした方が良いと思います。 地味にロジックが入り組んでいるため、はじめから TDD を意図した設計・作り方ができれば工数削減にも繋がると思います。

Cons

例えば Manager の分割では二つのファイルが増えており(IPresenter とその Impl クラス)、ルールを徹底するほど管理が煩雑化しそうです。 また複数メンバーでの開発を想定すると、レイヤーの分け方を具体的に定義・明文化しておかないと徐々に混沌としていきそうです。 実際、一人でやった今回でも割と迷走があったと思います。 アーキテクチャを主導できるスペシャリストがいる、継続してチームで学び続ける、など開発の土台が必要だと思います。

参考記事