I found when I started programming I never used OOP. Then I used it too much. And then recently I use it incredibly sparingly. I think this is most people's experience.
However, there are certain situations where I cannot imagine working without OOP.
For example, GUI development. Surely nobody would want to do without having a Textbox, Button, inherit from a general Widget, and have all the methods like .enable(), .click(), and properties like enabled, events like on_click, etc.?
Similarly, a computer game, having an EnemyDemon inherit from Enemy, so that it has .kill(), .damage(), and properties for health, speed etc.?
I'd really like to know how the most anti-OOPers think situations like this should be handled? (I'm not arguing, genuinely interested)
At least regarding games, ECS is the popular flavor-of-the-month alternative to OOP as a design strategy. The main problem being that games often have a lot of special cases, which break the inheritance hierarchy quite quickly.
E.g. Defining a weapon > {sword, wand} hierarchy, with respective properties for melee and casting, and then defining a unique weapon spellsword which is capable of both melee and casting. You could inherit from weapon, and copy & paste sword/wand code, or inherit from sword/wand, and copy & paste the other, but the hierarchy is broken.
ECS would rather have you define [melee] and [casting] components, and then define a sword to have [melee], wand to have [casting] and spellsword to have [melee, casting]. So instead of representing the relationships as a tree of inheritance, you represent it as a graph of components (properties). And then you generically process any object with the melee tag, and any object with the casting tag, as needed.
And of course then you could trivially go and reach out across the hierarchies and toss [melee] onto your house object and wield your house like a sword -- I don't know why you'd want to do that, but the architecture is flexible enough to do so (perhaps to your detriment).
That's probably more an example of "metadata-driven" but it's ultimately the same thing -- an entity in the game is defined by its components, and the job of the game engine is to simply drive those components through the simulation. That particular example has its metadata (e.g. aesthetics: [CREATURE_TILE:249][COLOR:2:0:0]), its capabilities (e.g. [AMPHIBIOUS][UNDERSWIM]) and its data (e.g. [PETVALUE:10][BODY_SIZE:0:0:200]).
exactly the post I was trying to remember when talking about spellswords :)
Those posts are also cool in that defining games as a set of rules that operate on things within it is a really neat mental model -- the program should basically look like a DnD rulebook, with statblocks and all.
The way to really grasp ECS architecture is not to look too hard at the implementations(which are all making specific trade-offs) but to recognize where it resembles and deviates from relational database design. A real-time game can't afford the overhead of storing data in a 3NF schema, but it can design a custom structure that preserves some data integrity and decoupling properties while getting an optimized result for the common forms of query.
The behavioral aspects are subsumed in ECS to sum types, simple branching and locks on resources, where the OOP-embracing mode was to focus on language-level polymorphism and true "black-boxing". Since the assumed default mode of a game engine is global access to data and the separation of concerns is built around maintaining certain concurrency guarantees(the order in which entities are updated should have minimal impact on outcomes), ECS makes more sense at scale.
The implementation trade-off comes in when you start examining how dynamic you want the resulting system to be: You could generate an optimal static memory layout for a scene(with object pools used to allow dynamic quantities to some limit) or you could have a dynamic type system, in essence. The latter is more straightforward to feed into the edit-test iteration loop, but the former comes with all the benefits of static assumptions. Most ECS represents a point in the middle where things are componentized to a hardcoded schema.
> E.g. Defining a weapon > {sword, wand} hierarchy, with respective properties for melee and casting, and then defining a unique weapon spellsword which is capable of both melee and casting. You could inherit from weapon, and copy & paste sword/wand code, or inherit from sword/wand, and copy & paste the other, but the hierarchy is broken.
Or, you could, in OO Python:
class SpellSword(Sword, Wand):
...
or, possibly even:
class Melee:
class Casting:
class Sword(Melee):
class Wand(Casting):
class SpellSword(Melee, Casting):
> So instead of representing the relationships as a tree of inheritance, you represent it as a graph of components (properties).
Multiple inheritance also represents relationships as a graph of properties.
I've been hearing about ECS for a decade so it's definitely more then flavor of the month. However the issue is that unfortunately Unreal/Unity are both OO first.
It's definitely been around but I think Unity's (never-finishing) ECS + Rust gamedev community's focus on it has really spiked its popularity/interest lately. Otherwise pretty much every recommendation/engine is OOP-based, with a few straggling extensions/libraries for ECS here and there.
This has been a recognized problem for game devs for a while and recently Unity introduced DOTS to gain performance. From what I understand, doing traditional OOP means going through a lot of pointers to find information. If you put everything into arrays instead, you reduce the time spent looking up things.
I’m not sure it is; the heart of OOP is encapsulation of data + methods — the object itself. (Or message-passing if you’re being kinky)
ECS is almost the opposite; the object in question is barely defined — rather it’s derived from the composition of its parts. Like a DB, there’s roughly no encapsulation at all; just relations defined where, should you put them all together properly, you get the “object” back.
That is, in OO a particular object is composed of certain properties and capabilities. In ECS, if certain properties and capabilities exist, we call it a particular object.
Implementation-wise, the main difference seems to be the SoA vs AoS argument
But there’s also things like unity’s ECS deviating from rust’s ECS definition — I believe in Unity, components (data) and systems (behavior) are coupled, which fits better towards the OO world (components are the object of question). In rust ECS they’re decoupled (which unity is moving to, someday) and you’re really just dealing with structs and functions. And there’s a whole thing about the “true” ECS definition but it doesn’t really matter.
I've described an ECS to my friend as a combination Strategy pattern and Composition, so you can have a huge list of "traits/components/behaviours" and then build your object hierarchies (and their behaviours) from that, instead of from Inheritance. The way most game editors are made, when you configure things using the ECS, it visually looks like inheritance, but under the hood it is more like Composition. Once you go down the rabbit hole, you would see that and ECS/Strategy pattern & Composition makes more sense (and way easier to cater to exceptions) than plain old inheritance.
I want dare say that the enterprisey-version of an ECS would be the Onion Architecture that's been floating around in Java/C# camps. Not 100% match but they feel the same to me.
Obviously for normal GUI programming (at the outer layer that we see at least), inheritance is still king.
the patterns can still be completely the same without OOP - pairing data and functions that operate on said data. enemy.damage() and Enemy::damage(enemy) are functionally and semantically equivalent. But in the latter case (where you separate data and code) you don't need to worry about object assembly/IoC, how to pass a reference to A all the way through the object graph to object B, composition over inheritance becomes the default (at least in my experience using TypeScript interfaces, YMMV with other languages). The benefits of OOP, primarily state encapsulation, stop looking like benefits when it turns out your state boundaries weren't quite right.
Of course I'm biased as I went through the same "procedural => OOP => case-by-case" learning curve as the GP. But I ended up spending a lot of time trying to satisfy vague rules when using OOP - with procedural/functional programming with schema'd data, I get to spend a lot more time on what I actually want to do. Not worrying about SRP, SOLID, object assembly, how to fix my object graph now that A needs to know about B, and so on.
> how to pass a reference to A all the way through the object graph to object B
you just end up replacing the object graph by the call graph, which makes all the business logic much messier as now every function call takes a "context" argument
That's not been true in my experience - what you do end up with is a global data structure, which is the shared state for all or most non-ephemeral top level concerns. Aside from recursive functions I find call stacks tend to be quite short.
I completely agree with you when it comes to UI although the key here is that OOP is syntactic sugar, it shouldn't be the overarching pattern. I think of it as augmenting types, or prototype-based OO.
And when it comes to games, nope; entity patterns with composable behaviours added to dumb objects is far more productive that traditional OOP, as you need many objects with slightly different behaviours/abilities/types. Composition over inheritance is key here.
The weakness of OOP structurally stems almost entirely from inheritance, which I think is very poor construct for most complex programs.
How should the widget situation be handled? Well, what is a widget? It's hard to define, because it's a poor abstraction.
Maybe all your "widgets" should be hide-able, so you implement a `.hide()` and `.show()` method on `Widget`. Oh, and all your widgets are clickable, so let's implement a `.click()` method.
Oh wait.. but this widget `some-unclickable-overlay` is not clickable, so let's build a `ClickableWidget` and `ClickableWidget` will extend `Widget`. Boom, you're already on your way to `AbstractBeanFactory`.
We got inheritance because it's an easy concept to sell. However, what if we talked about code re-use in terms of traits instead of fixed hierarchies?
So, our `some-unclickable-overlay` implements Hideable. Button implements Hideable, Clickable. We have common combination of these traits we'd like to bundle together into a default implementation? Great, create super trait which "inherits" from multiple traits.
Rust uses such a system. They don't have classes at all. Once you use a trait system, the whole OOP discussion becomes very obvious IMO.
1. Shared state can be bad, avoid if possible
2. Inheritance is a poor construct, use traits, interfaces, and composition instead.
3. Don't obsess about DRY and build poor abstractions. A poor abstraction is often more costly than some duplicated code.
4. Use classes if they're the best tool in your environment to bundle up some context together and pass it around, otherwise don't
> Similarly, a computer game, having an EnemyDemon inherit from Enemy, so that it has .kill(), .damage(), and properties for health, speed etc.?
I'm not a game developer in the slightest but as a gamer and developer I've often thought about similar things a little bit.
In another example, let's say you were playing a game like Diablo II / Path of Exile where you have items that could drop with random properties. Both of those games support the idea of "legacy" items. The basic idea is the developers might have allowed some armor to drop with a range of +150-300% defense in version 1.0 of the game but then in 1.1 decided to nerf the item by reducing its range to +150-200% defense.
Instead of going back and modifying all 1.0 versions of the item to fit the new restrictions, the game keeps the old 1.0 item around as its own entity. It has the same visible name to the player but the legacy version has the higher stats. Newer versions of the item that drop will adhere to the new 1.1 stat range.
That made me think that they are probably not using a highly normalized + OOP approach to generate items. I have a hunch every item is very denormalized and maybe even exists as its own individual entity with a set of stats associated to it based on whenever it happened to be generated. Sort of like an invoice item in a traditional web app. You wouldn't store a foreign key reference to the price of the item in the invoice because that might change. Instead you would store the price at the time of the transaction.
I guess this isn't quite OOP vs not OOP but it sort of maybe is to some degree.
I'd be curious if any game devs in the ARPG genre post here. How do you deal with such high amounts of stat variance, legacy attribute persistence, etc.?
There are a bunch of FP GUI development styles without OO. The Elm style[1], which propagated to a whole family tree of similarly structured system, and similar ways in Reagent based apps in the ClojureScript world.
>However, there are certain situations where I cannot imagine working without OOP. (...) Similarly, a computer game, having an EnemyDemon inherit from Enemy, so that it has .kill(), .damage(), and properties for health, speed etc.?
You'd be surprised. This is much cleaner, and more efficient too:
In game development even there seems to be a shift away from OO, to data + functions under the guise of "Data Orientated/Driven Development" - eg: https://www.youtube.com/watch?v=0_Byw9UMn9g
If we step into the haskell land of monads having explicit functionality kept in individual monads with their own instances would be one way to segment this type of stuff. Something vaguely written as below would let you run actions where anyone can move or is an enemy, and default implementations can be provided as well.
data Demon = { ... }
data Action
= Dead
| KnockedBack
| Polymorphed
class Character a where
health :: Int
class (Character a) => Movement a where
speed :: Int
class (Character a) => Enemy a where
kill :: a -> Action
damage :: a -> Action
instance Character Demon where
health = 30
instance Movement Demon where
speed = 5
instance Enemy Demon where
kill _ = _
damage _ = _
https://soupi.github.io/rfc/pfgames/ is a talk going through an experience building a game in a pure fp way with Haskell and how they modelled certain aspects of game dev. Most of the code examples are when you press down in the slides.
I used to think this too. And I have to admit OO does lend itself well to GUI and hierarchical structures (IMO probably the only case I think it is actually useful). But that's not to say that there aren't declarative methods that are equally nice to use, Elm has done a lot to popularize this style of coding GUIs.
However, there are certain situations where I cannot imagine working without OOP.
For example, GUI development. Surely nobody would want to do without having a Textbox, Button, inherit from a general Widget, and have all the methods like .enable(), .click(), and properties like enabled, events like on_click, etc.?
Similarly, a computer game, having an EnemyDemon inherit from Enemy, so that it has .kill(), .damage(), and properties for health, speed etc.?
I'd really like to know how the most anti-OOPers think situations like this should be handled? (I'm not arguing, genuinely interested)