Readit News logoReadit News
benkuhn · 5 years ago
The author’s OO example is hard to understand, but they’re wrong about why. It’s not bad because it’s OO, but that it’s very badly done OO: the class couples two different concerns (network API client and database). That’s why it makes more sense as a bag of functions.

The general version of the point doesn’t work very well, and many of the other OO use-cases the author discusses actually work much better than alternatives.

For example, on abstract base classes: if you replace this with a bag of functions I think you end up reinventing virtual dispatch—that is, each function’s top level is a bunch of `if isinstance(...)` branches. This is much harder to read, and harder to add new implementations to, than abstract methods. It’s also no easier to understand.

(There is a subset of this advice that I think does improve your code’s understandability, which is “only ever override abstract methods,” but that is very different from “don’t use OO.”)

For impure classes, the author suggests e.g. using `responses` (an HTTP-level mocking library) instead of encapsulating these behind an interface. This is a fine pattern for simple stuff, but it is not more understandable than a fake interface. The hand-written fake HTTP responses you end up having to write are a lot less readable than a mock implementation of a purpose-built Python interface. (Source: I once mocked a lot of XML-RPC APIs with `responses` before I knew better; it was not understandable.)

throwaway894345 · 5 years ago
> It’s not bad because it’s OO, but that it’s very badly done OO

This argument seems to come up for every criticism of OO. The criticism is invalid because true OO would never do that. It seems like a No True Scotsman. Notably, when you whittle away all of the things that aren’t true OOP, you seem to be left with something that looks functional or data-oriented (something like idiomatic Go or Rust). There isn’t much remaining that might characterize it as a distinct paradigm.

dkersten · 5 years ago
My problem with these arguments is that its meaningless, if this "bad OO" (or "bad <insert paradigm>") is what most developers write.

Its a big problem I have with ORM's. Every time a discussion about what's bad about ORM's is brought up, there's a multitude of people saying that its just used wrong, written wrong or otherwise done wrong, yet that's basically my experience with every non-toy codebase I've ever worked on as part of a larger team. Telling me that its just done wrong is useless because its out of my hands. Is it the ORM's fault? I argue yes because it encourages that kind of programming, even though its not technically the ORM's fault.

I see it the same with OO or anything else. Is this "bad OO" the type of OO people tend to write? If yes, then OO is the problem. If no, then OO is not the problem.

Personally, I like a mixed approach where I use some OO and a lot of functional. I'm writing a little toy game in C++ from scratch and I have a very flat design, preferring functional approaches where I can, but I have a few objects and even a little bit of inheritance where it makes sense. Sometimes dealing with an object-based API is just more convenient and I won't shy away from using OO principles, but I also don't default to them just because either.

mumblemumble · 5 years ago
> This argument seems to come up for every criticism of OO.

The same is true of functional and procedural programming, too.

The problem, as I see it, is that the profession has a history of treating programming paradigms as just being a bag of features. They're not just that, they're also sets of guiding principles. And, while there's something to be said for knowing when to judiciously break rules, blithely failing to follow a paradigm's principles is indeed doing it badly.

This is a particular problem for OO and procedural programming. At least as of this current renaissance that FP is enjoying, advocates seem to be doing a much better job of presenting functional programming as being an actual software design paradigm.

It's also the case that, frankly, a lot of influential thought leadership in object-oriented programming popularized some awful ideas whose lack of merit is only beginning to be widely recognized. If null is a billion dollar mistake, then the Java Bean, for example, is at least a $500M one.

ogre_codes · 5 years ago
I've found that objects best when they are used in moderation. Most of my code is plain old functions, but occasionally I find that using an object to do some things is just much cleaner. The rest of the time doing away with all the baggage associated with objects and using structs or other, simpler structures is just easier to wrap my head around.

Java IMO is the classic case of going way too far overboard with being object oriented. Much worse than Python.

theamk · 5 years ago
But that's exactly the point!

Object-oriented means few different things: it could mean "my program contains 'class' keyword" to "Everything must be a class which obeys SOLID".

