Subsystemをいつ別クラスとして分けるべきか
クラスを分けるかどうかの判断基準を、State・Lifetime・責務・読みやすさの観点から整理した。
背景
Engineを実装していると、処理が増えるたびに迷う場面が出てくる。
これは別クラスに分けるべきか
ただし、処理を細かく分ければ設計が良くなるわけではない。クラスを増やしすぎると、処理を追うために複数ファイルを行き来する必要が出てきて、かえって読みづらくなることもある。
そのため、現在は「分けられるか」ではなく、分ける理由があるかを見るようにしている。
独立したStateを持つか
まず見るのが、その処理が独立したStateを持っているかどうか。例えば、
class RenderSystem
{
private:
SwapChain m_swapChain;
};
SwapChainが、
- SwapChain本体
- Back Buffer
- Buffer Index
- 関連するResource
など、自分自身で管理すべき状態を持つのであれば、独立したクラスとして扱いやすい。
逆に、単に数行の処理をまとめただけでStateを持たない場合は、無理にクラスへ分ける必要はない。
独立したLifetimeを持つか
次に見るのがLifetime。DirectX 12では、Resourceごとに、
- いつ作るか
- いつ使えるか
- いつ解放するか
を意識する必要がある。
例えばGpuSynchronizerがFenceやEvent Handleを所有するのであれば、それらのLifetimeもGpuSynchronizer自身で管理した方が責務が分かりやすい。
GpuSynchronizer
├─ Fence
└─ Event Handle
このように、独立したResourceとLifetimeを持つものは、クラスとして分ける理由が明確になる。
単独で責務を説明できるか
クラスを分けるときは、次を一文で説明できるかも見る。
このクラスは何をするものか
例えば、
SwapChain
画面へ表示するBack Bufferを管理する
CommandContext
GPUへ送るCommandの記録と実行を管理する
GpuSynchronizer
CPUとGPUの同期を管理する
のように説明できるなら、それぞれの境界は比較的分かりやすい。
一方、RenderSystemから長くなった処理を何となく移したクラスでは、責務の境界が曖昧なままになる。
親クラスが読みづらくなっていないか
RenderSystemへ処理を追加し続けると、徐々に一つのクラスが多くのことを担当するようになる。例えば、
RenderSystem
Device作成
SwapChain作成
Command管理
Pipeline作成
GPU同期
Mesh管理
描画
まで一つに集まると、RenderSystemを読むだけでも多くの知識が必要になる。そこで現在は、必要に応じて、
RenderSystem
├─ GraphicsPipeline
├─ SwapChain
├─ CommandContext
├─ GpuSynchronizer
└─ MeshResource
のように分けている。RenderSystemは描画全体をまとめる役割に残し、それぞれの詳細を専用クラスへ任せる。
再利用できるかだけでは決めない
「他でも使えそうだから」という理由だけでクラスを分けることも避けたい。将来の再利用を予想して細かく分けすぎると、実際には一度しか使わない抽象化が増える可能性がある。
そのため、再利用性は判断材料の一つにはするが、
- State
- Lifetime
- 責務
- 読みやすさ
と合わせて考える。
小さい処理はそのままでもよい
例えば、
void RenderSystem::UpdateViewport()
{
// 数行の処理
}
しかなく、独立したStateもLifetimeもない場合、
class ViewportUpdater
{
};
のようなクラスを作っても、設計上のメリットはあまりない。ファイルやクラスが増える分、処理を追いづらくなる可能性もある。
そのため、「クラスへ分けられる」ことと、「クラスへ分けるべき」ことは別として考える。
まとめ
現在は、Subsystemや内部クラスを分けるとき、主に次の点を見る。
- 独立したStateがあるか
- 独立したLifetimeがあるか
- 責務を単独で説明できるか
- 親クラスが読みづらくなっていないか
- 再利用する意味があるか
すべてを満たす必要はない。ただし、単にコード量が増えたからという理由だけでは分けない。
実装を進めながら、「この処理を独立したObjectとして扱う理由があるか」を判断基準にしていく。