Hacker News new | past | comments | ask | show | jobs | submit login
OO in Python is mostly pointless (leontrolski.github.io)
370 points by leontrolski on Jan 27, 2021 | hide | past | favorite | 383 comments



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


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


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.


But every programming paradigm can be (and perhaps most often is) done badly. They each just create their own set of issues when "done badly". I think it just comes from not properly understanding why you're making certain choices. Deciding to use a design pattern is going to come with a certain set of strengths and weaknesses, so ideally you'd consider your problem and choose design patterns that makes the most of the strengths and minimizes the impact of the weaknesses. But I don't think it's very common to put that much thought into programming decisions, so people will often make a design decision, and then invest their effort into fighting the constraints it imposes, while usually failing to realize the potential benefits of it. OOP can be a perfectly suitable tool to solve a problem, but if you haven't thought about the actual reason why you want to use it, then you're just going to end up creating complicated abstractions that you're not properly benefitting from. ORMs can be great tools to, but if you want to use an ORM, you have to solve your problems using the approaches implemented by the ORM. If a programmer can't/won't do that, then they've just chosen the wrong tool for their problem/the approach they wanted to take to solve it.


Maybe the overall takeaway is that most programmers are bad.


Perhaps. I think a more likely explanation is that it's just not a very professional field. As a field it hasn't been around for very long at all, and it's changed a lot during that period. If it was obvious to tell the difference between bad and good programming, then perhaps things would be different. But I don't think that's obvious at all, and I don't think it's possible to create any meaningful level of agreement in that area. Look at how hard it is to agree on any sort of standard. IETF RFCs take years to process, with the typical outcome being a standard that enough people agreed to publish, but that 0 people faithfully implement.


I have another opinion about that. Most of us don't work in product companies but rather as consultants or in IT enterprise environments. In such contexts the real objective of software development is often the improvement of a process that is enabled by the software. As such the cons of bad software are not immediatly clear to any management layer above the first. Sometimes even the first layer of management doesn't really care until they get the process improvement they want. If the new software produces an increase of 30% in sales then the fact that it is badly written is just of secondary importance: the objective has been met. The fact that bad software quality will hurt maintanance in the mid-long term is often too far in the future for anybody to really care.

As software engineers I have seen that we usually make two mistakes in these cases. We focus only on our craft without taking into consideration the real business objectives of the customer and what he really cares about. We introduce unnecessary complexity just for the sake of it, often adding unnecessary abstractions. In a few words we often make the sin of believing that software is the objective while in most of the cases there is a business objective for which software is just the enabler and because of that we lose focus on what is the right thing to do.

In software product companies code is part of the final product and code quality directly impacts costs and the core business of the company.


I’d still say this is at least partially attributable to not having any real measurements of what “good” software is. At least aside from at a very basic level.

If you want to build a bridge, I’d assume there’s a set of standards it has to comply with. If it doesn’t meet the spec you could explain in very objective terms why the quality isn’t sufficient. If you had to sit in front of the board and explain why you think a piece of software wasn’t built to a sufficient standard of quality, you’re going to really struggle to do that in objective terms. The best you could really hope to do is present a well articulated opinion. Anybody who disagrees with you could present their own opinion, and unless they have a high degree of technical competence, they’re not going to have any objective approach for making judgements about that.

Sure lots of companies prioritize feature velocity over code quality. But I doubt most of them a very well informed about what quality compromises they’re actually making.


In some large Internet Scale firms there were ARBs, Architectural Review Boards. They served this purpose, by having very senior members of staff review proposed architecture and implementation approaches PRIOR to coding occurring. The danger was that over time, this sometimes devolved into the members waiting to just beatdown people presenting to them and being internally "political".


This resonates with me.

In other fields it can be easy to say when a job is done well. There can be clear guidelines. That said, other fields cut corners and do a “bad” job, too. Construction for example. It is probably possible to explain to average Joe why a damp proof course wasn’t installed properly or the insulation used is dangerous and flammable.

Programming as it exists now seems often in service of something else. So a deadline causes cut corners. So in this context a cut corner is fine and “we’ll fix it later”.

I suppose programming differs from a physical thing like construction. Sure you can replace the wiring and install a mezzanine but it’d be insanely impractical.

I’m going off on a tangent now...


Construction is a prime example, especially Home Construction. Just look at the TikTok videos showing horrendous plumbing and electrical work.

Hell, I moved into a house built 3 years ago by a major firm and the ceiling fans were ALL wired wrong, and many of the electrical plugs are upside down.

This is a house that had a specific design model. My presumption is that unskilled labor was brought in to complete work and just did a shitty job.

You can see this sometimes with outsourced programming work or local work that is poorly designed and inadequately tested.


Maybe its true, but that just shifts the blame and doesn't change anything or solve anything. Try as we might, "fixing" programmers just isn't realistic, so the only thing we can do is look for ways to improve the tools to make them encourage better practices.

Going back to the ORM example, I'm cool with just using SQL. The language encourages a style of thinking that fits with databases and it being a separate language makes it clear that there's a boundary there, that its not the same as application code. However, I'd also be ok with an ORM that enforced this boundary and clearly separated query logic from application logic.

As for OOP, I don't know what the solution there is. Maybe there isn't one. I like to use OOP, but sparingly, and at least in my personal code (where I have control over this), its worked out really well. A lot of code is transforming data structures and a functional approach maps really well to this, but for overall architecture/systems and even for some small things that just map well to objects, its great to have OOP too. In my toy game, I use inheritance to conveniently create external (dll/so) modules, but inside the engine, most data is transformed in a functional style. Its working quite well. I'm not sure how you could redesign the paradigms to encourage this though, outside of designing languages to emphasize these things.


>The language encourages a style of thinking that fits with databases and it being a separate language makes it clear that there's a boundary there, that its not the same as application code.

The separation is orthogonal to the data access style used, and you really have to make sure the engineers you're working with understand separation of concerns. I have seen many applications with controllers filled with loads of raw SQL, just as I have seen them filled with lots of ORM query building. If the programmers don't get why that's bad, they will do the wrong thing with whatever tools they have in front of them.


Take this contrived presudocode example:

    result = []
    for frob in Frob.get_all(where=something) {
        if frob.foo = expected {
            result = {
                frob: frob,
                quux: Quux.get_all(where=some-query-using-frob)
            }
            results.append(result)
Basically, the idea is that you fetch some data and check some condition or do some calculation on it, then fetch more data based on this condition/calculation. This entire thing could be a single logical process.

The only reason there's a separation of concerns here is because some of this is done in the database and some in the application. Logically, its still part of the same calculation.

But the ORM hides this distinction and makes both the part that runs in the database and the part that runs locally look the exact same and super easy to intermingle. Worse still if you access properties on your ORM-result-object which actually trigger further queries to get. It looks like a field access, but is actually a database query. I've seen this cripple performance.

In many cases, if you step back and don't think about it in terms of application code, but rather the data access and transformations that you want to achieve, then it can be rewritten as a query (joining related data in as needed etc). At the very least, it makes you aware of what the boundaries are.

I'm not saying that scrapping the ORM will magically make the problems go away and I know people also write terribly intermingled application and database logic when using SQL, but at least the boundary is more explicit and the different sides of the boundary actually look different instead of just looking like application code.

My point isn't that there's a silver bullet, but that we can nudge and encourage people to write better code by how the languages/libraries/tools structure solutions.

I'm also not necessarily saying that we have to use SQL instead of an ORM, that's just one possible suggestion that I personally find works due to the mental separation. I'm sure you can design ORM's that make the boundaries more explicit, or design frameworks that encourage thinking about application boundaries more explicitly. Same as how I'm not actually suggesting to get rid of OOP, just... if most people's OOP is so-called "Bad OOP", then we need to think about how to improve OOP, because changing "most people" is just not going to happen.


Yeah, I understand what N+1 queries are and how many ORMs make them too easy.

For me, ORMs become a problem when they're an excuse to avoid learning SQL. If you understand SQL, you will probably understand why the example you give is a bad idea. If you don't, you won't. I'm speaking from the point of view of having written, at this point, thousands of lines of SQL at minimum, and having decided that it's not how I primarily want to access data in the applications I write. The ORM queries I write are written with rough knowledge of the SQL they generate, and I try to be very careful with query builders to avoid the exact case you bring up.

I think LINQ in C# does a pretty good job of bridging this gap, actually. It could be better, but it discourages looping and encourages the declarative style that efficient SQL requires.


I think this is the problem that tools can’t solve. If you want to interface with a database, you need to know how a database works, regardless of what tools you use. I like using ORMs, and I think I write good ORM code. But I also think that I know quite a bit about how databases work, and that I’d be incapable of writing good ORM code if I didn’t.

If you give a novice developer an OOP assignment, they’re likely to struggle with the learning curve, and likely to ultimately create something that has a lot of unnecessary complexity. If you give a novice developer MEAN assignment, they’ll create something that “works” a lot easier. But it’ll likely be full of bugs created by not understanding the underlying problems that the framework is simply not forcing you to address.

Which is what I think these “simple” frameworks do. Allow you to create working features without necessarily considering things like consistency and time complexity. I also think it’s why things like Mongo have been slowly adding more ACID consistency, and schema design features. Because people are coming to realize that those are problems that simply can’t go unaddressed. And why ORMs and things like GraphQL have become so popular, which to me look remarkably similar to the structured approach of OOP design patterns.


I read somewhere that the amount of programmers in the world doubles like every 5 years. That means that at any given moment, half of programmers have less than 5 years of experience.


I've been programming for 30+ years. I still have less than 5 years of experience in anything that's currently popular.


The biggest problem with intermediate level programmers is their love of hard and fast rules. It's like they have a backpack full of hammers ("best practices") and they're running around hammering everything in sight. For this I feel like it doesn't really matter how much experience you have in an individual piece of tech - the overall amount matters more.


> The biggest problem with intermediate level programmers is their love of hard and fast rules

I think that’s just a problem of dogmatism, which doesn’t necessarily go away with experience.


I'll second that. I have seen that by "developers" with 20 years of experience. Dogmatism is the real enemy.


I find it hilarious that you’d mention “20 years of experience”. I work on a team at the moment with one of the most dogmatic developers I’ve ever met, and when challenged his rationale for any decision is “20 years of experience”.

He was recently asked to fix a rather simple bug in a C# project, a language he doesn’t use very often, but he was certain this would be an easy task for him. The project used a small amount of unmanaged resources, and he just couldn’t figure out how the ‘using’ keyword worked. He spent a few days trying to get to the bottom of ‘using’ before demanding to be sent on a C# course, which everybody thought was a great idea because it would keep him busy for a while. Maybe he’ll have “21 years of experience” by the time he gets around to fixing this bug.


Yup. Also, unwillingness to learn, which, to me, is a crime.

Learning is difficult; maybe because it starts with admission of ignorance.

Most of my work, these days, is a ghastly chimera of techniques, from 30 years ago, grafted onto patterns from 30 minutes ago. Real Victor Frankenstein stuff.

It seems to work, though.


> Learning is difficult

But not insurmountable. In my own experience, all it takes is a small amount of consistent effort each day. I've learned a few programming languages that I use regularly like this, I learned sleight of hand card magic like this and most recently, I've learned to play guitar like this. This year, I hope to learn Spanish like this too. Many people don't want to put consistent regular effort in and are looking for shortcuts. That doesn't work.

> a ghastly chimera of techniques

Well, yeah, the right tool for the job, right? But it does work, so...


In order for a programmer to program in many patterns elegantly, one must first have understanding of those patterns and how they are roughly reduced to machine code.

The skill of experiencing trumps the amount of experience.

Bad programmers need to hone their skill to experience in order to become good programmers. The most popular way is to provide them a safe environment to make mistake and learn from it. But the most important way is actually knowing that skill of experiencing and getting out of biases are important, like in this case "if OOP is indeed bad, why is it invented in the first place", and the rabbit hole actually gets interesting from here.

At least that's my take after a few years in the industry.


And yet after 2-3 years they are appointed as "senior" in some of the most developed countries :)


My main point is that the things we use encourage a certain approach and if most people are "doing it wrong [tm]", maybe its not the people's fault, maybe the tools are encouraging wrong approaches.

I mean, sure, you can just say most developers aren't very good, aren't disciplined enough, don't think things through. Maybe its true. But that doesn't solve the problem, it just shifts the blame. You can't fix the people, because its a never-ending problem: move team or company and you're back at square one. Good luck trying to change all the university courses to try and teach better practices.

So if you can't fix the people, the only thing left is to take a long hard look at the tools and see if we can't improve them in a way that encourages better code.


It’s really hard to make a really good ORM.

Except rails ActiveRecord there is probably no one.

Even though AR also has some issues.


Diesel (rust)

Also it's maybe notably by a (former?) maintainer of ActiveRecord, though, I haven't really used RoR but I think the design is probably quite different - Diesel is very much just the SQL with a veneer of Rust, almost FFI wrapper like.

Why should I have to remember to use '.values' instead of 'GROUP BY'; '.filter'/'.except' instead of 'SELECT'? It's not helpful. I frequently have a clearer idea of the SQL I want (I'm certainly no DB expert) and have to make it at least twice as long, install a plugin, or in some cases just can't mangle it into Django. For what?


Diesel is nowhere near being able to fully map SQL to rust. It's ok for relatively simple queries that are typical for OLTP applications. Anything beyond that and you're going to run into trouble.

If you want to do something more OLAP-ish or insert data in bulk, you have a huge problem. SQLAlchemy is far better, even though SQLAlchemy also has its own share of limitations (dealing with jsonb_to_recordset and functions like it for example).


sqlalchemy in Python. Having used both ActiveRecord and sqlalchemy, I'll take sqlalchemy all day everyday.


SQLAlchemy is really special - coming from Django I wasn't convinced initially, but I think the reason it works so well is it doesn't try to hide any magic. It's simply a nice way to represent objects in your database, you still have to explicitly request it to commit / run queries.


I've messed around with activerecord for sqlalchemy :)

https://twitter.com/arundsharma/status/1338596939600781313


My main issue with ORM's is that they encourage you to mix application code and queries (by making it easy and by making the code for both look the same), blurring the boundary between your application and the database.

This is a problem, because there is a real boundary, typically including a network round trip. I've been on way too many projects where ORM-using code would pull some stuff from the database, do some application logic to filter or process the results, then loop and do another query for each result. Instead of doing it in the database query itself using joins (and possibly being able to use indexes for the filtering).

Even when people are very disciplined about this, I find that once you get to non-trivial queries, they become a lot harder to read in ORM code. I tend to have to think in terms of SQL, then translate between that and the ORM code on the fly. Its not so easy.

Sure, you could say all the teams I've ever worked on that did stuff like this are just bad at using ORM's, but after a few experiences like this on different teams, in different companies, with different developers, its time to stop blaming the people and maybe take another look at the tools.


I really feel your pain. This is not yours or ORM concept fault.

There is just no good ORMs out there.

Except ActiveRecord (and maybe mongoose for mongo). And even these 2 are not the best choice for all use cases.


Most ORMs fails because they map one table with one class.


It is really hard - DBIx::Class (Perl) is an example of one of the better ones, in my opinion.


Hibernate and Entity Framework?


Hibernate is ok technologically but it is a usability nightmare. However its reliance on reflection makes me want to stay away from it.


Do you care about performance? In my experience, N+1 queries are impossible to avoid with hibernate (or any JPA framework, generally). But maybe I’m just using it wrong.


See https://vladmihalcea.com/

He has a lot of info on high performance Hibernate and particularly the n + 1 issue.

It should be mostly avoidable.


Thanks for sharing this.

For anyone else interested, here’s one post he has on the problem: https://vladmihalcea.com/n-plus-1-query-problem/


Thanks for digging out this direct link.

I haven't been there for a while but I'm fairly sure there should be a few more.


I haven't found that to be the case. In particular I think expecting the ORM to always do the best thing no matter how you use it is folly, ORMs are a tool that you have to take the time to understand in much of their full complexity.

Some would say this makes them a bad abstraction, but to me, data mapping is going to have to happen somewhere, and I would rather be building on someone else's work to write the data mapping for my applications than do it from scratch. You have to know when the ORM is the right tool to use, and when to drop into plain SQL for your querying, because they do not eliminate the need to write SQL, just reduce it significantly.


We are currently considering moving away from Entity Framework Core. Simple things work fine, but it generates ridiculous queries if stuff gets more complex.


Many ORMs are going to have trouble at some point when you start making more complex queries. I believe using an ORM well is largely in understanding the balance between when its tradeoffs are acceptable and when they are not. Per the other reply, I would probably start with EF Core in new applications, but would not hesitate to add Dapper or just plain SQL with ADO if I saw the need.


Is there any reason why it needs to be either/or, or are you suffering from OCD light like so many others of us? ;-)


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

Only if that's more true than “bad <foo> is what most developers write in paradigm <foo>” where <foo> is the presented alternative paradigm, otherwise we’re comparing the platonic ideal of <foo> against real world OO.

Sturgeon’s Law may not be quantitatively precise, but it addresses a real qualitative issue that must be considered.


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


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

There are some specific criticisms that are levied against FP for which proponents respond “that’s not true FP”. For example, the criticism “FP is too preoccupied with monads” might engender such a response; however, this response is appropriate because it’s not one of the defining features of FP. Yet for FP, there are still pretty widely-agreed upon features or conventions (functional composition, strong preference for immutability, etc).

For OOP, I can’t think of any features or styles that are widely agreed upon. If you mention inheritance, half of OOP proponents will argue that inheritance isn’t a defining characteristic because Kay didn’t mention it in his definition. If you mention message-passing, many others will object. If you mention encapsulation, then you’ve accidentally included virtually all mainstream paradigms.

If OOP is a distinct thing, and it isn’t about inheritance or message passing or encapsulation or gratuitous references between objects (the gorilla/banana/jungle problem), then what exactly is it?


The term may be muddied but it’s not meaningless. Alan Kay’s vision of OO (“it’s about message passing”) never seemed to really take off in the world. When people talk about OO they usually mean classes, objects and methods, used for almost everything as the primary unit of composition. And classes with public methods encapsulating private fields. And often with a dash of inheritance. C++ and Java seem like the flag bearers for this style of programming. It’s popular in C# too.

When people criticise OO, they’re usually criticising this the “Effective Java” style, expressed in whatever language. This style is deserving of some criticism - it’s usually more verbose and harder to debug than pure functional code. And it’s usually less performant than the data oriented / data flow programming style you see in most modern game engines.


Agreed. The term is muddled and diverging.

OOP the paradigm (messaging and memory encapsulation) is pretty different from OOP the style (access modifier, class+method+properties, inheritance).

As a strong proponent of FP the paradigm, I insist OOP the paradigm is great to study and apply in a system software where there are multiple agencies. For example, in a browser there are multiple "agencies", network-facing workers, storage-facing workers, human-facing workers, etc, as each "agencies" runs at different to pace to make its "client" (networkAPI, storageAPI, human) happy, therefore messaging and buffering between those "agencies" inevitable.

FP also suffers the same issue.

FP the paradigm: pure functions, expression-based, recursion, first-class function, parsing > validation (universal, almost applicable in any programming language) FP the style: monad, functional-based language, tail-call optimization

Being overly-critical over style is not productive in the long term. Someday one will have to leave the tool for a new one.

Learning the universal part of paradigms is useful because it is not dependent to tools.


I couldn’t agree more whole heartedly with this comment, and I love FP. Unfortunately OO has come to mean this Java style.


So, that's still focusing on features. If we're talking about it from a paradigmatic perspective, instead, then I would argue that the prototypical idea behind object-oriented design is indeed encapsulation.

It's true that this is not a distinguishing feature. I don't see that as problematic, it's just how things work. Object-oriented programming, like any paradigm, evolved over time, as people built languages with new ideas, and then spent time figuring out how best to make use of them. And there's no rule saying that nobody is allowed to subsequently adopt and adapt a useful idea in other domains. Fuzzy boundaries are part of the natural order, and that is a good thing.

But, if you go back and read the seminal papers, it's clear that the common thread is encapsulation, every bit as much as referential transparency is the core idea that unites all the seminal work in functional programming. The idea was that programs would be decomposed into modules that were empowered to make their own decisions about what code to execute in response to some instruction. And that this was supposed to liberate the programmer from needing to micro-manage a bunch of state manipulation. Not by eliminating state, as is the ideal in FP, but by delegating the responsibility to manage it.

This is why the concept of Beans bothers me. A Bean is a stateful object that throws its state into your face, and forces you to directly manage it. That sort of approach directly contradicts what OOP was originally supposed to be trying to accomplish. For my part, I am convinced that the bulk of the backlash against OOP is really a response to the consequences of that sort of approach having become orthodox in OOP. Which is deeply ironic, because this is an approach that enshrines the very kinds of practices that the paradigm originally sought to eliminate.


The problem with encapsulation in OOP, is that the (mutable) state is not truly encapsulated, it is just hidden. State encapsulation would be a pure function that uses mutation internally. OOP encourages mutable state and in general it is non-trivial to compose such effectful code. The state space of an OOP application can become a combinatorial explosion. This might be what you want if you are modelling something with emergent complexity, e.g. a simulation or game, but doesn't sound good for a line-of-business app.

As an anecdote, I once saw a stack overflow reply from a distinguished OOP engineer advocating for modelling a bank account with an object containing a mutable balance field! That is certainly not modelling the domain (an immutable ledger). OOP might fit some problems well, but the cult of presenting it as the one-true-way (made concrete in 90's Java) is deserving of a backlash IMHO.


> But, if you go back and read the seminal papers, it's clear that the common thread is encapsulation, every bit as much as referential transparency is the core idea that unites all the seminal work in functional programming. The idea was that programs would be decomposed into modules that were empowered to make their own decisions about what code to execute in response to some instruction. And that this was supposed to liberate the programmer from needing to micro-manage a bunch of state manipulation. Not by eliminating state, as is the ideal in FP, but by delegating the responsibility to manage it.

I agree with that assessment, but I also think the problems with OO are inherent to that paradigm, and it's time to acknowledge that the paradigm is bad and has failed.


> This is why the concept of Beans bothers me. A Bean is a stateful object that throws its state into your face, and forces you to directly manage it.

I disagree.

Beans are Java's version of the ideas expressed in Visual Basic and, later, COM -- ideas often called something like "software componentry". They are objects that can be instantiated and configured without having to write custom code to invoke their methods, because the instantiation and configuration follow accepted standards, as well as the means by which they register to subscribe to and publish events. This lets them be instantiated and manipulated by other tools in the pipeline, such as a GUI builder tool or an automatic configurator like the one in Spring Framework.


If we go by Alan Kay's definition, then we can argue Elixir/Erlang is an OO language [0]. It's not the first language one would think of when talking about OOP, is it?

To me, OOP is all about implementation details.

[0] https://elixirforum.com/t/the-oop-concept-according-to-erlan...


See: https://www.quora.com/What-does-Alan-Kay-think-about-Joe-Arm...

In which Alan Kay himself answers the question, saying, "Erlang is much closer to the original ideas I had about “objects” and how to use them."


Whether total functional purity is desirable or not is a huge divide in the FP community, as large as anything in the OO world.


OOP is about code organization. Type dependent namespaces (also known as classes) are pretty much the core of OOP.


So is it dot-method notation (`foo.Bar()`)? If so, does that make Go and Rust OOP as well? That's just syntax though; what about the C convention of `ClassName_method_name(ClassName obj)`? That's still namespace by the type even if the language* doesn't have a first-class notion of a type prefix.


I don't see how Java Bean is a costly mistake. It's just a convention. It is annoying to follow but it doesn't cause damage directly.


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.


I feel like OO is okay if objects implement good well thought out interfaces. The problem is designing good well thought out interfaces is hard. Also the rise of distributed computing causes problems for OO. Passing data between systems is pretty easy. Passing functions with that data, not so much.


> I feel like OO is okay if objects implement good well thought out interfaces.

I find that much of what I do is taking something from structure A and manipulating structure B. If you are a slave to OOP, you end up bolting that method to either one or the other object when really it affects both and belongs in a separate place entirely.

The other big issue I have with OOP is frequently I don't want or care for reference passing, I just want value passing. That is usually the place where I start using objects is when passing references is useful.

It's particularly useful if you are building a thing which requires multiple steps and you need that state to persist between steps. I've seen it done well with functional programming, but for me it's just easier to build something out with an object that holds persistent state.


My experience with distributed computing is that the biggest issue isn't passing functions with data. Instead the biggest issue is sharing state between systems. This is part of why immutability is very nice.


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


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.


And yet a lot of this so-called "bad OO" is representative of most of the OO I see written. If something that is bad is the norm, that says something about the paradigm itself, as well.


Bad code is the norm (regardless of paradigm), because it's a profitable industry that rewards entry but not necessarily skill. It's not really a failing of the paradigm that it's applied wrong when that is the norm in all aspects of the practice.


That is not my experience. I see a lot of boring, working, OO code. Nothing that I’d excitedly put on the front of HN as an example of anything, but code that’s reasonably easy to read, and just works day in and day out.


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.


The style OP is advocating lines well with Rust at least. Rust still tends to use method syntax, but it is very strict around mutability and virtual dispatch is rarely used, so you tend to write your programs with similar flavor as the OP is arguing for.


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.


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.


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.


>> ...idiomatic Go or Rust

> You can create an OO mess in Go and Rust that is equivalent to this too.

To be quite clear, I qualified with idiomatic. I don't think you can create an OO mess with idiomatic Go or Rust, but then again, the whole point of this thread is that there's no clear consensus on what OO actually means, so what does "OO mess" mean.


Idiomatic says nothing about code design. Code design is far above what idiomatic means. You can still have dependencies where you shouldn't have them. Your services can still be designed without composability in mind.


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.


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?


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


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

This is pretty false on its face. Let's say that some paradigm insists that inheritance should be used for every problem. Almost all programmers agree that this is bad code, thus code written in this paradigm is bad because of the paradigm.

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

Agreed. Instead you should start with your current paradigm and remove the problematic aspects until you're left with something that works reasonably well. I posit that when you start with OO and drop its problematic aspects like inheritance or Armstrong's observation of banana-gorilla-jungle architecture, you end up with something that looks pretty functional or data-oriented, a la idiomatic Go or Rust. If your definition of "OO" already excludes these things, then it probably looks something like Go or Rust or perhaps even Erlang.

The issue I'm raising with "OO" is that there is no consensus about what OO is; rather every defense of OO is about what it isn't (and then there's the ever predictable straw man, "But you can write bad code in any paradigm!").


> Let's say that some paradigm insists that inheritance should be used for every problem.

There is no such paradigm, including and specially OO.

In fact, your example clearly illustrates the perils of blaming the tools for problems caused by incompetence.

I mean, how many decades have passed since "composition over inheritance" has been taught like a mantra in every OO programming 101 course?

And still the best OO example you could manage to come up wit is a blatant error that would lead you to fail a OO programming 101 course?

I'll restate the obvious: the reason why bad code is bad is not because it's OO, it's because it's bad code.

And here you are, trying to pass blatantly bad code as somehow an OO problem?


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


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.


The function system doesn't hold 'global' state. It holds the state in arguments given to functions. If you want a client, you still have a bit of client 'data' and you pass that around your functions.

If you wanna do stuff to the client, you call functions with the client as an argument. If you want polymorphic clients, you define a client typeclass / trait / interface called Client, and then your functions take a generic Client bit of data.

In some sense, this approach is a lot like replacing foo.bar() with foo(bar). Which doesn't do much to change your program. The interesting difference in FP is how you 'change the state' of your client.

In FP, if you want your client to e.g. count its connections you would do something like `nextClient = connect(oldClient)` instead of the OOP `client.connect()`. This means that you are a lot more explicit about your state changes. This has a lot of advantages that can be hard to wrap your head around. It also comes with some disadvantages.

As a result though, all of your state is carried very 'locally' in your functions scopes.


How do you manage to not have global objects?

FP and imperative handle data without huge amounts of global state the same way. By structuring your code properly.

Per your example, your Client instance still has to be passed around everywhere it's needed, and you call client.push_data() on it; in FP or imperative approaches, you would pass around a Client struct or tuple or similar, and call push_data(client).

OO just bundles state and functions together, as fields and methods on an object. Which has its pros, and its cons.


> Per your example, your Client instance still has to be passed around everywhere it's needed

Yes, because our program probably needs more than one Client.


Not sure if you're trying to make a point here?


I mean in Java or C# there are "global" objects, being the Main class, I suppose. There's always a top-level construct.

But my Sword class doesn't hold a reference to a NetState object.


That was a rhetorical statement. The same reason you don't need global objects in an OO language is the reason you don't need global data structures in an imperative or functional one.

Your Sword class doesn't hold a reference to a NetState object, but then, my Sword struct doesn't need a reference to a NetState struct, either.

If I were to climb the reference hierarchies back to my 'main' function, I would find a common ancestor, just like in OO if I were to climb my reference hierarchies, I would get back to the class with my 'main' method. But that doesn't imply global state; it's all scoped down in the code where I defined and use it.


I haven't the faintest clue what point you're trying to make but I'm sure it's very clever.


The point is the graph of data dependencies in an FP system doesn't necessarily look any different than in an OO one.


Closures and internal state of objects are basically the same thing.


I don't usually deal with stateful closures. In OO we often have file objects, and if you call Close() on said object, subsequent Read() invocations fail because the encapsulated state has changed. What's the closure analog for this?


You've never worked with a generator before? That's a stateful function. For throwing after a file handle is closed, something like -

  const read = (fileHandle) => {
     return () => {
       if io:isOpen(fileHandle) && io:hasNext(fileHandle) => {
          return io:readNext(fileHandle)
       } else if io:isOpen(fileHandle) {return undefined
       } else {throw "File is closed"}
     } 
  }(myFile)

  read()
  read()
  io:close(myFile)
  read() //throws


Separating concerns the way you describe can be accomplished leveraging the polymorphism mechanisms in an OO language (objects and subclasses) or just as easily in a functional style (multimethods, protocols, interfaces).

Show me an example of something you think is decoupled in OO style which could not be similarly decoupled in FP, and I'll give you the FP version which does the same.

Not saying one is better than the other, but you can definitely keep your code decoupled in both styles.


In an OO program all state is global, because you can't understand any given object's behaviour without knowing what its state is and what the behaviours of all the objects it communicates with are, and you can't understand those without knowing their states either.

State can and should be hierarchical - and you can do that easily in FP - but the idea that OO lets you make it somehow non-global is a myth.


>In an OO program all state is global, because you can't understand any given object's behaviour without knowing what its state is and what the behaviours of all the objects it communicates with are, and you can't understand those without knowing their states either.

By that logic all state in any program is global.

But if I'm writing an MMO server my GoldCoin class doesn't need to know about the client's connection state.

Have you actually written or worked with OO code before? Your comment reads like you have not.


> By that logic all state in any program is global.

All state in any program is global in the sense of being reachable from top-level. You can, and should, make your state hierarchical - reachable from top-level should not mean reachable from anywhere, you can have (non-toplevel) parts of your program that only access parts of your state. Unfortunately OO is uniquely bad at this, because objects are encouraged to have hidden coupling.

> But if I'm writing an MMO server my GoldCoin class doesn't need to know about the client's connection state.

But you have no way of knowing or enforcing that. "You wanted a banana but what you got was a gorilla holding the banana and the entire jungle" - your GoldCoin might contain a ("encapsulated" i.e. hidden) reference to another class that has a reference to another class and so on, and so eventually it does depend on the client's connection state.

> Have you actually written or worked with OO code before?

Yes, for many years, which is why I've become an advocate of FP style instead.


> All state in any program is global in the sense of being reachable from top-level.

Well, think about a random number generator. It has some internal state, which gets set by some action taken (perhaps indirectly) by the top level. And that state is "reachable" by getting the next random number, but that random number may not be a direct representation of any part of the generator's state. Also, after initialization, the generator's state should not be alterable by the rest of the program.

So to me, that's not really "reachable". The entire point of encapulating that state in the random number generator is to make it not reachable.


That's a perfect example; in my experience that kind of RNG state is global (or at least, it has all the problems of traditional global state). Potentially any object might behave in a strange way, because it contains a reference to that RNG (hidden from you): you might find a sequence of calls that usually, but not always, reproduces a bug in test, and then it turns out that it depends on whether other tests running in parallel are affecting the RNG state. Essentially any test you write that's bigger than a single-class mock test has to be aware of the RNG and stub it out, even if you're testing something that on the face of it has nothing to do with RNG.

In a functional program you would make the RNG state an explicit value, and pass it where it's used (but not where it isn't - so there might be large program regions that don't have access to it). It'll be an explicit part of the top-level state of your program. I'd argue that that's not actually any more global - or at least, it causes fewer problems - than the OO approach.


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.


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.


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


Depends on the problem domain. See the posts on Wizards and warriors:

https://ericlippert.com/2015/04/27/wizards-and-warriors-part...

For card games, OOP is very well-suited (so to speak).


"Easier to understand" and "performant" are different axes.


There is a fantastic GDC video about Blizzard's use of ECS in Overwatch and how it made it far easier to understand the system due to all of the interacting subsystems.

https://www.youtube.com/watch?v=W3aieHjyNvw


It's funny because a few years ago I read about how the developers of a game from the same studio -- specifically StarCraft -- ran into the exact same problems I did when trying to write a game in the then-common "bag of subclassed game objects framework" in the 90s. To this day I thank the gods that I chose ECS for my latest game because it has made changes, bugfixes, and extensions of functionality so much easier. To say nothing of more data-driven.


Some day I believe that when you think for performance your brain gets things faster because it's not potential benefits or stylistic argument. It's raw perf and everybody seeks that (not at the expense of tech debt ofc.)


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


Isn't the point here whether nouns should be able to perform verbs or not?

In a "noun driven domain" you can still disallow the noun to perform the verb.


More do verbs -have- to be bundled with nouns (or live second class citizen lives), as that's more what OO requires. Even in functional languages, you -can- have verbs living with nouns (your struct can have a field that is of function type); it just isn't required (or recommended for most use cases).

It's not a question of can they, or should we prevent them, it's 'should it be required?'

It may make sense to require all verbs to be coupled to a noun when your problem domain is mostly modeling nouns, and verbs only describe how those models change and interact with each other. But when the problem domain is mostly process, with nouns being ancillary, it is unwieldy to talk that way. It's why you have so many "ThingDoer" and "Utils" and the like in most OO codebases.


Exactly, my take is "use the right tool for the right job"


> Now refactor a gui ... without OO

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


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.


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.


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.


I'm currently on a project with 100s of thousands of lines of code (admittedly not millions) - working in the bag-of-functions style - and I've yet to miss any OO features.

Surely it's possible to construct a smaller example? It's not like the millions of lines codebase isn't de-composable?


For what it's worth, Emacs is an example of what you describe and it's extremely functional (and carries a huge amount of global state). Millions of lines, an insane number of contributors, development period of decades with a very decentralised management structure.

Emacs has a lot of problems but once you learn the base language, it's quite easy to interface with. I'd argue it's because of the limitations on structural complexity - you don't need to wrap your head around non-locally-defined structure in order to understand any one line. The parts that attempt to emulate OO tend to be a lot harder to deal with as an outsider.


you end up reinventing virtual dispatch

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


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.


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


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.


me.variable sounds Irish to me, at least compared to my.variable.


And my.variable sounds Perlish.


That's because Perl was intended (Larry Wall was trained as a Linguist) to somewhat mimic natural English.


That's a fascinating tidbit. You wouldn't exactly guess it from reading Perl.


>Larry Wall was trained as a Linguist

Having an undergraduate degree doesn't make somebody a linguist. Let me know when he has contributed to the field through scholarly work. Doing Bible translations doesn't count.


> me.variable sounds Irish to me

Only if you're used to the derogatory, clichéd "American TV" version of Irish.


> 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


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.


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

The best example of this in Python is operator overloading. We need to define both `__add__` and `__radd__` methods to overload a single operator. Even then, there are situations where Python will call one of those where the other would be better.


How do you compensate for the lack of type-dependent name resolution? MyModule.foo(my_t) seems verbose, compared to my_t.foo()


Most of the time you have:

- short names inside modules. I.e. you might have a function called Foo.merge(...) instead of x.merge_with_foo(...)

- a way to bring modules into scope so you don’t need to specify the name

- not using that many modules. Most lines of code won’t have more than one or two function calls so it shouldn’t matter that much (other techniques can be used in complicated situations)

The key advantage of type-dependant name resolution is in using the same names for different types. You might want to write code like foo.map(...) and it is ok if you don’t know the exact type of foo. With modules you may need to know whether to call SimpleFoo.map or CompoundFoo.map.


If anything, this is an aid to type-checking. Since MyModule.foo takes only things of type t, the type-checker's job is extremely easy and it will help you a lot more in cases when your program is incomplete.

So often when using C#-style fluent APIs I find that I'm completely on my own and have to turn a half-written line into something syntactically correct before Intellisense gives me anything useful. Using an F#-style MyModule.foo, the compiler can tell me everything.


Nim handles this quite nicely for you: either syntax works!


For those unfamiliar, the UFCS¹ wikipedia page has an explanation and a few examples.

¹ https://en.m.wikipedia.org/wiki/Uniform_Function_Call_Syntax


FWIW this is how Elixir works. You just do MyModule.foo.


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.


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.


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.


Isn't that just reinventing classes?


It's the good part of classes (namespacing) without the bad part (inheritance).


Doesn't seem like it necessarily stops inheritance. You need to throw away method references or you're just back to open faced classes.

You can still write new methods that blow away any assumptions previously made about type state,causing bugs. It seems like it could be even worse seeing as you possibly have less access controls for fields.


With the typing system of Haskell, maybe Rust too, you can do a lot to prevent the type from being able to represent wrong state. Immutability also does a lot to prevent issues with false state and public fields.

If you want to prevent people outside the module from adding functions, you can do that in Haskell by only exporting the type-name. This prevents other modules from actually accessing any data inside the type, whilst still allowing them to use the type in functions. This includes sometimes making getter functions to read values from the type. I'm pretty sure Rust allows for something similar.

In general, FP still allows for encapsulation. Moreover, it uses immutability and to some extend the type-system to prevent illegal states of data types.


No, a class is a data type coupled to functions operating on that class. For the purpose of this discussion a module is a way to semantically group types and functions without coupling them (languages like OCaml get fancy by actually typing modules which enables some neat ideas, but is not important to this discussion).


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


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


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


Python's actual behavior in this case is far stranger than static methods, which are a standard feature of many object-oriented languages (albeit one that's fairly redundant in a language that allows free functions).


I'm not sure what you're saying here.

If you want `def foo(a, b)` inside a class to be a static method, you need to define it as

    class Foo:
        @staticmethod
        def foo(a, b):
            ...
Now `f = Foo(); f.foo(1, 2)` will work.

However, you need to be explicit about this because it is much more common for this situation to arise because someone forgot a self somewhere than for it to have been intentional. Requiring @staticmethod serves to signal to humans that this odd behavior is intentional (but again I'd say that, stylistically, if you're using a staticmethod, just move it out of the class and into the module namespace, module namespaces in python don't bite).


I just don't think the decorator should be required for static methods. If you write `def foo(a, b)` inside a class, the lack of `self` should be sufficient to make `foo` a static method.

IMO it would make sense for Python to look at `a, b` and assume you wanted a static method with two parameters. Instead it assumes you're using `a` instead of `self` and really wanted an instance method with one parameter.


This requires blessing "self" as special (like this in java etc.). Python made a specific choice not to do that (and there are cases where you don't want to use "self", such as metaclasses, classmethods, and (rarely) nested classes that close over their parent class.

Like I said: most of the time, a missing `self` is an error by the user, and so python requires explicit notification both to the language, and to other people, that you're doing what you mean to do, and not just making a mistake.


I think that's the point: requiring the user to explicitly pass "self" rather than making it a language keyword mostly just gives the user an unnecessary opportunity to make mistakes.


Except in Pythonland, "explicit is better than implicit"

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


Wouldn't it be more explicit if there was a designated keyword that objects could use to access their own members instead of implicitly assuming that any first argument to a method is a reference to them, only called 'self' by convention?


> Wouldn't it be more explicit if there was a designated keyword that objects could use to access their own members

Not really. You end up implicitly injecting some value into local namespaces instead of having a function that takes an argument.

The implicitness of methods working differently than normal functions > the implicitness of classes passing a first argument to methods.


Should super then also be a method argument?


Honestly, I think it would have made some amount of sense to make super a method defined on the base object so that

    def __init__(self, x, y):
        self.super(x, y)
would be the convention. There may be some infra issue with this (and in general, `super` has to be kind of magical no matter how you handle it). But yes, in general I can vibe with "super is too implicit".


Ruby generally (I think? I haven't seen much of Ruby code) uses "@" instead of "self.", and "@@" instead of "ParticularClassName." (btw, "self.__class__" doesn't cut it), and it seems to produce no namespace pollution.


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


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


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 :)


The one that bugs me the most is:

    from datetime import datetime


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


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


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/


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.


> 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?


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?


It seems to me that you're conflating two unrelated things. Yes, I agree -- there are perfectly legitimate reasons to implement module level functions instead of class and instance methods; but avoiding typing four extra characters just to be more like some other arbitrary language is not one of them.

Python has a different "philosophy" than "C++ et al". Literally everything is an object, and everything has attributes; some of those attributes are objects that implement the Callable protocol. And it's a syntactic sugar that some of those callables are implicitly passed certain predefined arguments; the default is passing the instance of the object itself. The exact same thing could have been implemented without it, like this:

    foo.some_callable_attribute(foo, another_argument)
Since methods are usually called more often than they are declared, it makes sense to make this first argument the default.


Conversely, I think the beauty is that they _are_ the same as top-level functions, just in a special namespace. This means that you can call "instance methods" directly from a class, i.e. the following are equivalent:

    (1) + 2
    (1).__add__(2)
    int.__add__(1, 2)
This comports with the "explicit is better than implicit" policy in Python. The only "magic" part is when dot-notation is used to call an instance method, which is just syntactic sugar. Another example of this philosophy is that operator overloading is simply reduced to implementing a method with a specific name.

I think a "magic" this keyword can create a lot of nasty edge cases that can be difficult to reason about; the way "this" is used in JavaScript is notoriously complex in ways that it might not be in a statically typed language like C++. What should "this" evaluate to outside of an instance method? What about in a class definition inside an instance method? What if an instance method is called directly instead of on an instance? All of these situations require making their own "rules", whereas in Python the correct answers can be easily reasoned about by starting from first principles.


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.


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


That’s a compelling point.

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


> 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,


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.


It makes a lot of sense for modeling real world objects that can do things and have state.


Which makes a lot of sense for the situations where modeling real world objects that can do things and have state makes a lot of sense. Which isn't as often as the OOP advocates would have one believe.


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.


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.


They are far less primitive than Java's - they are classes too, but can't be extended.


I'm not sure which language did you mean there, but in Python those classes can be extended.

Also, the main reasons for special cases are historical; also, they predate PEP8 by a long time.


Sorry. That's something that was true in 2.x but not anymore. There used to be UserString, UserList, UserDict classes for serving as base classes. We still have those, but, at least now, we can extend the fundamental classes directly.

They are a bit more convenient than the built-ins.


We can inherit from `list`, `dict` etc., but objects of those classes are still not as flexible as "normal" objects. For example, we can't add an attribute to a list using `l = []; l.a = 1`.


I believe that makes them the same as any other class defined with `__slots__`:

    >>> class Foo:
    ...   __slots__ = ('a', 'b')
    ...
    >>> f = Foo()
    >>> f.a = 1
    >>> f.b = 2
    >>> f.c = 3
        Traceback (most recent call last):
          File "<stdin>", line 1, in <module>
        AttributeError: 'Foo' object has no attribute 'c'


> Are there psychotic devs out there who call it something other than `self`?

I call it "me".


[flagged]


Sorry I'm late here, but obviously you can't post like this. We've banned the account.

Please don't create accounts to break HN's guidelines with.

https://news.ycombinator.com/newsguidelines.html


You spelled it wrong!

Ruby’s approach is the most delightful, I find. elf is an instance of Elf and in elf.health, self is the elf.

Elf is an instance of Class and in Elf.surpass, self is the Elf class itself.


Ruby's irregular syntax drives me crazy. But it is beautiful.


I gotta say as a Python developer I kinda like this compared to thing = Thing()


Except the binding always implies the object is mutated on changes, which make any form of reasoning or concurrent programming difficult.


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.


I should have written almost always.

You can but it's not common. It's like saying you can write functional programing in C. You can but the whole ecosystem is not doing it.

If you set attributes, you know for sure that they changed at this particular place in the program but later in another position in the program, can you reason about the value of the object? You don't even who mutated it. And don't tell me "you can get its state", I'm talking about reasoning about the program here.

There is a reason why concurrent programming is more difficult than necessary with objects.

My point is that this implicit management of the state in "OOP" brings a lot of problems


If I were to design an idiot-proof OOP model that takes this into account, I'd force sequential processing of mutating methods - if the running method starts a mutation and there is another method doing the same, we roll it back until the previous method finishes.

Performance would suck, but that's life. You can't have your cake and eat it too.


Why? There are plenty of OO libraries where method calls do not change the object.


There is but it's mostly not how OO programs are built. And even assuming you have most of the core of your program behaving properly and clearly with non-mutable objects. What do you gain from having objects at this point?


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.


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.


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.


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.


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.


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


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


I found when I started programming I never used OOP. Then I used it too much. And then recently I use it incredibly sparingly. I think this is most people's experience.

However, there are certain situations where I cannot imagine working without OOP.

For example, GUI development. Surely nobody would want to do without having a Textbox, Button, inherit from a general Widget, and have all the methods like .enable(), .click(), and properties like enabled, events like on_click, etc.?

Similarly, a computer game, having an EnemyDemon inherit from Enemy, so that it has .kill(), .damage(), and properties for health, speed etc.?

I'd really like to know how the most anti-OOPers think situations like this should be handled? (I'm not arguing, genuinely interested)


At least regarding games, ECS is the popular flavor-of-the-month alternative to OOP as a design strategy. The main problem being that games often have a lot of special cases, which break the inheritance hierarchy quite quickly.

E.g. Defining a weapon > {sword, wand} hierarchy, with respective properties for melee and casting, and then defining a unique weapon spellsword which is capable of both melee and casting. You could inherit from weapon, and copy & paste sword/wand code, or inherit from sword/wand, and copy & paste the other, but the hierarchy is broken.

ECS would rather have you define [melee] and [casting] components, and then define a sword to have [melee], wand to have [casting] and spellsword to have [melee, casting]. So instead of representing the relationships as a tree of inheritance, you represent it as a graph of components (properties). And then you generically process any object with the melee tag, and any object with the casting tag, as needed.

And of course then you could trivially go and reach out across the hierarchies and toss [melee] onto your house object and wield your house like a sword -- I don't know why you'd want to do that, but the architecture is flexible enough to do so (perhaps to your detriment).

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]


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