If you say general statement, like "OO in Python is mostly pointless", you are likely wrong. There are ways to use 'class' keyword to make programs better (shorter, more readable) and there are libraries out there which use OO this way.

If you want to write something which can be proven true, you want to have something much more specific. Here are some possible titles one can have meaningful discussion about:

"SOLID everywhere is not worth it"

"Don't use objects where a function will do"

"single responsibility is the key to usable OO"

and so on

ashtonkem · 5 years ago
You’re missing the selection bias at work here. The issue is that good OO doesn’t get you upvoted in programmer related forums like HN because it’s not terribly popular. Instead what does get upvoted is arguments against OO, and these often contain a lot of bad OO code as counter-examples.
benkuhn · 5 years ago
I think you took me to be arguing for a stronger claim than I actually was. My preferred paradigm is also roughly “idiomatic go or rust.” But the OP isn’t arguing for idiomatic-go-or-rust, as far as I can tell they’re arguing against ever using methods or virtual dispatch (which are core features of both of those languages)! My claim is that the OP’s diagnosis of the problem—“virtual dispatch causes your code to be confusing, get rid of it”—is incorrect.
IOT_Apprentice · 5 years ago
This is an industry where the waterfall lifecycle was presented as being a bad methodology, which was then promptly adopted by almost every firm in existence.

Then Agile & SCRUM arrived and now people complain about giving status on what they are working on now, what issues they have and what they intend to work on today. You see posts here bitching about it.

I still get software from engineering that appears to lack ANY sense of what we are supposed to use it for. The developer gets tons of back slapping by engineering management, and it leaves us STILL doing grunt work with the poorly designed tooling, that the developer is now writing perl scripts to deal with issues in his delivered work.

krageon · 5 years ago
It's not like the criticism in this case is particularly esoteric: Literally the first explanation is that the proposed classes don't encapsulate single concepts. That's the theory of the first course you'd get about this in school, or if you're self taught something you'd see within the first 30 minutes of casual googling about the subject.

I think it's fair to question what is going on in the article if the mistakes made are of such a basic nature.

knuthsat · 5 years ago
You can create an OO mess in Go and Rust that is equivalent to this too.

Dependency injection is a nice name for factory functions + dependency list + topological sort, but people still use it horribly, injecting dependencies everywhere, not writing functional and data-oriented composable code.

It's nice to learn about patterns but it's wrong to think about patterns as a solution.

keithnz · 5 years ago
many software problems boil down to badly done X, where X is some paradigm, technology or technique. Then someone concludes, like this article, that it is pointless / to be avoided etc. It would instead be better that you write an article where you steelman X, then compare it to Y, and look at the comparative advantages. Badly done X or Y should always be highlighted but not really used as a reason not to do X or Y. If you can steelman X and show that even best effort has far too many disadvantages, and there doesn't seem a way forward to overcome them compared to other approaches, then sure, call it out. What I see is often new things have less history of people badly doing it compared to the old things that have a lot of examples of people badly doing it and they mistake that as the old thing as being bad.
theelous3 · 5 years ago
Right but it actually is just horrible code with zero separation of concerns. Having a client class is a great example of good OO. The library you're using having a Session object is basically essential in python. Any other paradigm would be a mess. Things storing state about themselves in a totally unambiguous way is brilliant. Most complaints about OO just focus on bad usecases. Calling that is not a no true scottsman.

If you saw someone only driving in reverse, complaining about car UX, would you not state the obvious?

rualca · 5 years ago
> This argument seems to come up for every criticism of OO. The criticism is invalid because true OO would never do that.

This passertion is disingenuous. The whole point is that the reason why bad code is bad is not because it's OO, it's because it's bad code.

You do not fix bad code by switching programming paradigm, specially if it's to move back to procedural programming and wheel reinvention due to irrational class-phobia.

cztomsik · 5 years ago
If you want to see real OO, look here: - https://pharo.org/ - https://gtoolkit.com/

If you don't want to, it's ok, just stop bashing it without first learning it (you learned FP too, it took some time too).

na85 · 5 years ago
I disagree. Good OO is about encapsulation of concerns. Make a Client object and push data to it; let the class worry about the networking and sockets. Make another object to deal with the database.

