Readit News logoReadit News
ferdowsi · 4 years ago
In all my experience with OOP, it's always been inheritance that is the root of all evil. Rust and Go got this correct by having class-like objects with no inheritance, to achieve encapsulation without fragility.

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.

colllectorof · 4 years ago
>In all my experience with OOP, it's always been inheritance that is the root of all evil.

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.

bccdee · 4 years ago
You're describing interface-based polymorphism, which is what go and rust use. In go, I can have a struct with methods that implements a particular interface by implementing all the methods described in that interface, but I can't inhereit from another struct. The person you're replying to called this out as a better system too.
nine_k · 4 years ago
Polymorphism is good. You describe polymorphism.

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.

throwaway894345 · 4 years ago
> 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.

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.

philwelch · 4 years ago
To me there are about three tiers of this basic insight:

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.

SPBS · 4 years ago
> 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.

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?

Zababa · 4 years ago
> In all my experience with OOP, it's always been inheritance that is the root of all evil.

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.

hcarvalhoalves · 4 years ago
Data modelling in OOP is an exercise in coming up with Platonic ideals, resulting in a hierarchical (tree-like) ontology as you try to choose of the atributes as the categorisation dimension, and leaving everything else as properties.

    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.

oftenwrong · 4 years ago
There's a great old (2005) blog post on this topic:

Clay Shirky - Ontology is Overrated: Categories, Links, and Tags

https://web.archive.org/web/20191117162526/http://shirky.com...

dexwiz · 4 years ago
Humans have a natural tendency to refine a single idea by splitting it into two based on a differentiating factor. This ends up looking like a tree when applied repeatedly. Dichotomous keys for species identification are another example of this.
prionassembly · 4 years ago
Oh man, I have a rabbit hole for you.

https://en.wikipedia.org/wiki/Rhizome_(philosophy)

duped · 4 years ago
A tree is a graph where all vertices have exactly one path through them.

Trees are often implemented using composition, and inheritance can be graphical (the Diamond Problem is not game over).

jhgb · 4 years ago
> 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.

Have you by any chance read the relevant passages in SICP? It has some things to say about OOP ontologies.

firethief · 4 years ago
You might enjoy this essay from 1965, "A City is Not a Tree": https://www.patternlanguage.com/archive/cityisnotatree.html
necheffa · 4 years ago
Except that trees are by definition graphs with specific conditions on direction and cycles.
lkrubner · 4 years ago
I've previously suggested that initiation is the "root of all evil." See my essay:

Object Oriented Programming Is An Expensive Disaster Which Must End:

http://www.smashcompany.com/technology/object-oriented-progr...

Arch-TK · 4 years ago
Funny to see the author of one of my favourite blog posts on the internet get downvoted.
meheleventyone · 4 years ago
The weirdest thing is that the ECS as a way of building a game is inherently object oriented. You take a set of components and compose an object called an entity. The components on the entity define not only it's data but also it's behavior by the set of systems that act on the corresponding components. And you can take these object definitions and inherit them to add additional behavior or change the existing behavior by adding more components to the new definition.

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.

dkersten · 4 years ago
> You take a set of components and compose an object called an entity.

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.

Bekwnn · 4 years ago
> The components on the entity define not only it's data but also it's behavior by the set of systems that act on the corresponding components. And you can take these object definitions and inherit them to add additional behavior or change the existing behavior by adding more components to the new definition.

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

adamdusty · 4 years ago
Sure ECS is object oriented in the same way C99 is. Yeah, technically you are building up some OOP functionality, the same way you emulate constructors and instance methods in C by making functions to init data structures and functions that take references to a struct to modify it's data. That doesn't make C object oriented.

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.

Zababa · 4 years ago
From a distant point of view, everything is OOP. You can treat anything like a black box that you push button on to make things. You push things on your keyboard, without knowing how it works. Your keyboards activate things on your computer, without knowing how it works. The computer ask the screen to update with the new date, without knowing how it works.

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.

