目次
- コンテキストの考え方と評価
- 設計の役割・性質・コスト ←ココ
- 依存と結合度
- オブジェクト指向と継承
- 依存性逆転
- etc..
一つ一つのロジックを小さく積み上げて解説していくつもりなので、Unity開発に特化した設計に関しては果てしなく後の方になりそうです。
「俺は今すぐUnity用の最強アーキテクチャが欲しいんだ!」という方は以下が参考になると思います。
はじめに
前回のエントリでは物事の良い/悪いを論じるためのコンテキストという考え方について述べました。
今回のエントリではこれから考える「良い設計」における評価軸の優先度を決めること、および設計の役割やコストなどについて説明することを通じてより具体的なコンテキストを明らかにしていきます。
また、本エントリの内容は以下の記事に対する自分の考えを含みますので合わせて紹介させていただきます。
前提とするコンテキストと優先度
タイトルにあるとおり、本エントリと以降のエントリについては特に断りがない限り
- ゲーム開発
- Unity使用
- 開発言語はC#
を前提とします。また、前回のエントリでは設計における評価軸と制約として以下の5+3つを挙げました。
5つの評価軸
- 機能追加のしやすさ
- 機能変更のしやすさ
- スケールしやすさ
- パフォーマンス
- 安定性・可用性
3つの制約
- ハードウェア(HW)制約:CPU性能やメモリ容量など
- ソフトウェア(SW)制約:開発言語やフレームワークの選択肢など
- 納期
これから「良い設計」を考えるにあたって重視する項目をはっきりさせましょう。
まず評価軸についてですが、パフォーマンスに関しては一旦忘れることにします。
前回のエントリで触れたように、パフォーマンスはどこかの時点で他の評価軸と大きくトレードオフとなります。逆にパフォーマンス以外の評価を可能な限り高めた結果としてパフォーマンスが著しく落ちるということもないはずです。あるとすればそれはおそらく設計ではなくアルゴリズムや実装方法に問題があります1。
3つの制約についても一旦考慮から外します。
現実問題としてこれらの制約について考えずに済むことはないですが、制約に関するコンテキストは現場によって全く異なり、その全ての組み合わせについて考えるのは難しいので、これから説明していく設計に関する理論を理解した上で各々どうすべきか個別に考えてもらうのがよいと思います。納期はさすがに無視できないので、設計にかかるコストについては後ほど述べます。
よって、本エントリとそれ以降においては以下のコンテキストを大前提として話を進めます。
以後前提とするコンテキスト
- Unityを使用したゲーム開発(使用言語はC#)
- 設計で重視する評価項目
- 機能追加のしやすさ
- 機能変更のしやすさ
- スケールしやすさ
- 安定性・可用性
ちなみにクライアント側の話なので「スケールしやすさ」は「たくさんユーザを受け入れ可能か」ではなく「たくさん機能を追加しても破綻しないかどうか」を指します。
クラス図の書き方とコスト
ソースコードを眺めるだけで各クラス・モジュールの関係性をすべて正しく把握するのは難しいです。
現実には一つのクラスが参照しているクラスが10もあり、そういうクラスが10以上あるのが普通です。大きなモニタと画面分割可能なエディタを使ったとしても、画面に意味のある表示ができるクラス数はせいぜい一度に15がいいところでしょう。
良い設計を考えるためにはアプリケーション全体の関係性を可視化し、問題の所在を明らかにする必要があります。
全体の関係性を可視化するおすすめの方法はPlantUMLでクラス図を作ることです。
上記の記事のようにエディタにプラグインを導入することでクラス図をリアルタイムに作成できるようになります。
「記法を学ぶコストが高いのでは?」と思われるかもしれないので、基本的な書き方だけ紹介しておきます。
@startuml
'1行コメント(「#」ではなくシングルクォート)
/'ブロックコメント'/
'単純なクラス・インタフェースの宣言
class A
abstract class B
interface IRunnable
'メンバがあるクラス・インターフェース(-,+などのアクセス修飾子は必須ではない)
class C {
- string _id
+ void ShowId()
}
interface IDisposable {
+ void Dispose()
}
'後からメンバを追加できる
IRunnable : void Run()
'関連
A -> C
'関係性を記述するためにクラスを予め宣言しておく必要はない
C <- D
'名前空間の記述
namespace MyNamespace {
class A
'他のnamespaceを参照
interface System.IDisposable
'複雑なクラス宣言
class B extends A, C implements System.IDisposable, System.Collections.IList
}
@enduml
startuml~endumlの外は解釈されない
[アクセス修飾子]
- private
# protected
~ internal
+ public
[矢印による関係性の記述]
A -|> B // A extends B(汎化/継承)
A .|> B // A implements B(実現/実装)
A -> B // A has B(関連)
A .> B // A depend on B(依存)
レイアウトは
* 「<」「>」の位置と向き
* 「-」「.」の数
* 「-」「.」の間にキーワード(up,down,left,right)を記述
で調整可能
直感的に書けてドキュメントも日本語化されているので特に難しいところはないと思います。ただし、レイアウトが半自動なので配置がキモい場合直すのに若干苦労します。以下の記事を参考にされると良いです。
各クラスのメンバやメソッドをアクセス修飾子含め細かく記述することも可能ですが、全体の関係性を把握する用途であればそこまで細かく書き込む必要はありません。おすすめなのは
- namespace
- クラス名
- インターフェース名
- 依存、関連、実装、継承の矢印
だけを記述することです。
実装時に手が止まる場合の多くは以下の作業をしているときなのですが、これらの多くは全体を見渡してある程度のルールを持って決めたほうがよいものです。
- 命名
- namespace
- クラス名
- 変数名
- メソッド名
- インターフェース名
- クラス・モジュールの責務範囲決め
- 継承ツリーの設計
- APIの調査
- 難しいアルゴリズムの実装
どうせ実装時に発生する作業なら事前に設計の工数を取って効率的に消化すべきタスクだと思いますし、かといってこれらの作業のために細かくクラス図を作り込む必要はありません(必要だと感じたものは書いてもいいと思いますが)。
注意してほしいのが、クラス図を作るということはすなわちドキュメントを作るということです。真面目にやろうとするとソースコードとの整合性を保ち続けるコストが発生します。
こういったコストは
- クラス図 → スケルトンコード生成:「PlantUMLのクラス図からスケルトンコード生成」( @Tanakancolle さん)
- ソースコード → クラス図生成:「C#のソースコードからPlantUMLのクラス図を作成するツールのバージョンアップとVS Codeの拡張を公開しました!」
などの自動ツールで軽減できるかもしれませんので必要に応じて導入を検討してみてください。
設計と仕様変更の関係
設計の観点から「仕様変更」を一言で説明すると「確率的にアプリケーションを破壊する行為」と言えます。
エンジニアが仕様変更を嫌うのは、それによって自分が築き上げた設計やソースコードが破壊され、その影響が多岐におよぶからです。ひどい場合には災害と呼べるレベルですべてがひっくり返されることもあるでしょう。仕様をコロコロ変えるプランナーやディレクターが憎まれるのは、エンジニアから見た彼らが賽の河原の鬼だからです。
ここで「確率的に」と表現したのは「アプリケーションは仕様変更の発生によって事前にいつどこが破壊されるか知らない」ということを意味します。
一方で、現実のアプリケーションには毎回同じようなコンテキストが登場することがわかっていて、それらは
- UI:インプット,アウトプット
- データの永続化:DB,ファイル
- アプリケーションを特徴づけるロジック
- その他
くらいに表現できます。MVCなどのアーキテクチャはこういった定番のコンテキストに対しての大まかな設計指針を与えてくれるものです。
「確率的に」の部分がどうにもしっくりこない人は、上記を理論的にではなく直感的に理解しているからでしょう。どちらにせよどこが破壊されるかは設計する側がプロダクトのコンテキストから読み取った上で実際の実装や設計にあたっているはずです。
仕様変更がアプリケーションを破壊する行為なら、設計は「あらかじめ予想される攻撃に備え、アプリケーションに対して防御態勢を敷く行為」です。良い設計は仕様変更によってアプリケーションが受ける被害をできるだけ小さくするものとも言えます。
設計とアップデートの関係
昨今のスマホゲームに限って言えば、リリース後アップデートせずに十分な収益を上げ続けるのは難しいでしょう。
設計が柔軟でなければ改修の難易度が加速度的に上がるためにアップデートコストが嵩み、ゲームの寿命も短くなります。次第に実装がトランプタワーのように先細りかつ不安定になり、エンジニアはゲームタイトルが長く続くことよりもこれ以上の機能追加が発生しないことを望むようになります2。
良い設計はコードを柔軟に保つことでアプリケーションの寿命を延ばすだけでなく、エンジニアのモチベーションをプランナーやディレクターと同じ方向に揃え、チームを一丸にする役割もあります。
設計の性質と価値
設計は対象となるアプリケーションの社会的意義や価値・評価とは独立した行為です。正しく美しく設計したとしても必ずしも売上の立つアプリケーションになるわけでもありませんし、その逆もまた然りです。
「仕様変更」を「『確率的』にアプリケーションを破壊する行為」と例えたように、そもそも仕様変更が発生しなかったり、発生したとしても軽微な変更で済む可能性もあります。
設計に対する「できる人がマウントを取ったり、勉強した人間が気持ちよくなるためのものだ」という主張はこういった独立性と確率的な性質からくるものでしょう。前回の話で言い換えると、このような意見はエンジニアを取り巻く
- 設計をしなくてもなんとかなった
- アプリケーションが十分小さかった
- そもそもアップデートが(少)なかった
- たまたまアップデートと既存実装との相性がよかった
- 設計してもうまくいかなかった
- 正しい進め方がわからず、工数が無駄になった
- 影響範囲が広い仕様変更が発生し、設計に要した工数が無駄になった
という大きく2つのコンテキストが作用した結果生まれたのだと考えられます。
設計の価値は特殊な事例や偉そうな誰かの意見で決まるものではなく、実際のアプリケーションを取り巻くコンテキストによって正しく評価されるべきです。極端な意見には特殊なコンテキストが用いられています。
「いつもそうだった」「誰かがそう言っていた」ではなく「今の自分の状況やアプリケーションにとって価値があるか?」を常に考えてほしいものです。
設計の難しさ
先述のとおりMVCなどのアーキテクチャは定番のコンテキストに対して設計指針を与えてくれるものですが、あなたが作ろうとしているアプリケーションに最適かどうかはあなたが判断しなければいけません。
「良い設計」とはどこかの強そうなエンジニアが提唱した高尚そうなアーキテクチャを敷けば完成するものではなく、そこにプロダクトのコンテキストを正しく理解した設計者の判断が含まれることで初めて誕生するものです。
設計を難しく感じるのはまさにこの点に端を発します。ほとんどの場合、既存の有名なアーキテクチャはあなたの抱えている問題を解決するのに最適化されているわけではありません。
設計について最初に理解すべきなのは、設計は
- プロダクトのコンテキストを正しく理解し
- 変更の可能性が ないor低い 処理をコアとして依存を集め
- 逆に仕様変更の可能性が高い処理を表面に押し出して依存を減らすことで
- 機能追加・変更を容易にし再利用性を高める
という作業の繰り返しだということです。
良い設計というのは常にプロダクトを取り巻くコンテキストに大きく左右されます。プロダクトのコンテキストを正しく把握することなしに良い設計を作るのは難しいでしょう。
設計すべきかどうか
先程も述べましたが設計を可視化するということはドキュメントを作るということなので、それをソースコードとともにアップデートしていくコストが発生します。ゲーム開発においては現状の設計に対しクリティカルな仕様変更が走る可能性も高く、その場合は実装の修正に加えて再設計のコストがかかるかもしれません。
「わざわざ工数を使って設計をしても大きな仕様変更が発生したら意味がない」と思われるかもしれません。しかし、変更の可能性があればあらかじめ仕様決め担当者と握り合い、変更が発生する可能性のある箇所とそうでない箇所を可能な限り明確にすべきです。
心臓を撃ち抜かれたら人間だろうがアプリケーションだろうが死にます。握り合いもせず適当な設計で実装を進めると、撃ち抜かれたときに致命傷になる心臓がたくさんあるような状態になります。
設計とはアプリケーションを取り巻くコンテキストを正しく認識した上で、変更が走ったら致命傷となる処理を定めてその数を減らし、それを守るための戦略を練って形に落とし込むことです。そのためには仕様決め担当者との握り合いが必要不可欠です3。
逆に何も決まっていない、どこが変更されるか検討もつかないのであればあらかじめ設計するメリットは薄いでしょう。また、継続的なアップデートが必要ない場合も同様です。
* * *
設計を難しく感じる理由はたくさんありますが、
- 設計の意義やメリットがわからない
- 設計の原理原則や理論、およびそれらの適用タイミングがわからない
- 設計の進め方がわからない
- 設計にかかるコストがわからない
- 仕様決め担当者との握り合いが難しい
をすべて一緒くたにして「設計はよくわからないからやらなくていい」と考えるべきではありません。
上記それぞれについて自分のスキルや状況を正しく把握し、何を優先すべきか考えた上で発生するコストを抑え、その上で「設計にコストをかけるべきか」を判断することが大事です。
設計を学ぶことの難しさ
「設計の意義がわかったしやってみるか!」となって設計のやり方や一般解を模索し始めると、今度はSOLID原則やらなにやら難しそうな原理原則が並び、英語を直訳したような全くピンとこない説明がたくさんでてきます。
たとえ「単一責任原則」を覚えたとしても「クラスを変更する理由は一つでなければならない」を具体的にプログラムでどう表現すべきか、いつ適用すべきかがわからないという問題があります。
この点については次回以降のエントリでなるべく一本筋の通ったルールとともに自分の言葉でわかりやすく説明していきたいと思っていますので、どうぞお付き合いください。
まとめ
今回のエントリでは以下について述べました。
- 以後前提とするコンテキストと優先度
- クラス図の書き方とコスト
- 設計と仕様変更の関係
- 設計とアップデートの関係
- 設計の性質と価値
- 設計の難しさ
- 設計すべきかどうか
- 設計を学ぶことの難しさ
設計に対するモヤモヤした気持ちが晴れ、「いっちょやってみっか!」となってもらえたら嬉しいです。理想論ばかり述べて「設計不要」の反論になってないように感じたかもしれませんが、まだまだ先は長いので現時点ではしょうがないかもしれません。
次のエントリでは「良い設計」を導出するための基礎的な考え方である「依存」と「結合度」について説明する予定です。
ではまた。