ゲーム開発やハイパフォーマンスグラフィックスの世界では、古典的なオブジェクト指向プログラミング(OOP)が純粋なデータ指向設計(DOD)に対してまだ通用するのかがよく議論されます。キャッシュ局所性や連続したメモリバッファが最新のGPUパイプラインに不可欠である一方で、Object-Oriented Graphics Rendering Engine(OGRE 3D)は、正しく実装されたOOPの代表的な例として屹立しています。
1. 真の抽象化:低レベルグラフィックスAPIを隠蔽する
OOPの中核的な目標の一つは抽象化です — 複雑な実装の詳細を隠しつつ、クリーンで高レベルなメンタルモデルを提供することです。
OGREは、シーン管理をドライバレベルのレンダリングから切り離すことで、低レベルグラフィックスAPIを抽象化しています。CppDependを使えば、具体的なドライバ(RenderSystem_GLやRenderSystem_Direct3D11など)がOgreMainの抽象化とどのように相互作用しているかを調べられます:
アーキテクチャ上の教訓
具体的なバックエンドをRenderSystem、Texture、HardwareVertexBufferといった重要な抽象契約から派生させることで、OGREは新しいレンダリングAPI(VulkanやWebAssembly/WebGLなど)の追加に、アプリケーションロジックやシーングラフ走査への構造的な変更を一切必要としないことを保証しています。
2. 継承の濫用のないポリモーフィズム
C++フレームワークによくある落とし穴は過剰な継承です — 深くて脆いクラス階層を作ったり、多重継承を乱用したりすることです。OGREは継承よりもコンポジションを優先し、階層を浅く保つことで、卓越した凝集性を維持しています。
コアライブラリ全体の継承の深さは、Code Questクエリで確認できます:
from t in Types
where t.BaseClasses.Count() > 1
select new { t, NbBaseClasses = t.BaseClasses.Count(), t.DepthOfInheritance }
データが示すもの
- 浅い継承ツリー:OgreMainのほとんどのクラスは、継承ツリーの深さ(DIT)が3以下です。
- 制御された多重継承:複数の基底クラスから派生する型はごくわずかです。OGREにおける多重継承は、主にmixinインターフェース(ドメインインターフェースとリスナーまたはファクトリ基底クラスの両方から派生するなど)に限定されています。
3. FactoryパターンとSingletonパターンによるクリーンなインスタンス化
大規模なC++アプリケーションでは、制御されないオブジェクトのインスタンス化が強い結合を生み出します。OGREは二つの基本的な生成パターンでこれを解決しています:
オブジェクト生成のためのFactoryパターン
クライアントコードがnewで具体的なオブジェクトを直接インスタンス化するのを許す代わりに、OGREは生成を専門のファクトリクラス(EntityFactory、SceneManagerFactoryなど)に委譲します。
from m in Methods
where m.DepthOfCreateA("Ogre.Entity") == 1
select m
結果:EntityFactoryのみがEntityオブジェクトを直接インスタンス化しており、メモリ管理とオブジェクトのライフサイクルフックが完全にカプセル化されていることが保証されます。
システム状態のためのSingletonマネージャー
OGREは、スレッドセーフなテンプレート(Ogre::Singleton<T>)から派生したシングルトンを使って、サブシステムの管理を一元化しています:
MeshManager、TextureManager、MaterialManagerなどのサブシステムはこの統一されたテンプレートを継承し、開発者にエンジンリソースへの予測可能でスレッドセーフなアクセスを提供します。
4. Facadeパターン:Ogre::Rootによるエンジンサブシステムの簡素化
何十もの専門マネージャー、ローダー、レンダーターゲットの管理は、クライアントアプリケーションを圧倒しかねません。OGREはエントリーポイントクラスOgre::RootにFacadeパターンを採用しています。
クラスが依存する型の数を測るCppDependの出向結合度(Efferent Coupling、EC)メトリックを使うと、Ogre::Rootの結合度が高い(EC > 100)ことがわかります。
高い結合度は通常のクラスではコードスメルですが、Facadeにとっては意図的な設計上の選択です。Rootは、初期化、フレームリスナー、レンダリングループ、マネージャーのライフサイクルを、統一された使いやすいAPIの背後でオーケストレーションします。
5. 抽象度対不安定度グラフ
Robert C. Martinは、オブジェクト指向設計の品質を、その設計のサブシステム間の相互依存関係の観点から測定する一連のメトリックについて、興味深い記事を書いています。
モジュール間の相互依存について、彼は記事の中でこう述べています:
設計を硬直させ、脆弱にし、再利用を困難にするものは何か。それは設計内のサブシステム同士の相互依存である。設計が容易に変更できないとき、その設計は硬直している。このような硬直性は、強く相互依存したソフトウェアへの単一の変更が、依存するモジュールに変更のカスケードを引き起こすことに由来する。その変更のカスケードの範囲を設計者や保守担当者が予測できないとき、変更の影響を見積もることができない。これにより変更のコストの見積もりが不可能になる。こうした予測不可能性に直面したマネージャーは、変更の承認を躊躇うようになる。こうして設計は硬直化するのである。
そして硬直性に対抗するために、彼は求心結合度、遠心結合度、抽象度、不安定度といったメトリックを導入しています。
求心結合度(Afferent Coupling):このプロジェクト外の型のうち、このプロジェクト内の型に依存しているものの数。
遠心結合度(Efferent Coupling):このプロジェクトの型が使用している、このプロジェクト外の型の数。
遠心結合度と求心結合度は、名前空間や型にも適用できます。例えば、特定の型の遠心結合度は、その型が直接依存する型の数です。TypeCeが非常に高い型は、他の多くの型に依存しすぎています。それらは複雑で、一般に複数の責務を持っています。
抽象度
内部の抽象型(抽象クラスとインターフェース)の数と内部の型の総数の比率。このメトリックの範囲は0から1で、A=0は完全に具象的なプロジェクト、A=1は完全に抽象的なプロジェクトを示します。
A = Na / Nc
ここで:
- A = モジュールの抽象度。ゼロは完全に具象的なモジュール、1は完全に抽象的なモジュールです。
- Na = モジュール内の抽象クラスの数。
- Nc = モジュール内の具象クラスの数。
不安定度
遠心結合度(Ce)と総結合度の比率。このメトリックはプロジェクトの変更に対する耐性の指標です。範囲は0から1で、I=0は完全に安定したプロジェクト、I=1は完全に不安定なプロジェクトを示します。
I = Ce / (Ce + Ca)
- Iはプロジェクトに関連する不安定度を表します。
- Caは求心結合度、つまり入ってくる依存を表します。
- Ceは遠心結合度、つまり出ていく依存を表します。
抽象度対不安定度グラフとZone of Pain
Ogreプロジェクトの抽象度対不安定度グラフを示します。
このグラフの背後にある考え方は、プログラム内で広く使われるコード要素ほど、より抽象的であるべきだということです。言い換えれば、具体的な実装に過度に依存するのを避け、代わりに抽象化に依存すべきです。ここでいう「よく使われるコード要素」とは、プログラムの他のプロジェクトから大規模に使用されるプロジェクトのことです(この考え方はパッケージや型にも当てはまります)。
コードベース全体で広く使われる具象型を持つのは良い考えではありません。それはプログラム内にZone of Painを生み出し、実装の変更がプログラムの大部分に影響を及ぼす可能性があります。そして実装は抽象化よりも頻繁に変更されることが知られています。
上図の主系列線(点線)は、抽象度と不安定度がどのようにバランスされるべきかを示しています。安定したコンポーネントは左側に位置します。主系列を見ると、そのようなコンポーネントは望ましい線の近くに位置するために非常に抽象的であるべきだとわかります — 一方、抽象度が低い場合は「Zone of Pain」と呼ばれる領域に位置することになります。
OOPアプローチで硬直性と戦うには?
Robert C. Martinが記事に書いているように、プロジェクトをより柔軟にし、コード要素間の高い結合を減らすには、抽象クラスとインターフェースを使わなければなりません。
OOPにおける結合は、次のことによってもたらされます:
- 継承:OOPパラダイムを採用する際に過度に使われることが多く、残念ながら多くの場合コードをより硬直させます。継承によってもたらされる硬直性を解決するのに有用なデザインパターンもあります。例えばAdapterパターンは、継承による硬直性を最小化します。
- 具象実装の直接使用:この場合も、何らかの理由で別のライブラリやフレームワークを使う必要が生じたときに変更が難しいため、コードは硬直化します。継承と同様に、BridgeやProxyのようなデザインパターンで硬直性を最小化できます。
OOPアプローチを使う場合、GoFの構造パターンを習得することが推奨されます。それらは結合によってもたらされる硬直性の低減に役立ちます。
メトリックヒートマップ分析:OgreMainにおけるメソッドレベルのリスク集中
ツリーマップはOgreMainコンポーネント内のコードベース階層を可視化したもので、矩形のサイズはコードサイズ(例:コード行数)を、色のエンコーディングはメトリックの深刻度(低リスクの青/緑から、高リスクの赤まで、例えば高い循環的複雑度や高い技術的負債)を表しています。
この可視化から得られる重要な教訓は、高リスクのメトリックがクラスや名前空間全体にわたるシステム全体の構造的劣化を示すのではなく、メソッドレベルにのみ局在しているということです:
- メソッドレベルのホットスポット:ツリーマップに散在する目立つ赤い矩形は、
translate、parse、log、visitなどの特定の個別ルーチンに対応しています。これらの関数は複雑性のホットスポットとして機能しており、密な制御フロー(大きなswitchブロックや入れ子の条件ループなど)を含んでいると考えられます。 - 健全なクラスと構造コンテナ:これらのメソッドホットスポットの周囲では、親コンテナ(クラスと名前空間)は主に緑と黄色のままです。これは、OgreMainにおけるクラス構成とモジュール境界が構造的に健全であり、品質のボトルネックが孤立したメンバー関数内に集中していることを示しています。
- 的を絞ったリファクタリング戦略:赤い深刻度フラグが
parseやtranslateのようなメソッドに厳密に局在しているため、改善にクラス階層やインターフェースの再設計は必要ありません。リファクタリングは、これらの特定の関数を、単一責務のより小さなヘルパールーチンに分解することに直接狙いを定められます。
サマリーメトリック:OGREが時の試練に耐える理由
OgreMainをCppDependで解析すると、高レベルの構造メトリックは、開発者が20年以上にわたって賞賛してきたことを裏付けています:
- 高い凝集性:コアシーンコンポーネント全体の低いLCOM(Lack of Cohesion in Methods)スコアは、焦点の定まった単一責務のクラスを示しています。
- 低い循環的複雑度:メソッドは短く、焦点が定まり、テストしやすく、巨大なモノリシックなディスパッチループを避けています。
- クリーンなモジュール境界:名前空間間のヘッダー循環依存が最小限に抑えられています。
結論
OGREは、アーキテクチャの原則が厳格に適用される限り、C++におけるオブジェクト指向プログラミングは本質的に遅くも過度に複雑でもないことを証明しています。CppDependのような最新の静的解析ツールを活用することで、チームはOGREのようなエンジンを研究し、クリーンなインターフェースを強制し、アーキテクチャの劣化を防ぎ、長持ちするC++ソフトウェアを構築できます。