professoretc · 4 years ago
ECS is really just OOP with dynamic multiple inheritance: an object can inherit from multiple base "classes" (with "components" providing the data and "systems" providing the code) and this inheritance structure can be changed at runtime, by adding/removing components. Everything else (struct-of-arrays vs. array-of-structs) is just low-level implementation details.

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.

colllectorof · 4 years ago
ECS (which I have not used) sounds a lot like Traits. The name and core concepts for Traits were defined in 2003 in an ECOOP paper [1]. I think traits were first implemented by Squeak Smalltalk in 2005.

[1] http://scg.unibe.ch/archive/papers/Scha03aTraits.pdf

lowbloodsugar · 4 years ago
Like anything, inheritance can be used poorly, just as anyone can right poorly encapsulated code in Rust or Go. You might be able to convince me that inheritance is too dangerous for idiots, but then so is a computer, and we'd be debating where to draw the line of how smart/experienced you have to be to use it safely.

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".

jayd16 · 4 years ago
It's the dose that makes the poison. Of course we want nice typed collection libraries and interfaces. The kids go overboard though.
simiones · 4 years ago
Go actually does implement inheritance, albeit in a roundabout sort of way: a struct can have one or more base members, and any method defined on the base members is accessible from the new struct implicitly, so they also implicitly implement any interface that was implemented by their base members.

Here's an example (in the playground, because it gets a bit long): https://play.golang.org/p/TblQypAbIL2

throwaway894345 · 4 years ago
That's not inheritance, it's just syntax sugar that lets a struct delegate method calls to one of its members. In other words:

    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).

vlunkr · 4 years ago
I think that it can work. IMO ActiveRecord is a perfect use of inheritance. You get tons of useful functionality out of the box, you don't have to worry about what that code looks like, and it's easy to extend or modify it. But often when I see co-workers come up with their own hierarchies, it saves maybe a couple of lines of code and makes it 5x more difficult to read, since you're jumping between parent and child classes and trying to keep track of the order of execution.
duped · 4 years ago
I don't agree with this, and I'm personally an anti-OOP militant.

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.

bccdee · 4 years ago
What enormous cost — slightly slower function calls? That's a pretty minor cost, and dynamic dispatch can be extremely useful sometimes.
AdieuToLogic · 4 years ago
> Inheritance isn't the root of all evil, dynamic dispatch is. It's a remarkably powerful implementation detail but one with enormous cost ...

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.

0 - https://en.wikipedia.org/wiki/Virtual_method_table

throwaway894345 · 4 years ago
Disagree. Go and Rust both have dynamic dispatch and neither have the problems that inheritance has. Even in C which lacks dynamic dispatch, people will either try to build it at the expense of type safety or they will try to manage an impossibly complex implicit state machine (I've seen this in a lot of critical real time systems).
axilmar · 4 years ago
There is some middle ground between data-oriented design and OOP: just organize your objects in such a way that:

a) objects of the same type occupy continuous blocks in memory,

b) messages are passed to objects of the same type, then to objects of another type etc.

In this way, you don't lose the advantages of encapsulation, inheritance, polymorphism etc but you also don't sacrifice cache coherence much.

OOP does not enforce a 'random' memory access order, you can very will organize your objects in such a way that speed is not sacrificed much.

ajuc · 4 years ago
This is kinda what Entity Component Systems do - they implement in-memory relational database for game objects, handle dependenceis and allow your game logic code to run efficiently over them while still keeping the pretense of OOP :)

Why pretense? Because behaviors (Systems in ECS terms) are completely separated from data (Components) and data for different game objects (Entities) is kept together in regular or sparse arrays.

Encapsulation is nowhere to be seen, code is written to specify the components it depends on and run on these arrays.

ECS is very fashionable in gamedev lately as it allows for efficient multithreading, explicit depencencies for each subsystem, cache locality and trivial (de)serialization. Used together with handles (tagged indexes instead of direct pointers) it reduces likelihood of dangling pointers and other memory management bugs.

