ECS for die-hard OO developers
When you are a die-hard OO developer, I guess it is safe to assume you are familiar with concept of dependency injection. And I don’t mean a particular dependency injection framework, I mean the concept, which also some times called inversion of control, or the Hollywood Principle.
Dependency injection in OO is used primarily to decouple code and make it more accessible for unit testing and future evolutions. Most of the classes are not self sufficient (for multiple reasons) and therefore rely on other classes to hold data, or expose methods. Those other classes are called dependencies.
If a class creates instances of it’s dependencies by itself, it becomes opaque / non configurable. When we are following the dependency injection principal, we expose the dependencies of the class, making it transparent and configurable from the outside.
In order to reduce coupling to a certain class even further, it is considered best practice to define dependencies as an interface/protocol. This way different implementations can be provided to the class without big hassle. This technique increases, what I previously called accessibility for unit testing and future evolutions.
In canonical ECS we don’t think in terms of classes, we think in terms of entities and systems. An entity is a logical aggregation of components and a system is behaviour which transforms data / state. Systems follow the dependency injection principal, but in case of the system — dependency is data. A system describes which data it needs in order to achieve the data transformation.
For die-hard OO developers I need a die-hard OO example. Lets take the calculating area example. It is used in many tutorials about SOLID design principals.
In a typical OO fashion we would define a
Shape interface/protocol which will have a
ComputeArea method. This
ComputeArea method returns the computed result. Every class, which can be seen as a shape (e.g. Circle, Rectangle and Triangle). Implements
Shape and provides it’s own implementation of the
As I mentioned before in ECS we don’t think in terms of classes, we think in systems. A system performs a concrete transformation. There is no generic shape system. We need to have a look at the data at hand and figure out what kind of systems we need to introduce in order to calculate the area.
For an area of circle we need a radius. So a
ComputeCircleAreaSystem need to ask for all entities which have
ComputeRectangleAreaSystem asks for all entities which have
Height components. And
ComputeTriangleAreaSystem needs all entities which have
I like to say that in ECS we design bottom-up. We look at data and which behaviour depends on which data. In OO we design top-down, we search for abstractions and generic behaviours / definitions.
Now here is another twist you might not think of while being a die-hard OO developer. A system in ECS does not return values. In our OO solution, we defined a generic
ComputeArea method which returns the computed value. Normally we don’t just compute values, we compute them so we can process them further. Meaning that the result of
ComputeArea method call will be stored somewhere, or passed to another method.
In ECS, systems which compute an area, adds an
Area component to the entity, they used to compute the area. This means that:
Radiuscomponent and writes
Say we want to compute the sum of all areas. In order to do this, we need to define a system
ComputeSumOfAllAreasSystem. This systems gets all entities which have
Area component and can sum them up, storing the result in a separate component, or producing a side effect like printing to screen, or sending the result over the network.
As you can see, with ECS we are able to avoid abstractions.
ComputeSumOfAllAreasSystem is not dependent on an abstract
Shape which has a generic
ComputeArea method. It is dependent on
Area component (data). It does not care about the origin of the data. The Area value could be created due to a computation based on other components, or set directly based on user input, data received from network etc…
This is a high degree of decoupling business logic, which in terms leads to accessibility for unit testing and future evolutions.
The goals of ECS and dependency injection in OO are similar, with the difference that ECS goes bottom-up and OO goes top-down.