GRASP Principles: Exploring Irrlicht 3D Engine

A design pattern is a general reusable solution to a commonly occurring problem within a given context in software design. Patterns are formalized best practices that the programmer can use to solve common problems when designing an application or system.

“Gang of Four” patterns are maybe the most popular ones. However, there are some basic design principles not well-known by developers, it’s the General Responsibility Assignment Software Principles, abbreviated GRASP.

Here’s its definition from Wikipedia:

General responsibility assignment software patterns (or principles), abbreviated GRASP, consist of guidelines for assigning responsibility to classes and objects in object-oriented design.

The different patterns and principles used in GRASP are: controller, creator, indirection, information expert, high cohesion, low coupling, polymorphism, protected variations, and pure fabrication. All these patterns answer some software problem, and these problems are common to almost every software development project. These techniques have not been invented to create new ways of working, but to better document and standardize old, tried-and-tested programming principles in object-oriented design.

Irrlicht is a 3D engine library using many GRASP principles, let’s discover the benefits of using this kind of patterns.

Creator

Creation of objects is one of the most common activities in an object-oriented system. Which class is responsible for creating objects is a fundamental property of the relationship between objects of particular classes.
Let’s take as example the Gui skin class and discover where in the Irrlicht library it’s created. For that we can execute this cqlinq query:

SELECT METHODS WHERE DepthOfCreateA “irr.gui.CGUISkin” == 1

The CGUIEnvironement is the only class that creates the CGUISkin instances, and almost all the GUIElement are created by CGUIEnvironement class except the CGUIButton class.

SELECT METHODS WHERE DepthOfCreateA “irr.gui.CGUIButton” == 1

As we observe the CGUIButton is created in three different places, maybe it’s better to refactor the code and delegate the creation responsibility to the CGUIEnvironement class like all the other GUI classes.

Controler

The controller pattern assigns the responsibility of dealing with system events to a non-UI class that represents the overall system or a use case scenario. A controller object is a non-user interface object responsible for receiving or handling a system event.

A use case controller should be used to deal with all system events of a use case and may be used for more than one use case (for instance, for use cases Create User and Delete User, one can have a single UserController, instead of two separate use case controllers).

Let’s discover the Controler for the GUIElements which must at least manage the event processing, this treatment is processed by CGuiEnvironement::OnEvent.

Let’s see which methods are used by OnEvent

SELECT METHODS WHERE IsUsedBy “irr.gui.CGUIEnvironment.OnEvent(constSEvent&)”

The events fired are processed by a class that implements the IEventReceiver abstract class. Let’s explorer which classes implements it.

Each gui element treats the events related to it.

What are the other responsibilities of CGUIEnvironement?

As we have seen before this class create the concrete classes and also manage the event processing, and to discover if it does another responsibility we can search for methods used by this class:

SELECT METHODS WHERE IsDirectlyUsedBy “irr.gui.CGUIEnvironment”

This class use also some classes from irr::io namespace to persist and load into xml files ,  maybe this class has many responsibilities and it can impact its cohesion, but it still tolerable because this class has all the data needed to persist data,  this class follows the “Information Expert” principle of GRASP.

Low Coupling

Low coupling is desirable because a change in one area of an application will require fewer changes throughout the entire application. In the long run, this could alleviate a lot of time, effort, and cost associated with modifying and adding new features to an application.

Using abstract classes can improve the low coupling and we can evaluate the abstractness of a defined module by the following metric:

A = Na / Nc

Where:
* A = abstractness of a module Zero is a completely concrete module. One is a completely abstract module.
* Na = number of abstract classes in the module.
* Nc = number of concrete classes in the module.

The abstractness of Irrlich is equal to 0.1245972, and it contains 125 abstract classes.
And for irr::gui namespaces there are 28 abstract classes, for each GUI element there’s the equivalent interface.

SELECT TYPES FROM NAMESPACES “irr.gui” WHERE IsAbstract

CppDepend provides the  DSM graph, and we can triangularize this matrix to focus on red borders highly dependent classes, and to detect modules.

As we can observe all the abstract classes are grouped, and they can be isolated in another namespace or maybe in another project.
And to see the benefit of using an abstract class to improve the low coupling, let’s search for classes that use the concrete class CGUISkin.

SELECT METHODS WHERE IsDirectlyUsing “irr.gui.CGUISkin”

Only one class know directly this class, it’s his creator. And the other concrete classes are used through the abstract classes which is enforce the loose coupling.

What about the coupling between namespaces:

A dependency cycle exists between some namespaces, having this dependency could be not problematic.However, avoiding it enforce the loose coupling.

We can also discover how the namespaces interact with each other. For that let’s see what the namespace “irr” use as classes and methods.

SELECT METHODS WHERE IsDirectlyUsedBy “irr”

As we can see almost all interaction with the other namespaces pass by the abstract classes, except for irr::video::CVideoModelList and irr::scene::CMeshBuffer.

Let’s discover the origin of the dependency with irr::video::CVideoModelList. For that we can execute the following CQLinq query:

SELECT METHODS OUT OF TYPES “irr.video.CVideoModeList” WHERE IsUsing “irr.video.CVideoModeList”

The class  irr::CIrrDeviceWin32 use it because this class declares a field as video::CVideoModeList instead of video::IVideoModeList. Maybe a refactoring could be done to improve the interaction between the namespaces.

High Cohesion

The single responsibility principle states that a class should have more than one reason to change. Such a class is said to be cohesive. A high LCOM value generally pinpoints a poorly cohesive class. There are several LCOM metrics. The LCOM takes its values in the range [0-1]. The LCOMHS (HS stands for Henderson-Sellers) takes its values in the range [0-2]. Note that the LCOMHS metric is often considered as more efficient to detect non-cohesive types.
LCOMHS value higher than 1 should be considered alarming.

SELECT TYPES WHERE LCOMHS > 0.95 AND NbFields > 10 AND NbMethods >10 AND!IsGlobal ORDER BY LCOMHS DESC

Only a few classes are considered as no cohesive.

Conclusion

Irrlicht uses namespaces to modularize the code base and abstract classes to improve low coupling which makes it very easy to understand and maintain.
It’s a good example to follow if you want to improve your design quality.