Hacker Newsnew | past | comments | ask | show | jobs | submitlogin

Good article. My own thinking when coding performance is also towards data oriented approaches. For example Bevy in Rust. If stuff needs to be fast, it needs to be in cache. To do that, have everything nicely packed so you only ask for a chunk as often as you need. Then when you need it, it's already there.

Thing about old fashioned OO is it's often fine enough for your run-of-the-mill CRUD app. If you look at the latency chart he provides, there's another level to it: going to a database server. If you're gonna do that, you're not really in the speed game anyway, so why not make it simple? Just write it in the most uncomplicated way the language allows, and that's it. Waiting for the DB will take way longer than filtering the ants, forget all the optimizations.

Edit:

Forgot to mention with ECS, you have a different organizing principle. It can be quite enlightening to work with "systems that operate on data" as opposed to OO where you have the data declared next to the functions that mutate it. It can add clarity for certain problems. Somehow over the years I've found that the animals/cars analogies given in OO tutorials are one of the few places that fit well with the model.

If you imagine a game that has characters, relationships, items, spells, and so on, it's often not that easy to model as encapsulated objects. Does each character have a list of other characters that they have a relationship with? What happens when you want to change the relationship, or a character casts a spell that temporarily changes the relationship? Can easily end up a mess, because it's not obvious where such things should live in an OO model.



> Somehow over the years I've found that the animals/cars analogies given in OO tutorials are one of the few places that fit well with the model.

Yes, and I've never actually needed to implement a cat or a cow in any project :)

The other thing for which OO works much better than plain data is GUIs - and I think it is not a coincidence that OO popularity exploded together with the the coming-of-age of GUIs.

The other canonical example of OO - "Shapes" - doesn't actually work well at all; It doesn't work better with "plain data", but it exposes the fallacies of trying to use OO inheritance to model the real world. Every square is a rectangle, so square should inherit from rectangle ... but, you can't stretch width and height independently in a square, so it's not really a rectangle, etc. etc.


> The other canonical example of OO - "Shapes" - doesn't actually work well at all.

Shapes work quite well with OO if you design the heirarchy right using is-a relationships, the typically cited problem involves applying is-a relationships which apply to immutable geometric concept of shapes to mutable representations whose mutation contracts don’t support the is-a relationship of the immutable shapes. This would actually be fine if the contracts were written in a way which respects the it's-a relationships properly. E.g., the common Circle-Ellipse problem goes away if the contract for the mutator for, say, the major axis of an Ellipse doesn’t specify what the effect is on the minor axis. For the base class, the two can then be independent with the Circle subclass having them invariably equal under mutation.

Alternatively, its not a problem with a Smalltalk like “become”, in which case an unequally stretched Circle becomes an Ellipse with appropriate attributes.

Or, the mutable elements arr restricted to things thst don’t change the shape like scale and location, and shape transforms return a new Shape; if Ellipse’s StretchMajorAxis returns an Ellipse and so does Circle’s, there’s no problem, either.


I should have added "in C / C++ / C#".

Everything OOP works well in Smalltalk (and most of it does in Python), but you pay dearly for that in efficiency - which is what this article as about.


> I should have added "in C / C++ / C#".

The only part of my response that would have effected was the aside about Smalltalk-style “become”, not the main point about the problem being one of constructing an inheritance heirarchy based on relations of immutable entity and using it for mutable entities with mutation contracts that don’t observe the same is-a heirarchy.


I agree, but that's the thing: It is non idomatic to write immutable Shape-style classes in C++. Possible, yes. Idiomatic, no.

IIRC in Stroustroup's book (the one I read a decade ago, anyway), he concludes that Rect:Square and Circle:Ellipse can not inherit from each other in either direction, and did not suggest switching to immutable everything.


> The other thing for which OO works much better than plain data is GUIs - and I think it is not a coincidence that OO popularity exploded together with the the coming-of-age of GUIs.

Yeah, and I think the reason has nothing to do with objects per se. I've recently been coming to conclusion that the one thing that makes OOP last is that it's neatly packaging an important feature that other programming paradigms struggle with or ignore: late binding (aka. dynamic linking).

Is it better to write a class Foo with a method Bar(stuffs), or a struct Foo and a function Bar(Foo&, stuffs)? I don't care. They're literally the same thing.

But say I want to change the system so that, in some cases, it calls Baz instead of Bar - but I don't want to rip the system apart and change every call site. With OOP, I just make a class Quux inheriting from Foo, have it override Bar(stuffs) to do Baz code, and shove a Quux object into the system. Done. Dynamic linking ensure that my new implementation gets called for Quux objects.

Meanwhile, in most programming languages, you can't pull that same trick with a struct. Not without having to manually reimplement dynamic dispatch - at which point you may as well use an OOP language.

