Better than unlearning OOP might be learning OOP, then transcending it and just thinking about polymorphism or problem modelling or stable APIs or extensability/modularity.
Learning OOP thoroughly involves understanding that it’s a collection of loosely related ideas, some of which are good, some of which are bad, and some of which are occasionally useful.
But I think the article is actually about unlearning the habits of OOP, not the concepts themselves.
I really think the problem with OOP is it teaches people a pretty restrictive and roundabout method of problem solving that only really shines in a few domains. OOP in C++ was part of my first programming class and that did a disservice to us imo.
Input/Output/Processing is what I’d start with. After a decade of programming I don’t think I’ve ever ran into a case where that would be a bad call.
C++ really does not lend itself to OOP. I had a “intro to OOP in C++” class at my University and in predictably taught zero OOP. I mean, the “class” keyword every came up I suppose, so if you fuzz your eyes real hard then maybe…
These are all valid criticisms of certain patterns in software engineering, but I wouldn’t really say they’re about OOP.
This paper goes into some of the distinctions of OOP and ADTs, but the summary is basically this:
ADTs allow complex functions that operate on many data abstractions – so the Player.hits(Monster) example might be rewritten in ADT-style as hit(Player, Monster[, Weapon]).
Objects, on the other hand, allow interface-based polymorphism – so you might have some kind of interface Character { position: Coordinates, hp: int, name: String }, which Player and Monster both implement.
Now, interface-based polymorphism is an interesting thing to think about and criticise in its own right. It requires some kind of dynamic dispatch (or monomorphization), and hinders optimization across interface boundaries. But the critique of OOP presented in the OP is nothing to do with interfaces or polymorphism.
The author just dislikes using classes to hold data, but a class that doesn’t implement an interface is basically the same as an ADT. And yet one of the first recommendations in the article is to design your data structures well up-front!
The main problem I have with these “X is dead” type article is they are almost always straw man arguments setup in a way to prove a point. The other issue I have is the definition or interpretation of OOP is so varied that I don’t think you can in good faith just say OOP as a whole is bad and be at all clear to the reader. As an industry I actually think we need to get past these self constructed camps of OOP vs Functional because to me they are disingenuous and the truth, as it always does, lies in the middle.
Personally, coming mainly from a Ruby/Rails environment, use ActiveRecord/Class to almost exclusively encapsulate data and abstract the interaction with the database transformations and then move logic into a place where it really only cares about data in and data out. Is that OOP or Functional? I would argue a combination of both and I think the power lies in the middle not one versus the other as most articles stipulate. But a middle ground approach doesnt get the clicks i guess so here we are
the definition or interpretation of OOP is so varied that I don’t think you can in good faith just say OOP as a whole is bad and be at all clear to the reader
Wholly agreed.
The main problem I have with these “X is dead” type article is they are almost always straw man arguments setup in a way to prove a point.
For a term that evokes such strong emotions, it really is poorly defined (as you observed). Are these straw man arguments, or is the author responding to a set of pro-OOP arguments which don’t represent the pro-OOP arguments with which you’re familiar?
Just like these criticisms of OOP feel like straw men to you, I imagine all of the “but that’s not real OOP!” responses that follow any criticism of OOP must feel a lot like disingenuous No-True-Scotsman arguments to critics of OOP.
Personally, I’m a critic, and the only way I know how to navigate the “not true OOP” dodges is to ask what features distinguish OOP from other paradigms in the opinion of the OOP proponent and then debate whether that feature really is unique to OOP or whether it’s pervasive in other paradigms as well and once in a while a feature will actually pass through that filter such that we can debate its merits (e.g., inheritance).
I imagine all of the “but that’s not real OOP!” responses that follow any criticism of OOP must feel a lot like disingenuous No-True-Scotsman arguments to critics of OOP.
One thing I have observed about OOP is how protean it is: whenever there’s a good idea around, it absorbs it then pretend it is an inherent part of it. Then it deflects criticism by crying “strawman”, or, if we point out the shapes and animals that are taught for real in school, they’ll point out that “proper” OOP is hard, and provide little to no help in how to design an actual program.
Here’s what I think: in its current form, OOP won’t last, same as previous form of OOP didn’t last. Just don’t be surprised if whatever follows ends up being called “OOP” as well.
The model presented for monsters and players can itself be considered an OO design that misses the overarching problem in such domains. Here’s a well-reasoned, in-depth article on why it is folly. Part five has the riveting conclusion:
Of course, your point isn’t about OOP-based RPGs, but how the article fails to critique OOP.
After Alan Kay coined OOP, he realized, in retrospect, that the term would have been better as message-oriented programming. Too many people fixate on objects, rather than the messages passed betwixt. Recall that the inspiration for OOP was based upon how messages pass between biological cells. Put another way, when you move your finger: messages from the brain pass to the motor neurons, neurons release a chemical (a type of message), muscles receive those chemical impulses, then muscle fibers react, and so forth. At no point does any information about the brain’s state leak into other systems; your fingers know nothing about your brain, although they can pass messages back (e.g., pain signals).
(This is the main reason why get and set accessors are often frowned upon: they break encapsulation, they break modularity, they leak data between components.)
Many critique OOP, but few seem to study its origins and how—through nature-inspired modularity—it allowed systems to increase in complexity by an order of magnitude over its procedural programming predecessor. There are so many critiques of OOP that don’t pick apart actual message-oriented code that beats at the heart of OOP’s origins.
Many critique OOP, but few seem to study its origins and how—through nature-inspired modularity—it allowed systems to increase in complexity by an order of magnitude over its procedural programming predecessor.
Of note, modularity requires neither objects nor message passing!
For example, the Modula programming language was procedural. Modula came out around the same time as Smalltalk, and introduced the concept of first-class modules (with the data hiding feature that Smalltalk objects had, except at the module level instead of the object level) that practically every modern programming language has adopted today - including both OO and non-OO languages.
I have to say, after read the first few paragraphs, I skipped to ‘What to do Instead’. I am aware of many limitations of OOP and have no issue with the idea of learning something new so, hit me with it. Then the article is like ’hmm well datastores are nice. The end.”
The irony is that I feel like I learned more from your comment than from the whole article so thanks for that. While reading the Player.hits(Monster) example I was hoping for the same example reformulated in a non-OOP way. No luck.
If anyone has actual suggestions for how I could move away from OOP in a practical and achievable way within the areas of software I am active in (game prototypes, e.g. Godot or Unity, Windows desktop applications to pay the bills), I am certainly listening.
Glad I was helpful! I’d really recommend reading the article I linked and summarised – it took me a few goes to get through it (and I had to skip a few sections), but it changed my thinking a lot.
[interface-based polymorphism] requires some kind of dynamic dispatch (or monomorphization), and hinders optimization across interface boundaries
You needed to do dispatch anyway, though; if you wanted to treat players and monsters homogenously in some context and then discriminate, then you need to branch on the discriminant.
Objects, on the other hand, allow interface-based polymorphism – so you might have some kind of interface […] which Player and Monster both implement
Typeclasses are haskell’s answer to this; notably, while they do enable interface-based polymorphism, they do not natively admit inheritance or other (arguably—I will not touch these aspects of the present discussion) malaise aspects of OOP.
You needed to do dispatch anyway, though; if you wanted to treat players and monsters homogenously in some context and then discriminate, then you need to branch on the discriminant.
Yes, this is a good point. So it’s not like you’re saving any performance by doing the dispatch in ADT handling code rather than in a method polymorphism kind of way. I guess that still leaves the stylistic argument against polymorphism though.
Just to emphasize your point on Cook’s paper, here is a juicy bit from the paper.
Any time an object is passed as a value, or returned as a value, the object-oriented program is passing functions as values and returning functions as values. The fact that the functions are collected into records and called methods is irrelevant. As a result, the typical object-oriented program makes far more use of higher-order values than many functional programs.
Now, interface-based polymorphism is an interesting thing to think about and criticise in its own right. It requires some kind of dynamic dispatch (or monomorphization), and hinders optimization across interface boundaries.
After coming from java/python where essentially dynamic dispatch and methods go hand in hand I found go’s approach, which clearly differentiates between regular methods and interface methods, really opened my eyes to overuse of dynamic dispatch in designing OO apis. Extreme late binding is super cool and all… but so is static analysis and jump to definition.
I find these kind of articles exhausting. They usually come down to the same:
pattern X seen in paradigm Y is bad, therefor Y is bad. The alternatives
proposed usually aren’t really good either. Basically once you’ve read one of
these, you’ve read all of them.
For example:
If it looks like a candidate for a class, it goes into a class. Do I have a
Customer? It goes into class Customer. Do I have a rendering context? It goes
into class RenderingContext.
Then the “solution” presented in this article:
The data itself will be in form of an ADT/PoD structures, and any references
between the data records will be of a form of an ID (number, uuid, or a
deterministic hash). Under the hood, it typically closely resembles or
actually is backed by a relational database: Vectors or HashMaps storing bulk
of the data by Index or ID, some other ones for “indices” that are required
for fast lookup and so on.
Now there’s nothing wrong with plain old data structures. But eventually those
data structures are going to need a bunch of associated functions. And then you
pretty much have a class. In other words, the solution is basically the same as
the problem.
The real problem isn’t unique to OOP and can just as easily occur in say
functional programming languages. That is, the problem is people over-applying
patterns without thinking “Hey, do we actually need this?”. Traditional OOP
languages may promote this in some way, but again that’s a problem with those
languages. The concept of OOP has nothing to do with any of this.
Random example: in any language that has support for macros, inevitably some
will start abusing macros. But that doesn’t mean the entire language or its
paradigm is bad and should be avoided.
As an aside, every time I see a quote from Dijkstra I can’t help but feel this
man must have been absolutely insufferable to work with. Yes, he was very
smart and made many contributions. But holy moly, his “I am right and everybody
else is wrong” attitude (at least that’s how it comes across to me) is
off putting to say the least.
Now there’s nothing wrong with plain old data structures. But eventually those data structures are going to need a bunch of associated functions. And then you pretty much have a class. In other words, the solution is basically the same as the problem.
It is the other way around. With OOP, you pretty much end up jamming into data structure definitions what essentially are functions.
The concept of class is fuzzy and it’s not a clear well define starting point for a thought process.
Notice that you said “associated functions”. I think it’s all the OOP non sense cornering you into that unclear language. What exactly are those? Functions that accept the type you are defining? Functions that manipulate the state of said data structure? Functions that return a reference to it?
If you think about this questions and find clear answers for them, you will realize that there is absolutely no reason to make functions have all sorts of tricky behaviours based on state or even “belonging to an instance”. At which point the concept of class becomes pointless.
Relational algebra was developed with solid theory behind it. To my knowledge, OOP was just something thrown together “because it is a good idea”.
Records, as in compound types, are very useful in many fields even outside programming languages. Hooking functions to them is just a strange idea whose motivation I am yet to discover.
Notice that you said “associated functions”. I think it’s all the OOP non sense cornering you into that unclear language. What exactly are those?
When I say “associated function” I mean that in the most basic sense: it simply does something with the data, regardless of how the code is organised, named, etc.
If you think about this questions and find clear answers for them, you will realize that there is absolutely no reason to make functions have all sorts of tricky behaviours based on state or even “belonging to an instance”. At which point the concept of class becomes pointless.
I’m not sure what tricky behaviour have to do with anything. That just seems like you’re inventing problems to justify your arguments.
even “belonging to an instance”
Perhaps this comes as a surprise, but this exists in functional programming too. For example, if you have a String module with a to_lowercase() function, and that function only operates on strings, then that function basically “belongs to an instance”. How exactly you store that function (in the instance, elsewhere, etc) doesn’t matter; the concept is the same. Whether the data is mutable is also completely unrelated to that, as you can have OOP in a completely immutable language.
Relational algebra was developed with solid theory behind it. To my knowledge, OOP was just something thrown together “because it is a good idea”.
I suggest you do some actual research into the origins of OOP, instead of spewing nonsense like this. It’s frankly embarrassing.
Have you ever stumbled upon good OOP code that actually looked OOP?
I haven’t. The good code I’ve seen was inevitably a mix of procedural, modular, and functional code, with a heavy slant towards either procedural or functional, with maybe a couple instances of inheritance, for polymorphism’s sake (and even then, sometimes we just pass functions directly).
The most distinguishing characteristic I see in OOP is how it stole almost every features to other paradigms or languages. ADT, encapsulation? Modular programming, from Modula. Generics? Parametric polymorphism from ML and Miranda. Lambdas? From every functional language ever. The only things left are inheritance, which was added in Simula to implement intrusive lists (which were needed because there was no C++ like templates), and subtype polymorphism, which is often better replaced by good old closures.
And guess what, inheritance is now mostly discouraged (we prefer composition). The only thing left is subtype polymorphism. OOP is an empty husk, that only survives by rebranding other programming styles.
Have you ever stumbled upon good OOP code that actually looked OOP?
This depends on what one would consider OOP, as the opinions/interpretations differ. Have I seen good OOP? Yes. Was that Java-like OOP as I would imagine most people think OOP is like? No. But just because something is OOP doesn’t mean it can’t have elements from other paradigms.
The most distinguishing characteristic I see in OOP is how it stole almost every features to other paradigms or languages. ADT, encapsulation? Modular programming, from Modula. Generics? Parametric polymorphism from ML and Miranda. Lambdas? From every functional language ever
Ah yes: functional languages invented everything, and every other language using elements from this is “stealing” them.
I’m honestly not sure what point you’re trying to make. X sharing elements with Y doesn’t mean somehow X isn’t, well, X. Just as how X having flaw Y doesn’t completely invalidate X. Having such a single minded (if that’s the right term) attitude isn’t productive.
Ah yes: functional languages invented everything, and every other language using elements from this is “stealing” them.
Not just functional. Modules did not come from functional languages, that I recall.
To some extent though, yes: functional languages invented a lot. Especially the statically typed ones, whose inventors realise this fundamental truth that often gets me downvoted in programming forums if I voice it: that programming is applied mathematics, and by treating it as such we can find neat ways to make programs better (shorter, clearer, or even faster). Dijkstra was right. Even Alan Kay recognises now through his STEPS project that “math wins”. (Of course, it’s very different from calculus, or most of what you were taught in high school. If anything, it’s even more rigorous and demanding, because at the end of the day, a program has to run on a dumb formal engine: the computer.)
I’m honestly not sure what point you’re trying to make.
That to many OOP proponents, “OOP” mostly means “good”, and as we learn how to program better over the decades, they shift the definition of “OOP” to match what they think is good. It takes a serious break, like data oriented programming, to realise that there are other ways. To give but an example: back in 2007, I designed some over-complicated program in C++, with lots of stuff from the <algorithm> header so I could pretend I was using OCaml (I was an FP weenie at the time). My supervisor look at the code (or maybe I was outlying my design to them, I don’t remember), and said “well, this is very OO and all, but maybe it’s a bit over-complicated?”
That’s how pervasive OOP is. Show a programmer functional patterns (with freaking folds!), they will see OOP.
OOP is no longer a paradigm. It devolved into a brand.
But eventually those data structures are going to need a bunch of associated functions. And then you pretty much have a class.
Running in the risk of taking your quote out of context, I think the mindset OOP is simply data structures with encapsulated functions is actually one of the biggest real dangers of OOP, because it hides its biggest flaw: pervasive proliferation of (global) states.
Thus, I understand where you are leading your argument, but I disagree with it.
As an aside, every time I see a quote from Dijkstra I can’t help but feel this man must have been absolutely insufferable to work with. Yes, he was very smart and made many contributions. But holy moly, his “I am right and everybody else is wrong” attitude (at least that’s how it comes across to me) is off putting to say the least.
I happen to know a lot of people who directly worked (or had classes) with him, and unanimously I hear both adjectives: genius and pretentious.
Of course those are just others’ opinions, not mine, but I share your (and those people) feelings.
Running in the risk of taking your quote out of context, I think the mindset OOP is simply data structures with encapsulated functions is actually one of the biggest real dangers of OOP, because it hides its biggest flaw: pervasive proliferation of (global) states.
I agree a lot of OOP languages/projects suffer from too much (global) mutable state. But I’m not sure if that’s necessarily due to OOP. I think this is a case of “correlation is not causation”. Perhaps a silly example: if functional languages had mutable state, I think they would have similar issues. In other words, I think the issue is mutability being “attractive”/tempting, not so much the code organisation paradigm.
Another example: I think if you take away the ability to assign non-constant types (basically anything but an int, float, string, etc) to constants/globals, and maybe remove inheritance, you already solve a lot of the common issues seen in OOP projects. This is basically what I’m doing with Inko (among other things, such as replacing the GC with single ownership).
I do think for such languages we need a better term for the paradigm. Calling it X when it mixes properties from A, B, and C is confusing. Unfortunately, I haven’t found a good alternative term.
I view OOP as an organizing principle—when you have few types, but lots of actions, then procedural is probably the way to organize the program (send the data to the action). When you have a few actions, but lots of types, then OOP is the way to organize the program (send the action to the data). When you have few actions, few types, then it doesn’t matter. It’s that last quadrant, lots of types, lots of actions, there is currently no good method to handle.
When you have X types and Y actions, unless you have many of both, I believe your program is already mostly organised. Many types and few actions? It will end up looking OOP even if you write it in C. Few types and many actions? It will end up looking procedural even if you write it in Java.
I still have PTSD from working with an OOP zealot. He would change all of my functions to be internal state in object so everything become opaque. Instead of having a class that you could instantiate with external parameters, he created three classes sharing 90% of code and overrode some of the functions. He could not explain to me why he is doing it. Just because OOP says so.
That is the problem. It doesn’t really have a point. It was presented as a modern better paradigm and embraced by megacorps without ever having proper research backing it.
I don’t really see how your question relates to my comment, but for my view of the items which approximate “the point” see my top-level comment at the top of the thread.
The article starts with a classy appeal to authority.
At its core, all software is about manipulating data to achieve a certain goal. The goal determines how the data should be structured, and the structure of the data determines what code is necessary.
Yes, exactly. Therefore, you can first define a logical data structure, and then add methods that operate on said structure. This idea does not conflict with OOP. In fact, prototype-inheritance languages take this a step further and separate data and code, just like you want it; it’s called a “traits object”, and it looks like this:
"Assume this is traits yourObject."
(|
parent*= traits clonable.
doSomethingWith: foo = (
bar: foo baz.
"Code to do something with foo goes here."
).
"More code here as necessary."
|)
"Then, let's have a prototype for this object. Assume it is globals yourObject."
(|
parent* = traits yourObject.
bar <- nil.
"More constant or mutable slots go here."
|).
"Now, let's use this object."
| obj |
obj: yourObject copy.
obj doSomethingWith: foo copy.
"etc. etc."
This “traits object” method allows you to keep code and data in two separate objects. You can even make the parent slot (the one with the star) assignable, so you can use the same object with a different set of methods.
In my experience, the biggest problem with OOP is that encourages ignoring the data model architecture and applying a mindless pattern of storing everything in objects, promising some vague benefits.
An integer is an object, and so is a struct. Everything can be done wrong if applied poorly.
Instead of building a good data architecture, the developer attention is moved toward inventing “good” classes, relations between them, taxonomies, inheritance hierarchies and so on. Not only is this a useless effort. It’s actually deeply harmful.
Not if you use a top-down approach. With such an approach, you first think about what your application should look like, and then implement the layer below that, then the layer below that, and so on and so forth. Application architecture can be done poorly in non-OOP contexts as well.
FizzBuzz Enterprise Edition
Yes, it’s a meme that is purposefully verbose. I’m sure you can also spawn an abomination called FizzBuzz Academic Edition implements it in something like Haskell or Idris and turns it into type spaghetti instead.
OOP apologists will respond that it’s a matter of developer skill, to keep abstractions in check.
It’s a human skill to visit the toilet when one has to discharge. Everyone expects it, and there is no excuse other than a medical condition. Similarly, if you can’t keep your architecture in check then this is not the fault of a programming paradigm, but rather your application of it.
Your class Customer has a reference to class Order and vice versa. class OrderManager holds references to all Orders, and thus indirectly to Customer’s. Everything tends to point to everything else because as time passes, there are more and more places in the code that require referring to a related object.
I don’t really see how you would improve upon this in a non-OOP setting? You will need to manage those relations somehow. Are you implying that having references to other objects is bad? Also, an orderManager need not hold references to all orders, why would it? It would simply deliver you the order instances. This sounds like a strawman.
Another appeal to authority.
Example: when class Player hits() a class Monster, where exactly do we modify data?
(|
hit = (|
parent* = traits clonable.
damage.
copyDamage: damage = (| c |
c: self copy.
c damage: damage.
c.
).
|).
player = (|
parent* = traits clonable.
"Fields etc."
defaultDamage.
hitsMonster: monster = (
monster receivedHit: defaultDamage.
monster dead ifTrue: [
kills: kills succ.
].
).
|).
monster = (|
parent* = traits clonable.
receivedHit = (
"Do whatever you need to modify the state."
).
).
test = (| p. m. |
p: player copy.
p defaultDamage: damage copyDamage: 50.
m: monsters example.
p hitsMonster: m.
).
|).
encapsulation on a granularity of an object or a class often leads to code trying to separate everything from everything else (from itself).
This can often have good reason. A property might have a very good reason to perform an operation when it’s set. I actually disagree with Python’s or C#‘s properties here, because they hide what’s actually going on. In Self, everything is a message pass; therefore, you can simply provide a message that performs the action you want.
In my opinion classes and objects are just too granular, and the right place to focus on the isolation, APIs etc. are “modules”/“components”/“libraries” boundaries.
That has its own share of problems. “Exporting” from a library is basically same thing as making a member public. I don’t quite understand the difference between having public and private methods on a “class”, and having public and private functions in a module.
If program data is stored e.g. in a tabular, data-oriented form, it’s possible to have two or more modules each operating on the same data structure, but in a different way.
See above for Self’s dynamic polymorphism.
Combination of data scattered between many small objects, heavy use of indirection and pointers and lack of right data architecture in the first place leads to poor runtime performance. Nuff said.
Not “nuff said”. Self’s generational GC concept has proven itself quite useful, and forms the basis of the Java HotSpot VM (one of the fastest VMs around, I think most would agree) along with the JIT. Object oriented programming languages do not create non-performant code; bad code creates non-performant code.
The DataStore approach is just bizarre and seems like it’s implementing a slow, half-broken version of an RDBMS within the application. If you need to go brrrr fast then just use a query builder.
All in all, 4/10, could use less inspirational quotes from figureheads.
One thing I’ve noticed in most non-OOP frameworks is that you have a lot of functions that just take in a data structure as their first argument. GTK does this and so does Elixir (and has a |> operator for calling multiple functions on some data). You essentially have a similar data mechanic, but the state is clearly separate. You can then create more functions that do not have side-effects.
The big advantage of this is unit testing. There’s less to mock. You don’t have to get your object in the right state during a setup() call before running your tests. You can pass one state and check the return state. You can also greatly reduce the number of mocks you need.
I still do a lot of OOP and non-OOP and I can see clear advantages and disadvantages to both. It’s all about tradeoffs. Testing is one thing non-OOP seems have some advantages in though.
Yeah, GUIs are a slam dunk for OOP. The metaphor works outrageously well, and the data model actually does fit an inheritance structure for once.
I’ll always remember my CS 102 class, where we were taught that Square naturally subclasses Rectangle. It still bothers me that I had to set width and height for Square.
Are they a slam dunk? React (which is effectively a GUI framework) has basically abandoned oop in favor of functional prop-drilling, and it’s wildly successful.
To be clear, I’m mostly referring to the library implementation, not necessarily the paradigm used by application authors. A lot of what I’m talking about is part of the browser layer, namely the implementation and interface of HTML elements. For example, consider HTMLFormElement, which has the following ancestry:
EventTarget
Node
Element
HTMLElement
HTMLFormElement
Those superclasses contain tons of common behavior necessary to tie together the system. All of that stuff is just as necessary for HTMLInputElement or any other HTML GUI widget. For example all HTML elements support core events like onclick.
React isn’t responsible for doing any of that. It stands on the shoulders of a huge OOP GUI framework implemented by browsers. And React Native similarly stands on the shoulders of native toolkits, also OOP based.
That react doesn’t reach further down into the underlying workings of the DOM is simply a matter of convenience, react is not served by rewriting how javascript works, nor is it served by rewriting how, say, android’s Java engine works, but that doesn’t mean one couldn’t continue the prop-drill-ification all the way down into deeper layers (and possibly improve ergonomics, at least). Sibling comment by doug-moen talks about how DOD/ECS system can be a better model for the lower-level stuff than an object-oriented system, in terms of performance.
I stand by my assertion that it’s not a “slam dunk” (for, at least what I understand to be a “slam dunk”), for the lower levels, too, not just for consumers of GUI systems.
The GUI components of most games are built with OOP, DOD is used for managing game entities, interaction, and motion. And those game entities are a perfect example of OOP mismatch, where the entity ontology doesn’t match the required data model. Entities in a game have traits like “has velocity” and “affected by gravity” and “collides with player” that don’t have a strict hierarchy.
GUI components on the other hand have common behavior that’s extremely hierarchical. A Button may provide an “on pressed” interface by consuming the raw mouse down, mouse up, and keypress events from its Widget parent class. A layout container would instead forward mouse events to its child Widgets based on the coordinates of the event, and the sizes of its children. The children are polymorphic subclasses of Widget, and could be Buttons, Inputs, other layout containers, or anything else. Pretty much all GUI widgets specialize their parent’s behavior. React works at a higher level, composing and orchestrating core widgets to build specific things like dialog boxes or chat windows. It’s completely different.
but that doesn’t mean one couldn’t continue the prop-drill-ification all the way down into deeper layers (and possibly improve ergonomics, at least)
This would be a complete nightmare. OOP works for GUI libraries because events, geometry, layout, and a zillion other things are 100% common between all elements. Prop drilling massive amounts of literally identical fields would be nothing but boilerplate.
That kind of near-perfect code and data sharing is extremely rare. Most real world problems have some aspect that doesn’t model nicely with inheritance hierarchies, making OOP a poor fit. That’s what I mean when I say GUIs are a slam dunk for OOP: all the promises about code organization and deduplication hold true when using OOP to implement a GUI library.
The reason OOP is important for GUIs is that the original GUI, Smalltalk, was written in the original OOP language, Smalltalk. Every GUI toolkit since then (until recently) has been based on OOP. So that’s just the way things are done (historically).
But consider that Data Oriented Design (cited by the author as the alternative to OOP) was invented by game programmers to get away from the performance problems and maintenance issues caused by OOP. Games have a lot in common with GUIs; they are both mouse- and keyboard- driven graphical user interfaces.
Thanks for this viewpoint. I’ve always had a pet theory that the relative decline of OO is due to the fact that classic, desktop UI (exemplified by programs like Photoshop and Visual Studio) is also in relative decline. But I must admit I know very little about the state of the art nowadays.
I can’t speak to GTK, but the various MVC-flavored UI frameworks I’ve tried over the years were hardly a slam dunk on a scale beyond roughly three to five screens. The idiomatic way of managing state in MVC is either a) synchronizing multiple local states, or b) sharing globally mutable state through services. Option A does not scale. Option B becomes a debugging problem unless your services use getters and setters for everything so you can set breakpoints to figure out what’s mutating what. Even when you’re working in an MVC codebase where everyone’s agreed to use thin controllers and thick services with lots of getters and setters, race conditions and other more garden-variety mutation conflicts abound. I’ve noticed that these are the kinds of footguns that are especially prevalent in OOP and are virtually non-existent in Elm and other decidedly non-OOP state management architectures inspired by it.
I think we’re on to something. State is inherently dangerous, and so are implicit dependencies. Minimizing these is the name of the game for making predictable, testable and ultimately dependable software.
OOP is good at hiding things – an actually appropriate tool to enforce correct use of an object. But hiding the bad things – state and object references – does not solve the problem that they exist. On the contrary: The paradigm’s tendency to encourage them is a bad trait. Object references are better passed down the call hierarchy. Too many arguments is no real problem, and is trivially solvable if perceived to be, whereas too few is, even often.
Unit testing:
Absolutely. State is input, and unit testing a piece of logic necessitates controlling all its inputs. As soon as you apply OOP and make some of that private, it becomes untestable. And that is just half the reason; the other being the need to mock dependencies, as you also mentioned.
What does this mean? Can you explain it to someone who is maybe a bit behind the curve with regard to software paradigms? What if I need to track the state of something? Like if I am making a video game, say real time strategy. The entire program and it’s reason for existing is to track and manipulate the state of the game world and the locations of the units in it. If state is bad in its own right then there must be some way to implement a traditional RTS with no state. But if it has no state then it is not a traditional RTS…
I wouldn’t say state is “bad in its own right”, but state is equivalent to a system changing in time (if it didn’t, there couldn’t be state!), and dynamic systems have a whole host of complications that static systems don’t. Systems are easier to reason about when there’s less dynamism, which means reducing state mutations to “essential” changes, which also means reducing state overall. That’s easily confused for “state is bad”.
I should probably write a blog post about this or something
Of course, state is not generally avoidable. Your state tracker is a good example of something with a well defined minimum amount of state. I did say it should be minimized, and I mean that.
As for why state is dangerous, it makes software behave differently for the same inputs (unless you count the state as input, which I argued it really is). Ask any FP person about impure functions…
There’s not much difference between i.e button_text(button, text) and button.text(text) other than where they’re declared. What matters more is that the values aren’t mutated, but returned anew. This is the whole Elixir |> operator and the “stream-like” foo().bar() one can do.
I think this is the way to go in general, especially when modeling real world systems. When I did robotics work the idea of avoiding side effects was useless since the entire point of the program is to move things in the real world. It makes much more sense to have a small class Motor with some tiny functions like setSpeed etc., and pass those class instances to functions that do work with the physical motors. This way you don’t have to refactor Motor whenever you want a helper function to e.g. check if the motor is in reverse or not.
Note that Motor wouldn’t be a pure data structure, it would still be a class, but the idea is to treat Motor more like a dumb API that bridges hardware to software, and to write the actual business logic inside functions.
Why doesn’t the Game send an instance of Hit to the Monster instance? What in OOP stops this? Object oriented doesn’t mean that we constrain ourselves to two classes, we can still use data transfer objects.
Better than unlearning OOP might be learning OOP, then transcending it and just thinking about polymorphism or problem modelling or stable APIs or extensability/modularity.
Learning OOP thoroughly involves understanding that it’s a collection of loosely related ideas, some of which are good, some of which are bad, and some of which are occasionally useful.
But I think the article is actually about unlearning the habits of OOP, not the concepts themselves.
I really think the problem with OOP is it teaches people a pretty restrictive and roundabout method of problem solving that only really shines in a few domains. OOP in C++ was part of my first programming class and that did a disservice to us imo.
Input/Output/Processing is what I’d start with. After a decade of programming I don’t think I’ve ever ran into a case where that would be a bad call.
C++ really does not lend itself to OOP. I had a “intro to OOP in C++” class at my University and in predictably taught zero OOP. I mean, the “class” keyword every came up I suppose, so if you fuzz your eyes real hard then maybe…
These are all valid criticisms of certain patterns in software engineering, but I wouldn’t really say they’re about OOP.
This paper goes into some of the distinctions of OOP and ADTs, but the summary is basically this:
Player.hits(Monster)
example might be rewritten in ADT-style ashit(Player, Monster[, Weapon])
.interface Character { position: Coordinates, hp: int, name: String }
, whichPlayer
andMonster
both implement.Now, interface-based polymorphism is an interesting thing to think about and criticise in its own right. It requires some kind of dynamic dispatch (or monomorphization), and hinders optimization across interface boundaries. But the critique of OOP presented in the OP is nothing to do with interfaces or polymorphism.
The author just dislikes using classes to hold data, but a class that doesn’t implement an interface is basically the same as an ADT. And yet one of the first recommendations in the article is to design your data structures well up-front!
The main problem I have with these “X is dead” type article is they are almost always straw man arguments setup in a way to prove a point. The other issue I have is the definition or interpretation of OOP is so varied that I don’t think you can in good faith just say OOP as a whole is bad and be at all clear to the reader. As an industry I actually think we need to get past these self constructed camps of OOP vs Functional because to me they are disingenuous and the truth, as it always does, lies in the middle.
Personally, coming mainly from a Ruby/Rails environment, use ActiveRecord/Class to almost exclusively encapsulate data and abstract the interaction with the database transformations and then move logic into a place where it really only cares about data in and data out. Is that OOP or Functional? I would argue a combination of both and I think the power lies in the middle not one versus the other as most articles stipulate. But a middle ground approach doesnt get the clicks i guess so here we are
Wholly agreed.
For a term that evokes such strong emotions, it really is poorly defined (as you observed). Are these straw man arguments, or is the author responding to a set of pro-OOP arguments which don’t represent the pro-OOP arguments with which you’re familiar?
Just like these criticisms of OOP feel like straw men to you, I imagine all of the “but that’s not real OOP!” responses that follow any criticism of OOP must feel a lot like disingenuous No-True-Scotsman arguments to critics of OOP.
Personally, I’m a critic, and the only way I know how to navigate the “not true OOP” dodges is to ask what features distinguish OOP from other paradigms in the opinion of the OOP proponent and then debate whether that feature really is unique to OOP or whether it’s pervasive in other paradigms as well and once in a while a feature will actually pass through that filter such that we can debate its merits (e.g., inheritance).
One thing I have observed about OOP is how protean it is: whenever there’s a good idea around, it absorbs it then pretend it is an inherent part of it. Then it deflects criticism by crying “strawman”, or, if we point out the shapes and animals that are taught for real in school, they’ll point out that “proper” OOP is hard, and provide little to no help in how to design an actual program.
Here’s what I think: in its current form, OOP won’t last, same as previous form of OOP didn’t last. Just don’t be surprised if whatever follows ends up being called “OOP” as well.
The model presented for monsters and players can itself be considered an OO design that misses the overarching problem in such domains. Here’s a well-reasoned, in-depth article on why it is folly. Part five has the riveting conclusion:
Of course, your point isn’t about OOP-based RPGs, but how the article fails to critique OOP.
After Alan Kay coined OOP, he realized, in retrospect, that the term would have been better as message-oriented programming. Too many people fixate on objects, rather than the messages passed betwixt. Recall that the inspiration for OOP was based upon how messages pass between biological cells. Put another way, when you move your finger: messages from the brain pass to the motor neurons, neurons release a chemical (a type of message), muscles receive those chemical impulses, then muscle fibers react, and so forth. At no point does any information about the brain’s state leak into other systems; your fingers know nothing about your brain, although they can pass messages back (e.g., pain signals).
(This is the main reason why get and set accessors are often frowned upon: they break encapsulation, they break modularity, they leak data between components.)
Many critique OOP, but few seem to study its origins and how—through nature-inspired modularity—it allowed systems to increase in complexity by an order of magnitude over its procedural programming predecessor. There are so many critiques of OOP that don’t pick apart actual message-oriented code that beats at the heart of OOP’s origins.
Of note, modularity requires neither objects nor message passing!
For example, the Modula programming language was procedural. Modula came out around the same time as Smalltalk, and introduced the concept of first-class modules (with the data hiding feature that Smalltalk objects had, except at the module level instead of the object level) that practically every modern programming language has adopted today - including both OO and non-OO languages.
I have to say, after read the first few paragraphs, I skipped to ‘What to do Instead’. I am aware of many limitations of OOP and have no issue with the idea of learning something new so, hit me with it. Then the article is like ’hmm well datastores are nice. The end.”
The irony is that I feel like I learned more from your comment than from the whole article so thanks for that. While reading the Player.hits(Monster) example I was hoping for the same example reformulated in a non-OOP way. No luck.
If anyone has actual suggestions for how I could move away from OOP in a practical and achievable way within the areas of software I am active in (game prototypes, e.g. Godot or Unity, Windows desktop applications to pay the bills), I am certainly listening.
If you haven’t already, I highly recommend watching Mike Acton’s 2014 talk on Data Oriented Design: https://youtu.be/rX0ItVEVjHc
Rather than focusing on debunking OOP, it focuses on developing the ideal model for software development from first principles.
Glad I was helpful! I’d really recommend reading the article I linked and summarised – it took me a few goes to get through it (and I had to skip a few sections), but it changed my thinking a lot.
You needed to do dispatch anyway, though; if you wanted to treat players and monsters homogenously in some context and then discriminate, then you need to branch on the discriminant.
Typeclasses are haskell’s answer to this; notably, while they do enable interface-based polymorphism, they do not natively admit inheritance or other (arguably—I will not touch these aspects of the present discussion) malaise aspects of OOP.
Yes, this is a good point. So it’s not like you’re saving any performance by doing the dispatch in ADT handling code rather than in a method polymorphism kind of way. I guess that still leaves the stylistic argument against polymorphism though.
Just to emphasize your point on Cook’s paper, here is a juicy bit from the paper.
After coming from java/python where essentially dynamic dispatch and methods go hand in hand I found go’s approach, which clearly differentiates between regular methods and interface methods, really opened my eyes to overuse of dynamic dispatch in designing OO apis. Extreme late binding is super cool and all… but so is static analysis and jump to definition.
I find these kind of articles exhausting. They usually come down to the same: pattern X seen in paradigm Y is bad, therefor Y is bad. The alternatives proposed usually aren’t really good either. Basically once you’ve read one of these, you’ve read all of them.
For example:
Then the “solution” presented in this article:
Now there’s nothing wrong with plain old data structures. But eventually those data structures are going to need a bunch of associated functions. And then you pretty much have a class. In other words, the solution is basically the same as the problem.
The real problem isn’t unique to OOP and can just as easily occur in say functional programming languages. That is, the problem is people over-applying patterns without thinking “Hey, do we actually need this?”. Traditional OOP languages may promote this in some way, but again that’s a problem with those languages. The concept of OOP has nothing to do with any of this.
Random example: in any language that has support for macros, inevitably some will start abusing macros. But that doesn’t mean the entire language or its paradigm is bad and should be avoided.
As an aside, every time I see a quote from Dijkstra I can’t help but feel this man must have been absolutely insufferable to work with. Yes, he was very smart and made many contributions. But holy moly, his “I am right and everybody else is wrong” attitude (at least that’s how it comes across to me) is off putting to say the least.
It is the other way around. With OOP, you pretty much end up jamming into data structure definitions what essentially are functions. The concept of class is fuzzy and it’s not a clear well define starting point for a thought process.
Notice that you said “associated functions”. I think it’s all the OOP non sense cornering you into that unclear language. What exactly are those? Functions that accept the type you are defining? Functions that manipulate the state of said data structure? Functions that return a reference to it?
If you think about this questions and find clear answers for them, you will realize that there is absolutely no reason to make functions have all sorts of tricky behaviours based on state or even “belonging to an instance”. At which point the concept of class becomes pointless.
Relational algebra was developed with solid theory behind it. To my knowledge, OOP was just something thrown together “because it is a good idea”.
Records, as in compound types, are very useful in many fields even outside programming languages. Hooking functions to them is just a strange idea whose motivation I am yet to discover.
When I say “associated function” I mean that in the most basic sense: it simply does something with the data, regardless of how the code is organised, named, etc.
I’m not sure what tricky behaviour have to do with anything. That just seems like you’re inventing problems to justify your arguments.
Perhaps this comes as a surprise, but this exists in functional programming too. For example, if you have a
String
module with ato_lowercase()
function, and that function only operates on strings, then that function basically “belongs to an instance”. How exactly you store that function (in the instance, elsewhere, etc) doesn’t matter; the concept is the same. Whether the data is mutable is also completely unrelated to that, as you can have OOP in a completely immutable language.I suggest you do some actual research into the origins of OOP, instead of spewing nonsense like this. It’s frankly embarrassing.
Have you ever stumbled upon good OOP code that actually looked OOP?
I haven’t. The good code I’ve seen was inevitably a mix of procedural, modular, and functional code, with a heavy slant towards either procedural or functional, with maybe a couple instances of inheritance, for polymorphism’s sake (and even then, sometimes we just pass functions directly).
The most distinguishing characteristic I see in OOP is how it stole almost every features to other paradigms or languages. ADT, encapsulation? Modular programming, from Modula. Generics? Parametric polymorphism from ML and Miranda. Lambdas? From every functional language ever. The only things left are inheritance, which was added in Simula to implement intrusive lists (which were needed because there was no C++ like templates), and subtype polymorphism, which is often better replaced by good old closures.
And guess what, inheritance is now mostly discouraged (we prefer composition). The only thing left is subtype polymorphism. OOP is an empty husk, that only survives by rebranding other programming styles.
ADTs come from Barbara Liskov’s CLU, which cites Simula’s inheritance as inspiration.
Hmm, didn’t know, thanks. I looked Modula up as well, it seems both languages appeared at rather the same time.
This depends on what one would consider OOP, as the opinions/interpretations differ. Have I seen good OOP? Yes. Was that Java-like OOP as I would imagine most people think OOP is like? No. But just because something is OOP doesn’t mean it can’t have elements from other paradigms.
Ah yes: functional languages invented everything, and every other language using elements from this is “stealing” them.
I’m honestly not sure what point you’re trying to make. X sharing elements with Y doesn’t mean somehow X isn’t, well, X. Just as how X having flaw Y doesn’t completely invalidate X. Having such a single minded (if that’s the right term) attitude isn’t productive.
Not just functional. Modules did not come from functional languages, that I recall.
To some extent though, yes: functional languages invented a lot. Especially the statically typed ones, whose inventors realise this fundamental truth that often gets me downvoted in programming forums if I voice it: that programming is applied mathematics, and by treating it as such we can find neat ways to make programs better (shorter, clearer, or even faster). Dijkstra was right. Even Alan Kay recognises now through his STEPS project that “math wins”. (Of course, it’s very different from calculus, or most of what you were taught in high school. If anything, it’s even more rigorous and demanding, because at the end of the day, a program has to run on a dumb formal engine: the computer.)
That to many OOP proponents, “OOP” mostly means “good”, and as we learn how to program better over the decades, they shift the definition of “OOP” to match what they think is good. It takes a serious break, like data oriented programming, to realise that there are other ways. To give but an example: back in 2007, I designed some over-complicated program in C++, with lots of stuff from the
<algorithm>
header so I could pretend I was using OCaml (I was an FP weenie at the time). My supervisor look at the code (or maybe I was outlying my design to them, I don’t remember), and said “well, this is very OO and all, but maybe it’s a bit over-complicated?”That’s how pervasive OOP is. Show a programmer functional patterns (with freaking folds!), they will see OOP.
OOP is no longer a paradigm. It devolved into a brand.
Running in the risk of taking your quote out of context, I think the mindset OOP is simply data structures with encapsulated functions is actually one of the biggest real dangers of OOP, because it hides its biggest flaw: pervasive proliferation of (global) states.
Thus, I understand where you are leading your argument, but I disagree with it.
I happen to know a lot of people who directly worked (or had classes) with him, and unanimously I hear both adjectives: genius and pretentious.
Of course those are just others’ opinions, not mine, but I share your (and those people) feelings.
I agree a lot of OOP languages/projects suffer from too much (global) mutable state. But I’m not sure if that’s necessarily due to OOP. I think this is a case of “correlation is not causation”. Perhaps a silly example: if functional languages had mutable state, I think they would have similar issues. In other words, I think the issue is mutability being “attractive”/tempting, not so much the code organisation paradigm.
Another example: I think if you take away the ability to assign non-constant types (basically anything but an int, float, string, etc) to constants/globals, and maybe remove inheritance, you already solve a lot of the common issues seen in OOP projects. This is basically what I’m doing with Inko (among other things, such as replacing the GC with single ownership).
I do think for such languages we need a better term for the paradigm. Calling it X when it mixes properties from A, B, and C is confusing. Unfortunately, I haven’t found a good alternative term.
I view OOP as an organizing principle—when you have few types, but lots of actions, then procedural is probably the way to organize the program (send the data to the action). When you have a few actions, but lots of types, then OOP is the way to organize the program (send the action to the data). When you have few actions, few types, then it doesn’t matter. It’s that last quadrant, lots of types, lots of actions, there is currently no good method to handle.
When you have X types and Y actions, unless you have many of both, I believe your program is already mostly organised. Many types and few actions? It will end up looking OOP even if you write it in C. Few types and many actions? It will end up looking procedural even if you write it in Java.
I still have PTSD from working with an OOP zealot. He would change all of my functions to be internal state in object so everything become opaque. Instead of having a class that you could instantiate with external parameters, he created three classes sharing 90% of code and overrode some of the functions. He could not explain to me why he is doing it. Just because OOP says so.
So, less “OOP zealot” and more “read one book and didn’t understand the point, but yelled OOP as a defence when coding poorly
What is the point then?
That is the problem. It doesn’t really have a point. It was presented as a modern better paradigm and embraced by megacorps without ever having proper research backing it.
I don’t really see how your question relates to my comment, but for my view of the items which approximate “the point” see my top-level comment at the top of the thread.
The article starts with a classy appeal to authority.
Yes, exactly. Therefore, you can first define a logical data structure, and then add methods that operate on said structure. This idea does not conflict with OOP. In fact, prototype-inheritance languages take this a step further and separate data and code, just like you want it; it’s called a “traits object”, and it looks like this:
This “traits object” method allows you to keep code and data in two separate objects. You can even make the parent slot (the one with the star) assignable, so you can use the same object with a different set of methods.
An integer is an object, and so is a struct. Everything can be done wrong if applied poorly.
Not if you use a top-down approach. With such an approach, you first think about what your application should look like, and then implement the layer below that, then the layer below that, and so on and so forth. Application architecture can be done poorly in non-OOP contexts as well.
Yes, it’s a meme that is purposefully verbose. I’m sure you can also spawn an abomination called FizzBuzz Academic Edition implements it in something like Haskell or Idris and turns it into type spaghetti instead.
It’s a human skill to visit the toilet when one has to discharge. Everyone expects it, and there is no excuse other than a medical condition. Similarly, if you can’t keep your architecture in check then this is not the fault of a programming paradigm, but rather your application of it.
I don’t really see how you would improve upon this in a non-OOP setting? You will need to manage those relations somehow. Are you implying that having references to other objects is bad? Also, an orderManager need not hold references to all orders, why would it? It would simply deliver you the order instances. This sounds like a strawman.
Another appeal to authority.
This can often have good reason. A property might have a very good reason to perform an operation when it’s set. I actually disagree with Python’s or C#‘s properties here, because they hide what’s actually going on. In Self, everything is a message pass; therefore, you can simply provide a message that performs the action you want.
That has its own share of problems. “Exporting” from a library is basically same thing as making a member public. I don’t quite understand the difference between having public and private methods on a “class”, and having public and private functions in a module.
See above for Self’s dynamic polymorphism.
Not “nuff said”. Self’s generational GC concept has proven itself quite useful, and forms the basis of the Java HotSpot VM (one of the fastest VMs around, I think most would agree) along with the JIT. Object oriented programming languages do not create non-performant code; bad code creates non-performant code.
The DataStore approach is just bizarre and seems like it’s implementing a slow, half-broken version of an RDBMS within the application. If you need to go brrrr fast then just use a query builder.
All in all, 4/10, could use less inspirational quotes from figureheads.
Aside: What language are you using for your examples? It doesn’t look familiar so I’m having a hard time understanding what it’s meant to convey.
I am using the Self programming language, which is a prototype inheritance-based object oriented programming language.
Thanks!
One thing I’ve noticed in most non-OOP frameworks is that you have a lot of functions that just take in a data structure as their first argument. GTK does this and so does Elixir (and has a
|>
operator for calling multiple functions on some data). You essentially have a similar data mechanic, but the state is clearly separate. You can then create more functions that do not have side-effects.The big advantage of this is unit testing. There’s less to mock. You don’t have to get your object in the right state during a
setup()
call before running your tests. You can pass one state and check the return state. You can also greatly reduce the number of mocks you need.I still do a lot of OOP and non-OOP and I can see clear advantages and disadvantages to both. It’s all about tradeoffs. Testing is one thing non-OOP seems have some advantages in though.
GTK is definitely built on an OOP framework (GObject)
Yeah, GUIs are a slam dunk for OOP. The metaphor works outrageously well, and the data model actually does fit an inheritance structure for once.
I’ll always remember my CS 102 class, where we were taught that Square naturally subclasses Rectangle. It still bothers me that I had to set width and height for Square.
Are they a slam dunk? React (which is effectively a GUI framework) has basically abandoned oop in favor of functional prop-drilling, and it’s wildly successful.
The existence of another legitimate paradigm doesn’t invalidate the legitimacy of its predecessors.
I think you and I have a different interpretation of what “slam dunk” means.
To be clear, I’m mostly referring to the library implementation, not necessarily the paradigm used by application authors. A lot of what I’m talking about is part of the browser layer, namely the implementation and interface of HTML elements. For example, consider HTMLFormElement, which has the following ancestry:
Those superclasses contain tons of common behavior necessary to tie together the system. All of that stuff is just as necessary for HTMLInputElement or any other HTML GUI widget. For example all HTML elements support core events like
onclick
.React isn’t responsible for doing any of that. It stands on the shoulders of a huge OOP GUI framework implemented by browsers. And React Native similarly stands on the shoulders of native toolkits, also OOP based.
That react doesn’t reach further down into the underlying workings of the DOM is simply a matter of convenience, react is not served by rewriting how javascript works, nor is it served by rewriting how, say, android’s Java engine works, but that doesn’t mean one couldn’t continue the prop-drill-ification all the way down into deeper layers (and possibly improve ergonomics, at least). Sibling comment by doug-moen talks about how DOD/ECS system can be a better model for the lower-level stuff than an object-oriented system, in terms of performance.
I stand by my assertion that it’s not a “slam dunk” (for, at least what I understand to be a “slam dunk”), for the lower levels, too, not just for consumers of GUI systems.
The GUI components of most games are built with OOP, DOD is used for managing game entities, interaction, and motion. And those game entities are a perfect example of OOP mismatch, where the entity ontology doesn’t match the required data model. Entities in a game have traits like “has velocity” and “affected by gravity” and “collides with player” that don’t have a strict hierarchy.
GUI components on the other hand have common behavior that’s extremely hierarchical. A Button may provide an “on pressed” interface by consuming the raw mouse down, mouse up, and keypress events from its Widget parent class. A layout container would instead forward mouse events to its child Widgets based on the coordinates of the event, and the sizes of its children. The children are polymorphic subclasses of Widget, and could be Buttons, Inputs, other layout containers, or anything else. Pretty much all GUI widgets specialize their parent’s behavior. React works at a higher level, composing and orchestrating core widgets to build specific things like dialog boxes or chat windows. It’s completely different.
This would be a complete nightmare. OOP works for GUI libraries because events, geometry, layout, and a zillion other things are 100% common between all elements. Prop drilling massive amounts of literally identical fields would be nothing but boilerplate.
That kind of near-perfect code and data sharing is extremely rare. Most real world problems have some aspect that doesn’t model nicely with inheritance hierarchies, making OOP a poor fit. That’s what I mean when I say GUIs are a slam dunk for OOP: all the promises about code organization and deduplication hold true when using OOP to implement a GUI library.
Wouldn’t it also be fair to say that inheritance trees are only one piece of the OOP pie?
The reason OOP is important for GUIs is that the original GUI, Smalltalk, was written in the original OOP language, Smalltalk. Every GUI toolkit since then (until recently) has been based on OOP. So that’s just the way things are done (historically).
But consider that Data Oriented Design (cited by the author as the alternative to OOP) was invented by game programmers to get away from the performance problems and maintenance issues caused by OOP. Games have a lot in common with GUIs; they are both mouse- and keyboard- driven graphical user interfaces.
For insight on why OOP is bad for GUIs (performance, scalability, modifiability, testability), watch this: https://www.youtube.com/watch?v=yy8jQgmhbAU
Thanks for this viewpoint. I’ve always had a pet theory that the relative decline of OO is due to the fact that classic, desktop UI (exemplified by programs like Photoshop and Visual Studio) is also in relative decline. But I must admit I know very little about the state of the art nowadays.
Agreed. Basically the only time I use inheritance is when customising a view or control in UIKit.
I can’t speak to GTK, but the various MVC-flavored UI frameworks I’ve tried over the years were hardly a slam dunk on a scale beyond roughly three to five screens. The idiomatic way of managing state in MVC is either a) synchronizing multiple local states, or b) sharing globally mutable state through services. Option A does not scale. Option B becomes a debugging problem unless your services use getters and setters for everything so you can set breakpoints to figure out what’s mutating what. Even when you’re working in an MVC codebase where everyone’s agreed to use thin controllers and thick services with lots of getters and setters, race conditions and other more garden-variety mutation conflicts abound. I’ve noticed that these are the kinds of footguns that are especially prevalent in OOP and are virtually non-existent in Elm and other decidedly non-OOP state management architectures inspired by it.
Separate state:
I think we’re on to something. State is inherently dangerous, and so are implicit dependencies. Minimizing these is the name of the game for making predictable, testable and ultimately dependable software.
OOP is good at hiding things – an actually appropriate tool to enforce correct use of an object. But hiding the bad things – state and object references – does not solve the problem that they exist. On the contrary: The paradigm’s tendency to encourage them is a bad trait. Object references are better passed down the call hierarchy. Too many arguments is no real problem, and is trivially solvable if perceived to be, whereas too few is, even often.
Unit testing:
Absolutely. State is input, and unit testing a piece of logic necessitates controlling all its inputs. As soon as you apply OOP and make some of that private, it becomes untestable. And that is just half the reason; the other being the need to mock dependencies, as you also mentioned.
What does this mean? Can you explain it to someone who is maybe a bit behind the curve with regard to software paradigms? What if I need to track the state of something? Like if I am making a video game, say real time strategy. The entire program and it’s reason for existing is to track and manipulate the state of the game world and the locations of the units in it. If state is bad in its own right then there must be some way to implement a traditional RTS with no state. But if it has no state then it is not a traditional RTS…
I hope you understand my confusion.
I wouldn’t say state is “bad in its own right”, but state is equivalent to a system changing in time (if it didn’t, there couldn’t be state!), and dynamic systems have a whole host of complications that static systems don’t. Systems are easier to reason about when there’s less dynamism, which means reducing state mutations to “essential” changes, which also means reducing state overall. That’s easily confused for “state is bad”.
I should probably write a blog post about this or something
I was just inspecific. Let’s say dangerous.
Of course, state is not generally avoidable. Your state tracker is a good example of something with a well defined minimum amount of state. I did say it should be minimized, and I mean that.
As for why state is dangerous, it makes software behave differently for the same inputs (unless you count the state as input, which I argued it really is). Ask any FP person about impure functions…
Would you elaborate please? Perhaps with an example?
There’s not much difference between i.e
button_text(button, text)
andbutton.text(text)
other than where they’re declared. What matters more is that the values aren’t mutated, but returned anew. This is the whole Elixir|>
operator and the “stream-like”foo().bar()
one can do.I think this is the way to go in general, especially when modeling real world systems. When I did robotics work the idea of avoiding side effects was useless since the entire point of the program is to move things in the real world. It makes much more sense to have a small class Motor with some tiny functions like setSpeed etc., and pass those class instances to functions that do work with the physical motors. This way you don’t have to refactor Motor whenever you want a helper function to e.g. check if the motor is in reverse or not.
Note that Motor wouldn’t be a pure data structure, it would still be a class, but the idea is to treat Motor more like a dumb API that bridges hardware to software, and to write the actual business logic inside functions.
Why doesn’t the Game send an instance of Hit to the Monster instance? What in OOP stops this? Object oriented doesn’t mean that we constrain ourselves to two classes, we can still use data transfer objects.