For sufficiently complex systems I don't see how functional or other paradigms can manage without holding a huge amount of global state.

cmiller1 · 5 years ago
It's also a cherry picked example, networking code, of a problem that can be expressed equally as nicely with OO or functions. Now refactor a gui or a game without OO and you'll find the OO version easier to understand or cleaner.
jlokier · 5 years ago
Fwiw, many games uses a different representation of state and behaviour than standard OO, called entity-component-system (ECS): https://en.wikipedia.org/wiki/Entity_component_system

ECS doesn't represent game objects with standard OO such as classes in C++/Java/C#. You can argue about whether it's still OO in principle, but it's not the kind of OO an enterprise developer would recognise.

henry_bone · 5 years ago
Is this true for games? I read an article[1] some years ago that pointed out the OO design (with C++ in this case) was bad for performance due to the data not being arranged in a particularly cache-friendly way.

[1] http://harmful.cat-v.org/software/OO_programming/_pdf/Pitfal...

lostcolony · 5 years ago
Well, yes. When in a domain that is heavily noun driven it makes sense to reach for OO. It's just that most domains aren't (hence Yegge's famous article re: nouns and verbs).
Rumudiez · 5 years ago
> Now refactor a gui ... without OO

made me chuckle — that’s my day job! Check out RamdaJS btw

leontrolski · 5 years ago
Hi, what I'd really like is to test my conjecture - would you (or anyone reading) be able to email me a (< 500 line, ideally not super-deep-library-code) bit of code (constructed or otherwise) that you think is "so innately OO friendly that the conjecture won't hold".

Given some examples, I'll do a follow-up blog post.

teawrecks · 5 years ago
Fwiw I've felt this way for the 10 years I've used python. IMO all of Python's strengths lie outside of it's oo functionality.
AQXt · 5 years ago
Your conjecture may only hold for tiny (< 500 lines of code) problems.

But when you deal with big problems -- millions of lines of code, written by hundreds of developers over a period of years -- you'll definitely see the benefit of OO.

Of course, most projects are somewhere in the middle; but you shouldn't dismiss OO just because you can't see the benefit in tiny projects.

kleiba · 5 years ago
you end up reinventing virtual dispatch

But you can have virtual dispatch without OO, see e.g. Clojure.

Deleted Comment

gorgoiler · 5 years ago
In general, object orientation is a reasonably elegant way of binding together a compound data type and functions that operate on that data type. Let us accept this at least, and be happy to use it if we want to! It is useful.

What are emphatically not pretty or useful are Python’s leading underscores to loosely enforce encapsulation. Ugh. I’d sooner use camelCase.

Nor do I find charming the belligerent lack of any magical syntactic sugar for `self`. Does Python force you to pass it as an argument to make some kind of clever point? Are there psychotic devs out there who call it something other than `self`? Yuck!

And why are some classes (int, str, float) allowed to be lower case but when I try to join that club I draw the ire from the linters? The arrogance!

...but I still adore Python. People call these things imperfections but it’s just who we are.

PS I liked the Python5 joke a lot.

klyrs · 5 years ago
> Are there psychotic devs out there who call it something other than `self`? Yuck!

I have, under duress. It was a result of using syntactic sugar wrapping a PHP website to make a Python API; it was convenient to pass form variables into a generic handler method of a class. The problem? The website, fully outside of my control, had a page which had an input named "self" which resulted in two values for that argument. Rather than refactor the class and dozens of scripts that depended on it, I renamed 'self' to 's_lf' in that one function and called it a day.

Also, python has class methods. The convention is to use 'cls' in that context, to avoid confusing the parameter (a class) with an instance of that class.

GeorgeTirebiter · 5 years ago
for my own code, I use 'me' instead of 'self'. Just makes more sense to me; e.g. me.variable that is, the one referenced right here ('me') is being referenced, not from anywhere else. It's also shorter; and I think more descriptive. But that's just me.
Ankintol · 5 years ago
> In general, object orientation is a reasonably elegant way of binding together a compound data type and functions that operate on that data type. Let us accept this at least, and be happy to use it if we want to!

Is it elegant? OO couples data types to the functions that operate on them. After years of working on production OO I've still never come across a scenario where I wouldn't have been equally or better served by a module system that lets me co-locate the type with the most common operations on that type with all the auto-complete I want:

  //type
  MyModule {
    type t = ...
  
    func foo = ...
    func bar = ...
  }

If I want to make use of the type without futzing around with the module, I just grab it and write my own function

ssivark · 5 years ago
Totally. That structure also allows for multiple dispatch, which makes generic programming much much more pleasant than OO (which is basically single dispatch). Eg. See Julia.

To elaborate, tying methods with the individual/first argument makes it very difficult to model interactions of multiple objects.

fleabitdev · 5 years ago
How do you compensate for the lack of type-dependent name resolution? MyModule.foo(my_t) seems verbose, compared to my_t.foo()
dan-robertson · 5 years ago
I agree. Also, here are two things I find not great with OO style apis:

1. Functions with two arguments of your type (or operators). Maybe I want to do Rational.(a + b), or Int.gcd x y, or Set.union s t. In a Java style api I think it looks ugly to write a.add(b) or whatever.

2. Mutability can be hidden. If you see a method of an object that returns a value of the same type, you can’t really know whether it is returning a new value of the same type or if it is mutating the type but returning itself to offer you a convenient chaining api. With modules there isn’t so much point in chaining so that case may be more easily hidden.

andrewflnr · 5 years ago
I'm sure it enables other things, but I find the "Module.t" thing very inelegant. Names should be meaningful and distinguish the named object from other things that could go in the same place, but "t" is meaningless and there's often nothing else (no other types anyway) for it to distinguish itself from. It feels like a hack, and I much prefer the mainstream dot notation.
jayd16 · 5 years ago
Whats the advantage other than some intellisense clean up through hiding some functions? As you're well aware, you can write new functions of OO types as well.

Either you lose private/protected state and the advantages or you replace them with module visible state and the disadvantages.

ehutch79 · 5 years ago
Isn't that just reinventing classes?
pansa2 · 5 years ago
> What are emphatically not pretty or useful are Python’s leading underscores to loosely enforce encapsulation.

IMO Go's use of upper/lower case is even worse than Python's use of underscores. What would be better? Explicitly labeling everything as `public` or `private`, C++/Java style?

> Does Python force you to pass [self] as an argument to make some kind of clever point?

I think the idea is that `a.foo(b)` should act similarly to `type(a).foo(a, b)`.

However, what happens when you define a method and miss off the `self` parameter is crazy. Inside a class, surely `def f(a, b): print(a, b)` should be a method that can be called with two arguments.

> And why are some classes (int, str, float) allowed to be lower case

Historical reasons. AFAIK in early versions of Python these weren't classes at all - the built-in types were completely separate.

EdwardDiego · 5 years ago
> IMO Go's use of upper/lower case is even worse than Python's use of underscores.

Especially when serialising a struct to JSON - only public members are serialised, so your JSON ends up with a bunch of keys called Foo, so you have to override serialisation names on all the fields to get them lowercased.

joshuamorton · 5 years ago
> However, what happens when you define a method and miss off the `self` parameter is crazy.

@staticmethod

(although why you want this instead of just a module scope function is unclear). If you want this strange behavior, you should be explicit about it.

pansa2 · 5 years ago
> Are there psychotic devs out there who call it something other than `self`? Yuck!

Sometimes I like to overload operators using `def __add__(lhs, rhs)` and `def __radd__(rhs, lhs)`.

rbanffy · 5 years ago
> What are emphatically not pretty or useful are Python’s leading underscores to loosely enforce encapsulation.

Python will not prevent anyone from doing something dumb. It'll just force them to acknowledge that by forcing them to use a convention. As a library writer, I'm free to change or remove anything that starts with an underscore because, if someone else is depending on it, frankly, they had it coming. I can assume everyone who uses my library is a responsible adult and I can treat them as that.

> And why are some classes (int, str, float) allowed to be lower case

Because they are part of the language like `else` or `def`. Only Guido can do that. ;-)

gfaure · 5 years ago
If we look in `collections`, we can see `defaultdict` sitting side-by-side with `OrderedDict` and `Counter`. I've long since resigned myself to the fact that naming consistency is not Python's strong suit :)
takeda · 5 years ago
> As a library writer, I'm free to change or remove anything that starts with an underscore because, if someone else is depending on it, frankly, they had it coming. I can assume everyone who uses my library is a responsible adult and I can treat them as that.

Totally agree, as an user, I feel anxious whenever I have to use underscored names. It's great that Python still allows me to do it and there were few times when it was useful, but when it stops working I know it's 100% on me.

afarrell · 5 years ago
> I can assume everyone who uses my library is a responsible adult and I can treat them as that.

What is great about python is that it acknowledges that even adults can forget things. Underscores are an affordance.

mrslave · 5 years ago
PEP 20 -- The Zen of Python [0] ... "Explicit is better than implicit."

In the case of the unnecessarily repetitious `self` it means to violate DRY, make the mundane manual - and therefore error prone - and tedious.

[0] https://www.python.org/dev/peps/pep-0020/

xapata · 5 years ago
Backwards compatibility. You remember all those folks complaining about Python 2 vs 3? Turns out even renaming Thread.isalive to is_alive caused a ruckus. Imagine how much yelling you'd hear if you changed float to Float.

I've come to like the self variable name convention. It takes so little effort to type and makes the behavior more clear. Well, some aspects of it, I suppose. I make a fair amount of use of unbound methods.

BerislavLopac · 5 years ago
> Nor do I find charming the belligerent lack of any magical syntactic sugar for `self`.

And what would that look like exactly? The Java-like approach, with any non-declared variables being the current object's attributes, wouldn't work because Python doesn't use declarations and can have named objects in any scope. The alternative is to use a "magic" variable appearing out of thin air, like Javascript's "this" -- but this is basically what "self" does, only explicitly.

Any other ideas?

BalinKing · 5 years ago
C++ et al's "this" keyword seems perfectly reasonable to me--the whole point of being a member method is that you have some sort of implicit state. Otherwise, why not just make them top-level functions, if you have to take the object as a parameter anyway?
Joker_vD · 5 years ago
Decorating with "@", as Ruby does? I.e., "a = 42" assigns to a local variable "a", and "@a = 42" assigns to the instance's field "a". Clearly visible, and no magic variables.
closed · 5 years ago
> Nor do I find charming the belligerent lack of any magical syntactic sugar for `self`. Does Python force you to pass it as an argument to make some kind of clever point?

On the class, you can call the method like a normal function (passing the self arg manually). Seems like a nice connection to just raw functions. Also explains how you can add methods after a class has been defined (set a function on it, whose first param is a class instance).

gorgoiler · 5 years ago
That’s a compelling point.

    zoo = [‘goat4’, ‘tiger7’]
    map(Pet.stroke, zoo)

mohaine · 5 years ago
> Are there psychotic devs out there who call it something other than `self`? Yuck!