Dynamic dispatch is a super useful thing to have. You can minimize its use - and probably should, for performance reasons - but when your language doesn't support it as a built-in feature, these few cases in which you need it get very painful to write.


> But say I want to change the system so that, in some cases, it calls Baz instead of Bar - but I don't want to rip the system apart and change every call site. With OOP, I just make a class Quux inheriting from Foo, have it override Bar(stuffs) to do Baz code, and shove a Quux object into the system. Done. Dynamic linking ensure that my new implementation gets called for Quux objects.

Functional programming does this with pattern matching, it is just as easy and avoids the dangers of sub-classing.


There’s another side, which is that from a source code management perspective, modern pattern matching forces you to group implementation by operation (to get all the benefits), modern OO let’s you group by “class”.

There’s no right or wrong answer here. For GUIs, it appears grouping by class is more effective. But many other times, it’s not.


The source of this capability is hidden in that you had to pass in a Foo to the call site.

If you had done similarly in the struct example by passing in the function Bar to the caller then you could achieve similar functionality by shoving in a Baz function instead.


Yeah, but that's the thing: in case of dynamic dispatch in OOP, the function is tied to the argument you're passing. In a typical OOP language, your object carries an extra pointer that's hidden from your view - a pointer to a table of pointers to functions, specific for that object's class. Dynamic dispatch goes through that pointer on the object.

Doing what you describe with a struct would require at least keeping explicit function pointers on the structure; the OOP mechanism described above is a generalization of that.

(There are other, neater approach too, allowing late binding dispatched on the types of more than one argument - see e.g. methods in Common Lisp.)


OO is a tree and html (UI) on a page is a tree

The problem is when you want to go to page two and want to display the same data as page 1 but in a different configuration

OO is suddenly terrible because the data is the wrong shape, you should hold your data as a graph then derive trees out of it to satisfy views

OO is not good for UIs unless you have one page and the contents on that page is static and doesn't change it's placement outside the statically defined tree shape of OO


> OO is a tree

Inheritance heirarchies in single-inheritance languages are trees. In MI they are DAGs. But neither of those constrains data representations, OO can support arbitrary data graphs.

> OO is suddenly terrible because the data is the wrong shape, you should hold your data as a graph then derive trees out of it to satisfy views

How does this make OO terrible? What you describe is exactly standard OO GUI practice (specifically, in MV<whatever> architecture, the model layer is an arbitrary graph representing the data, from which the view is derived.)


I Was referring to the lower level GUI world, that in which you implement "Button" and "TextField" - OO was, and still is, a great win there. The Win32 / Cocoa / Xt level.

But even inside the browser, the fact that even div/span/thing is an object, to which you can attach listeners, and otherwise apply methods, works rather well - I'm not familiar with a better alternative for implementation and dynamic control of this layer


OO can be a tree, as well as pretty arbitrary graphs. As has been said for over a decade, prefer composition over inheritance. There are cases where inheritance is really cool, but the main point of OOP is encapsulation. It can enforce class invariants.