exactly the post I was trying to remember when talking about spellswords :)

Those posts are also cool in that defining games as a set of rules that operate on things within it is a really neat mental model -- the program should basically look like a DnD rulebook, with statblocks and all.


Yeah, it's definitely among my favorite essays. Btw, your explanation was excellent as well!


The way to really grasp ECS architecture is not to look too hard at the implementations(which are all making specific trade-offs) but to recognize where it resembles and deviates from relational database design. A real-time game can't afford the overhead of storing data in a 3NF schema, but it can design a custom structure that preserves some data integrity and decoupling properties while getting an optimized result for the common forms of query.

The behavioral aspects are subsumed in ECS to sum types, simple branching and locks on resources, where the OOP-embracing mode was to focus on language-level polymorphism and true "black-boxing". Since the assumed default mode of a game engine is global access to data and the separation of concerns is built around maintaining certain concurrency guarantees(the order in which entities are updated should have minimal impact on outcomes), ECS makes more sense at scale.

The implementation trade-off comes in when you start examining how dynamic you want the resulting system to be: You could generate an optimal static memory layout for a scene(with object pools used to allow dynamic quantities to some limit) or you could have a dynamic type system, in essence. The latter is more straightforward to feed into the edit-test iteration loop, but the former comes with all the benefits of static assumptions. Most ECS represents a point in the middle where things are componentized to a hardcoded schema.


> E.g. Defining a weapon > {sword, wand} hierarchy, with respective properties for melee and casting, and then defining a unique weapon spellsword which is capable of both melee and casting. You could inherit from weapon, and copy & paste sword/wand code, or inherit from sword/wand, and copy & paste the other, but the hierarchy is broken.

Or, you could, in OO Python:

  class SpellSword(Sword, Wand):
    ...
or, possibly even:

  class Melee:

  class Casting:

  class Sword(Melee):

  class Wand(Casting):

  class SpellSword(Melee, Casting):
> So instead of representing the relationships as a tree of inheritance, you represent it as a graph of components (properties).

Multiple inheritance also represents relationships as a graph of properties.



I've been hearing about ECS for a decade so it's definitely more then flavor of the month. However the issue is that unfortunately Unreal/Unity are both OO first.


It's definitely been around but I think Unity's (never-finishing) ECS + Rust gamedev community's focus on it has really spiked its popularity/interest lately. Otherwise pretty much every recommendation/engine is OOP-based, with a few straggling extensions/libraries for ECS here and there.

No idea about usage in industry though, but it comes up randomly e.g blizzard: https://www.youtube.com/watch?v=W3aieHjyNvw


This has been a recognized problem for game devs for a while and recently Unity introduced DOTS to gain performance. From what I understand, doing traditional OOP means going through a lot of pointers to find information. If you put everything into arrays instead, you reduce the time spent looking up things.

https://unity.com/dots

IIRC, this is the textbook that unity pulls from if you want to learn more. https://www.dataorienteddesign.com/dodbook/


isn't ECS a composition pattern on top of OO?


I’m not sure it is; the heart of OOP is encapsulation of data + methods — the object itself. (Or message-passing if you’re being kinky)

ECS is almost the opposite; the object in question is barely defined — rather it’s derived from the composition of its parts. Like a DB, there’s roughly no encapsulation at all; just relations defined where, should you put them all together properly, you get the “object” back.

That is, in OO a particular object is composed of certain properties and capabilities. In ECS, if certain properties and capabilities exist, we call it a particular object.

Implementation-wise, the main difference seems to be the SoA vs AoS argument

But there’s also things like unity’s ECS deviating from rust’s ECS definition — I believe in Unity, components (data) and systems (behavior) are coupled, which fits better towards the OO world (components are the object of question). In rust ECS they’re decoupled (which unity is moving to, someday) and you’re really just dealing with structs and functions. And there’s a whole thing about the “true” ECS definition but it doesn’t really matter.


Yeah, it is basically OO with most of the classes having no parents (e.g. without inheritance, but still incapsulated, and polymorphic).


I've described an ECS to my friend as a combination Strategy pattern and Composition, so you can have a huge list of "traits/components/behaviours" and then build your object hierarchies (and their behaviours) from that, instead of from Inheritance. The way most game editors are made, when you configure things using the ECS, it visually looks like inheritance, but under the hood it is more like Composition. Once you go down the rabbit hole, you would see that and ECS/Strategy pattern & Composition makes more sense (and way easier to cater to exceptions) than plain old inheritance.

