Introduction
Entity–component systems are an object-oriented architectural technique.
There is no universal consensus of what the term means, same as object-oriented programming. However, it is clear that entity–component systems are specifically intended as an architectural alternative to inheritance. Inheritance hierarchies are natural for expressing what an object is, but in certain kinds of software (such as games), you would rather express what an object does.
It is a different object model than the “classes and inheritance” one to which you’re most likely accustomed from working in C++ or Java. Entities are as expressive as classes, just like prototypes as in JavaScript or Self—all of these systems can be implemented in terms of one another.
Examples
Let’s say that Player
is an entity with Position
, Velocity
, and KeyboardControlled
components, which do the obvious things.
entity Player:
Position
Velocity
KeyboardControlled
We know Position
must be affected by Velocity
, and Velocity
by KeyboardControlled
. The question is how we would like to model those effects.
Entities, Components, and Systems
Suppose that components have no references to one another; an external Physics
system traverses all Velocity
components and updates the Position
of the corresponding entity; an Input
system traverses all KeyboardControlled
components and updates the Velocity
.
Player
+--------------------+
| Position | \
| | Physics
/ | Velocity | /
Input | |
\ | KeyboardControlled |
+--------------------+
This satisfies the criteria:
The systems are now responsible for handling events and enacting the behaviour described by the components. They are also responsible for handling interactions between entities, such as collisions.
Entities and Components
However, suppose that components do have references to one another. Now the entity is simply a constructor which creates some components, binds them together, and manages their lifetimes:
class Player:
construct():
this.p = Position()
this.v = Velocity(this.p)
this.c = KeyboardControlled(this.v)
The entity might now dispatch input and update events directly to its components. Velocity
would respond to updates, and KeyboardControlled
would respond to input. This still satisfies our criteria:
Here component interactions are explicit, not imposed from outside by a system. The data describing a behaviour (what is the amount of velocity?) and the code that enacts it (what is velocity?) are coupled, but in a natural fashion. The data can be viewed as parameters to the behaviour. And some components don’t act at all—a Position
is the behaviour of being in a place.
Interactions can be handled at the level of the entity (“when a Player
collides with an Enemy
…”) or at the level of individual components (“when an entity with Life
collides with an entity with Strength
…”).
Components
What is the reason for the entity to exist? If it is merely a constructor, then we can replace it with a function returning a set of components. If we later want to query entities by their type, we can just as well have a Tag
component which lets us do just that:
function Player():
t = Tag("Player")
p = Position()
v = Velocity(p)
c = KeyboardControlled(v)
return {t, p, v, c}
Interactions must now be handled by abstract queries, completely decoupling events from entity types. There are no more entity types to query—arbitrary Tag
data is probably better used for debugging than game logic.
Conclusion
Entities are not functions, rules, actors, or dataflow combinators. They are nouns which model concrete phenomena—in other words, they are objects. It is as Wikipedia says—entity–component systems are a software architecture pattern for modeling general objects.
The article you mention is about modularity in general, and it would apply equally to structured, functional, and object-oriented programs. I have heard of that article before from someone who was a big OOP guy, but I read it as an article about programming in general, not something OOP specific. There is a famous article about functional programming, Why Functional Programming Matters, and the first sentence of the conclusion states "In this paper, we’ve argued that modularity is the key to successful programming." So the answer to (1) is no.
Well designed functions don't assume more about their data than they need to, so the part about "intimately aware of the data" is wrong. (Or at least as wrong as it would be of OOP. You can't program strictly at a high level of abstraction and ignore all details forever in any paradigm. In the end, some part of the program does actually need to know about the specific details of the data.)
Data hiding is an OOP specific term, and it isn't exactly the same as the information hiding discussed in the article. Information hiding in the article is about design decisions that were hard to make or are likely to change. Not every design decision about a data format is hard or likely to change, and not every decision that is hard or likely to change is about a data format. Personally, I can't see why OO programmers want everything to be an object. Sometimes, a simple data structure is all you need.
Edit:
I found a relevant quote from an interview with Rich Hickey.
Fogus: Following that idea—some people are surprised by the fact that Clojure does not engage in data-hiding encapsulation on its types. Why did you decide to forgo data-hiding?
Hickey: Let’s be clear that Clojure strongly emphasizes programming to abstractions. At some point though, someone is going to need to have access to the data. And if you have a notion of “private”, you need corresponding notions of privilege and trust. And that adds a whole ton of complexity and little value, creates rigidity in a system, and often forces things to live in places they shouldn’t. This is in addition to the other losing that occurs when simple information is put into classes. To the extent the data is immutable, there is little harm that can come of providing access, other than that someone could come to depend upon something that might change. Well, okay, people do that all the time in real life, and when things change, they adapt. And if they are rational, they know when they make a decision based upon something that can change that they might in the future need to adapt. So, it’s a risk management decision, one I think programmers should be free to make.
If people don’t have the sensibilities to desire to program to abstractions and to be wary of marrying implementation details, then they are never going to be good programmers.
Best Answer
The main reason FP aims for this and C# OOP does not is that in FP the focus is on referential transparency; that is, data goes into a function and data comes out, but the original data is not changed.
In C# OOP there's a concept of delegation of responsibility where you delegate an object's management to it, and therefore you want it to change its own internals.
In FP you never want to change the values in an object, therefore having your functions embedded in your object doesn't make sense.
Further in FP you have higher kinded polymorphism allowing your functions to be far more generalized than C# OOP allows. In this way you may write a function that works for any
a
, and therefore having it embedded in a block of data doesn't make sense; that would tightly couple the method so that it only works with that particular kind ofa
. Behaviour like that is all well and common in C# OOP because you don't have the ability to abstract functions so generally anyway, but in FP it's a tradeoff.The biggest problem I've seen in anemic domain models in C# OOP is that you end up with duplicate code because you have DTO x, and 4 different functions that commits activity f to DTO x because 4 different people didn't see the other implementation. When you put the method directly on DTO x, then those 4 people all see the implementation of f and reuse it.
Anemic data models in C# OOP hinder code reuse, but this isn't the case in FP because a single function is generalized across so many different types that you get greater code reuse since that function is usable in so many more scenarios than a function you would write for a single DTO in C#.
As pointed out in comments, type inference is one of the benefits FP relies on to allow such significant polymorphism, and specifically you can trace this back to the Hindley Milner type system with Algorithm W type inference; such type inference in the C# OOP type system was avoided because the compilation time when constraint-based inference is added becomes extremely long due to the exhaustive search necessary, details here: https://stackoverflow.com/questions/3968834/generics-why-cant-the-compiler-infer-the-type-arguments-in-this-case