been thinking about this comment since I read it and really love it (even though OO isn't always a tree). This graph <> tree relationship seems central to the successes and failures of many implementation approaches I've seen over the years. Thanks for sharing.


> The other thing for which OO works much better than plain data is GUIs

This cannot be further from my experience. Switching from Java/AWT/Swing to ClojureScript/Reagent/Re-frame has been suoer liberating.

Every GUI programmer must fiddle with reagent once:

https://reagent-project.github.io/


Well, you really picked the worst representative with awt/swing. I'd say Qt or even classic VB6/Delphi are great examples for OOP gui development.


How about JavaFX? Swing has been old hat for years now.


I haven't touched java gui code in almost a decade, but didn't they suddenly pull a 180 on JavaFX a few years ago?


Not exactly, they moved it out of the core Java standard library. The main effect of this seems to have been that many developers are now uncertain of whether JavaFX can be relied on, but it is still maintained.


Cocoa has been a great OOP-based UI framework forever. The key is that it doesn't use inheritance and has messaging (https://wiki.c2.com/?AlternateHardAndSoftLayers).


> The key is that it doesn't use inheritance

Cocoa is literally based on inheriting from NSView (which itself inherits from NSResponder)

https://developer.apple.com/documentation/appkit/nsview


That's if you want to make a custom view, which very often you don't. Placing default views in a window and receiving events/values from them doesn't use inheritance, instead using composition and delegate patterns.

UIKit lost some of the convenience features like bindings when it shrunk to fit on phones, which might be why people got annoyed enough to invent entirely new UI frameworks.


> That's if you want to make a custom view, which very often you don't.

I have never seen any non-trivial software not have custom-drawn widgets - grepping for `: NSView` in my ~ gives me a few thousand matches (and it's not even a mac)

> Placing default views in a window and receiving events/values from them doesn't use inheritance, instead using composition and delegate patterns.

... but you can't have composition without at least interface inheritance, and in this case you really want access to the parent methods & properties of NSView when creating your own widgets (e.g. bounds, rects, tooltips, etc) so you also want implementation inheritance.


I just...I really want to like Clojure, but I cannot get past the brackety notation of lisp-likes. Surely there's a way to do it where I'm not in bracket hell?


Use tools like paredit that support structural editing. There are tools like this for all major editors.

https://calva.io/paredit/


But you're super limited if you're trying to make a GUI inside a browser, how is this a good example ?


The same technique can be used outside the browser with Clojure + JavaFX [0] and even at the terminal [1]!

[0] https://github.com/cljfx/cljfx

[1] https://github.com/eccentric-j/cljs-tui-template


I’m on thin ice here. A systems complexity can be measured in the number of tops it has. As in different ways to see it as hierarchical.

Could it be that OO favors systems with one top? The tomato example comes to mind. Is it a fruit or vegetables? According to American law it was classified as vegetable at some point to meet a political need to limit imports. Of course botanically it’s a fruit but usually perceived as a vegetable. So three tops identified just in this example. Depends on context; import, science or consumers.


> The other canonical example of OO - "Shapes" - doesn't actually work well at all; It doesn't work better with "plain data", but it exposes the fallacies of trying to use OO inheritance to model the real world.

Shapes work well, but people do the wrong things with them, as you state...

> Every square is a rectangle, so square should inherit from rectangle ... but, you can't stretch width and height independently in a square, so it's not really a rectangle, etc. etc.

A square is a just a rectangle where the length and width happen to be the same size and shouldn't be its own class. Books and websites teaching inheritance should stop trying to use it as an example.



> The other thing for which OO works much better than plain data is GUIs

I think the absolute dominance of React and its functional style (especially among juniors) is pretty good evidence against this belief.


It is pretty much an MVC separation that was mainstream in OOP-based GUI frameworks since the 90s at least.

But instead of “state”, you have a model, which uses the Observable pattern to notify the view of changes.


> If you imagine a game that has characters, relationships, items, spells, and so on, it's often not that easy to model as encapsulated objects.

I...disagree. ECS definitely has efficiency advantages for games in terms of support performant implementations, which for games is often overwhelmingly critical, but there’s well understand OO ways to address the modelling issue tou raise.

> Does each character have a list of other characters that they have a relationship with?

It probably acts like it does, but it probably doesn’t maintain it, instead deferring to a separate object that acts as a repository of relationships, which are themselves objects, which allows you to modify relationships without touching the characters at each side (which may be more than two for multilateral relationships).

> What happens when you want to change the relationship, or a character casts a spell that temporarily changes the relationship?

You retrieve and act on the relationship object, which knows how to handle modifications.


That doesn't really encapsulate anything, then. You end up with global data wrapped in classes.


You always have "global data" in your program - i.e. the sum of all it models. The issue is with controlling who can see and modify it. Encapsulation is a way to establish that control, and it's purpose is to minimize cognitive load on the programmer.

The problem with relationship example is that modelling this is tricky, and you have to be careful - and that's regardless of the programming paradigm used. For instance, if you're talking about "a relationship between two characters" as an objective, categorical thing (e.g. "A and B are friends", "C and D are enemies"), you're thinking about a concept that's separate from the characters themselves. So you don't want your characters to have a list of other characters, because it splits a single concept into two pieces and risks creating inconsistencies (A has a "friend" pointer to B, but B doesn't have a "friend" pointer to A). On the other hand, character A may want to query the list of characters to whom they're related, so you'll want to be able to generate that list on the fly.

Thus the OOP design would be: some object that manages all the relationships, that object being reachable from every character, and able to answer a query "what are all relationships for character $X?". An action modifying a relationship (say a spell temporarily making A and B enemies) would therefore act on that relationship manager too - which makes sense, because the action is modifying a relationship, a concept that's separate from characters themselves.

On the other hand, if you wanted to model relationships from the point of view of your characters, i.e. what they think of another character, then such relationships stop being an independent concept. You're now modelling the subjective perspective - and thus these would belong to characters, and in OOP, you'd likely model them as a list on the character object.

Whether you're doing classic OOP, plain functional programming, data-oriented programming - you still have to think about the questions described above in order to pick a correct representation for your paradigm. These questions are independent from the programming paradigm.

(And to add a further example to the mix, if you're storing character data in a relational database, these questions boil down to "is 'relationship' many:many, or 1:many?", and the answer determines the kind of bridge table you'll use.)


It's mostly a question of hierarchy of data and responsibilities. Character objects that administer changing relationships are doing too much.

For example, relationships involving "characters", "relationships", "items", "spells" etc can be handled by objects like "party" and "inventory" and "spell book" (glorified lists supporting a few custom named operations that basically do the same things) which contain the links to other objects. A spell that temporarily replaces your equipment simply swaps out your "character's" "inventory" object with some magical temporary one, and then swaps the real one back again when the "effect" from that "spell's" "invocation" ends. After this, adding support for something else like a spell that swaps inventory with the victim or steals their equipment would be trivial.

Convert this to functional terms and you've basically got a hierarchy of data that you operate functions on and generate new data to put back into the hierarchy, matching the morphing environment. It's all about the data in the end, regardless of your source point of view.


> If you imagine a game that has characters, relationships, items, spells, and so on, it's often not that easy to model as encapsulated objects. Does each character have a list of other characters that they have a relationship with? What happens when you want to change the relationship, or a character casts a spell that temporarily changes the relationship? Can easily end up a mess, because it's not obvious where such things should live in >an OO model.

It's not really that hard. You just have one RelationshipManager that every Character have access to via simple public methods. OO is not so bad, especially when you forget about inheritance and use composition.


That sounds like a poor man's component if you already separated it into another class...? And that data is no longer belonging to the instance now.


> And that data is no longer belonging to the instance now.

It belongs to the correct instance. The noun in the domain to which the information logically uniquely belongs is a relationship, not a character (of which there is typically more than one with different roles in a relationship, the number and roles varying by type of relationship.)

One problem that textbook OOP examples produce is that they tend to favor a heirarchy of physical entities as examples, which has the virtue of familiarity but really obscures the analysis of nouns in the domain, the most important of which in most real domains are the nouns describing relationships, activities, and events.


The same thing can apply to every other property I think? probably you can seperate the User::name into a PersonIdentityProperty something like that, and remove the type limit to allow it to contain any property. Then it is basically a ecs now. And that do actually has some pros. People can have multi name, you system at least works properly with it now(and for free).


OOP based component approaches are really common so it shouldn't be surprising.


Ah ... here we are again.

http://www.paulgraham.com/reesoo.html

OO is not so bad because OO is not well defined.


But splitting it out into another class is halfway there already, no? The relations object doesn't correspond to a real domain object anymore. Next step is just to split out all the other components.


> The relations object doesn't correspond to a real domain object anymore.

“relationship” is a noun describing a real feature of the domain, and thus is a real domain object.


You could say that about anything you construct in your model though? Stops being OO if it's not somehow similar to an admittedly vague concept of what an object is, surely?

I could write the whole thing as an ECS and call all the objects systems or components, and then they are nouns in the domain?


> You could say that about anything you construct in your model though?

That’s kind of the point, yes, model -> nouns -> objects.

> Stops being OO if it's not somehow similar to an admittedly vague concept of what an object is, surely?

There’s nothing particularly vague about “noun in the domain”; linguistic-based modelling certainly isn’t the be-all and end-all of OOP modelling, but its one of the traditional approaches dating back to the early 80s and its more sophisticated than thr kind of naive tangible-item approach that seems to be set up as a strawman here.

> I could write the whole thing as an ECS and call all the objects systems or components, and then they are nouns in the domain?

A relationship is a real thing in the domain being modelled, “systems” and “components” are (in the sense you are using them) not, unless the thing you are modelling is a an ECS implementation of a domain. There might be some utility for such a second-order model in OOP (if the OOP is for a code generator, for instance), but it wouldn’t be a model of the domain addressed by the ECS system, but of the ECS system itself.


ECS can definitely be built in an OO fashion.


> If you look at the latency chart he provides, there's another level to it: going to a database server. If you're gonna do that, you're not really in the speed game anyway, so why not make it simple?

OTOH, this is where I can see the biggest benefits from building a language/system around the article’s approach. You’d really like to execute a lot of the logic on the DB (or some other distributed system) which needs to be deeply integrated with the DB to be as convenient and expressive as pulling the data and operating locally.


I always laugh when people talk about ECS vs OO as if the entities, components and systems weren't objects. Its an OO pattern.

Even in the most abstract form when entities are just ids, and you combine components and systems, you're still going to implement the component-system like an object because OO is super useful. Using arrays doesn't mean you're not using OO.


You're talking like ECS and OOP are separate, while ECS is actually relational OOP : https://www.gamedev.net/blogs/entry/2265481-oop-is-dead-long...


To elaborate on Bevy: the data-oriented approach used is called an Entity Component System.


And super importantly an ECS is not necessarily data-oriented.


> as opposed to OO where you have the data declared next to the functions that mutate it.

In the popular OO languages. As usual, the Common Lisp Object System is always worth a look. You have classes that encapsulate data, and then you have "free" generic functions (well, methods that implement those generic functions) that operate on that data.


How does that enforce class invariants?


Character interacts with the 'world' or another local context object which contains all characters it can interact with.




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: