作業ログ

Subsystemをいつ別クラスとして分けるべきか

クラスを分けるかどうかの判断基準を、State・Lifetime・責務・読みやすさの観点から整理した。

  • game-engine
  • software-design
  • cpp

背景

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として扱う理由があるか」を判断基準にしていく。