I was converting some old Java code to Python recently and I almost decided to switch to `this` just to make the task less repetitive. Luckily, sanity returned after I saw the first def abc(this,

jghn · 5 years ago
I'd personally contest the notion that binding state and functionality directly together is "reasonably elegant" in the first place.

But ignoring that, it depends on the flavor of object orientation. Yes, the most mainstream style bundles state directly with functionality but not all do. But for instance the CLOS family of OOP maintains separate state and functionality and one binds desired functionality to those classes which should have it. This is not too dissimilar from typeclasses IMO.

rbanffy · 5 years ago
It makes a lot of sense for modeling real world objects that can do things and have state.
dfinninger · 5 years ago
Just to discuss one point:

> And why are some classes (int, str, float) allowed to be lower case

Also, boolean. These are primitive data types. For instance, in Java there's a difference between int and Integer. I'd assume that Python special-cases these because they are primitive. But I haven't been through the Python internals, so it's only a guess.

bobbyi_settv · 5 years ago
It's just for historical/ backwards compatibility reasons. These types have been part of Python from before it had classes. Today, they are classes and can be subclassed, etc, but too much code relies on their existing names to change them to match the modern convention.
rbanffy · 5 years ago
They are far less primitive than Java's - they are classes too, but can't be extended.

Deleted Comment

pgcj_poster · 5 years ago
> Are there psychotic devs out there who call it something other than `self`?

I call it "me".

Dead Comment

Kototama · 5 years ago
Except the binding always implies the object is mutated on changes, which make any form of reasoning or concurrent programming difficult.
rbanffy · 5 years ago
You can always mutate objects using functions that return new objects.

If you use methods of an instance you know the object's state may or may not change. If you set attributes, you know for sure they changed. If you use a function that returns a new objects you know you paid the object allocation tax.

riffraff · 5 years ago
Why? There are plenty of OO libraries where method calls do not change the object.
kevin_thibedeau · 5 years ago
Python OO is a bolt on. That's why it doesn't have syntactical support for magic functions or self. Nothing prevents you from implementing your own alternate object system. Python 2 had two of them.

As for the linters, just disable the rules you don't like.

acdha · 5 years ago
This post says more about the author's experience than Python, especially with the list of exceptions. The common pattern behind those exceptions is that they're not as simple as the examples, which gets to a better lesson that OO is not pointless but that you should use it cautiously when you know that your problem domain has a sufficiently complex combination of state and code. Anything is going to seem simpler when you have 2 variables and the whole thing is 20 lines and while it's definitely useful to pause before accreting complexity it's also important to remember that projects and teams come in many different forms and there's no guarantee that something which works for you on a project now will be the best option for someone else with different needs.
megameter · 5 years ago
A decent rule of thumb I sometimes apply is:

1. What I have could be formalized into a state machine. 2. That state machine needs to be reused and re-entered. 3. I want to apply inputs to the state machine with method calls.

Of course you can overapply the thought and end up with an enterprise architecture - that's why YAGNI is important. But when a section builds up a bit of conceptual redundancy there's usually a state machine that can be pulled out of it in OO form.

And I would certainly do it that way in Python, as well as other languages.

leontrolski · 5 years ago
Hi, what I'd really like is to test my conjecture - would you (or anyone reading) be able to email me a (< 500 line, ideally not super-deep-library-code) bit of code (constructed or otherwise) that you think is "so innately OO friendly that the conjecture won't hold".

Given some examples, I'll do a follow-up blog post.

leontrolski · 5 years ago
Hi, I think you're correct in some domains - I probably should have spelt out that I'm by-and-large talking about application development, where if you have a "sufficiently complex combination of state and code" you probably have a problem rather than a candidate for wrapping in a class.
acdha · 5 years ago
> I probably should have spelt out that I'm by-and-large talking about application development

I was as well. My point is just that not everyone works on the same things or has the same resources, and there is no global optimum. I'm generally in the “more functions, fewer classes, minimize inheritance” camp personally but the most important underlying principal is remembering that methodology is a tool which is supposed to help, not hinder, and adjusting based on local conditions.

Part of the value an experienced developer brings should be having a good sense for when a particular approach is a good fit for the problem at hand and being willing to say that whatever you're doing needs changes. This can be tricky because methodologies often have a quasi religious aspect where people take the decisions very personally but the worst project outcomes I've seen have been from cases where people treated the status quo as holy writ and refused to consider how problems could be avoided or transformed by picking something different.

yowlingcat · 5 years ago
> where if you have a "sufficiently complex combination of state and code" you probably have a problem rather than a candidate for wrapping in a class

Ironically, I think most people in that situation would agree with you that they "probably have a problem" -- the difference is that they see the problem as the important thing to focus investing resources into solving whereas you see its existence as an inherently poisoned entity that must be conceptually purified. While both parties would probably agree on the existence of /a/ problem, the definition of what needs to be addressed and how is likely to differ, and I can't say that the latter attitude is the norm at any high productivity engineering organization I've ever worked at, built, or encountered.

jphoward · 5 years ago
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)

setr · 5 years ago
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).

