Unfortunately, all the other languages that included inheritance in their design can't wish it away. Devs are going to keep reaching for inheritance as the closest, most comfortable abstraction.
Unfortunately, all the other languages that included inheritance in their design can't wish it away. Devs are going to keep reaching for inheritance as the closest, most comfortable abstraction.
It's not, though, and the fact that people keep repeating this meme shows that most developers don't even bother thinking about issues they face beyond superficial blamesplaining.
The reason inheritance causes so many issues in languages like Java is because they are statically typed and also use classes as types[1]. Classes must be somewhere in the inheritance tree, hence you are forced into some place of that tree. To make things worse, Java has many keywords that restrict what inheritor of a class can do (private, final, etc).
Inheritance is much less troublesome in, say, Smalltalk, since the language is dynamically typed. If someone expects you to implement Foo, you can (almost always) just implement its relevant methods without explicitly extending the class. Thus, a whole host of annoying scenarios simply does not occur.
--
[1] BTW, this breaks one of the fundamental commandments of classic OOP: you should not depend on implementation details of an object, only on its message protocol. Obviously, it's impossible to be independent of implementation details if some library forces you to use a particular class.
Sorry, I don't understand this sentence. Isn't inheritance simply a way to avoid writing duplicate code? If you write the code to implement methods, isn't that not inheritance anymore?
Outside of OOP, we use composition for reuse and interfaces for polymorphism, and we don't trampoline method calls up and down a hierarchy because it's (probably?) always a bad idea. When we really need reuse and polymorphism, we can use both composition and interfaces, since the two are correctly orthogonal.
Note that languages like C++ allow for inheritance without polymorphism, i.e. pure implementation inheritance.
However, I also think that composition should be preferred whenever possible.
Also, like the sibling said, inheritance is a tool that does multiple things: code reuse, which we call implementation inheritance, being the one everyone hates (the age-old advice is to use composition for code reuse instead), and interface inheritance being the one everyone loves.
I don't know that I'd say "inheritance is the root of all evil" (there are lots of antipatterns in OOP that are unrelated to inheritance, like Joe Armstrong's banana-gorilla-jungle observation) but I will say that inheritance is pretty close to useless in the best case and harmful in most cases. And I say this as someone who learned to program and then became a professional programmer when OOP was all the rage. I was taught OOP without the previous bias of other paradigms; it was only after learning other paradigms that I was able to articulate frustrations I was having with OOP. The implication that people who criticize inheritance in this way "haven't bothered to think" is patently false in the best case, and laughably arrogant in the worst case.
> The reason inheritance causes so many issues in languages like Java is because they are statically typed and also use classes as types[1]. Classes must be somewhere in the inheritance tree, hence you are forced into some place of that tree. To make things worse, Java has many keywords that restrict what inheritor of a class can do (private, final, etc).
Fear not, Python is dynamically typed and inheritance is a mess there as well.
> If someone expects you to implement Foo, you can (almost always) just implement its relevant methods without explicitly extending the class.
This is just structural subtyping (see Go's interfaces for a statically typed example of structural subtyping) also known as "duck typing". It seems like you're positing that the problems with inheritance derive from nominal subtyping (e.g., Java's `implements` keyword), but these things are orthogonal. Python has duck typing ("structural subtyping") and its inheritance is no less painful than Java's. Similarly, Rust has nominal subtyping (a type must explicitly implement a trait) and it has none of the inheritance-related problems that Python and Java have.
1. Inheritance causes all kinds of issues so you shouldn’t use it.
2. Actually, inheritance is fine as long as you do it right (e.g. Liskov)
3. Actually, getting part 2 right is difficult, and the heavy risks of getting it wrong aren’t worth the minor benefits of inheritance.
Inheritance is bad. Inheritance is patching a class and overriding some of its methods, while leaving others intact. This brings all kinds of unexpected interplay between methods of different levels of overriding. A typical example is http://www.cse.psu.edu/~deh25/cmpsc473/jokes00/joke01.html
Ideally all "concrete classes" with method implementations should be final, and the polymorphism should be achieved via interfaces / typeclasses / traits, or purely abstract classes where these are not available. Reuse of implementation should be achieved via composition; there are several ergonomic ways to express it.
Won't fix; working as intended.
It also seems like they're describing the nominal typing of Java versus a structural approach.
This allows to have the upsides of structural polymorphism without losing static checks.
Go, OTOH, goes all the way structural.
I believe at least in the case of Haskell, you are referring to type classes[0].
Rust's way can help avoid some errors. You can't accidentally implement an interface, whereas in Go you can if you happen to implement a group of functions with appropriate names and type signatures. It's unlikely to cause actual bugs (you'd have to misuse the resulting implementation) but can be conceptually somewhat confusing.
[0] https://doc.rust-lang.org/book/ch10-02-traits.html#returning...
I have this "theory" in the back of my head that trees are usually the wrong things to model thing in life but it's what come to us naturally. For example, a blog with categories and sub-catogories for articles (a tree, inheritence) can often describe the content better by using tags (a graph, composition). I think that's because trees are easy to deal with and understand, but graphs are more "open" with what you can do.
Trees are often implemented using composition, and inheritance can be graphical (the Diamond Problem is not game over).
A
/ \
B C
/ \ / \
D E F G
This is clearly a tree, but doesn't B have 2 paths through it? A->B->D and A->B->E.Fun fact -- since graph-theory trees are undirected by definition, an inheritance graph is more properly called an arborescence (for single inheritance). For multiple inheritance it's a DAG (with diamond-pattern) or a directed tree (without).
No.
You just described a connected acyclic graph, not a tree.
In addition to being connected and acyclic, a tree must also have a root, and is thus implicitly directed.
class Animal
class Mammal inherits Animal
class Feline inherits Mammal
class Cat inherits Feline
...
This is different than just asserting facts with data, which can lie in multiple dimensions. Is Feline
Is Mammal
Is Fluffy
Is White
Does Meow
The later is a much more flexible data model as it more closely mimics observed (subjective) reality, and is less disturbed when a new (counter-)example is introduced, but is also harder to reason about than idealised categories.Years ago, when ECS was just starting to be talked about (after Adam Matrins blog posts), I wrote a toy ECS where I used that naming convention. Nowadays I stick to the mainstream terminology since that’s what other people know.
"Component Software: Beyond Object-Oriented Programming"
https://www.amazon.com/Component-Software-Beyond-Object-Orie...
1st edition, 1998
struct Entity {
name: string,
parent: Option<Entity>,
}
let city = Entity { name: "city", parent: None };
let munich = Entity { name: "Munich", parent: Some(city) };
That said, if you really wanted to make life hard for yourself, you could use types as data provided your language has a runtime type system and reflection (you could dynamically generate `class City` and `class Munich extends City` when deserializing `[{name: "city", parent: null}, {name: "Munich", parent: "city"}]` or something). But this is the kind of Rube-Goldberg territory that "Kingdom of Nouns" thinking leads us toward.Spoken like someone who has never seen any kind of representative sample subset of real world code...
Only if that is how you choose to model the problem domain.
> This is different than just asserting facts with data, which can lie in multiple dimensions.
The "Is ..." examples you detail can just as easily be modeled "in OOP" as:
class Animal {
private knownFacts = ...
def is (fact) ...
}
Without the need for "a hierarchical (tree-like) ontology", since obviously this would be a poor choice in this situation.Structural typing is really cool though. An object built from a named, saved recipe will work just as well as something cobbled together on the fly and at runtime you won’t even know which is which. It’s the basis of a lot of general purpose game engines composition based game object interface.
I’ve also found it extremely fun to use with TypeScript.
No, I'm not, because I didn't assert this fact (it's a Cat).
See how hard it is to break free from this mindset of objects.
It's like trying to categorize your photos in a directory tree. Do you categorize by year first, by person, or location? There is no correct answer. What people want instead is a photo album with tags. The same problem applies to OOP.
What you really want is hierarchal tags :)
A problem that made me think about tags instead of categories was precisely that: I have photos that I want to organize. I started by organizing them by person with a folder for each person. But how do I handle a photo where multiple people are in it? Tags don't have this problem. Unfortunately file systems don't support tags.
Silly monkeys
Give them thumbs, they forge a blade
And where there's one they're bound to divide it
Right in two
SCNR :)For everyone that doesn't know the text: I recommend listening to Tool's "Right in two". Although the text originally talks about war and strive, not programming. ;)
Have you by any chance read the relevant passages in SICP? It has some things to say about OOP ontologies.
Possibly that section. It's not about OOP specifically, but about type hierarchies generally.
Clay Shirky - Ontology is Overrated: Categories, Links, and Tags
https://web.archive.org/web/20191117162526/http://shirky.com...
https://en.m.wikipedia.org/wiki/Pyrrhonism
For example, putting regions under a cold climates category, might not make sense to someone living near the pole where they would consider the same regions to be warm climates.
I haven't watched this in like... a long time, so maybe I'd think it's bad now.
The relevant Deleuze texts (A Thousand Plateaus) can be infuriating if you’re not open to this whole other style of thinking, but Deleuze is no postmodern, he’s a realist and a materialist and sort of a science worshipper, albeit from an angle that would make Neil deGrasse Tyson start bleeding from his nose until he passed out, if he ever grokked it. Start with Delanda, probably.
[googling a little, this isn’t great but isn’t bad for something readily accessible: http://dar.aucegypt.edu/bitstream/handle/10526/3534/DeLanda%... ]
Object Oriented Programming Is An Expensive Disaster Which Must End:
http://www.smashcompany.com/technology/object-oriented-progr...
Then if you solve the entity communication conundrum with message passing and don't allow entities to directly access one another's data you basically have all the elements.
When I implemented a variation on ECS for a game I'm building, I did exactly as you suggest, re, message passing: components receive and respond to messages but their implementation is hidden.
Some languages have class-based systems with inheritance: one class inherits from another, and methods implemented in the superclass can be used in the subclass. Some languages have prototype-based systems with inheritance: one object inherits from another, and methods implemented in the prototype can be used in the object.
Component-based systems don't really fit my mental model of inheritance here.
It's composition, not inheritance.
There's nothing special about composition that it requires language support, or that it has to be static for it to be considered composition. You can implement dynamic composition in OOP simply by having a List of components, and that's how many games that don't use ECS did and still do. Composition has absolutely nothing to do with inheritance or with requiring static data members.
Unlike you're claiming, the relationship between entities and components definitely does exist in ECS, just not in an OOP way, because ECS is not OOP (even though ECS can be implemented in any language).
This seems to miss what ECS actually is, unless you're just referring to the old-school way of doing entity components and not the data-oriented way.
Data-oriented ECS way of doing things is to separate state and behaviour. Entity components essentially become structs where their only behaviour is potentially some getter/setter utilities.
Behaviours are then state-less systems (just functions, essentially) which act on a set of components.
For example, a PhysicsUpdateBehaviour might take in a RigidBodyComponent and a HealthComponent to perform a physics update and apply physics/fall based damage.
The main benefit of ECS (imo) isn't even really performance. It makes code in complicated game projects much easier to manage by clarifying the game loop and by making it much more obvious how and when entity state is being modified.
It's the kind of thing that potentially complicates a smaller project, but makes larger more complex projects easier to manage.
This Overwatch GDC talk is the best breakdown/example of data-oriented ECS in a AAA game that I know of: https://www.youtube.com/watch?v=W3aieHjyNvw
That’s an overly broad definition of “object”, since under that same definition a record type (C struct) or any other blob of memory is an object.
In the common type of “components only store data” ECS, the entity is an ID (think a foreign key) that connects multiple records together and systems are independent functions (they are not tied to nor live in an entity) that operate on collections of subsets of these components.
That sounds a lot more like old school C-like procedural programming to me than it does like OOP. There’s more to OOP than the data attributes a class contains (eg the associated methods)
I suppose it depends on your game engine and your ECS, but since entities don’t contain logic, it’s the systems that communicate between each other (either by sending messages or by accessing the other entities components or by just calling functions of other systems). This isn’t all that different from different parts of a procedural program communicating. Although I do personally think that making a system be an OOP object does makes sense, but it doesn’t have to be.
With that said, it seems pretty common in games to use a component system that isn’t “pure ECS” (like the default Unity components prior to their new ECS), which definitely seems like typical OOP to me, just decomposed a bit more.
I think that's because you seem to have stopped at the second sentence the rest is important as well. I'm also talking about a level above the ECS implementation. What is the running thing actually doing.
> With that said, it seems pretty common in games to use a component system that isn’t “pure ECS” (like the default Unity components prior to their new ECS), which definitely seems like typical OOP to me, just decomposed a bit more.
Yes this also models much the same thing at runtime.
> Yes this also models much the same thing at runtime.
In a different way, though. It also generally misses out on the data-oriented benefits of an ECS.
The underlying implementation is irrelevant basically. You could implement the ECS in an OOP style and the same it true. You could do it in a functional style and it would be true. You could do it in straight bytecode for some obscure hobby VM and it would be true.
That is, you can add components that don’t get operated on by any particular systems because the entity doesn’t have the other prerequisite components and you can have systems that don’t operate on the components. You can have many systems operate on one particular component and many components operated on by a system.
In OOP, the data and the operations are packaged to whether as one. You also typically have encapsulation and it’s considered bad practice for one class to operate on another classes data directly.
It seems that both models achieve similar things, but they’re far from the same thing. Just like how procedural or functional programming achieve similar things to OOP, and you can do OOP in these paradigms or these paradigms in OOP. There’s a lot of cross over, but that doesn’t make them all the same thing.
If anything, I’d say that ECS are a relational model but with a very limited query system compared to something like SQL.
Except the data and behavior aren't decoupled. The components are decoupled from the systems, but the systems are still very much dependent on the components. Just like a method is usually dependent on the instances data or a function is dependent on the data passed in.
> That is, you can add components that don’t get operated on by any particular systems because the entity doesn’t have the other prerequisite components and you can have systems that don’t operate on the components. You can have many systems operate on one particular component and many components operated on by a system.
You can have a member that isn't operated on by any methods and methods that don't operate on members.
At the level your talking about there isn't much difference between a function and a method. It's mostly syntax.
method(instancedata);
verses
instancedata.method();
Really we're getting caught up in implementation details because a class definition isn't the be all of how to define an object. There is really no reason we couldn't define objects in a programming language through composition.
ECS very much is a relational model and you're right it's very limited in comparison to things like SQL because it's trying to model something very simple. Game Objects! The relations defined are exactly what brings data and behavior together under to create the runtime object we call an Entity under the pattern conventions.
Just like a C function operating on a C struct. So, what, in your opinion, is the difference between procedural programming and OOP?
> It's mostly syntax.
Which is why I think there is more to OOP than a classes attributes and it’s methods. There is also inheritance, encapsulation levels, the fact that an objects identity is its attributes (the object is its data, an entity has its components but is separate from them), the fact that an object is a singular thing which it’s methods operate on (as opposed to how systems operate on collections of components, imagine a class system where a method operated on all instances of that class!).
Sure at the end of the day it’s all the same and we’re just arguing semantics, but that was my point and what I lead with: it’s an overly broad definition. If definitions are too broad then they really don’t add any value, but I believe a distinction between OOP and ECS is useful because they are used in different ways.
But fundamentally I don’t disagree, I even once wrote a blog post about how all of the OOP principles exist in an ECS! I just don’t believe that thinking of them as slightly different implantations of OOP is useful because of how their properties differ.
I'm also mostly talking about the runtime consequences of the things that most people worry about at the time of programming.
But thanks for making me defend my thought!
In ECS you are decoupling data from behavior, which is basically the entire paradigm of languages like rust and go. You could argue that by defining systems in a way that they run on certain components you are defining behavior and data in one, but I think that's a stretch.
It clearly differs from OOP when I have 2 entities with components that have overlapping and non-overlapping systems. If e1 has components c1 and c2, and e2 has components c2 and c3, and c1 and c2 are used in system s1 while c2 and c3 are used in system s2, I don't see how you would model that with OOP without adding data to classes that don't need it. In OOP both e1 and e2 would need all the logic from s1 and s2, or needlessly specialized versions of s1 and s2. Which would be solved via inheritance (either class based or interface based).
In ECS your data exists in an array of components and any part of your program can operate on any component however it wants. I've never needed message passing for anything I've worked on.
That's not to mention that the main benefits of ECS have nothing to do with language paradigm. ECS main advantage is cache coherency and easier parallelism.
> That's not to mention that the main benefits of ECS have nothing to do with language paradigm. ECS main advantage is cache coherency and easier parallelism.
ECS is not data-oriented by default. :)
I have a long post explaining things in this thread here: https://news.ycombinator.com/item?id=27663218
Your entire post on ECS is, "If you don't use DoD with ECS than you're not using DoD". Well yeah.. obviously? If you implement an ECS and then use it without data-oriented structures, then yes obviously you don't have data-oriented design.
You're creating a strawman. You're saying if you take ECS, remove the idea of storing components independently of entities, and pass them in an inefficient manner to systems, then you don't have DoD. ECS isn't inherently DoD, literally nothing is. Arrays aren't inherently cache friendly. There's nothing stopping you from making a language that allocates data randomly throughout reserved memory and every array element points to each location. No one is arguing ECS is inherently DoD, but it is a good design to facilitate DoD.
> For example we might want to do damage to another entity entirely.
Add a damaged component to the entity to damage. Consume damage component in a system.
> Or we might want to look up the properties of the piece of ground we're stood on
Use a position component on the entity standing on the ground. Consume the position in a system and look at the properties of the terrain map at that position. Even simpler for grid based maps.
> We're also ignoring interacting with other components or the world and how that might work
You interact with other components by defining interactions in systems based on those components.
You've created an ECS in a way that doesn't take advantage of any of the benefits, and complaining that all you're left with is the disadvantages.
The ECS approach can lead to some confusing things like adding a component to an Entity and having strange behaviour result as a system the programmer didn’t expect to be triggered is run. This can lead to systems having quite complex definitions based not just on the components the system needs to run but also on the components that shouldn’t be present and so on.
From a distant point of view, everything is data oriented. Your thought are transformed into keyboards presses by the keyboards, that are transformed into events by your computer, that is transformed into what you see by your screen.
I could do the same with a frozen pizza factory: you can see the ingredients flow in the machines (functions), or you can see the different machines passing things to others like objects. The problem is that then the classification between "OOP" and "non-OOP" doesn't mean anything anymore and is now useless.
This article from Noel, and the ones from Mike he links to, get under the hood and into "what is the compiler doing" and "what is the CPU doing". Down here, we're looking at how to use the features of whatever language we're using to get the results we want, rather than "how should i program oop gud".
Here's an example (in the playground, because it gets a bit long): https://play.golang.org/p/TblQypAbIL2
struct Foo {}
func (f *Foo) baz() { println("Foo.baz()") }
func (f *Foo) qux() { f.baz() }
Given the above, `struct Bar { Foo }` is the same as: struct Bar { Foo Foo } // field called `Foo` of type `Foo`
func (b Bar) baz() { b.Foo.baz() }
func (b Bar) qux() { b.Foo.qux() }
Since it's just syntax sugar and not inheritance, we can't put a `Bar` in a list of `Foo`s nor can we pass a `Bar` into a function that expects a `Foo`. It also means that if Bar overrides its `baz()` method like so: func (b Bar) baz() { println("Bar.baz()") }
that calling `Bar.qux()` will still print "Foo.baz" and not "Bar.baz" (most languages with inheritance will print "Bar.baz", which is to say methods are virtual by default). type FooI interface{ baz(); qux(); }
foos := []FooI{&Foo{}, &Bar{}}
Regarding overriding, you're right that it doesn't work out of the box. However, you can make "extensible classes" with just a little boilerplate: type Foo struct {FooI this}
func (f *Foo) baz() { println("Foo.baz()") }
func (f *Foo) qux() { f.this.baz() }
func NewFoo() Foo { f := Foo{}; f.this = &f; return f }
Now, to extend Foo: type Bar struct {Foo}
func (b *Bar) baz() { println("Bar.baz()") } //the override
func NewBar() Bar { b := Bar{}; b.Foo.this = &b; return b } //this would work even if we didn't override baz
func main() {
foo := NewFoo()
bar := NewBar()
foos := []FooI{&foo, &bar}
for _,f := range foos {
f.qux()
} //prints Foo.baz(), then Bar.baz()
}
Since best practice even in C++ or C# or Java is to only allow inheritance for classes that are designed with it in mind, and since Go anyway has lots of other boilerplate, this shouldn't be unbearable if required.Playground link for anyone curious: https://play.golang.org/p/SKGhANuBGgB
Inheritance isn't the root of all evil, dynamic dispatch is. It's a remarkably powerful implementation detail but one with enormous cost, regardless of whether you're using an AoT/JIT compiled or interpreted language.
Dynamic dispatching typically costs one pointer lookup in a vtable[0]. By "typically", I specifically mean "in any production quality run-time environment." This is not an "enormous cost" by any reasonable definition.