I want dare say that the enterprisey-version of an ECS would be the Onion Architecture that's been floating around in Java/C# camps. Not 100% match but they feel the same to me.

Obviously for normal GUI programming (at the outer layer that we see at least), inheritance is still king.


It's an alternative to inheritance, which is the defining part of traditional OO.


Thanks for sharing that Dwarf Fortress file, I never thought about the structure for all those attributes.


the patterns can still be completely the same without OOP - pairing data and functions that operate on said data. enemy.damage() and Enemy::damage(enemy) are functionally and semantically equivalent. But in the latter case (where you separate data and code) you don't need to worry about object assembly/IoC, how to pass a reference to A all the way through the object graph to object B, composition over inheritance becomes the default (at least in my experience using TypeScript interfaces, YMMV with other languages). The benefits of OOP, primarily state encapsulation, stop looking like benefits when it turns out your state boundaries weren't quite right.

Of course I'm biased as I went through the same "procedural => OOP => case-by-case" learning curve as the GP. But I ended up spending a lot of time trying to satisfy vague rules when using OOP - with procedural/functional programming with schema'd data, I get to spend a lot more time on what I actually want to do. Not worrying about SRP, SOLID, object assembly, how to fix my object graph now that A needs to know about B, and so on.


> how to pass a reference to A all the way through the object graph to object B

you just end up replacing the object graph by the call graph, which makes all the business logic much messier as now every function call takes a "context" argument


That's not been true in my experience - what you do end up with is a global data structure, which is the shared state for all or most non-ephemeral top level concerns. Aside from recursive functions I find call stacks tend to be quite short.


I completely agree with you when it comes to UI although the key here is that OOP is syntactic sugar, it shouldn't be the overarching pattern. I think of it as augmenting types, or prototype-based OO.

And when it comes to games, nope; entity patterns with composable behaviours added to dumb objects is far more productive that traditional OOP, as you need many objects with slightly different behaviours/abilities/types. Composition over inheritance is key here.


The 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


> Similarly, a computer game, having an EnemyDemon inherit from Enemy, so that it has .kill(), .damage(), and properties for health, speed etc.?

I'm not a game developer in the slightest but as a gamer and developer I've often thought about similar things a little bit.

In another example, let's say you were playing a game like Diablo II / Path of Exile where you have items that could drop with random properties. Both of those games support the idea of "legacy" items. The basic idea is the developers might have allowed some armor to drop with a range of +150-300% defense in version 1.0 of the game but then in 1.1 decided to nerf the item by reducing its range to +150-200% defense.

Instead of going back and modifying all 1.0 versions of the item to fit the new restrictions, the game keeps the old 1.0 item around as its own entity. It has the same visible name to the player but the legacy version has the higher stats. Newer versions of the item that drop will adhere to the new 1.1 stat range.

That made me think that they are probably not using a highly normalized + OOP approach to generate items. I have a hunch every item is very denormalized and maybe even exists as its own individual entity with a set of stats associated to it based on whenever it happened to be generated. Sort of like an invoice item in a traditional web app. You wouldn't store a foreign key reference to the price of the item in the invoice because that might change. Instead you would store the price at the time of the transaction.

I guess this isn't quite OOP vs not OOP but it sort of maybe is to some degree.

I'd be curious if any game devs in the ARPG genre post here. How do you deal with such high amounts of stat variance, legacy attribute persistence, etc.?


There are a bunch of FP GUI development styles without OO. The Elm style[1], which propagated to a whole family tree of similarly structured system, and similar ways in Reagent based apps in the ClojureScript world.

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


>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/


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


If we step into the haskell land of monads having explicit functionality kept in individual monads with their own instances would be one way to segment this type of stuff. Something vaguely written as below would let you run actions where anyone can move or is an enemy, and default implementations can be provided as well.

    data Demon = { ... }

    data Action
      = Dead
      | KnockedBack
      | Polymorphed

    class Character a where
      health :: Int

    class (Character a) => Movement a where
      speed :: Int

    class (Character a) => Enemy a where
      kill :: a -> Action
      damage :: a -> Action

    instance Character Demon where
      health = 30

    instance Movement Demon where
      speed = 5

    instance Enemy Demon where
      kill _ = _
      damage _ = _

https://soupi.github.io/rfc/pfgames/ is a talk going through an experience building a game in a pure fp way with Haskell and how they modelled certain aspects of game dev. Most of the code examples are when you press down in the slides.


I used to think this too. And I have to admit OO does lend itself well to GUI and hierarchical structures (IMO probably the only case I think it is actually useful). But that's not to say that there aren't declarative methods that are equally nice to use, Elm has done a lot to popularize this style of coding GUIs.

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


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.


> I cannot imagine working without OOP ... in GUI development

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


But react isn’t the widget library


Is this meaningful? You can define a widget library easily in React and it's capable of representing hierarchical GUIs in a way that's not OO


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.


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


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.


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.


You can just define them as functions that are part of the class.

Something like:

@dataclass class AuthMethod: credentials: typing.Any cache: typing.Any apply_to_request: typing.Callable[[typing.Any, typing.Any, Request], None]

@dataclass class AuthMapping: method: AuthMethod matches: typing.Callable[[str], bool]

class Client: root_url: str url_format: str auth_methods: typing.List[AuthMethod]

def auth_request(client: Client, req: Request): applies = filter(client.auth_methods, lambda x: x.matches(req)) if len(applies) > 1: raise Exception("multiple auth methods found") elif len(applies) == 1: method = applies[0] method.apply_to_request(method.credentials, method.cache, req)

Then you would statically define your service's auth methods by URL, so users can just do:

    my_client = Client(auth_methods=MyServicesAuthMethods)
or you can create a helper function like:

def create_client_a(base_url: str): return Client(base_url, auth_methods=[...])

Alternately I think you can use closures, but I'm not totally positive about the scoping.

AuthMethod = typing.Callable[[Request], None]

def auth_method_a(username, password) -> AuthMethod: my_token = None

    def __auth_method_a(req: Request):
        if my_token is None:
            my_token = do_login(username, password)
        req.headers['token'] = my_token
    return __auth_method
That creates a standard interface (a function that accepts a Request), without assuming anything about the underlying protocol.


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"


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.


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


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.


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.


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.


I'm curious to know which "serious warts" in C++ would be fixed by making everything an object.


Indeed... in Java this is reasonably clear, but I've never found I have a problem in C++. I suppose if you're inheriting from your template argument and you template on an int? Seems awfully contrived and useless though.


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.


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


Hi Can you please link to some examples of python great OO code?


The author has created a pseudo-class out of the module. There is still a family of functions operating on the same exact data. All that he has done is throw away the init and require more imports.

Given that python is an object oriented language, the correct abstraction for this pseudo-class is... a class.

As others have already pointed out, the example would benefit from a refactor that separates concerns but this so-called "functional" approach makes that harder to accomplish if these functions have been peppered throughout the codebase.

I'm tired of people making trivial arrangements to a codebase like this and calling it "functional", losing most of the OOP benefits of python in the process. Maybe stop rearranging the chairs and get on with sailing the ship.

Maybe I'm just a little salty today. ¯\_(ツ)_/¯


> stop rearranging the chairs and get on with sailing the ship

I had never heard this before (though it even has a wiktionary entry), what a beautiful way to put it, thank you. It's really ridiculous, the amount of time we waste on readable, testable implementations with enough performance, just because they're not the proper version of an idea in the reviewer's mind.

It's not what DDD says! This is not how Scrum works! That's not functional programming! These aren't real unit tests!

One thing in common I notice in this sort of pointless bickering is that how they're always about how "X is not real/proper Y", and they almost never point out an actual antipattern you might need to avoid due to reasons. The argument is purely philosophical from the very beginning, without much thought on what the implications are, if any.

And I'm beginning to attribute this to incompetence. Not always -- there is always that one colleague who's a genius thinker in one dimension, not quite focused on delivery, and that's fine. We all need the occasional course correction, we all stand on the shoulders of giants. But it's often enough that I feel I'm expected to give one of those acronyms, just so that the other person can use a preset line of scrutiny: nevermind paying any attention to whether the tests actually validate the program, or if the design is unambigous and extensible, etc.

It's like a version of the "factory worker"-style developer -- you know, he who exists to be a pseudocode-to-actualcode translator, always expects a perfectly well-defined set of specs, a design to implement that within, and won't move a muscle except to complain about how everyone else isn't doing their job unless those are all satisfied. Yes, it's exhausting.

Thank you for accommodating my rant if you read this far, dear reader; thank you OP for bringing out the salt I didn't realize I had today! Apologies if I digressed too far there.


This may be a bit pedantic, but OOP != inheritance. Inheritance is the worst part of OOP. OOP is also modules and namespaces and encapsulation, all of which are great.


By and large, I think inheritance is a major mistake. Interfaces are the only "good" part of inheritance.

Sure, there are edge cases where inheritance is truly the best way to do things, but those are few and far between.

I cringe every time I dive into some of my day job's more deeply inheritance based code. It is SO hard to make changes there without breaking a bunch of stuff unintentionally.


The way I'd put it, the mistake was to make inheritance the default. It's a sometimes useful mechanism (called "open recursion" in the FP world).


Ironically, in Python you can use interfaces without ever resorting to inheritance. :)


Truly a description of dynamic typing :) Interfaces without declaration.


I didn't say that they were not declared. You can have proper, type-checked and runtime enforceable interfaces, without having to inherit from them.


Things which all exist in non-OOP languages and before OOP became a fad. No OO isn't inheritance, but implementation inheritance is one of the few things which is unique to OOP based programming. Most other aspects of OOP will be found within other paradigms.

More importantly OOP is not merely a bag of syntax and features but a way of thinking about software development and structuring your programs. OOP says you organize your code around objects and the actions done on those objects.

That is something I find that often isn't a great way of structuring your code. But I don't think OOP is useless. I do use OO thinking in my code, just not as much as I used to. I prefer functional thinking. Often I organize code arounds verbs rather than nouns. So one file may be a similar kind of action performed on many different kinds of objects.

E.g. when writing a rocker simulator, I would have one source code file which contained mass calculations for a variety of objects.

Another file would contain rocket thrust calculations.

I would say though that in GUI programming I find that OO thinking tends to make a lot of sense.


I don't think modules or namespaces really count as OOP. And encapsulation can exist in the absence of OOP if the privacy barrier exists at (eg) the module level instead of the class level.

I'd argue that inheritance is really the core of OOP (and by extension methods, which are only different from functions insofar as they interact with inheritance).


How are modules and namespaces OOP?


To me OOP is about separation of concerns, abstract data types, information hiding. Modules/namespaces support decomposition the same way that objects do, and historically they became popular because of and in tandem with object-oriented languages. So it's true as others have pointed out that they can and do exist in other paradigms; I would argue that is evidence of the influence of OOP and the fact that its best ideas have been incorporated all over the place.


Those are just good programming principles in general though. Abstract data types, information-hiding, and separation-of-concerns were being done in C well before OOP was a thing. Granted, the information-hiding wasn't as sophisticated as it got once OOP was introduced, but I don't think it makes sense to attribute information-hiding to OOP as a result, especially since newer programming language have used visibility attributes in the absence of OOP classes.


Some would say abstract data types are not OOP -- OOP is defined by how it's different from ADTs: https://www.cs.utexas.edu/~wcook/Drafts/2009/essay.pdf


Agreed. Inheritance CAN be very useful but I would consider it an advanced feature for special cases. It’s definitely not something that should be taught to beginners.


How long has the author used Python? OO, while responsible for much of the footguns, is also responsible for an enormous amount of Python's expressive power. Being able iterate or key into arbitrary objects as if they were lists or dictionaries is enormously powerful, but all of that structure is OO based. It's a huge part of what "pythonic" means.

> If you've taken the pure-FP/hexagonal-architecture pill

Writing software is not something you take a conceptual "pill" for. You learn new tools that you add to your toolchest and use at your own discretion. Tools which purport to be able to displace a whole arena of otherwise stable, mature, and high productivity tools better be at least comparable if not significantly better to encourage upgrade. FP as a tool definitely comes in handy for quite a few use cases. FP as an ideology? Not so much.

Regarding the latter, I'm getting a little sick of the FP chauvinism that that pretends that pre-existing software and how it was written is obviously inferior to the new, purely FP way without any real persuasive evidence. Code that's written in an overly FP oriented way is no less immune to the same codebase diseases that code written in an overly OO oriented way is. It's the map-territory problem all over again.


That’s because the ideologists operate based on intuition rather then identifying the exact reason why one paradigm is better. OO is bad because it promotes writing logic with free variables and context. Logic that relies on free variables and context is inherently less modular. Lack of modularity is the root of all structural technical debt.