Dwarf Fortress probably has the best example of this: https://github.com/BenLubar/raws/blob/archive/objects/creatu...

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

And it even has inheritance :-)

    [CREATURE:TOAD_MAN]
       [COPY_TAGS_FROM:TOAD]
       [APPLY_CREATURE_VARIATION:ANIMAL_PERSON]

anaerobicover · 5 years ago
Note for readers who want to search for more: ECS is "Entity Component System"

And Eric Lippert has a fantastic series of blog posts where he also discusses this problem: https://ericlippert.com/2015/04/27/wizards-and-warriors-part...

megameter · 5 years ago
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.

dragonwriter · 5 years ago
> 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.

jgwil2 · 5 years ago
vadansky · 5 years ago
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.
t-writescode · 5 years ago
isn't ECS a composition pattern on top of OO?
reidjs · 5 years ago
Thanks for sharing that Dwarf Fortress file, I never thought about the structure for all those attributes.
beaconstudios · 5 years ago
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.

jcelerier · 5 years ago
> 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

keyle · 5 years ago
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.

mistersys · 5 years ago
The answer is simple: Traits

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

nickjj · 5 years ago
> 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.?

fulafel · 5 years ago
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.

https://guide.elm-lang.org/architecture/

coldtea · 5 years ago
>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:

https://medium.com/ingeniouslysimple/entities-components-and...

more in depth: https://www.dataorienteddesign.com/dodbook/

leontrolski · 5 years ago
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

Edit: ignore me - this person seems to know more what they're talking about - https://news.ycombinator.com/item?id=25933781

thepratt · 5 years ago
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.

leshow · 5 years ago
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.

example in Haskell, https://owickstrom.github.io/gi-gtk-declarative/app-simple/ or even take a look at yew in Rust which is also elm-style, https://github.com/yewstack/yew/blob/master/examples/counter...

Deleted Comment

nicoburns · 5 years ago
React with hooks offers one decent paradigm for this. I would definitely class it as an area that doesn't yet have a canonical settled solution yet.
pje · 5 years ago
> I cannot imagine working without OOP ... in GUI development

What is React but essentially a (wildly popular) functional GUI framework?

joeberon · 5 years ago
But react isn’t the widget library
zmmmmm · 5 years ago
So the example manages to evade any need for encapsulation. It's viable because the internal hidden state is so cheap to compute (the full URL) that it can afford to reconstruct it within every call.

But imagine if constructing the url was a more expensive operation. Perhaps it has to read from disk or even make a database call. Now you really need to cache that as internal state. But doing that means external manipulation of root_url or url_layout will break things. Those operations need to be protected now. So do you make "set_url_layout" and "set_root_url" functions? And hope people know they have to call those and not manipulate the attributes directly? Probably you'd feel safer if you can put that state into a protected data structure that isn't so easily visible. It makes it clear those are not for external manipulation and you should only be interacting with the data through its "public interface".

Of course this brings about all the evils of impure functions, mutated state etc. But if you are going hardcore functional in the first place then it becomes rather obvious OO is not going to fit well there, so its a bit of a redundant argument then.

Blikkentrekker · 5 years ago
> So the example manages to evade any need for encapsulation.

In Scheme and many other lisps there is no difference between “objects” and “records” and “fields” and “methods”.

Defining a record with a number of fields also defines accessor functions, such that:

  (define-record point
    (fields (mutable x) (mutable y)))
defines the following procedures:

  make-point
  point?
  point-x
  point-y
  point-set-x!
  point-set-y!
All these names can optionally be changed to something else.

Encapsulation is simply enforced by not exporting any of these procedures from the module wherein they are defined.

Any arbitrarily more complicated functions can be built on top of these which can be exported.

As such, there is no meaningful difference any more between an object and a record, there is no special “dot notation” for field access and method calls, and normal procedure calls are used. This follows the general lisp design principle of avoiding special syntax as much as possible and using ordinary procedure calls for everything. There is no special “subscript notation” for `vector[i]` either, and `(vector-ref vector i)` is used instead.

