The Dependency Inversion Principle is part of the SOLID principles. If you want a formal definition of DIP, please read the articles written by Martin and Schuchert.
We explained that, in order to invert the control of the flow, our specialized code must never call directly methods of more abstracted classes. The methods of the classes of our framework are not explicitly used, but it is the framework to control the flow of our less abstracted code. This is also called (sarcastically) the Hollywood principle, stated as “don’t call us, we’ll call you.”.
Although the framework code must take control of less abstracted code, it would not make any sense to couple our framework with less abstracted implementations. A generic framework doesn’t have the faintest idea of what our game needs to do and, therefore, wouldn’t understand anything declared outside its scope.
Hence, the only way our framework can use objects defined in the game layer, is through the use of interfaces, but this is not enough. The framework cannot know interfaces defined in a less abstracted layer of our application, this would not make any sense as well.
The Dependency Inversion Principle introduces a new rule: our less abstracted objects must implement interfaces declared in the higher abstracting layers. In other words, the framework layer defines the interfaces that the game entities must implement.
In a practical example, RendererSystem handles a list of IRendering “Nodes”. The IRendering node is an interface that declares, as properties, all the components needed to render the Entities, such as GetWorldMatrix, GetMaterial and so on. Both the RendererSystem class and the IRendering interface are declared inside the framework layer. Our specialised code needs to implement IRendering in order to be usable by the framework.
Designing layered code
So far I used the word “framework” to identify the most abstracted code and “game” to identify the least abstracted code. However framework and game don’t mean much. Limiting our layers to just “the game layer” and “the framework layer” would be a mistake. Let’s say we have systems that handle very generic problems that can be found in every game, like the rendering of the entities, and we want to enclose this layer in to a namespace. We have defined a layer that can be even compiled in a DLL and be shipped with whatever game.
Now, let’s say we have to implement the logic that is closer to the game domain. Let’s say that we want to create a HealthSystem that handles the health of the game entities with health. Is HealthSystem part of a very generic framework? Surely not. However while HealthSystem will handle the common logic of the IHaveHealth entities, not all these entities will be of the same type. Hence HealthSystem is more abstracted than more specialised entity behavior implementations. While this abstraction wouldn’t probably justify the creation of another framework, I believe that thinking in terms of layered code helps designing better systems and nodes.
Putting ECS, IoC and DIP all together
As we have seen the flow is not inverted when, in order to find a solution, a bottom up design approach is used to break down the problem or in other words when the specialized behaviors of the entities are modeled before the generic ones.
In my vision of Inversion of Control, it’s needed to break down the solutions using a top down approach. We should think of the solutions starting from the most abstracted classes. What are the common behaviors of the game entities? What are the most abstracted systems we should write? What once would have been solved specializing classes through inheritance, it should be now solved layering our systems within different levels of code abstraction and declaring the relative interfaces to be used by the less abstracted code. Generic systems should be written before the specialized ones.
I believe that in this way we could benefit from the following:
- We will be sure that our systems will have just one responsibility, modeling just one behavior
- We will basically never break the Open/Close principle, since new behaviors means creating new systems
- We will inject way less dependencies, avoiding using a IoC container as a Singleton alternative
- It will be simpler to write reusable code
- We could potentially achieve real encapsulation
In the next article I will explain how I would put all these concepts together in practice
I strongly suggest to read all my articles on the topic: