ECS for die-hard OO developers
Another attempt to explain Entity Component System
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 ComputeArea
method.
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 Radius
component.
ComputeRectangleAreaSystem
asks for all entities which have Width
and Height
components. And ComputeTriangleAreaSystem
needs all entities which have Base
and Height
components.
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 genericComputeArea
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:
ComputeCircleAreaSystem
readsRadius
component and writesArea
componentComputeRectangleAreaSystem
readsWidth
andHeight
and writesArea
ComputeTriangleAreaSystem
readsBase
andHeight
and writesArea
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.
Conclusion
The goals of ECS and dependency injection in OO are similar, with the difference that ECS goes bottom-up and OO goes top-down.