Ein Design Pattern ist eine allgemein wiederverwendbare Lösung für ein häufig auftretendes Problem in einem bestimmten Kontext des Softwaredesigns. Patterns sind formalisierte Best Practices, mit denen Entwickler typische Probleme beim Entwurf einer Anwendung oder eines Systems lösen können.
Die Patterns der „Gang of Four“ gehören wohl zu den bekanntesten. Daneben gibt es jedoch grundlegende Designprinzipien, die unter Entwicklern weniger bekannt sind: die General Responsibility Assignment Software Principles, kurz GRASP.
Hier ist die Definition aus Wikipedia:
General responsibility assignment software patterns (or principles), abbreviated GRASP, bestehen aus Richtlinien zur Zuweisung von Verantwortlichkeiten an Klassen und Objekte im objektorientierten Design. Zu den in GRASP verwendeten Mustern und Prinzipien gehören: Controller, Creator, Indirection, Information Expert, High Cohesion, Low Coupling, Polymorphism, Protected Variations und Pure Fabrication. Jedes dieser Muster adressiert ein bestimmtes Softwareproblem, und solche Probleme treten in nahezu jedem Softwareentwicklungsprojekt auf. Diese Techniken wurden nicht erfunden, um völlig neue Arbeitsweisen zu schaffen, sondern um bewährte Programmierprinzipien des objektorientierten Designs besser zu dokumentieren und zu standardisieren.
Irrlicht ist eine 3D-Engine-Bibliothek, die viele GRASP-Prinzipien nutzt. Sehen wir uns an, welche Vorteile der Einsatz solcher Patterns bietet.
Creator
Das Erzeugen von Objekten gehört zu den häufigsten Aufgaben in einem objektorientierten System. Zu bestimmen, welche Klasse für die Objekterzeugung verantwortlich ist, ist ein grundlegender Aspekt der Beziehungen zwischen Objekten bestimmter Klassen. Nehmen wir die GUI-Skin-Klasse als Beispiel und untersuchen, wo sie in der Irrlicht-Bibliothek erzeugt wird. Dazu können wir folgende CQLinq-Abfrage ausführen:
SELECT METHODS WHERE DepthOfCreateA “irr.gui.CGUISkin” == 1

CGUIEnvironment ist die einzige Klasse, die CGUISkin-Instanzen erzeugt. Auch fast alle GUI-Elemente werden von CGUIEnvironment erstellt – mit Ausnahme von CGUIButton.
SELECT METHODS WHERE DepthOfCreateA “irr.gui.CGUIButton” == 1

Wie wir sehen, wird CGUIButton an drei verschiedenen Stellen erzeugt. Es könnte sinnvoll sein, den Code zu refaktorieren und die Verantwortung für die Erzeugung – wie bei den anderen GUI-Klassen – an CGUIEnvironment zu delegieren.
Controller
The controller -Pattern weist die Verantwortung für die Verarbeitung von Systemereignissen einer Nicht-UI-Klasse zu, die das Gesamtsystem oder ein Use-Case-Szenario repräsentiert. Ein Controller-Objekt ist somit ein Objekt ohne Benutzeroberfläche, das Systemereignisse empfängt oder verarbeitet.
Ein Use-Case-Controller sollte verwendet werden, um alle Systemereignisse eines Use Cases zu verarbeiten und kann auch für mehrere Use Cases eingesetzt werden (beispielsweise für die Use Cases Create User und Delete User, one can have a single UserController, instead of two separate use case controllers).
Sehen wir uns den Controller für die GUI-Elemente an. Er muss zumindest die Ereignisverarbeitung übernehmen; diese wird von CGUIEnvironment::OnEvent ausgeführt.
Sehen wir uns an, welche Methoden von OnEvent verwendet werden:
SELECT METHODS WHERE IsUsedBy “irr.gui.CGUIEnvironment.OnEvent(constSEvent&)”

Die ausgelösten Ereignisse werden von einer Klasse verarbeitet, die die abstrakte Klasse IEventReceiver implementiert. Sehen wir uns an, welche Klassen sie implementieren.

Jedes GUI-Element verarbeitet die Ereignisse, die es selbst betreffen.
Welche weiteren Verantwortlichkeiten hat CGUIEnvironment?
Wie bereits gesehen, erzeugt diese Klasse die konkreten Klassen und verwaltet außerdem die Ereignisverarbeitung. Um weitere Verantwortlichkeiten zu erkennen, können wir nach den von dieser Klasse verwendeten Methoden suchen:
SELECT METHODS WHERE IsDirectlyUsedBy “irr.gui.CGUIEnvironment”