While FP doesn’t force someone to eliminate context or free variables it certainly does not promote writing logic that includes it by default. This generally leads to more modular code. Hence the reason why FP just "feels" better to practitioners and leads to people promoting it without clear reasoning. This in turn makes the promotion of FP seem "ideological."

Maximizing modularity in code requires greater restrictions then the ones FP provides, but FP in general is heading in the right direction.


Django's class-based views are a perfect example of well-written OO python.

A view takes a request, gets some data, and renders a response.

Each part of the process (and its subparts) is abstracted in methods that can be overridden. This lets you implement certain things (authentication, formatting, compression, caching, logging, pagination) once in an abstract class or mixin, and add it to all your views.

Of course, Django already has those classes and mixins.

This means you can write very simple views. Just define the queryset and the template, and you're done. Everything else just works, because it's already implemented in well-tested parent classes.


I think OO caught on because it encourages people to organize their thoughts, and good structure alone brings a tremendous improvement in code.

I think the reaction to it is more the "everything is mutable" approach most OO languages took, which led to things that looked organized but that were a hot mess of side-effects.

With dataclasses (or attrs) you can cut back on that by freezing everything, and still get the clarity of methods that lay out the essential functionality of a type.

In their own example, "get_items" and "save_items" make a bit more sense if they're stashed away in the Client namespace, and you can see that they're essential to what a Client does.

And while Oil's conjecture is probably right, I'm not sure it survives if you add the caveat, "in that same language."

For instance, Python doesn't have a native way of expressing sum types outside of inheritance. That's not to say inheritance, especially an always open style, is a good way of working with a sum type, it's just the only native way.


I think OOP caught on because of IDEs with auto-complete functionality, you type object dot and it offers all the method names for you.


OOP was around long before IDEs were that popular.


When was that?


Depends on how far back you want to go. The term "object oriented programming" was coined back in the 60s. Languages like Smalltalk were introduced in the 70s.


The example given in the article is not compelling -- there are only two arguments in play, so passing two arguments directly instead of passing an object holding those two things isn't painful.

To me the big win of classes isn't inheritance, it is that functions and related data live in a common scope.

If you write your code as a bag of data structures and a bag of functions that you pass those ad-hoc data structures to, it is less clear which things play together.


I would say that is simply down you you not being experienced with writing code this way.

Once you begin writing functional code more regularly, you will get much the opposite way of thinking. Today I find it frustrating that I need an object first. In my mind, I already know what action I want to do. I know what function I want to call. I start with that and look at what arguments it takes in.

OOP programming is frustrating now, because instead of going straight for the function I have to locate the appropriate object first. That is like a big detour.

> If you write your code as a bag of data structures and a bag of functions that you pass those ad-hoc data structures to, it is less clear which things play together.

Noting suggest you need to write code like that. People who write functional code tend to have a lot of well defined data types. I use well defined data types in Julia. Haskell and OCaml developers use well defined data types. We are not using using dictionaries or something.


This is exactly how I felt after learning Elixir and then coming back to Python. Object orchestration is painful.


Not only that, but the second example is object-oriented, he's just doing it "by hand" rather than taking advantage of the built-in language syntax (which, in spite of his insistence to the contrary, does make it clearer what's actually going on).


the author is right of course, but in practice he's wrong.

I've been in a 500k loc codebase mostly in python. As things got complicated, instead of the couple of arguments seen in his example you need up with half a dozen or more, or variable argument lists, people passing callbacks with callbacks and all sorts of shit like that instead of just writing an object and context manager or two.

In a large codebase these rapidly become very difficult to reason about and are better structured as objects with a discrete set of operations.


(Author here) I used to think that, but I've since found myself working on a similar sized codebase where we use very little OO and none of these problems have manifested themselves. Avoiding large function signatures and passing higher order functions around takes a little discipline, but wrapping those things with a bit of OO doesn't make the root problems go away, it just encourages their proliferation. (To reiterate the article, I'm by and large talking application development as opposed to library development).


So you're trading object with closures?

  The venerable master Qc Na was walking with his student, Anton.  Hoping to
prompt the master into a discussion, Anton said "Master, I have heard that objects are a very good thing - is this true?" Qc Na looked pityingly at his student and replied, "Foolish pupil - objects are merely a poor man's closures."

  Chastised, Anton took his leave from his master and returned to his cell,
intent on studying closures. He carefully read the entire "Lambda: The Ultimate..." series of papers and its cousins, and implemented a small Scheme interpreter with a closure-based object system. He learned much, and looked forward to informing his master of his progress.

  On his next walk with Qc Na, Anton attempted to impress his master by
saying "Master, I have diligently studied the matter, and now understand that objects are truly a poor man's closures." Qc Na responded by hitting Anton with his stick, saying "When will you learn? Closures are a poor man's object." At that moment, Anton became enlightened.

From https://people.csail.mit.edu/gregs/ll1-discuss-archive-html/...


Good koan, utterly irrelevant here. OP is still using good old structures for their state, not closures.


I would not recommend trying to use closures in python.


Why not? `nonlocal` is pretty straightforward, isn’t it?


I can get what the author is trying to say. But the article missed the most important point - OO for dynamically typed languages like Python/Ruby/JavaScript is for polymorphism, so that you can write a function accept an object that has print() method without knowing what it is.

Languages extensively use the 'bag-of-functions' style usually have a way to dispatch functions based on type, so that people can extend packages without having to modify the source code. It could be multi-methods in Clojure, protocols in Elixir, polymorphic modules in OCaml, or even powerful ones like type classes in Haskell.

But yeah, I agree 'bag-of-functions' is generally a net win in terms of language design - if the polymorphism counterpart exists.


Couldn't agree more. I'm a sysadmin living in a powershell world right now, and the ability to abstract away "things that mostly behave like filesystems, but aren't” behind some classes with standard interfaces is extremely useful, as it clarifies the actual logic of the code a lot, and means I can support different backends usually with just a new class

I absolutely could have done it without classes, in fact I did that first, until it clicked that I really was doing things the dumb way.

Everything else is a bag of functions.. its only a couple of thousand lines of code, but limited OO was still very useful, and to me, more elegant


I've done plenty of maintenance programming. Most OO involves abuse of the paradigm anyway.

Most people don't use inheritance to large AND good effect. Or they use it and create a mess. Deep hierarchies of inheritance need to be carefully designed or they become a mess. All the hiding means less clarity about what is happening when things go wrong.

That said, its a valid approach and can yield useful results. Especially when you can just add a thing to a list and simply call a method on it without having to bother to know what it is.


>Deep hierarchies of inheritance need to be carefully designed or they become a mess.

Deep hierarchies of anything need to be carefully designed.


The flip side of my impromptu "rule" is that you can drop the "careful" part as long as you are comfortable with a little or a lot of mess. Obvious common sense lowers the tolerance. I wonder how others set their tolerance? My perspective is that most OO hoerarchies don't go too deep. I've seen 4-5 at rare extremes. Around 2-3 is common. I'm sure someone has encountered 10 or more. I shudder to think how well that turns out in practice.


I try to use the rule of after three levels of inheritance or abstraction then it needs to be a black box and then you can reset your count.


> It's somewhat just the return of global variables.

No, it's not. This is far from the only objection/disagreement I have with the article, but I can take almost everything else as either opinions or convenient examples in support of those opinions.

That sentence, though, just stuck with me as particularly odd because, among other things, using classes doesn't make you more likely to use global variables than using functions exclusively. And I'm not even talking about Python in specific.

Anyhow, it had been a while, I think, since a post pitting FP against OOP had reached the front page, and thus causing yet another iteration of that traditional flamewar in the comments. Guess it was due.


The author doesn't mean literally globally scoped variables, but more generally data with a larger scope than necessary. For example, you might have instance methods that only need a small part of the "self" object, but they get access to the entire thing.


This is basically how I use Python :) Truly, recently I tried to remember how to create a class method so that I can have different constructors and I forgot! Got back go just structures and functions.

Most OO in most OO languages is just polymorphism and inheritance is merely a roundabout way to construct or traverse the method graph for polymorphic objects. It's often much easier to do polymorphism directly and only if you really need it: quite often you can fare well without polymorphism at all and simply write branching instructions.

The useful OO must be about changing state in a controlled manner, but this remains largely unaddressed.


There's a lot of backwards and forwards in the comments. What I'd really like is to test my conjecture - could people post/email me a (< 500 line, ideally not deep library code) bit of code that they think is "so innately OO friendly that the conjecture won't hold".

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


What is asserted without evidence can be dismissed without evidence. It's not our job to debunk a poorly formed argument.

Regardless:

pathlib.Path:

- https://docs.python.org/3/library/pathlib.html

Django's class-based views and mixins:

- https://docs.djangoproject.com/en/3.1/topics/class-based-vie...

- https://docs.djangoproject.com/en/3.1/topics/class-based-vie...

Hell, you don't even need to look into other code. Just use the sample from the article.

What if client_a and client_b write to different APIs? One expects authentication, the other has pagination and rate limiting. Actually, one of them returns XML. Their URL structure is completely different.

Classes are great when you want a bunch of different things to produce a similar result. You don't care how the client fetches and transforms data, as long as it returns it in a certain format.

For example, you might want TwitterTweetSource.get_items() and FacebookPostSource.get_items() to both return a list of Posts that have certain attributes. The implementation details don't matter; you just want these methods to have the same signature, because you call [s.get_posts() for s in source_list]. Maybe you'll add FacebookCommentSource, which reuses the same authentication strategy, but on a different endpoint.


> What is asserted without evidence can be dismissed without evidence

I was hoping the code in the article would form (an admittedly small) piece of evidence. I'd just really like someone to show off a nice neat counter example, that demonstrates the true beauty of Python OO, and is "unrefactorable" to a data types + functions equivalent.

Pathlib is a great example, maybe a canonical one, of:

> Very occasionally, you come up with a core type that's used so often, it's nice to have the cutesy stuff.

Django's class based views I think could probably reasonably refactored into data types + functions. I'm not too au-fait with Django or its internals, but if someone could send me a "demonstration Django clone in 800 lines", I'd give it a go.

> What if client_a and client_b write to different APIs? ...

Then you use an if statement to split at that point. Again, given a specific toy example, I'm fairly certain a reasonable refactor is possible.


> Then you use an if statement to split at that point

This quickly falls apart as the difference between the classes grow. A view that turns a Facebook API call into Post-like Facebook objects has very little in common with a view that turns Twitter API call into Post-like Twitter objects. Doubly so if you factor other parts of the request like authentication, pagination and rate-limiting. You'll need a bunch of if-else statements in a bunch of different places.

This gets even hairier when you need to test that code. It's a lot easier to make sure a FacebookView calls the right endpoint and returns the right data, than to test every path of a combination of if-else blocks.

And what if you want to extend this code, but don't have control of it (external module or different team)? It's easy to extend parts of a class that do exactly one thing. It's not easy to extend big if-else block without overwriting it.

I have seen the benefits of this approach first-hand. We got more reliable code and better test coverage with less effort, and greatly simplified maintenance and development.

> I'm fairly certain a reasonable refactor is possible

To what benefit? There is already a nice construct for self-contained objects that offer a similar interface, that can be inherited from, and that can be unit-tested. Objects.


How about file-like objects in python? Many file-like interfaces are necessarily stateful, and having the state wrapped in an object that encapsulates the state allows for graceful reuse.

As a bonus, there's no need for inheritance in python due to duck typing.


> How about file-like objects in python? Many file-like interfaces are necessarily stateful.

Still the state and the functions that operate on them can be separated out. Right? No reason why the state and the functions need to be tied together inside a class.


Are you suggesting everything that currently takes a file-loke object "f" instead takes "fstate" "fread" "fwrite" "fclose" "fflush"...?


I think you could be on to something - that feels like an area with tightly coupled state and functionality. (Sorry for spamming this thread with comments like this), but would you mind emailing some example code, and we can test what happens?


Honest question, what do you expect OO to solve for you?

Things like

  - Maintainability
  - Extendibility
  - Testability
  - Reusability
etc

One problem with OO is that you can organize your code in many different OO styles and somewhat depending what you try to achieve you should pick a style matching that.

Personally I think your example is not separated enough,

  - Client configuration
  - Url construction
  - Reading data from API
  - Saving data
In the first example everything is mixed up in one class, it is like having a huge main function. I would probably have three different classes to solve that.


How about C#'s Task Parallel Library? There's a lot of complexity that is (imo elegantly) abstracted below the surface with the usage of inheritance, while still remaining very configurable and extendable.

I'm not sure how you would go about replicating similar async/await sugar without tying functionality to Task instances but I'd like to see it.


The key thing I disagree about in the article is this:

> What about interfaces/abstract base classes? Just try writing without them, I promise it's going to be OK.

You could write the code without formal interfaces, but I hope you're going to at least put in comments that define what things you expect to be on the modules/objects you're plugging in. And hopefully your large system is composed of many modules that work together in harmony, as opposed to being just a huge mass of code.

