OGRE (Object-Oriented Graphics Rendering Engine) ist eine szenenorientierte, flexible 3D-Rendering-Engine in C++. Sie wurde entwickelt, um Entwicklern die Erstellung von Anwendungen mit hardwarebeschleunigter 3D-Grafik einfacher und intuitiver zu machen. Die Klassenbibliothek abstrahiert die Details der zugrunde liegenden Systembibliotheken wie Direct3D und OpenGL und stellt eine Schnittstelle bereit, die auf Weltobjekten und anderen High-Level-Klassen basiert.
Analysieren wir OGRE mit CppDepend, um die Stärken seines Designs zu entdecken.

Architektur von OGRE3D
Der Abhängigkeitsgraph zeigt die Beziehungen zwischen den OGRE3D-Projekten:

Die Architektur ist pluginorientiert. Dadurch lässt sich Ogre3D erweitern, ohne das Kernel-Projekt „OgreMain“ ändern zu müssen. Zur Unterstützung dieser Architektur stellt OgreMain die Klassen bereit, die für die Kommunikation mit den Plugins benötigt werden.
Sehen wir uns an, wie das Kernel-Projekt mit den Plugins kommuniziert. Dazu suchen wir mit der folgenden Abfrage nach den OgreMain-Klassen, die vom Plugin RenderSystem_GL verwendet werden:
SELECT TYPES WHERE IsDirectlyUsedBy "RenderSystem_GL"

Das Plugin verwendet einige Hilfsklassen und Modell-Entitäten und überschreibt mehrere abstrakte Klassen, um sich in das Ogre3D-Ökosystem zu integrieren.
Filtern wir das Ergebnis und suchen wir ausschließlich nach den abstrakten Klassen, die vom Plugin RenderSystem_GL verwendet werden:
SELECT TYPES WHERE IsDirectlyUsedBy "RenderSystem_GL" AND IsAbstract

Das Plugin kann Szenen, Texturen, Rendering-Verhalten und vieles mehr überschreiben. Ogre3D bietet eine leistungsfähige API, mit der sich das Rendering einfach anpassen und damit sehr leicht auf die eigenen Anforderungen zuschneiden lässt.
Vererbung und Polymorphie
Ein objektorientierter Ansatz kann dazu verleiten, Vererbung übermäßig einzusetzen, um die Möglichkeiten der Polymorphie möglichst umfassend auszunutzen.
Dies gilt insbesondere für OgreMain, das den Kern des Ogre3D-Frameworks bildet. Viele Klassen sind dort so konzipiert, dass sie von den Plugin-Projekten überschrieben werden. SELECT TYPES FROM PROJECTS "OgreMain" WHERE NbBaseClass >0

Mehrfachvererbung erhöht jedoch die Komplexität und sollte daher mit Bedacht eingesetzt werden. Suchen wir nach Klassen mit vielen Basisklassen.

Nur wenige Klassen erben von mehr als einer Klasse.
Abstraktheit
Bei einer pluginorientierten Architektur benötigt der Host zahlreiche abstrakte Klassen, damit er flexibel und erweiterbar bleibt.
Let's search for abstract classes in OgreMain: SELECT TYPES FROM PROJECTS "OgreMain" WHERE IsAbstract

Schichtung der Namespaces
CppDepend stellt einen DSM-Graphen bereit. Durch Triangularisierung der Matrix können wir uns auf die roten Ränder konzentrieren, die Abhängigkeitszyklen hervorheben.

Zwischen Ogre, Ogre::EmitterCommands und Ogre::OverlayElementCommands besteht ein Abhängigkeitszyklus. Eine solche Abhängigkeit ist nicht zwangsläufig problematisch, doch ihre Vermeidung fördert eine lose Kopplung. Dieser interessante post erläutert die Vorteile einer Schichtenarchitektur.
Suchen wir nach dem Ursprung der Abhängigkeit zwischen Ogre und den beiden anderen Namespaces. SELECT TYPES WHERE IsDirectlyUsedBy "Ogre"

Der Namespace Ogre verwendet sämtliche Cmd-Klassen aus den anderen Namespaces. Alle diese Klassen werden von Ogre::ParticleEmitter als statische Felder verwendet; ParticleEmitter fügt sie dem CmdParam-Dictionary hinzu.
Möglicherweise lässt sich dies anders implementieren und der Abhängigkeitszyklus dadurch vermeiden, auch wenn er kein gravierendes Problem darstellt.
Kohäsion von Typen
Das Single-Responsibility-Prinzip besagt, dass eine Klasse nicht mehr als einen Grund für Änderungen haben sollte. Eine solche Klasse gilt als kohäsiv. Ein hoher LCOM-Wert weist im Allgemeinen auf eine geringe Kohäsion hin. Es gibt mehrere LCOM-Metriken. LCOM nimmt Werte im Bereich [0–1] an. LCOMHS (HS steht für Henderson-Sellers) liegt im Bereich [0–2]. Die LCOMHS-Metrik gilt häufig als besonders geeignet, nicht kohäsive Typen zu erkennen. Ein LCOMHS-Wert über 1 sollte als Warnsignal betrachtet werden. SELECT TYPES WHERE LCOMHS > 0.95 AND NbFields > 10 AND NbMethods >10 AND !IsGlobal