tffgg · 4 years ago
ECS ist Standard for enterprise web apps as well
bruce343434 · 4 years ago
> objects of the same type occupy continuous blocks in memory,

Depending on the language, a single object may have a lot of overhead that adds up in an array. What you often see is one ArrayObject with arrays of properties, kind of like a transposition.

A problem there is that in memory the arrays are of course laid out one after the other, which actually destroys cache locality if you need to access more than 1 property inside a loop (it will need to load back and forth to the different property arrays), so it's a somewhat dumb approach. But, at least it saves the overhead, so maybe not too bad. And in a high level interpreted language like php you likely weren't gonna get cache locality anyway.

The point is to group all properties you are going to be accessing in a hot loop together in a small-ish array.

C has structs for this, 0 overhead "entities" (although they may be padded to multiples of 4 bytes, so keep that in mind). You have compiler specific keywords to forego padding ("struct packing"), or maybe you're lucky and the data just fits exactly right. Either way, in such cases an array of structs is imo the most sane way to go.

In fact, C++ offers classes and structs. In my opinion, struct should be used for entities like "weapon" or "car". CLASSES (or objects) should be unix-philosophy adhering miniprograms that do one task and do it well (oh hey, it's the single responsibility principle!).

They way most programmers write OOP is a pretty convoluted way to model actual entities anyway. car.drive()? Oh? The car drives it self? No. agent.drive(car) should be the actual method. Agent, mind you, can be a driving AI, or a human driver, or whatever. Maybe the agent is a part of the car? In that case, use composition, not inheritance. (oh hey, entity component system!)

Firadeoclus · 4 years ago
> A problem there is that in memory the arrays are of course laid out one after the other, which actually destroys cache locality if you need to access more than 1 property inside a loop (it will need to load back and forth to the different property arrays), so it's a somewhat dumb approach.

Caches are perfectly capable of dealing with more than one stream of data (there are some very specific edge cases you may have to consider), accessing multiple arrays linearly in a loop is generally more efficient than accessing a single array of structs when you don't use almost all the struct elements.

jcelerier · 4 years ago
> In fact, C++ offers classes and structs. In my opinion, struct should be used for entities like "weapon" or "car". CLASSES (or objects) should be unix-philosophy adhering miniprograms that do one task and do it well (oh hey, it's the single responsibility principle!).

Please no. The only thing that matters is the language rules ; any non-computer-encodable arbitrary rule like this on top of the language rules just causes an additional lava layer.

There is one difference between class and struct and it's default visibility. Use one or the other according to which causes less tokens to appear in your code

skocznymroczny · 4 years ago
OOP doesn't force you do do car.drive().

You can have an abstract agent class/interface with virtual "drive(Car c)" method. The method would be overriden by AIAgent, HumanAgent etc.

The car itself would have more basic behavior, such as "accelerate()", "turnLeft()", "turnRight()"

duped · 4 years ago
> A problem there is that in memory the arrays are of course laid out one after the other, which actually destroys cache locality if you need to access more than 1 property inside a loop (it will need to load back and forth to the different property arrays), so it's a somewhat dumb approach.

This is actually why memory layout != DoD. You need to account for this in the architecture of the program, so that the systems only operate on a small amount of data that are relevant to them at one time.

The tradeoff is paying for all the data, all the time, and some of the data most of the time. For a large class of programs that can be architected around mostly non-unique, trivially copyable fields with few relations, the tradeoff between AoS and SoAs is obvious.

For other programs where your entities need relational information and form trees or graphs, it can be less obvious whether the data representation is going to be faster. However in these cases you store the relationship as your data (for example, as an adjacency matrix), but implementing any kind of textbook algorithm over it is basically reverse engineering pointers with indexes.

mytailorisrich · 4 years ago
> agent.drive(car) should be the actual method

That's equally object-oriented so I don't see how OOP is a nonsensical way to model. A sensible model is up to the designer, not OOP.

fennecfoxen · 4 years ago
You might be looking for "Entity - Component - System" design, common in video games. Entities are still virtual-world objects like you might expect, but none of them would dare keep track of something like their position or temperature or whatever. Instead, they register a component with the appropriate system, which keeps all the data colocated for efficient physics and the like.
megameter · 4 years ago
If we are speaking of C code, it's not quite so bad as it looks to have somewhat fat structs across multiple arrays, since you can fit 64 bytes in a cache line on contemporary desktop CPUs, and that sets your real max-unit-size; the CPU is actively trying to keep the line hot and it does so (in the average case) by speculating that you're going to fetch the next index of the array. Since you have multiple cache lines, you can keep multiple arrays hot at the same time, it's just a matter of keeping it easy to predict fetching behavior by using simple loops that don't jump around...which leads to the pattern parent suggests, of cascading messages or buffers in groups of same type so that you get a few big iterations out of the way, and then a much smaller number of indirected accesses.
volta83 · 4 years ago
If you loose vectorization, you might be loosing a 4x, 8x, 16, ... 32x perf difference by organizing your data in such a way that memory operations and data manipulation can't be vectorized.
physicsguy · 4 years ago
I find in simulation codes that lack of awareness of (a) is an absolute performance killer. Generally, it's better to use a pattern for an object that's a container for something - so don't have a 'Particle' object but a 'Particles' one that keeps things stores the properties of particles contiguously. In my old magnetics research area you have at least 8 and more frequently 10+ spatially varying parameters in double precision that you'd potentially need to store per particle/cell.
inopinatus · 4 years ago
Quite so. There’s a false equivalence in this article between data and encapsulated state, but if that were so then the flyweight pattern and its ilk couldn’t exist.
corty · 4 years ago
Only in C++. Most other OOP languages do not allow controlling allocation that way.

Also, OOP only allows array-of-structs continuous data. Struct-of-arrays and hybrid forms are usually awkward or impossible. And with everything except maybe C++ and Rust, those "structs" in OOP-land do have quite an overhead compared to C structs.

mytailorisrich · 4 years ago
OOP does not say anything about memory allocation.

OO principles are one thing, what specific languages do is quite another.

pulse7 · 4 years ago
Overuse/misuse of inheritance has triggered hatred of OOP among many software developers...
throwaway894345 · 4 years ago
I’ll go out on a limb and posit that there are virtually no valid uses of (implementation) inheritance. Perhaps one valid use is getting rid of delegation boilerplate (e.g., normally you would compose one object inside another but you want the outer object methods to delegate to the inner object methods but you don’t want to have to write N function definitions that just call the same methods on the inner object so instead your outer object inherits from your inner object). This problem is better solved by something like Go’s struct embedding since it doesn’t do anything more than this kind of automatic delegation.

And if you get rid of inheritance, there is very little left to distinguish OOP from procedural programming like one would do in C or Go. And this is the semantic problem: no one really agrees on what OOP is and proponents will rebut any criticism with “that’s not true OOP”. Any definitions of OOP that aren’t easily assailable are also indistinguishable from other existing paradigms.

Downvoters: i’m very interested in your opinions about why I’m wrong and specifically when you think inheritance is appropriate. Everyone says “there’s a time and a place!” but no one articulates when/where beyond cat/dog/animal toy examples.

colllectorof · 4 years ago
Alan Kay spent the last 40 years educating people on OOP and system design. His talks and research papers are now widely available on the internet. There are free, modern and easy-to-use versions of Smalltalk. Anyone who still remains ignorant about fundamental ideas behind classic OOP and the paradigm's history is willfully ignorant.
Ensorceled · 4 years ago
In my case, I'm using Django on several projects. It uses inheritance to implement the ORM, the views, filters, etc. etc.

When you say "there are virtually no valid uses of (implementation) inheritance" ... how do you expect the thousands of us using Django to respond to that? A link to the Django framework? To defend Django? What is the point?

You're right, maybe python's object model could have instead been implemented like Go's struct but it wasn't.

AdieuToLogic · 4 years ago
> I’ll go out on a limb and posit that there are virtually no valid uses of (implementation) inheritance.

  class Iterable { ... }

  class Tree extends Iterable { ... }
  class SortedTree extends Tree { ... }

  class Map extends Iterable { ... }
  class HashMap extends Map { ... }
  class InsertionMap extends Map { ... }
  class BiMap extends Map { ... }
Need more examples of "valid uses of (implementation) inheritance"?

> And if you get rid of inheritance, there is very little left to distinguish OOP from procedural programming like one would do in C or Go.

No. To make OOP indistinguishable from "C or Go", you would also need to eliminate at least; encapsulation, composition, access control, and compiler provided dynamic dispatching.

pulse7 · 4 years ago
Some cases where inheritance may be useful: UI widget libraries, graphical/drawing objects, (very similar) stream implementations. All this can of course be done without inheritance, but with inheritance it is much more elegant.
inopinatus · 4 years ago
Well, since you ask, I downvoted you for restating the false equivalence between OOP and inheritance, for the abjectly incorrect (and also, wholly unconstructive) straw-man statement that no-one agrees what OOP is, for the frankly absurd and wilfully ignorant claim that no-one articulates practical examples for the circumstances when inheritance rather than composition might actually be worth considering, and for bitching about downvotes, particularly that explicit assumption that they’re coming from people wedded to inheritance, and not from people who dislike disingenuous arguments and circular reasoning.

Read a fucking book, instead. David West’s Object Thinking, for example.

brobdingnagians · 4 years ago
Yeah, I've loved classes and OOP for a long time, but lately I've been on a project that is going into more and more contortions and complexity to make everything fit a theoretical ideal. It's revived my interest in learning FP languages to avoid the arcane complexity that some people make out of OOP.
lelag · 4 years ago
I feel complexity creep is often inevitable regardless of the paradigm or technology you use. Programmers have varying levels of complexity they can handle and the systems generally naturally grow to match the complexity that the people that work on them can support.

Experienced programmers will manage to keep that complexity creep under control for longer and smarter programmers will manage to keep working on complex systems that lesser peers would have no chance to understand.

But eventually, unless the software has a very clear functional boundary which is often not the case for business software, software will start to become increasingly complex and dev velocity will slow down, quality will drop... I've seen this happening at all skill levels regardless of the languages and paradigms used.

AdieuToLogic · 4 years ago
> Overuse/misuse of inheritance has triggered hatred of OOP among many software developers...

"It is not the tools we use that make us good, but rather how we employ them."[0]

0 - https://en.wiktionary.org/wiki/a_bad_workman_always_blames_h...

moth-fuzz · 4 years ago
Honestly I find ECS dogmatic and difficult. Data oriented design as a general practice is a good thing, but there’s layers to all things. OOP is a very broad category of practices and designs, and ECS usually refers to one specific architecture.

The specific architecture in question tends to be full of soft dependencies - most ECSes don’t allow you to simply store a piece of data without opting in to all systems matching the data type executing arbitrary code on it. So much for separation of data and behaviour. No, now they’re even More dependent than they are in the usual OOP sense and you might not even know it.

Furthermore, usually when you want to think about in-game entities, you want to look at a single class. Now all entities’ data is split into numerous components and all entities’ behaviour is split into numerous systems and you don’t know frame by frame how they’re gonna interact, provided you even know about all systems in the first place. It’s total spaghetti code.

I’m much more inclined lately towards a shallow actor system . If I want to know how player’s behaviour functions, I need only look at player.cpp and nothing else. Then, to reap the benefits of data oriented design, certain objects can use a certain allocation scheme that makes sense for the object in question. In the general sense, any Component<T> can just have a vector<T> or an unordered_map<T> of all components and the memory access is abstracted away without it being detrimental. That’s C++’s whole deal actually, zero cost abstractions.

I wouldn’t call an entire paradigm shift in which one rewrites everything from memory allocation all the way up to ‘use WASD to move’ zero-cost in any sense.

In C++ it is trivial to overload new, or derive from some class which does, or to write a custom allocator, and frankly there is zero need for the same person who’s writing ‘use WASD to move’ to know about memory management.

setr · 4 years ago
The way I view it is that you look at components like a database -- a bunch of tables that don't really tell you much about the business logic; the main thing they do is offer data coherence.

And like a webserver, the business logic has been entirely moved out of the data storage -- you construct an "object" out of the raw parts from the database, and you operate on that. The main thing is that you can construct multiple objects from the same raw dataset -- different views (as in MVC).

I think however it's a mistake when you assume most game's design -- where largely there are a very small amount of entities, and a small amount of behaviors, and really the game design is about careful placement of these fairly rudimentary entities on the map. In that scenario, the flexibility of ECS does you no good -- the game design is itself inflexible, so making your logic flexible is largely a premature optimization. If there's only one reasonable "View" of the entity, being able to construct an infinite set of alternative views is pointless.

ECS appeals to me more when you start talking about simulation-style games, where the game is far less hard-coded. Dwarf Fortress is ridiculously flexible in its game design (at runtime), and ECS would be a natural fit for that (entities in DF are literally defined by tags, and groups of tags, and those tags get modified at runtime[0]). It's not spaghetti code then -- it's really the only reasonable way to approach the problem.

Defining each entity uniquely makes a lot of sense when your entities are largely unique (perhaps with a common base, e.g. for physics). ECS makes more sense when your entities share of lot of logic, but random subsets of it, and especially so when the game itself treats that random subset as dynamic.

[0] http://www.dfwk.ru/Creature_standard_1.txt

moth-fuzz · 4 years ago
Dwarf Fortress is one of my favorite games if not my favorite so props for citing its internal representation. I definitely think ECS makes sense in many contexts, and I think it's at its most powerful in tandem with a more encapsulated actor system. Example -

  struct Player: public Actor {
    Component<Sprite> sprite;
    Component<Transform> transform;
    void update() override {...}
  };
Where Component<T> is a handle to some backing storage indexed by an Actor's ID. This way, you can go the traditional route of having Player update itself in its own update() method as well as being able to Component<Sprite>::iter() along with other components for the render loop ECS-style.

My point now and my point then was going all-in on ECS as the basis for your entire architecture rather than taking a more principled approach drawing the strengths of OOP and DOD is dogmatic and difficult.

It's interesting that you mention web services - I have a lot of respect for databases and I enjoy the process of using them, however, I wouldn't ever program a web app in SQL. That's what pure ECS feels like to me. I agree that having objects manipulate the state via upholding internal invariants is the way to go. Whether you call them Actors, or Systems, or Controllers, it's kinda one and the same. That's why I love DOD as a base architectural layer to be abstracted upon, but dislike ECS as a programming paradigm.

lamontcg · 4 years ago
After studying ECS for all of a week, I was left wondering if there wasn't a way to reintroduce strong typing to an ECS system (without reintroducing all the problems of inheritance). So you have a player_entity factory that ensures that a player_entity only wrap entities that are actually players. Then you can pass that around to strongly typed functions, but keep the overall design reasonably inheritance-free so it was more like rust/go's strong typing systems.
jms55 · 4 years ago
I've thought about this in the past too, and come to the conclusion it is too difficult. Part of what I don't like about ECS is that it's too dynamic, you can't add static typing like this.

Sure, you can say that a "Skeleton" entity has an HP component, an AI component, etc. But there's no way of enforcing static typing on this.

Say you have some function that takes an Entity, validates it's a Skeleton, and then returns a SkeletonEntity which is just a wrapper around Entity for static typing purposes. Perhaps add some helper methods for fetching components, allowing you to operate on an OOP-like API with very little runtime cost. Seems like it works.

But there's no way to guarantee the skeleton STAYS as a skeleton for the future. You might add another thing later on that converts an entity with AI into FriendlyAI, and your SkeletonEntity implicitly relied on the AI being a SkeletonAI. Heck, you can't trust _any_ Entity. That handle to an entity you have might not even point to an Entity anymore, it could've been deleted.

ECS is very dynamic, which is great for designing open-ended games where you don't/can't plan every interaction in advance. It also has great performance, and is typically the _only_ sane way to implement games in a language without inheritance (I don't think Composition + Interfaces is very scalable). But for heavily structured games that want tight coupling between entities, it relies on you implicitly keeping your promises about what an entity means. You can't go and add new behavior that modifies existing entities later on - The compiler won't warn you, and you may end up crashing your program at runtime, unless you were very diligent about adding checks in your code for the coupling you assumed existed, and having good fallbacks for one those assumptions are violated.

moth-fuzz · 4 years ago
Back when I was trying to design an ECS engine I had a class Prefab<T...> which you would inherit from using CRTP to define entities with stronger static typing. For example, Player would be

  class Player: public Prefab<PlayerController, Transform, Sprite> {};
or something like that. C++ is infinitely flexible in that regard. I was never quite able to work out what would happen if you were to add/remove components at runtime to such Prefab classes. The semantics never quite made sense to me in terms of static typing, because it's no longer static.

pjmlp · 4 years ago
In the case of C++, you can do that via templates and static dispatch, now with C++20 is even better as each entity can be modelled as a concept.

something like,

    template<typename T>
    concept Player = requires (T p) {
        p.jump(); 
    };

    class Factory {
        public:

        template<Player p>
        void register_entity(p player);
    };
Just as seed for the overall idea.

bob1029 · 4 years ago
You can get some pretty unbelievable performance gains out of a single writer and arrays of structs.

Bonus points if you figure out a way to have an array per type of struct pre-allocated with more elements than you will ever need. Even if you use a GC language you can almost eliminate collections with this approach.

PicassoCTs · 4 years ago
Even the array of structs is a non-ideal approach, as structs are usually viewed as a static collection of data.

But if you look at the hot loop, it usually boils down to a funnel - not unlike a furnace. Lots of highly spacious needing raw materials are gathered and passed through, to be condensed into relatively small output.

So the ideal structure is a sort of union-struct, that compresses the results down each step of the algo, keeping it all in cache, while keeping it slim..

moldavi · 4 years ago
Would love to hear more about that kind of compressing, how does that go?
urthor · 4 years ago
People forget what the intent of OOP originally was.

OOP was envisioned as a way to manage software projects with many contributors at a time when we didn't have half the tools for hiding context that we do now.

Micro-services and micro-kernels are far far far more prevalent these days.

Garbage collection was also far less of a thing in that era, as all programmers were squeezing every last iota out of the hardware.

Hence rogue pointers were far more of a risk.

Multi-core? Haha.

I know this is not particularly relevant to the original article, but if you don't know the history and the intent behind why something exists, you are reasonably likely to misapply it.

Most of the mistakes of OOP are from a lack of understanding of why things got invented in the first place.

AdieuToLogic · 4 years ago
> People forget what the intent of OOP originally was.

Not really.

> OOP was envisioned as a way to manage software projects with many contributors at a time when we didn't have half the tools for hiding context that we do now.

No, the purpose of "OOP" is specifically for "hiding context" by encapsulating implementation logic exposed via a collaboration contract.

> Micro-services and micro-kernels are far far far more prevalent these days.

Non-sequitur.

> Garbage collection was also far less of a thing in that era, as all programmers were squeezing every last iota out of the hardware.

This literally has nothing to do with a programming paradigm.

> Hence rogue pointers were far more of a risk. > Multi-core? Haha

Again, this literally has nothing to do with a programming paradigm.

> I know this is not particularly relevant to the original article, but if you don't know the history and the intent behind why something exists, you are reasonably likely to misapply it.

This is a wise statement, one which I hope you say aloud whilst reading this reply.

> Most of the mistakes of OOP are from a lack of understanding of why things got invented in the first place.

A programming paradigm is not the source of mistakes. Its practitioners certainly can be however.

BoiledCabbage · 4 years ago
Also, the article does a really poor job of describing any drawbacks of Data Oriented Design. It's a real pet-peeve of mine.

> Drawbacks of Data-Oriented Design Data-oriented design is not the silver bullet to all the problems in game development.

Ok, they don't view it as a silver bullet. This seems promising for an evenhanded discussion. I'm curious what the author thinks the drawbacks are.

> The main problem with data-oriented design is that it’s different from what most programmers are used to or learned in school.

So the first drawback is that nobody knows your silver-bullet? That's a cop out.

> Also, because it’s a different approach, it can be challenging to interface with existing code, written in a more OOP or procedural way.

And the second drawback is that code was written without using your silver-bullet? Seriously?

If the only two things you believe are drawbacks about your tech are that not enough people know it, and not enough people are using it then it's not an even handed discussion of your tech.

Discuss the actual trade-offs you've learned from using it. Not nonsense like nobody knows how wonderful it is, nor is using it.

And that's coming from someone who agrees that OOP has huge flaws and with the most common applications of inheritance creates many flawed program architectures.

beders · 4 years ago
The original intent had nothing to do with "many contributors".

The main ideas: encapsulation, message passing and late binding i.e. dynamic binding.

Zababa · 4 years ago
You're talking about solutions when they were talking about problems. What problems does encapsulation, message passing and late binding solve?
Zababa · 4 years ago
> OOP was envisioned as a way to manage software projects with many contributors at a time when we didn't have half the tools for hiding context that we do now.

> Micro-services and micro-kernels are far far far more prevalent these days.

I think that's a good analysis. If OOP was a solution to an organization problem, then microservices are the "new" way to do it. Microservices respect late binding, message passing, encapsulation. I don't really know how inheritence would fit into the equation, as I don't know exactly how companies with hundreds of microservices do it. And since we don't care about what's inside the objects (services), we're now free to write them in Java, C++, Smalltalk, Erlang, Haskell or Pascal.

davedx · 4 years ago
I was expecting to see some example code, or some actual performance metrics to show why data-oriented design is better.

I actually have written a game that was pure functional style with a single giant state object for the game data and it worked well for me. But I'd want to see some evidence for this approach before changing the entire architecture of my game.

meheleventyone · 4 years ago
This graph made the rounds on Twitter last week and I think encapsulates the answer really well: https://twitter.com/eric81766/status/1407393532562841607

Most games won’t benefit. Most AAA games won’t benefit for their gameplay code and are otherwise very data-oriented already.

davedx · 4 years ago
ECS isn’t the same thing as data-oriented programming is it? I’ve worked on AAA games and this whole discussion is quite confusing, lol.
lmilcin · 4 years ago
Sigh... This again.

I am going to repeat this for what seems like a ten thousandth time.

"OOP is a tool to solve a particular type of problem. It is your responsibility to know the tool, to understand its strengths and weaknesses and when it is applicable and when it is not. If the tool does not work it is not the tool that is faulty, it is you who are the problem -- by using the tool in a wrong situation or incorrectly."

In particular I detest "we are OOP shop" type of approach. This immediately advertises they have absolutely no idea how to use stuff -- by saying you know only one tool and sure you are going to use it to solve every kind of problem.

Those languages that were supposed to be "everything is an object" like Java? Now are learning that maybe that is not the most sound approach and trying to evolve to allow other paradigms under one roof.

throwaway894345 · 4 years ago
This is boilerplate that can be used to defend any idea. First of all, the article is trying to explain a domain for which OOP is not well-suited. Secondly, it’s unhelpful to write-off this article with “there are places for which OOP is well-suited” without any specifics about when it is the better approach, especially how it compares and contrasts with other approaches.
lmilcin · 4 years ago
What I object to is this "OOP is good / bad" type of approach.

OOP is neither good nor bad. What is good or bad is your selection of technique for the problem at hand.

That's the same misguided discussion as on whether strong typing is good or bad. It is neither. What you want to select depends on the kind of project you are trying to use it for.

Also, just because this template can be used for practically any idea doesn't make it less valid. Greatest laws tend to have universal applicability.