Diese Klasse verwendet außerdem einige Klassen aus dem Namespace irr::io, um XML-Dateien zu speichern und zu laden. Möglicherweise trägt sie viele Verantwortlichkeiten, was ihre Kohäsion beeinträchtigen kann. Das ist jedoch noch vertretbar, da sie über alle für die Persistierung benötigten Daten verfügt und damit dem GRASP-Prinzip „Information Expert“ folgt.
Geringe Kopplung
Eine geringe Kopplung ist wünschenswert, weil Änderungen in einem Bereich der Anwendung dadurch weniger Anpassungen im restlichen System erfordern. Langfristig lassen sich so viel Zeit, Aufwand und Kosten bei Änderungen und der Einführung neuer Funktionen sparen.
Abstrakte Klassen können dabei helfen, eine geringe Kopplung zu erreichen. Den Abstraktionsgrad eines Moduls können wir mit folgender Metrik bewerten:
A = Na / NcDabei gilt: * A = Abstraktheit eines Moduls. Null steht für ein vollständig konkretes Modul, eins für ein vollständig abstraktes Modul. * Na = Anzahl der abstrakten Klassen im Modul. * Nc = Anzahl der konkreten Klassen im Modul.
Der Abstraktionsgrad von Irrlicht beträgt 0,1245972; die Codebasis enthält 125 abstrakte Klassen. Im Namespace irr::gui gibt es 28 abstrakte Klassen – für jedes GUI-Element existiert eine entsprechende Schnittstelle.
SELECT TYPES FROM NAMESPACES “irr.gui” WHERE IsAbstract

CppDepend stellt eine DSM bereit. Wir können diese Matrix triangularisieren, um uns auf die roten Ränder zu konzentrieren, die stark voneinander abhängige Klassen hervorheben, und um Module zu erkennen.

Wie wir sehen, sind alle abstrakten Klassen zusammen gruppiert. Sie könnten in einem anderen Namespace oder sogar in einem separaten Projekt isoliert werden. Um den Nutzen abstrakter Klassen für eine geringe Kopplung zu erkennen, suchen wir nach Klassen, die die konkrete Klasse CGUISkin verwenden.
SELECT METHODS WHERE IsDirectlyUsing “irr.gui.CGUISkin”

Nur eine Klasse kennt diese Klasse direkt: ihr Erzeuger. Die übrigen konkreten Klassen werden über abstrakte Klassen verwendet, was eine lose Kopplung fördert.
Wie sieht es mit der Kopplung zwischen Namespaces aus?

Zwischen einigen Namespaces besteht ein Abhängigkeitszyklus. Eine solche Abhängigkeit ist nicht zwangsläufig problematisch; ihre Vermeidung fördert jedoch eine lose Kopplung.
Wir können auch untersuchen, wie die Namespaces miteinander interagieren. Sehen wir uns dazu an, welche Klassen und Methoden der Namespace „irr“ verwendet.
SELECT METHODS WHERE IsDirectlyUsedBy “irr”

Wie wir sehen, laufen fast alle Interaktionen mit den anderen Namespaces über abstrakte Klassen – mit Ausnahme von irr::video::CVideoModeList und irr::scene::CMeshBuffer.
Untersuchen wir den Ursprung der Abhängigkeit von irr::video::CVideoModeList. Dazu können wir folgende CQLinq-Abfrage ausführen:
SELECT METHODS OUT OF TYPES “irr.video.CVideoModeList” WHERE IsUsing “irr.video.CVideoModeList”

Die Klasse irr::CIrrDeviceWin32 verwendet sie, weil sie ein Feld als video::CVideoModeList statt als video::IVideoModeList deklariert. Ein Refactoring könnte hier die Interaktion zwischen den Namespaces verbessern.
Hohe Kohäsion
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 liegt im Bereich [0–1], LCOMHS (HS steht für Henderson-Sellers) im Bereich [0–2]. LCOMHS gilt häufig als effektiver bei der Erkennung nicht kohäsiver Typen. Ein LCOMHS-Wert über 1 sollte als Warnsignal betrachtet werden.
SELECT TYPES WHERE LCOMHS > 0.95 AND NbFields > 10 AND NbMethods >10 AND!IsGlobal ORDER BY LCOMHS DESC

Nur wenige Klassen werden als nicht kohäsiv eingestuft.
Fazit
Irrlicht nutzt Namespaces zur Modularisierung der Codebasis und abstrakte Klassen zur Verringerung der Kopplung. Dadurch ist der Code sehr gut verständlich und wartbar. Wer die Qualität seines Softwaredesigns verbessern möchte, findet hier ein gutes Vorbild.