Nur wenige Klassen weisen eine geringe Kohäsion auf.
Verwendete Entwurfsmuster
Singleton
Um sicherzustellen, dass von einer Klasse nur eine einzige Instanz existiert, bietet sich das Singleton-Pattern an.
Suchen wir nach Klassen, die als Singleton implementiert sind:
SELECT TYPES WHERE DeriveFrom "Ogre.Singleton"

Wie wir sehen, sind fast alle Manager-Klassen Singletons. Im Allgemeinen ist ein Manager ein guter Kandidat für dieses Pattern. Wir können außerdem nach Manager-Klassen suchen, die nicht von Singleton ableiten. SELECT TYPES WHERE !DeriveFrom "Ogre.Singleton" AND NameLike "Manager$"

Nur wenige Manager erben nicht von Singleton. Das ist nachvollziehbar, da diese Klassen mehrfach instanziiert werden können.
Factory
Das Factory-Pattern eignet sich hervorragend, um die Erzeugung von Objekten zu abstrahieren. Es fördert eine geringe Kopplung und eine hohe Kohäsion, wie in diesem post.

Eine Factory garantiert allerdings nicht, dass eine Instanz tatsächlich über sie erzeugt wird, da die Klasse weiterhin direkt instanziiert werden kann. Deshalb benötigen wir eine Regel, die sicherstellt, dass die Factory für die Instanziierung verwendet wird.
Für die Klasse Entity können wir beispielsweise die folgende Regel definieren, um jede Klasse zu ermitteln, die sie instanziiert: SELECT METHODS WHERE DepthOfCreateA "Ogre.Entity" == 1

Wie wir sehen, instanziiert ausschließlich EntityFactory die Klasse Entity.
Manager
Manager-Klassen ermöglichen den Zugriff auf Subsysteme und sind sehr hilfreich bei der Modularisierung eines Projekts. Ogre3D enthält zahlreiche Manager, von denen jeder ein anderes Subsystem repräsentiert.
SELECT TYPES WHERE NameLike "Manager$"

Fassade
Eine Fassade definiert eine übergeordnete Schnittstelle, die die Nutzung eines Subsystems vereinfacht. Mithilfe der Metrik Efferent Coupling lassen sich mögliche Fassaden in einem Projekt erkennen. Die efferente Kopplung eines Typs entspricht der Anzahl der Typen, von denen er direkt abhängt. Typen mit TypeCe > 50 hängen von sehr vielen anderen Typen ab. Sie sind komplex, besitzen häufig mehrere Verantwortlichkeiten und sind gute Kandidaten für ein Refactoring.
Bei einer Fassade kann ein hoher TypeCe-Wert jedoch völlig normal sein.
Let’s search for classes with a high TypeCe: SELECT TYPES ORDER BY TypeCe DESC

Nicht alle diese Klassen sind Fassaden, und für den hohen TypeCe-Wert kann es jeweils einen guten Grund geben. Einige ließen sich möglicherweise refaktorieren, doch dafür kennen wir Ogre3D nicht gut genug, um eine sichere Aussage zu treffen.
Die am häufigsten verwendete Fassade ist die Klasse Root. Sehen wir uns an, welche Subsysteme sie verwendet.

Wie wir sehen, verwendet die Klasse Root nahezu alle Manager-Klassen.
Mit der folgenden CQL-Abfrage können wir außerdem nach Managern suchen, die nicht über Root erreichbar sind:
SELECT TYPES WHERE !IsDirectlyUsedBy "Ogre.Root" AND NameLike "Manager$" AND !IsAbstract

Observer
Beim Observer-Pattern verwaltet ein Objekt, das sogenannte Subject, eine Liste seiner abhängigen Observer und benachrichtigt diese automatisch über Zustandsänderungen, meist durch den Aufruf einer ihrer Methoden. Das Pattern wird vor allem zur Implementierung von Ereignisbehandlungssystemen eingesetzt.
Ogre3D verwendet Listener-Klassen zur Implementierung des Observer-Patterns.
Suchen wir nach Listener-Klassen im OgreMain-Projekt. SELECT TYPES FROM PROJECTS "OgreMain" WHERE NameLike "Listener$"

Fazit
Ogre3D ist ein sehr sauberes, gut entworfenes und hervorragend dokumentiertes Framework. Der Zweck der eingesetzten Entwurfsmuster lässt sich leicht nachvollziehen, und die Modularität hilft dabei, die Möglichkeiten des Frameworks schneller zu verstehen.
