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 reads Radius component and writes Area component
  • ComputeRectangleAreaSystem reads Width and Height and writes Area
  • ComputeTriangleAreaSystem reads Base and Height and writes Area

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.