In any non-trivial codebase, having good interfaces / contracts is key to keeping a good clean design. You can make a nice clean large codebase without OO, but you can't without good clear interfaces. It's important to know what you can change in one part of the code without having to go change the rest of the code to adapt to that change.

Python doesn't have the equivalent of C# interfaces or Java interfaces, but having a clear definition of what's expected is still helpful even in a dynamic language like Python. These definitions are helpful to make the code clear, maintainable, and understandable. At a bare minimum it's helpful to do that with comments, but doing that with something like python's ABC module is even better.


Having recently started a script that avoided OO because I wrongly assumed that its was going to be 4 functions and done, I disagree.

I don't care about inheritance, that's just a way for someone to trip you up and make them feel smug about being "elegant"

I don't care for people trying to hide stuff with self._hiddenThing. Its python, nothing is hidden. If I want to reach in and grab your class by the ankles, prepending a "_" isn't going to stop me.

I agree wholeheartedly with dataclasses. I just wish that type annotations were actually enforced at run time. At the moment they are only really useful if you have pyre turned up to 11 (bye bye productivity) or have an IDE that understands type hints.

but, the hill I will die on is this: storing class state in self.blah. It is correct and proper to do that in a class, it limits the amount of args I have to shove into a function/method, it also allows me to pull out and check things before I do them. Yes they can be abused. Yes it means that you need to call functions in order. Yes it means you have to be more defensive about missing state.

But you need to be careful about that anyway. with minimal OO you can avoid a lot of typing and nasty verbose function args in python.


The issue the article presents is neither unique to Python, nor a good criticism of the OO model, so I'm not sure what to take away from this.

One thing I haven't seen many comments talk about is how methods as far more discoverable and memorable than functions. Let's say I have a User object. How do I get their (calculated) age? In OO, I can pretty much guess and let autocomplete do the rest: u.get<TAB> and I immediately see getAge(), along with everything else I can get from the object - much easier than looking through documentation or a huge list of functions.

I used to teach highschoolers how to write basic Minecraft plugins as quick intro to Java and the first thing I taught them was to write the name of the variable, then a dot, then press CTRL+Space. This worked absolutely brilliantly and it meant that they really didn't have to think about types and naming conventions all that much. Is it player_teleport() or entity_teleport()? What about NPCs? Nobody cares! thing.teleport() worked on anything you could reasonably be expected to teleport and everyone just understood that with basically no prior programming experience.


OO is applied for the wrong domains.

A typical web app with a database back-end is probably the worst offender.

Modelling your data with OO, fitting it somehow (ORM == OMG) into database tables and then use some annotation magic to turn those objects into JSON towards the frontend. Nothing there makes any sense whatsoever.

I built several of those and used terrible ORMs. They are hard to change. They are hard to reason about and they consume massive amounts of memory as each transaction creates fresh sets of objects - because we can't share data between threads simply - we throw those objects away again.

Gone is this whole ideal notion of objects being their own little agents, sending friendly messages between each other.

And the fact that message passing is not well-defined in a concurrent context in most OO-languages puts the icing on the cake. (i.e. objects can receive messages (i.e. calls) from any thread at any time and need to deal with it. Most of the OO code you wrote today cannot.


I feel the major reason OO doesn't fit well in Python is because Python was designed to harness "duck typing," which enables polymorphism with less code. Furthermore, attrs / dataclass does away with a lot of the Object boilerplate that most developers would prefer not to write when moving quickly.

To top it all off, context of use matters. For scripts without tests or limited extensibility, the value of OO is mostly in clarity to the reader, and here attrs / dataclass can be very concise and effective.

That said, when the object jungle gets very large, Python gets a lot more tricky. Look at matplotlib or try to write a billing system with a variety of account and transaction types.

Nevertheless it's nice for a post like this to call out the existence of the bijection between OO and 'non-OO' Python programming styles. The issue is worthy of contemplation for any early Python programmer once one gets the feet wet.


I'm not sure I follow. Python's duck typing explicitly relies on its object-oriented features. I'm not sure that it's even possible to implement duck typing in a language that isn't object-oriented.


When I talk about duck typing, I'm talking more about getattr and hasattr (explicit or implicit) rather than the everything-is-an-object structure under the hood. For example, you can invoke "foo()" on any thing no matter the type, and it'll work so long as you have "foo()" declared for the instance. You can't do that in many "OO-heavy" languages like C++ or Java. The advantage here is that in Python you don't have to explicit interfaces and base classes in order to achieve polymorphism.

You make a good point though-- the "duck typing" stuff is actually a consequence of Python using objects. Interesting: the "duck typing" paradigm should be able to be ported to other languages like Java then...


I am not a language expert, but Clojure (inspired by CLOS IIRC) does method resolution based on an arbitrary function that can ask any "is it a duck?" question of the arguments:

https://clojure.org/about/runtime_polymorphism

It's common for code to "choose" which method to call based on the class of the arguments, but it's also common to choose which method based on whether a particular key is in the hashmap, etc.


I suppose that this is edging dangerously close to starting a semantic argument, but I'd personally argue that ad-hoc polymorphism is a distinct concept from duck typing that just happens to have a lot of overlap in its use cases. One key distinction is that Clojure multimethods need to be explicitly declared somewhere, whereas duck typing is 100% implicit.


And in everyday Clojure usage the equivalent of Python attribute access is key access on maps, which is even more directly duck typed.


Object oriented programming is not about defining classes. It's about using objects. You don't have to define new classes to do OOP. Just use existing classes and objects polymorphically!

    url_layout.format()
    resp.raise_for_status()
    resp.json()
    session.add()
All those calls are dynamically dispatched - the essence of object oriented programming. This is what allows you to not worry about: * which string implementation `url_layout` uses * which HTTP protocol, encryption, authentication, chunking `resp` uses * what database is connected to `session`

You cannot avoid using objects - that's how all modern operating systems work.

Using classes without the need to call them polimorphically just as a nice namespace for methods is a separate issue.


OOP also works well when you want different _behavior_. In your bag of functions you have the same behavior for all objects. If you want different behavior, you're going to have to start adding branches in code that really has no business making those branch decisions.


I've underutilized Python's OO system and I've certainly overutilized it. The light bulb moment for me was when I read: "prefer composition to inheritance". Which Wikipedia tells me comes from "Design Patterns" but I'm pretty sure I remember the argument from "Working Effectively with Legacy Code." Obviously it's not a hard rule to avoid inheritance, but once I started asking myself, "Is this relationship an is or a has?", my designs got a lot better. And by better I mean they quit making me angry when I revisited them months later.

PS. Highly recommend that book BTW. Especially (and somewhat ironically) for people designing new systems. Everything is eventually legacy. :-)


Usually when someone says they hate OOP they really mean that they hate inheritance and no one taught them about composition.


I disagree that it's mostly pointless because if you're practicing Test-driven development OO provides a great way to abstract agents and their actions.

I've seen a lot of python code that looks like the example from the blog but that's simply just a narrow example and does not reflect all the potential of OO, not only in Python but in any programming language.

The thing I see in common in such poorly designed OO code is that it is usually not covered with unit tests that are actually useful and informative. Sometimes it's just not even tested at all.


At the end it comes down to how the developer designs their software, as Fred Brooks argued in "No Silver Bullet" (https://en.wikipedia.org/wiki/No_Silver_Bullet)

Brooks goes on to argue that there is a difference between "good" designers and "great" designers. He postulates that as programming is a creative process, some designers are inherently better than others. He suggests that there is as much as a tenfold difference between an ordinary designer and a great one. He then advocates treating star designers equally well as star managers, providing them not just with equal remuneration, but also all the perks of higher status: large office, staff, travel funds, etc.


Section from "No Silver Bullet"

Object-oriented programming.

Many students of the art hold out more hope for object-oriented programming than for any of the other technical fads of the day.

I am among them.

Mark Sherman of Dartmouth notes that we must be careful to distin- guish two separate ideas that go under that name: abstract data types and hierarchical types, also called classes.

The concept of the abstract data type is that an object's type should be defined by a name, a set of proper values, and a set of proper operations, rather than its storage structure, which should be hidden.

Examples are Ada packages (with private types) or Modula's modules.

Hierarchical types, such as Simula-67's classes, allow the definition of general interfaces that can be further refined by providing subordinate types.

The two concepts are orthogonal — there may be hierarchies without hiding and hiding without hierarchies.

Both concepts represent real advances in the art of building software.

Each removes one more accidental difficulty from the process, allowing the designer to express the essence of his design without having to express large amounts of syntactic material that add no new information content.

For both abstract types and hierarchical types, the result is to remove a higher-order sort of accidental difficulty and allow a higher-order expression of design.

Nevertheless, such advances can do no more than to remove all the accidental difficulties from the expression of the design.

The complexity of the design itself is essential; and such attacks make no change whatever in that.

An order-of-magnitude gain can be made by object-oriented programming only if the unnecessary underbrush of type specification remaining today in our programming language is itself responsible for nine- tenths of the work involved in designing a program product.

I doubt it.


I write a lot of Python code. Objects are absent in most of what I write. But the 10% or so of the time I use objects, it is because objects are the best way I can think of to structure the program.

So I don't think objects in Python are useless at all. I think Python is designed in a way that allows them to be used effectively if desired, or avoided all together when there's no apparent benefit.


I maintain a couple of Django projects for a customer of mine. I wrote almost 50% of the code base. Those two projects use very few classes, probably only the models. Everything else is a bag of functions organized in modules.

It's very similar to an Elixir /Phoenix project I'm working on for another customer. Modules and functions and a couple of dozens of GenServers that act as objects to maintain status where we need it.

And yet, I easily follow the OO model of Ruby of Rails. I feel it right in that context, probably because the rails of RoR are more well defined than the ones of Django. Of course when I script in Ruby I bet rarely write a class. A few functions, actually methods of Object, are all I need there.


I barely understand what this dudeson is aiming at. There is no need for OO-architecture if there is only one client for your services. He may not know that in python you can declare

    class Global:
      i=42
And then use it right away

    Global.i+=13
No inheritance-shit needed at all.


Slightly offtopic, what I liked about python OO is the smalltalkish vibe in relation to the syntactic overloads. Playing with dunder methods __or__ and all that is .. fun.

I rarely try this in other languages but in python it's a very natural thing.

ps: this is not a python fan comment. -- sent from my sml/nj


One good use for Python's OOP is operator overloading. Since Python doesn't have type signatures, you can't overload the "plus" operator with a free function like you can in C++. Writing numerical code without operator overloading is painful.


I'd argue that's not object-oriented programming.

You are using Python's class mechanism, but that's where the OOness ends. The types themselves are immutable value types, carrying no state and reacting to no messages, and they don't exploit a class hierarchy beyond having a common "Number" class. The "methods" don't even behave like methods, e.g. __add__(self, other) has special wiring so it obscures which side of the addition is "self".

That's very different than a classic OO scheme like a UI toolkit.


True!


I don't think a few code snippets prove the point. My conjecture is that any complex enough piece of code with essentially (not accidentally!) convoluted concepts will benefit from OOP more than any other style

Can we just appreciate the fact Python is such a Swiss Army Knife and allows for multi-paradigm approach?

Let's just not use low "bashing" of any paradigm as an excuse to neglect the design.

In some other thread someone mentioned lack of quality metrics. Well, this question has been asked previously, a recommended read from me: http://www.wirfs-brock.com/PDFs/How%20Designs%20Differ.pdf


Julia shines with this style, especially since the way modules compose works much better with multiple dispatch that crosses module boundaries. Sometimes a few surprises crop up, but by and large it's such a pleasure to work with.


Seeing is believing: could you translate an example to Julia?


That example in Julia would look the exact same since it's a very simple example with just one object/struct and only one version of each function (so it doesn't even need function overloading, let alone multiple dispatch), which is also why it might not be a good example against Python OOP, even though I'm inclined to agree, since it doesn't even need single dispatch (which if needed can be achieved by functools.singledispatch in Python).

The difference would start when you get later a different entity like for example an Organization. Now you have to either create a save_items_org (and check which one it is to choose the correct method) or you add the same check to save_items(Union[Client,Organization]...) and all other methods. If you had used the object then it would be client.save_items() or org.save_item(). Using functools.singledispatch or Julia, save_items(org...) and save_items(client...) will work like the object version and dispatch without trouble and keep as concise as the object version.