leontrolski · 5 years ago
Hi, what I'd really like is to test my conjecture - would you (or anyone reading) be able to email me a (< 500 line, ideally not super-deep-library-code) bit of code (constructed or otherwise) that you think is "so innately OO friendly that the conjecture won't hold".

Given some examples, I'll do a follow-up blog post.

zmmmmm · 5 years ago
You could take the example as-is with the new requirement that the remote end point may require up front authentication. How the authentication works depends on the service, and it needs to be done on a per-URL basis. The authentication protocol may return some kind of token or key needed to use with future requests. However what this is and exactly how it is used is service dependent (for example, it might be supplied verbatim in an auth header or it might be a signing key and it might even be salted with some credentials etc).

So challenge that is introduced is that you now have hidden state that is dependent on the public state but which cannot be computed by users of the client or the parent class. In fact, code in the parent class can't even know ahead of time what state might be needed to be retained.

I am guessing you will probably be able to propose that authentication be supported by some kind of pluggable authentication interface, but it will still be difficult to deal with the hidden state without introducing assumptions about the auth protocol or the type / nature of the state retained and without having that protrude into the parent interfaces.

brundolf · 5 years ago
I don't think the OP makes very good points, but one thing to point out is that Python only has access-control by convention. This creates a wrinkle for any argument based on using "protected data structures"
theamk · 5 years ago
We use python extensively, and I think python's "access control by convention" is pretty great.

The reason for access control is not to protect from malicious programmers -- it is protect from programmers who don't know better and from honest mistakes.

So all you need is a simple rule ("don't access private methods") and now everyone knows which fields are public (and thus are OK to use) and which fields can disappear any moment.

For extra enforcement, you can add pylint to your CI so it flags all the protected accesses. But really, a good team culture is often enough.

nemetroid · 5 years ago
When writing Python, I usually try to follow this item from the C++ Core Guidelines (replace "struct" with "dataclass"):

> C.2: Use class if the class has an invariant; use struct if the data members can vary independently

> An invariant is a logical condition for the members of an object that a constructor must establish for the public member functions to assume. After the invariant is established (typically by a constructor) every member function can be called for the object. An invariant can be stated informally (e.g., in a comment) or more formally using Expects.

> If all data members can vary independently of each other, no invariant is possible.

https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines...

mumblemumble · 5 years ago
I don't know if one has really made any case at all about OOP, neither for nor against, if one hasn't considered cases that involve any sort of conditional branching.

Because the key problem that OOP is supposed to solve is not lumping bits of data together. The problem that OOP is supposed to solve is using polymorphism to limit the proliferation of repetitive if-statements that need to be maintained every time the value they're switching on acquires a new interesting case to consider.

bccdee · 5 years ago
In practice, I've found that kind of use case for polymorphic dispatch to not be especially common — meanwhile, most OO languages strongly encourage making everything objects. There's absolutely a time and place for a good abstract interface, but it's always struck me that classes are overused in general.

That's why I really appreciate the go/rust model, where you can tack interfaces and methods onto structs if you want to, but there's no pressure to do so.

mumblemumble · 5 years ago
The main reason why OO languages typically make everything objects is that having only one kind of type greatly simplifies the programming language and the experience of programming in it.

There are only three common languages that are OO with a few non-object types: C++, Objective-C and Java. This feature is universally recognized as a serious wart in all three of them. It creates all sorts of little edge cases that you need to learn to program around.

jennyyang · 5 years ago
Pointing to someone's bad code example and saying "This is why all OO is pointless", is truly a lazy effort.

Good OOP is good. Bad OOP is bad. That's like every other piece of coding. Some excellent examples of great OO code that I've worked with have to do with having an abstract class to define a data api, and then being able to switch providers seamlessly because the internal interface is the same, and all you need to do is write a vendor-specific inherited class.

sweezyjeezy · 5 years ago
> Good OOP is good

That's not the point - the rub is that good OOP is HARD. People get sold on it with examples like your data API, where the abstractions are clear, and reasonably static, whereas for most problems, and for most developers, it's genuinely difficult to get this right.

hn_saver · 5 years ago
Hi Can you please link to some examples of python great OO code?