DoD is about recognising data use patterns in your software and organising your software architecture around them in a way that suits the constraints of modern hardware. That’s all.
DoD is about recognising data use patterns in your software and organising your software architecture around them in a way that suits the constraints of modern hardware. That’s all.
I’m sure this is true for some definition of OOP, but I’ve seen too much OOP code that insists on hanging every method off of the most plain-old-data of classes. A Book class has to have a buyFromAmazon() method and consequently each instance has a private reference to an Amazon SDK client, and if you want to buy 10K books, you iterate over a list and invoke this method 10K times.
Of course, some will argue that this is bad OOP and true OOP doesn’t look like this, and anyway you can write bad code in any paradigm! Of course, OOP is uniquely plagued with bad code (much more so than other paradigms) and this pretty transparent no-True-Scotsman argument is just moving semantic goalposts (as such arguments do).
Likewise critics might focus heavily on inheritance or some other feature of OOP that is easy to critique.
And whilst I’m not particularly interested in defending OOP I think describing it as uniquely plagued with bad code is not something anyone should just take axiomatically.
I like to address the boringly predictable responses up front so we can avoid rehashing the same silly arguments over and over. I apologize if that spoiled your fun.
> Is immutability a defining feature of FP? Let the battles commence.
I think most would agree that immutability is more prevalent in FP even if struct immutability isn’t required. Moreover, everyone would agree that first class functions and functional composition are key characteristics. Contrast that with OOP where you have some OOP enthusiasts arguing that inheritance is a defining feature and others who argue it isn’t. Some argue that the “kingdom of nouns”, banana-gorilla-jungle design is inherently OOP and others argue it’s “bad programming and not true OOP”. Others argue that message passing is required, but many others argue to the contrary. While other programming paradigms have fuzzy edges, OOP has no discernible shape at all.
> And whilst I’m not particularly interested in defending OOP I think describing it as uniquely plagued with bad code is not something anyone should just take axiomatically.
Why are there no equivalent criticisms of FP or DO? We might find FP codebases that are overly abstract or a bit slow, but we don’t tend to find (m)any that are designed such that a Book object holds a reference to an Amazon SDK client or a banana with a reference to the gorilla holding it with a reference to the entire jungle. There aren’t prevalent guidance to write code like that in other communities like there is (or perhaps “was”) in OOP circles. Similarly, there aren’t enterprise-y abstract factory beans or anything like that in the FP world.
Mind you, I’m not dumping on OOP—indeed I couldn’t if I wanted to because it has no agreed upon definition, per its proponents.
Object oriented to me has always been about encapsulation, sending messages, and late-binding.
Otherwise it's obvious, cheap to write, easier to get right than more complicated approaches, and good enough. True OOP is OOP that meets goals.
But more importantly, my point is that you have your definition of whether this is OOP or not but lots of OOP proponents will say that this is not true OOP.
Moreover, inevitably someone downstream from the author of the Book class will have a use case for dealing with books that the author hasn't accounted for, so they have to extend the book class for their own use case, tacking ever more fields onto that book even though their use case only cares about a few of the fields.
Additionally, some of the things you might want to do with a book might also involve some other plain-old-data-structure--how do you determine which plain-old-data should host the method? Why is it `Book.doThingWithCar(Car c);` and not `Car.doThingWithBook(Book b);` or simply a static method `doThing(Car c, Book b);`?
Further still, why create a Book.buy() method that's just going to delegate to `Retailer.buyBook(this)` anyway? If the advantage is that you don't have to explicitly pass the `retailer` around, that's fine enough but there are ways to do that without baking retailer details into every book instance (e.g., a closure: `buyBook = function() { retailer.buyBook(book) }` or if you're a glutton for punishment, you can create a class weds a book and a retailer together:
class BuyBookOperation {
private Retailer _retailer;
private Book _book;
public BuyBookOperation(Retailer retailer, Book book) {
this._retailer = retailer;
this._book = book;
}
public do() { this._retailer.buyBook(this._book); }
}
Further, you might want to buy a book from many retailers--why should you have to do `book.setRetailer(amazonClient); book.buyBook(); book.setRetailer(barnesAndNobleClient); book.buyBook();`? Why not simply `amazonClient.buyBook(book); barnesAndNobleClient.buyBook(book);`? Even if you abstract away the mutation by creating a `Book.buyFromRetailer(Retailer r)` method that sets the retailer and then calls Book.buy(), you now have a potential race condition in parallel code and you still have no advantage over retailer.buyBook(book).Lastly, if at some point you do need to buy books in batch, how do you support that? Does each book class also need a reference to every List<Book> that references it so it can do book.buyInBatch()? Do you make a book.buyInBatch(List<Book> otherBooks) method? Do you just eat the performance hit and make individual calls to book.buy() even though the Retailer interface has a buyManyBooks(List<Book> books) method that could buy all books in a single HTTP request? What advantages do these approaches have over `retailer.buyManyBooks(books)`?
So this "plain old data needs to depend on everything that might be necessary for any activity involving a book" pattern has no obvious benefits, but it creates a lot of unnecessary coupling, creates a lot of awkward interface decisions when there are other objects involved in an action (including efficiently dealing with collections of your plain old data structure), and it probably pushes you into mutation unnecessarily which makes it a lot more difficult to parallelize your code.
This pattern seems to be a pretty transparent anti-pattern to me, and if it's "inherently OOP" then OOP is problematic.
On the bright side, Rust has some damn good ECS libraries.
Not just Rust. The game dev community everywhere is infatuated with ECS.
It's basically cargo culting. There is a large base of amateur or indie game developers who want to feel like they are doing game development the "right" way. One big aspect of that is performance. ECS has a reputation for efficiency (which is true, when used well in a context where your performance problems are related to caching), so you see a lot of game devs slavishly applying it to their code in hopes that the "go as fast as a AAA game" Gods will land on their runway and deliver the goods.
Every time I see a new ECS framework in JavaScript, I die a little on the inside.
ECS is good though, it's a perfectly solid answer to a lot of thorny design questions. Where problems frequently arise is when you try to jam absolutely everything in your game into the ECS structure. In practice you're probably going to have a lot of data which lives outside the system and is not attached to any entity.
I think you're getting at the idea that instead of having objects differ in their behavior by overriding methods, you have them differ by having fields bound to objects that implement different behavior.
Assuming I understand you right, that's an excellent insight, but it's just the classic principle:
Favor object composition over class inheritance.
There's nothing new in that idea or anything special to ECS. Do you know how I know? Because that sentence here is directly quoted from page 20 of "Design Patterns", which ECS and DoD are often claimed to be in direct opposition to. :)
I guess I'm more getting at the idea of changing the game design at any point by adding or removing components, a way to make it easier for devs to cope with changing requirements (and they are always changing ofc), but you are right about favoring composition over inheritance, that by itself is pretty good.
I can't really talk about "things that are often claimed" and from the way you talk about this it seems like you have come across different opinions from mine on what ECS is or its value. Sad to see such a useful pattern get "corrupted", but I suppose that is inevitable.
But that is how cargo cult usually goes.
Polymorphism has costs, and while dynamic languages work hard to remove them, they still work best when they're able to monomorphize the call site, because that enables inlining without combinatoric explosion from chained polymorphic calls.
Having a single type in your array means field accesses, method calls etc. have the potential to be monomorphized. There are performance wins to laying out your data in ways that avoid the need for polymorphism.
I think the burden of proof is on the part of JS ECS frameworks to show they do have better performance by virtue of DoD and, if so, why. JS engine programmers have been optimizing object-oriented code for literally forty years, all the way back to when they were making Smalltalk VMs.
If somehow a couple of folks hacking on ECS frameworks have managed to write code that runs faster on those JS engines than the kind of code they were designed for, I'd like to see it.
> Having a single type in your array means field accesses, method calls etc. have the potential to be monomorphized.
Sure, but object-oriented code does not require any more polymorphism than DoD does. Consider:
* Iterate over an array of monomorphic components and call a method on each one.
* Iterate over an array of monomorphic entities, access a monomorphic property, and call a method on the latter.
There's an extra property access in the latter (which can easily be inlined), but no polymorphic dispatch. In practice, yes, it is possible to reorganize your JavaScript code in ways that play nicer with inline and possibly even code caching. But I have never seen any evidence that JS ECS frameworks actually do that. Instead, the few I've poked around in seem like typical slow imperative dynamically-typed JS.
If someone is going to take a pattern that was invented specifically for a language like C++ that gives you precise control over memory layout and then apply it to a language that not doesn't give you that control but often uses hash tables to store an object's state, I think the burden of proof is on the framework to show that the pattern actually applies.
https://github.com/thi-ng/umbrella/tree/master/packages/ecs
Optimized Typescript ECS with a demo rendering 100,000 live 3D particles
Super nice guy too, always willing to share information or explain stuff to you if he's around.
I'd expect the wins from reworking your JS so that your target JS engine lays out data out better would often be larger than the wins in C++, simply because the worst case is so bad.
I'd add a third option to your pair: iterating over several arrays of properties - especially numeric properties - rather than an array of objects which each have numeric properties. That can get you substantial wins in Java, never mind JS.
The type checks themselves have significant overhead, and it's easier to fall off the shadow class fast path than you might expect.
> Initialize your objects carefully in a deterministic order, and I'd expect you can control field order and adjacency.
True, but that's equally true of non-ECS architectures. I have yet to see much evidence that the ECS JS engines I've looked at are actually taking that into account.
I watched this sentence unfold with bated breath, waiting to yell "and ze sticks the landing!", only to see it end with "goods" instead of "cargo". It's frustratingly close to perfect, though perhaps to end the paragraph with "cargo", you'd have to begin with some synonym for cargo culting.
https://news.ycombinator.com/item?id=22700563
There was an early talk about using a job system to run the Destiny renderer and then this one for the whole engine which is a very similar premise to the way DOOM(2016) and then DOOM Eternal evolved.
https://www.gdcvault.com/play/1022164/Multithreading-the-Ent...
The renderer talk is here: https://youtu.be/0nTDFLMLX9k
So a 1986 concept rediscovered by game developers and cargo culted ever since, although it has its roots on the OOP they love to hate.