Now, the difference with Julia, is that Python's native dispatch capability ends here. If you now want a method to link Users with Users, Users with Organizations and Organizations with Organizations (sorry for the weird usecase, I don't want to add more types) you can just implement in Julia a link(Users, Organization), link (Organization, Organization), link(User, User) and any combination and whatever combination of User and Organization you receive it will dispatch to the correct version (while in Python you'll be forced to do the runtime check with an if to choose the implementation). For example the product operator (*) in Julia has around 300 implementations for all kinds of combinations of arguments, and the compiler makes it extremely fast by inferring and doing static dispatch.


@ddragon described it rather well. Some significant examples include JuMP (https://jump.dev) for mathematical optimization and how FluxML (Julia's "Keras"), Zygote (whole program automatic differentiation) and ODE solvers can work together easily to build Neural ODE solvers in relatively few lines of code. It is also why one person can write - https://github.com/yuehhua/GeometricFlux.jl - and benefit from the CUDA, FluxML, Zygote packages to get high performance.


OO is about managing state. Generally, stateless code is easier to test and maintain.

Around 2012 I started writing mostly functional code (in C++) to scratch my own maintainability itch.

I kept doing it through a long sojourn in JavaScript and now Python.

I haven’t looked back.


Why not have the client stored in a dictionary, it's just a url location, instead of having a dataclass? You don't even need a "construct_url" function.


I have worked both on OO and functional Python projects, and my conclusion is that any coding approach in Python is pointless if you are not a disciplined developer, and you don't care about refactoring your code once in a while.

For instance mature Flask apps usually have a mix of clean and barely readable modules. You can't force developers to care about whoever is going to replace them in the future. Some people care, some people don't.


Over the years I've learned when you think something is pointless, you just don't have enough experience to see why it's useful.

jphoward's comment is spot on.


> I've heard this style referred to as the bag-of-functions style.

If your main point of abstraction is the subroutine/procedure/function that modifies state, then it’s called procedural[0]. No need to rename it.

0: https://en.m.wikipedia.org/wiki/Procedural_programming


When Python was being conceived OOP was all the rage, with "everything is an object in Python" motto. Even if you want, you can't have bare structs, you need to create a class. At this point refusing to add a method to it and using external functions to modify its state is just a matter of taste - in many cases it makes sense, and in several it doesn't.


This is - as the kids put it - a very hot take.


>It encourages you to mutate.

I literally shouted "yes!" why throwing my fist in the air in solidarity. I will never understand how OOP became something such that warps everyone's minds. OOP is good for making clean and encapsulated types, beyond that the problems the author points out creep in encouraging bad behavior and obfuscating.


I found Python OO to be a super elegant and practical way to implement a common API for multiple databases.


One doesn't need OO for that tho, the module system applies just as well to solving that problem.


Module corresponds to a class, not to an object.

Afaik, you can't instantiate a module in python multiple times with different parameters.

In theory you would like to do this, but it is not supported in python, therefore you would use classes as poor man's modules.

    conn = import Database(host=..., user=...)


Both examples are OO. OO with and without syntactic sugar for methods of course, but both are 100% OO regardless. The implicit instance argument had just been replaced by an explicit instance argument.

Programming paradigms have nothing to do with syntactic sugar.


I agree with this assessment. I try to use OOP in Python but I found it useful in a very small amount of situations. Mostly it is just annoying to have the same function defined with (self, ...) and without when not using OOP.


I mean, it’s easy to say that OO is useless when you come up with a code example that doesn’t use features unique (or uniquely convenient) to OO languages. Throw in some inheritance or interfaces and it’ll be very different.


> You'll notice I put @dataclasss in the refactored code, these are fine - they're just record types. Python 5 will only have these, not 'normal' classes.

What is Python 5? Or is that supposed to be some inside joke?


Current version of Python is 3. It doesn't sound like there are any plans for Python 4[0], so I assume this is just a joke.

[0] https://twitter.com/gvanrossum/status/1306082472443084801


> OO in Python is mostly pointless

You could drop the "in Python" part and still be right.

Functional programming won in the end; Python programmers these days are mostly using Python because of pandas and similar packages.


Nonsense. Everything been a dictionary is incredibly important.


Largely I agree with the author's sentiment, but I'm not sure I totally agree with some of the alternatives. For example I think using an @lru_cache decorator over a `get_...()` function to return variables is much better at all than using a global var. In fact, it looks hackier if you ask me.

https://leontrolski.github.io/sane-config.html


The languages with full featured OO capability are slow to compile as interpreter.

Less features -> faster to compile and run


Isn't OO entirely pointless if you don't know what should be an object and what shouldn't?


More interesting than OO is why it was so popular for so long. Why is programming so faddish?


OOP is meant for re-use across different projects. Frameworks, or self-contained classes.


I am disappointed that this is not about writing OO Python in point free style.


One will appreciate OO once he/she saw a 2000-line code in a single file.


You don't need to use classes to break a big file into several smaller files.


> The difference between OOP and FP is the first argument of the functions


they should address these issues with significant OO syntax changes. let's call it python 4.


The author has just replaced one mechanism for OO with another one.

If you think you don't need OO, then prove it by doing this:

  # [0] is the root_url
  # [1] is the url_layout for formatting it: must contain {entity}.

  client_a = [ "https://client-a", "{root_url}/{entity}" ]
If you have client.root_url type stuff in your program, then it's using some degree of OO.

In he new program, this is obviously OOP:

  def construct_url(client: Client, entity: str) -> str:
      return client.url_layout.format(root_url=client.root_url,  entity=entity)
In the original program, inheritance was used to specialize the formatting behavior. That had to be somehow preserved.

How it was preserved was by aggregating a layout string and delegating the formatting task to its layout. (I smell a Design Pattern, but we are not doing OO!)

But that can be back-ported to the original program:

  class ApiClient:
    def __init__(self, root_url: str, session_cls: sessionmaker, layout: str):
        self.root_url = root_url
        self.session_cls = session_cls
        self.layout = layout

    def construct_url(self, entity: str) -> str:
        return self.layout.format(root_url=self.root_url,  entity=entity)

    def get_items(self, entity: str) -> List[Item]:
        resp = requests.get(self.construct_url(entity))
        resp.raise_for_status()
        return [Item(**n) for n in resp.json()["items"]]

    def save_items(self, entity: str) -> None:
        with scoped_session(self.session_cls) as session:
            session.add(self.get_items(entity))

  client_a = ApiClient("https://client-a", session_cls,
                       "{root_url}/{entity}")
  client_b = ApiClient("https://client-a", session_cls, "{root_url}/a/special/place/{entity}")

  client_a.save_items("bars")
Also, the author eliminated the session_cls member variable, which is only used in the save_items function. That also can be done to the original:

  class ApiClient:
    def __init__(self, root_url: str, layout: str):
        self.root_url = root_url
        self.layout = layout

    def construct_url(self, entity: str) -> str:
        return self.layout.format(root_url=self.root_url,  entity=entity)

    def get_items(self, entity: str) -> List[Item]:
        resp = requests.get(self.construct_url(entity))
        resp.raise_for_status()
        return [Item(**n) for n in resp.json()["items"]]

    def save_items(self, session_cls: sessionmaker, entity: str) -> None:
        with scoped_session(self.session_cls) as session:
            session.add(self.get_items(entity))

  client_a = ApiClient("https://client-a",
                       "{root_url}/{entity}")
  client_b = ApiClient("https://client-a",
                        "{root_url}/a/special/place/{entity}")

  client_a.save_items(session_cls, "bars")
In the end, all the author shows it that if all we need is minor variations in formatting some string by sticking values into a template string, we might want to do exactly that and not whip out inheritance.

But, note, that the original code can compile:

The following performs the formatting job using code that can compile:

  class ClientA(ApiClient):
      def construct_url(self, entity: str) -> str:
          return f"{self.root_url}/{entity}"
(If an f-string doesn't map to compiled code that doesn't interpret the format string at run-time, someone in Python-land has obvious work to do.)

This uses run-time interpretation of a dynamically supplied format string":

    def construct_url(self, entity: str) -> str:
        return self.layout.format(root_url=self.root_url,  entity=entity)
Though that could be optimized by, say, storing JIT-ted code into the layout string the first time format is called, it probably isn't.


Thems fightin words.


This guy makes a simple concept too complicated.

The main thing that's different about python OO is the method.

    class SomeClass
       def someMethod(self: SomeClass, someVar: int) -> int:
           return self.x + somVar
The explicit passing of context through "self" gives python methods combinator-like properties. Because of this, you can copy and paste the method and run it outside of the context of the class:

           def someMethod(self: SomeClass, someVar: int) -> int:
               return self.x + somVar
The above will work in global context. This makes python code significantly more refactorable than other OO languages. For example C++:

   class SomeClass {
       int someMethod(int someVar) {
             return this.x + someVar;
       }
   } 

If you copy and paste the method outside of the class:

       int someMethod(int someVar) {
           return this.x + someVar;
       }
It won't work.

The python way just allows the method to be easily refactored to operate on a generic type (caveat: you have to ignore typing which the python interpreter actually does). Given a self.x, any value for self will be valid so long as it contains an x. Python does this automatically and that is the main difference.

The main problem with OO is unfortunately still not resolved under either example. OO promotes context to follow logic around preventing the logic from being reused in other contexts. Ultimately, this is what's needed for the greatest modularity:

     def someMethod(x: int, someVar: int) -> int:
         return x + someVar
And although you can write the above with OO, the style ultimately promotes practices that steers programmers away it.


Surprised I had to scroll down this far to read this. Not just refactoring, also debugging. If there's a buggy method in a class somewhere, I can just copy the offending method into Jupyter, set self = a suitable instance of the class and start teasing apart what is going on line by line.


Why can't you just call the method on the instance directly? This is only useful for when a, is not quite the same type as the the class you're copying the method from.


Do you mean why not have the method be global in the first place? That's fine for simple pipelines, not ones that need to be flexible and have some kind of reusable interface. And I personally find it a bit annoying if you have to switch between class methods and global ones jumping in and out of the class to understand what's happening.


if the method is called "f" and it exists on "a" then you have access to "f" via "a.f()". It's redundant to call it via "f(a)".

If you're saying it's easier to edit "f" in the repl/notebook vs. on the class where it's defined, well that's a sort of a convenience factor, I wouldn't exactly call it a huge benefit overall as this is mostly a very specific use case.


[flagged]


I agree, OO can be useful and this article doesn't add much to the conversation of when stateful objects and inheritance are the best choice vs. functions and composition vs. some hybrid.

That said, please consider the community guidelines:

"When disagreeing, please reply to the argument instead of calling names."


In this case, I don't see how he is name calling. It's a bit harsh, but he isn't replying to an argument. He's clearly stating his own opinion on the matter. Hacker news shouldn't devolve into click bait titles of age old questions.

"Tabs vs spaces. A clear winner in productivity"

"Common programming paradigm somehow useless - Says this person"

And on and on. It's the mild version of shock journalism. It reaches exposure here, not because of the intellectual merit, but the contentious nature. Maybe not narcissistic, but certainly attention seeking.


Calling someone a "narcissist" is clearly name calling.

I did not find anything narcissistic in the OP's post. It is a controversial topic but nothing about it came off as narcissistic.


I won't presume to know the intention behind the original comment that is now removed, and I did feel it went a bit too far in how harsh it was. It would also be presumptive to know what the article writer had in mind as well.

That said, and this might be an age thing, I'm not sure. When I see these computer science concepts and constructs that were figured out half a decade ago or more, which have their particular usefulness and limitations. It is hard for me to believe intellectual honesty motivating those kinds of posts.

It's the same recipe. Take something trivial, well understood, and write a post with a title that suggests something highly opinionated, and possibly contrary to what the reader might have experienced themselves. Then explain how in some use case, it doesn't fit very well. Then, maybe for the kicks of it, walk back on the original "click-baity" statement that got people to read it to begin with. And wrap it up as if there some deeper or more profound knowledge to be gained.

I will admit, I haven't read the article, because my bullshit detector started ringing. When you've seen a hundred of these, you don't need to read one more. But I'll take a quick look now, for the sake of it. Yes, pretty much the same recipe as I mentioned. Perhaps slightly less pointless than I had expected.


Edit: Half a century, not decade. Also, here is another comment to downvote, I suppose.


> attention-seeking narcissist

Is not name calling?


I disagree. OO has been in use for decades, and I don't think the industry is experiencing benefits anywhere near its costs.


> Please, post your clickbait bullshit somewhere else.

This is needlessly insulting and aggressive. Flagged.


Hi, thanks for the comment. Given the number of _feelings_ in these threads, I'd really like to test my conjecture - if you'd 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", I'll do a follow-up blog post. Thanks!


> thanks for the comment

There is really no need to be respond nicely to someone hiding behind their anonymity and calling you names and throwing personal attacks at you. Your parent comment's account should simply be flagged and banned.


It is 2021. Criticizing OOP is preaching to the choir.


This. We teach coding, we start with logic, then move onto functions, then OOP, then patterns, then back to functional. Then it's up to the student or the business to make a choice. Saying that you don't think OOP is as good as functional programming is like, part of the course. Some people like it, some don't, most mix and match. 90% don't care enough to write blog posts.

The blog post feels like a short essay I may get from a student. shrug .... there's lots of nice research on the order in which to teach the elements of programming, lots of people say start with OOP and then do functions, as that's the more natural progression. Personally I disagree and teach it the way I described. But both are valid.


As someone without any particular bias towards one or the other it was interesting to see a simple example broken down in both styles and discussed.

So: Thanks for the article. Appreciated.




Join us for AI Startup School this June 16-17 in San Francisco!

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

Search: