Blog 6 min read

GRASP Principles: Exploring Irrlicht 3D Engine

Share this article
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.

The “Gang of Four” patterns are maybe the most popular ones. However, there are some basic design principles not well known by developers: 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 that uses many GRASP principles; let’s discover the benefits of using these kinds of patterns.

Creator

Creating 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 the GUI skin class as an example 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

CGUIEnvironment is the only class that creates the CGUISkin instances, and almost all GUI elements are created by the CGUIEnvironment class, except CGUIButton.

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

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

Controller

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 controller for the GUI elements, which must at least manage event processing; this processing is performed by CGUIEnvironment::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 explore which classes implement it.

Each gui element treats the events related to it.

What are the other responsibilities of CGUIEnvironment?

As we have seen before, this class creates the concrete classes and also manages event processing; to discover if it has other responsibilities, we can search for the methods used by this class:

SELECT METHODS WHERE IsDirectlyUsedBy “irr.gui.CGUIEnvironment”

This class also uses some classes from the irr::io namespace to persist and load XML files. Maybe this class has many responsibilities, which can impact its cohesion, but it’s still tolerable because this class has all the data needed to persist data — it 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 low coupling, and we can evaluate the abstractness of a given module with 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 Irrlicht is 0.1245972, and it contains 125 abstract classes. And in the irr::gui namespace there are 28 abstract classes — for each GUI element there’s an equivalent interface.

SELECT TYPES FROM NAMESPACES “irr.gui” WHERE IsAbstract

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

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

SELECT METHODS WHERE IsDirectlyUsing “irr.gui.CGUISkin”

Only one class knows this class directly: its creator. The other concrete classes are used through the abstract classes, which enforces loose coupling.

What about the coupling between namespaces:

A dependency cycle exists between some namespaces; having this dependency is not necessarily problematic. However, avoiding it enforces loose coupling.

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

SELECT METHODS WHERE IsDirectlyUsedBy “irr”

As we can see, almost all interactions with the other namespaces go through the abstract classes, except for irr::video::CVideoModeList and irr::scene::CMeshBuffer.

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

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

The irr::CIrrDeviceWin32 class uses it because it 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 not 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. LCOM takes its values in the range [0-1]. LCOMHS (HS stands for Henderson-Sellers) takes its values in the range [0-2]. Note that the LCOMHS metric is often considered more efficient at detecting non-cohesive types. An 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 non-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.

Share this article