I'd like to mention that despite OOP being in the title, I thought this book had a lot to teach that isn't specific to OOP architecture or OOP languages. Really, I think the star of the show is TDD and refactoring.
This is one of my favorite software development books of all time. It's the book that finally offered straight forward guidance and wisdom on how to properly utilize OOP language features.
I was so lucky to have run into poodr when I did. Early enough in my career to still feel like I didn't know anything, but with just enough experience to have encountered the problems she was addressing "in the wild." Absolutely formative for me I have no idea where I'd be without it. The only other book to even approach its impact for me is working effectively with legacy code.
Concur. I took Sandi's workshop based on the 99 Bottles book, in Ruby with a Ruby crowd, but was immediately able to apply it to Python programming.
Very helpful and clear thinking about refactoring out complexity—and not just refactoring for its own sake, but under the constraint that you want to move your program forward, add new functionality, etc. Refactoring with a direction, purpose, and direct payoff.
Maybe this is misguided, but it feels a bit to me (comparing the ruby and js versions for example) that this is using the same code examples in both, and neither are really typical of the sorts of code people in either language community would actually write?
The problem: How do you have examples that are simple enough for those learning the refactoring tricks and techniques to quickly grok and be able to work on, and not overly complicated by the kinds of "real-world complexities" that do regularly appear in "real-world code."
I had the same "this isn't realistic!" complaint when studying the book, but the examples nonetheless helped me see, practice, and adopt the techniques so that I could immediately apply them to the complex production examples I needed to improve. YMMV... but as a former skeptic, "trust the process." Walk that path an work those examples for 5 days, then see how you feel. I was already pretty skilled, including in complex refactorings, and it still leveled me up.
The site says, "Available in digital form only (epub, kepub, mobi, pdf). Includes separate books for JavaScript, PHP, Python, and Ruby languages, and beer and milk beverages."
There is no mention of needing special software to read them, so I think it's safe to guess that there is no DRM. And it's sold directly by the author. Publishers are generally the ones who insist on DRM. It would not surprise me if there was watermarking, but that is not DRM.
While I understand the complaints against OOP, I highly recommend this book to anyone working in an environment where they're working with OOP languages/frameworks. There are plenty of Ruby/Rails shops out there still. At the very least I love the mentality that this book teaches and often recommend Tidy First by Kent Beck at the same time.
I have the book and don't speak Ruby at all. Nevertheless, is so we'll written, that you can take the lessons and apply them in any language with virtual polymorphism. A Python version of the book means bigger audience, to the benefit of the trade.
I’m gonna buy the book but I prefer composition over OOP. I prefer to have an init that takes some params where those params are fully baked clients of whatever services I need and then the class just uses them as needed. I don’t see a lot of value in having a Python class that might have a few or more classes that it extends where all the functions from all the classes crowd up the classes namespace.
Class Foo.__init__(self, db, blob_storage, secrets_manager, …)
Why on earth do you put composition and OOP as opposing techniques? Composition is just one more technique in the OOP toolbox and there is nothing in OOP that mandates an inheritance based architecture.
The common toolkits today (spring boot, google guice, etc) are much more focused on composition over inheritance, by injecting arguments and implementing pure interfaces rather than extending base classes. Older legacy Java frameworks and bad teachers are more at fault than the Java language itself IMO.
There are some valid cases where extends really can help, and IMO the language would feel limited without it. Maybe if the language designers had their time back they could have taken an approach like Golang with nested structs and syntactic sugar for calling their attributes/methods.
The main reason I see new devs opt for extends, is because that was 99% of the content in their Java 101 programming course, not because it exists in the language. Imagine how many more `friend`s we would have in cpp if that was crammed down everyone's throats? :)
Very true, in Java, at least in the last 20 years, inheritance is de-facto deprecated, all new bits and bolts like enums, annotations, lambdas or records do not support inheritance.
class Delegated implements Base {
final Base b;
public Delegated(Base b) { this.b = b; }
@Override
public void printMessage() { b.printMessage(x); }
@Override
public void printMessageLine() { b.printMessageLine(x); }
OT1H, yes, sane people using IJ would just alt-Insert, choose delegate to, and move on with life. But those misguided folks using VS Code, vim, or a magnetized needle and a steady hand would for sure find delegating to a broader interface to be a huge PITA
Then you're going to be pleasantly surprised, because composition is actually a genuine OOP technique and Sandi Metz advocates for exactly this kind of sane OOP focused on encapsulation and objects making sense, instead of masturbating with class hierarchies.
But I've read the book, and her solution to the "bottles of beer" problem involves encoding all the logic into an elaborate class hierarchy!
I'm not rabidly anti-OOP, but the point at which I turn against it is when the pursuit of "properly" modelling your domain with objects obscures the underlying logic. I feel like this book reaches that point. This is her stance on polymorphism:
> As an OO practitioner, when you see a conditional, the hairs on your neck should stand up. Its very presence ought to offend your sensibilities. You should feel entitled to send messages to objects, and look for a way to write code that allows you to do so. The above pattern means that objects are missing, and suggests that subsequent refactorings are needed to reveal them.
Absolutely not! You should not, as a rule, be replacing conditional statements with polymorphic dispatch. Polymorphism can be a useful tool for separating behaviour into modules, but that trade-off is only worthwhile when the original behaviour is too bloated to be legible as a unit. I don't see an awareness of that trade-off here. That's my problem.
Well, the entire book is focused on solving a laughably trivial problem, any solution is going to feel excessive. The elaborate object hierarchy that she uses would obviously feel different in real world, complicated domain.
I found the excerpt in the book and I don't see her mentioning traditional class-level polymorphism (of the Java kind) anywhere around it. What SM generally advocates for is using OBJECT hierarchies to implement behaviors and encapsulate logic, the objects usually being instances of simple (and final!) free-standing classes. All thanks to the ability of any Ruby object to send messages to (call methods of) a different object, without knowing or caring about its type or origin, and the other object supplying the behavior without having to check its own type (because the correct behavior is the only one that the object, being a specialized object, even knows). This is done at runtime and is called "composition" (as in "composition over inheritance") and is different from using pre-built CLASS hierarchies to implement behaviors, aka "inheritance" (as in "composition over inheritance"). In Ruby, composition is Dog.new(Woofing.new), whereas using inheritance (class hierarchies) is Dog.new after you've done "include Woofing" inside the class.
I don't know Python well, but it seems like the person in the top-level comment expressed their dislike for the second kind.
I should clarify that the elaborate class hierarchy in the book is inheritance-based. When there's one bottle of beer on the wall, she instantiates `new BottleNumber1()`, which inherits from `BottleNumber` and overrides the method `container()` to return the singular "bottle" rather than the plural "bottles" which the base class's `BottleNumber::bottle()` would return (in the javascript edition, at least).
Inheritance is not a banned practice. It should just be your second choice when there's a better path through composition. Do you see one here? Interfacing to address Liskov's substitution is a perfectly valid reason to "extend" in many older languages, since their inheritance and interface mechanism are conflated. The way it's done here is fine. Single parent, shallow and only for the purpose of overriding and specializing.
Also, the real issue SM is trying to address is actually single responsibility and open-close, which aren't just an OO thing.
As you'll design your own libraries' functional APIs, you'll have to decide whether to publish fewer functions, with a rich set of behaviors controlled through the passing of (many) parameters (and conditionals in the function body); or take a finer grain approach with multiple functions that abide as much as possible to single responsibility and only take few input about the state. I'd bet that the former will quickly raise complaints, by both maintainers and users alike, because of all the ifs and buts typically associated with it.
For the same reasons, you don't want your methods to have divergent behavior based on state. You want multiple types.
Maybe it should be. Go and Rust do not provide implementation inheritance, and I think that's for the best. Few language features have led to so much spaghetti code.
> It should just be your second choice when there's a better path through composition. Do you see one here?
I don't think this logic should be split over a graph of objects at all. This is highly cohesive code; it shouldn't be factored apart. If Metz made it clear that this was an example just for the purposes of illustration, that'd be one thing. However, the stance taken is "this is good code, and you should try to write all your code like this." It's not, though, and you shouldn't.
> Maybe it should be. Go and Rust do not provide implementation inheritance, and I think that's for the best. Few language features have led to so much spaghetti code.
I agree, but there are reasons inheritance becomes problematic. Just throwing the baby is not helpful. Go, Elixir, Rust are all relatively young languages and although they did away with inheritance, they make use of interfaces/protocols/traits, hinting at that idea very much worth preserving. That is, regardless of which language you work with, if you have access to facilities that can make the concept of interface work decently, use them. Older languages (like Ruby, Python, Java) tended to use the inheritance mechanism to accomplish the same.
> If Metz made it clear that this was an example just for the purposes of illustration
She did. Perhaps read the book's introduction. She explains why she wrote a whole book that uses a banal example as a teaching tool to illustrate how SOLID should map to real world OOP. Her painstakingly going through refactoring and providing reasons for each decisions is not, in fact, to teach us to write code that generates a song.
> I don't think this logic should be split over a graph of objects at all [...] However, the stance taken is "this is good code, and you should try to write all your code like this." It's not, though, and you shouldn't.
Fair enough, but these are your very own and very isolated opinions. As an OO skeptic myself, I'll side with others like me, along with the FP enthusiasts, who originally approached this book with reserve, but came out with positive impressions.
Regarding the object graph, whether in Go, Elixir, or Rust, "No Bottle", "One Bottle", "Six Pack", and "Many Bottles" are distinct things and should be represented accordingly. Conflating them is a violation of principles that also apply in those languages. A very common, yet equally banal example that should put the debate to rest about this is the trifecta: Shape.Area(), Square.Area(), and Circle.Area(). Of course, it remains the programmer's prerogative whether they indulge with if/else in their implementation, but it should still be considered the exception rather than the rule.
Oh I very much agree, I love interface polymorphism.
> "No Bottle", "One Bottle", [etc] are distinct things and should be represented accordingly
I completely disagree. The problem is to generate a 100-line poem, line by line. Our challenge is to express the rules which govern how to derive a line from its line number in the clearest way possible. Creating an ontology for bottle types makes that overcomplicated. What if there were a special rule for bottle numbers divisible by 3? What if there were a special rule for doubled digits? Would we need to create a DivisibleByThreeWithDoubleDigitsBottle using multiple inheritance to write a line for 66 bottles? Why?
The big gift of OOP is interface polymorphism. The big curse of OOP is the philosophy that objects should model "real distinct things." Object-oriented domain modelling often causes more problems than it solves. Clear dataflow, cohesively represented logic, and loosely coupled modules are much more important than some philosophical notion about what sorts of ideas ought to be given associated objects. That's how you get Joe Armstrong's "gorilla holding the banana and the entire jungle."
What’s funny is I did composition for a take home project when interviewing at a place and they said the approach was too complicated and failed me for it. They wanted multiple inheritance instead. Fair enough. Their codebase probably had a lot of it and my not showing it probably told them I didn’t understand it.
>I’m gonna buy the book but I prefer composition over OOP.
The GoF book (the design patterns book) says in a page right near the start, "Prefer composition over inheritance", in the middle of an otherwise blank page, presumably to emphasize the importance of that advice.
As others have replied, composition is one technique you can use in OOP, not something that is the opposite of OOP.
You can also use composition in non-OOP procedural languages like C, by having a struct within a struct.
These are complementary not contradictory ideas. One of the principal takeaways from the Ruby edition (and many of Sandi Metz’s conference talks) is undoubtedly a mindset of, and techniques for, writing compositional OO code.
It definitely took me quite a bit of time from I joined HN until I learned that if you edit your submission title then you can override the automatic edits that HN makes to the title you originally submitted.
And I would guess that likewise there are still a lot of people that don’t know this.
Also, sometimes one might not realize that the title got changed until it’s too late to edit the title of the post.
That would be prone to title injection by malicious pages. ;)
More seriously, HN should instead display an instruction after submitting, informing that the title was changed and that the submitter should check the change and edit if necessary. The issue is that most submitters either don’t seem to notice that the title changed or don’t hit on the idea that they can edit it after the fact.
There's already a bottle web framework [1]. It came out around the same time as Flask I think. It might have even started as a spoof of Flask even? Or vice versa?
It’s supposed to kill clickbait titles. In practice it just randomly mangles titles. I don’t get why they don’t remove the feature and instead rely on flagging.
OOP is an industry of its own which generates a ton of incidental complexity. See "Object-Oriented Programming is Bad" by Brian Wills (https://www.youtube.com/watch?v=QM1iUe6IofM) and most of Rich Hickey's excellent videos, especially his keynote at Rails Conf 2012 where he basically told the Ruby crowd they're doing it wrong (https://www.youtube.com/watch?v=rI8tNMsozo0).
> OOP is an industry of its own which generates a ton of incidental complexity.
I think you're confusing "OOP is used in projects and I've seen accidental complexity in projects" with "OOP generates accidental complexity".
The truth of the matter is that developers create complexity. It just so happens that the vast majority use OOP.
I challenge you to a) start by stating what you think OOP is, b) present any approach that does not use OOP and does not end up with the same problems, if not worse.
a) I'm not sure what OOP is, and it doesn't seem like the people who tout it are either. I'm sure someone would look at code I think is good and call it OOP, and someone who wouldn't. It is so many buzzwords old at this point that using it is more a label of a viewpoint than a coding style. Combined with the book's apparent focus on TDD and carefully selecting names, it zeros me precisely in on a set of people I have worked with over the years. I don't, as a rule, like the code those people generate.
b) The best style is no style, or at least pick a more recently popular dogma like FP, at least it gets you easy/safe parallelism in exchange for throwing some of the tools out of your toolbox.
The core of OOP is encapsulation (into objects) and polymorphic interfaces. You program against interfaces with well-defined contracts. Implementation details are encapsulated behind those interfaces. The same interface can have different implementations. The same interface-typed variable can point to different implementations at different times within the same program execution. The caller who invokes operations on an interface is supposed to not care about the implementation details behind it, and thus be decoupled from them. Interfaces can have an inheritance/subtyping relationship. (Implementations may as well, but that’s optional.) This enables abstracting over the commonalities of several related interfaces without having to introduce an adapter/proxy object. That’s basically it.
After making those interfaces, the moment you need to do something that breaks those interfaces, you suddenly have a headache. I wrote a program in college that daemonizes by being an instance of a daemon class. Years later, people wanted the option to not daemonize. With C, this would be easy. With the way that I did things according to OOP principles in C++, I need to delete all of the code related to starting the program and write it from scratch.
You could say that I just did not do it right, but that is the problem. You need to know precisely what the future will want to do it right and that is never possible to know in advance. OOP encapsulation is heavily overrated. There are a ton of headaches in C++ that do not exist in C because C does not try to do these things. Ever hear of the diamond problem? It does not exist in C. Nonsensical types that span multiple lines when trying to figure out why there is a type error? Not an issue in C either.
C++ was advertised as reducing complexity, but in reality, it that encourages developers to drown themselves in complexity. If it were not for C never gaining a widespread STL equivalent, C++ would be far less popular. Sun Microsystems did make libuutil to provide such facilities, but sadly, it never caught on outside of Sun Microsystems technologies. The BSD sys/queue.h is the closest we have to it, but it is only for lists, and we need trees too to get a good equivalent to the C++ STL. That said, libuutil is available through ZFS, so it is not that people cannot adopt its awesome AVL tree implementation on other platforms. It is just that people do not know about it.
> You could say that I just did not do it right, but that is the problem. You need to know precisely what the future will want to do it right and that is never possible to know in advance.
Every time inheritance causes a headache, you can call it a misuse of inheritance, but that is only obvious after you have been to the future.
Misuse of inheritance is often the biggest generator of criticism of inheritance, sadly.
People use the wrong tool for the job, or use it incorrectly, and then blame the tool. It's like using a hammer to play drums, obliterating the drum set, then ranting against hammers.
OOP is a tool that causes people to hang themselves with it by design. The only way to avoid misusing it is to be to the future to see the impact of every single decision involving it to make only the ones that do not cause problems.
In the real world, where many people of varying backgrounds and skill levels are editing the same code, if the OOP method becomes a mess, why wouldn’t the functional approach also become a mess? I think that is more the point OP was making. In a vacuum with a single perfect Adonian programmer, seems like the OOP and functional approaches would becomes the same level of maintainable, because we’re in a vacuum of perfection anyway.
In my experience, the mess created using OOP is harder to untangle than the mess created using a functional approach. With the latter, it's simpler to replace a different function for any part of the logic, and the data is always just data; whereas with OOP the method is usually tied up with shared state and other functions.
> In my experience, the mess created using OOP is harder to untangle than the mess created using a functional approach.
OP complained about accidental complexity, not subjective takes on how hard it is to refactor code.
Even so, anyone who has any cursory experience with TypeScript projects that follow a functional style can tell you without any doubt whatsoever that functional style is incomparably harder to refactor than any "enterprise-grade" OOP.
> Even so, anyone who has any cursory experience with TypeScript projects that follow a functional style can tell you without any doubt whatsoever that functional style is incomparably harder to refactor than any "enterprise-grade" OOP.
My experience differs greatly. One can see functional style has taken off in the TS world, particularly with the popularity of React, so I suspect I'm not alone.
I suspect "functions and structs" here meant "functions and structs, separately, instead of gluing functions together with structs into unholy amalgams". Basically, Wirth's "Algorithms + Data Structures = Programs" idea.
Compare e.g. to "What should a language have instead of Lua-like tables? Maps and vectors" — "But that's what a table is, so your alternative to tables is... tables?"
>> A. OOP as practically implemented for the last 25 years is glueing functions to state
> I see you opt to go with a huge amount of handwaving over the question.
I think the question was answered pretty clearly. You can't ask for an opinion ( "what do you think" ) and then criticize the response as 'hand-waving'.
OOP is a mental crutch that breaks complex problems down into a easy mentally discoverable world/domain model with objects that have a life of their own, that is capable to derive correct results to complex problems via the relations of the objects.
Meanwhile its creators can not hold the whole complexity in mind (often barely in spec) and still can produce a artifact that produces correct results.
> OOP is an industry of its own which generates a ton of incidental complexity.
And that "ton" is still miniscule compared to front-end development which almost completely eschews OOP and has 10x more incidental complexity.
I guess my point is that, while OOP's incidental complexity is large, it's still insignificant compared to other technology stacks which developers are showing a great appetite for anyway. Things like "incidental complexity" is irrelevant to developers anyway, today, at the tail end of 2024.
IOW, OOP introduces significantly less $BAD_THING, when the clear majority of developers don't even care about the quantity of $BAD_THING in the first place, making the whole "should we use OOP" argument moot.
Doesn't matter if you use it or not, the extra introduced incidental complexity is still going to be insignificant due to the complexity load of the entire project, more so in front-end.
Hence, there's no point in having the argument in the first place.
> OOP is an industry of its own which generates a ton of incidental complexity.
Code in any form can generate a ton of incidental complexity. The issue isn't the tool rather than the education to properly wield those tools. Especially when you introduce the team dynamic where everyone has varying understandings of what is being built and how it should be built.
As it pertains to Python in particular I think OOP is great for libraries but of limited usefulness at the application layer. Things like pytorch's nn.module IMO is a great abstraction, but every time I've tried to map some concrete business concept to an OOP construct I've regretted it.
I believe in mechanical empathy. I think developers should - win performance is a concern - think about how their code ends up on the CPU, what ends up on the heap, how caches are used, and so on.
But for most Python developers, and Ruby developers, it's more important, in more use cases, to have clear and readable code that is easily maintainable.
I've met Sandi, I've gone through her Ruby books, I recommend her teaching and her books to others. I don't see the problem.
If you need ASM, use ASM. If you're on a team doing OOP, maybe learn about OOP.
Look at the fastest linting/build tools/runtimes for JavaScript. They are mostly not written in JavaScript, because JavaScript is slow and no amount of hotspot profiling/optimization will fix that. This is also why Python's core data types for dictionaries and so on are not implemented in Python.
You can also look at any microbenchmark between pure Python and a native language of your choice, even one that uses GC like OCaml or Go, and unless the Python code has been written so that it spends all its time calling C code the way you sometimes can with regex-heavy code, Python will generally lose by a varying margin. These micro inefficiencies multiply at every level hundreds of times over to create needlessly slow, inefficient applications.
The truth is I can link benchmarks, case studies of app rewrites from Ruby to Go or something comparable, measurements about the results of using NIFs in Erlang/Elixir, etc. all day long but you'll ignore all of them.
A lot of time (most?) writing software isn't about writing software for the pleasure of doing so, but for building something that helps generate revenue.
For businesses, having a highly optimized web application that takes a long time to develop and doesn't allow for quick additions of new features is not worth the cost. Instead, they can have a poorly optimized web application with way more features and a faster feature production because Python/Ruby/Javascript are more approachable than other native languages.
I'm glad my comment noting the usage of best practices when it comes to writing Python was downvoted. Says a lot about the immature user base around here.
I'd like to mention that despite OOP being in the title, I thought this book had a lot to teach that isn't specific to OOP architecture or OOP languages. Really, I think the star of the show is TDD and refactoring.
For a short intro to Sandi's style and approach, I always recommend this 35min talk: https://youtu.be/OMPfEXIlTVE?si=Ird6t8uDN86T06Y7
Aside from any specifically educational content, as a talk it is fantastic - funny, smart, well put together.
This is one of my favorite software development books of all time. It's the book that finally offered straight forward guidance and wisdom on how to properly utilize OOP language features.
I'm very happy to see it out for Python!
Sandi's earlier book, POODR, was also great. While it is focused on Ruby, most of the advice applies more broadly.
Reading these two really helped me understand just how impoverished the concept of OOP has become by C++ and Java, from its Smalltalk roots.
I was so lucky to have run into poodr when I did. Early enough in my career to still feel like I didn't know anything, but with just enough experience to have encountered the problems she was addressing "in the wild." Absolutely formative for me I have no idea where I'd be without it. The only other book to even approach its impact for me is working effectively with legacy code.
Avdi Grimm preaches a similar gospel.
Concur. I took Sandi's workshop based on the 99 Bottles book, in Ruby with a Ruby crowd, but was immediately able to apply it to Python programming.
Very helpful and clear thinking about refactoring out complexity—and not just refactoring for its own sake, but under the constraint that you want to move your program forward, add new functionality, etc. Refactoring with a direction, purpose, and direct payoff.
Maybe this is misguided, but it feels a bit to me (comparing the ruby and js versions for example) that this is using the same code examples in both, and neither are really typical of the sorts of code people in either language community would actually write?
The problem: How do you have examples that are simple enough for those learning the refactoring tricks and techniques to quickly grok and be able to work on, and not overly complicated by the kinds of "real-world complexities" that do regularly appear in "real-world code."
I had the same "this isn't realistic!" complaint when studying the book, but the examples nonetheless helped me see, practice, and adopt the techniques so that I could immediately apply them to the complex production examples I needed to improve. YMMV... but as a former skeptic, "trust the process." Walk that path an work those examples for 5 days, then see how you feel. I was already pretty skilled, including in complex refactorings, and it still leveled me up.
> The 2nd Edition contains 3 new chapters and is about 50% longer than the 1st.
I've never really had the problem that I've read an OOP text and felt "this was too short".
Is the book DRM free? Sorry to be this paranoid, but you cannot be sure today.
You could check?
The site says, "Available in digital form only (epub, kepub, mobi, pdf). Includes separate books for JavaScript, PHP, Python, and Ruby languages, and beer and milk beverages."
There is no mention of needing special software to read them, so I think it's safe to guess that there is no DRM. And it's sold directly by the author. Publishers are generally the ones who insist on DRM. It would not surprise me if there was watermarking, but that is not DRM.
I checked it but found only the same pages as you. This is the reason I asked.
Yes, I have one. It's DRM free.
Yes, it is.
[dead]
While I understand the complaints against OOP, I highly recommend this book to anyone working in an environment where they're working with OOP languages/frameworks. There are plenty of Ruby/Rails shops out there still. At the very least I love the mentality that this book teaches and often recommend Tidy First by Kent Beck at the same time.
Previous Discussion
Bottles of OOP - https://news.ycombinator.com/item?id=12129821 - July 2016 (71 comments)
that was not for the Python version
I have the book and don't speak Ruby at all. Nevertheless, is so we'll written, that you can take the lessons and apply them in any language with virtual polymorphism. A Python version of the book means bigger audience, to the benefit of the trade.
I’m gonna buy the book but I prefer composition over OOP. I prefer to have an init that takes some params where those params are fully baked clients of whatever services I need and then the class just uses them as needed. I don’t see a lot of value in having a Python class that might have a few or more classes that it extends where all the functions from all the classes crowd up the classes namespace.
Class Foo.__init__(self, db, blob_storage, secrets_manager, …)
Instead of class Foo(DB, BlobStorer, SecretsMgr)
Etc
Why on earth do you put composition and OOP as opposing techniques? Composition is just one more technique in the OOP toolbox and there is nothing in OOP that mandates an inheritance based architecture.
Mainstream OOP languages (looking at you Java) have failed to make composition as convenient as inheritance.
The common toolkits today (spring boot, google guice, etc) are much more focused on composition over inheritance, by injecting arguments and implementing pure interfaces rather than extending base classes. Older legacy Java frameworks and bad teachers are more at fault than the Java language itself IMO.
I take your point, though having `extends` as a first-class language feature surely encouraged that culture and approach in older frameworks right?
There are some valid cases where extends really can help, and IMO the language would feel limited without it. Maybe if the language designers had their time back they could have taken an approach like Golang with nested structs and syntactic sugar for calling their attributes/methods.
The main reason I see new devs opt for extends, is because that was 99% of the content in their Java 101 programming course, not because it exists in the language. Imagine how many more `friend`s we would have in cpp if that was crammed down everyone's throats? :)
Very true, in Java, at least in the last 20 years, inheritance is de-facto deprecated, all new bits and bolts like enums, annotations, lambdas or records do not support inheritance.
So you have to use composition.
How is composition inconvenient?
Contrast the Java way
with the Kotlin way https://kotlinlang.org/docs/delegation.html#overriding-a-mem...OT1H, yes, sane people using IJ would just alt-Insert, choose delegate to, and move on with life. But those misguided folks using VS Code, vim, or a magnetized needle and a steady hand would for sure find delegating to a broader interface to be a huge PITA
How is composition in Java inconvenient?
Then you're going to be pleasantly surprised, because composition is actually a genuine OOP technique and Sandi Metz advocates for exactly this kind of sane OOP focused on encapsulation and objects making sense, instead of masturbating with class hierarchies.
But I've read the book, and her solution to the "bottles of beer" problem involves encoding all the logic into an elaborate class hierarchy!
I'm not rabidly anti-OOP, but the point at which I turn against it is when the pursuit of "properly" modelling your domain with objects obscures the underlying logic. I feel like this book reaches that point. This is her stance on polymorphism:
> As an OO practitioner, when you see a conditional, the hairs on your neck should stand up. Its very presence ought to offend your sensibilities. You should feel entitled to send messages to objects, and look for a way to write code that allows you to do so. The above pattern means that objects are missing, and suggests that subsequent refactorings are needed to reveal them.
Absolutely not! You should not, as a rule, be replacing conditional statements with polymorphic dispatch. Polymorphism can be a useful tool for separating behaviour into modules, but that trade-off is only worthwhile when the original behaviour is too bloated to be legible as a unit. I don't see an awareness of that trade-off here. That's my problem.
Well, the entire book is focused on solving a laughably trivial problem, any solution is going to feel excessive. The elaborate object hierarchy that she uses would obviously feel different in real world, complicated domain.
I found the excerpt in the book and I don't see her mentioning traditional class-level polymorphism (of the Java kind) anywhere around it. What SM generally advocates for is using OBJECT hierarchies to implement behaviors and encapsulate logic, the objects usually being instances of simple (and final!) free-standing classes. All thanks to the ability of any Ruby object to send messages to (call methods of) a different object, without knowing or caring about its type or origin, and the other object supplying the behavior without having to check its own type (because the correct behavior is the only one that the object, being a specialized object, even knows). This is done at runtime and is called "composition" (as in "composition over inheritance") and is different from using pre-built CLASS hierarchies to implement behaviors, aka "inheritance" (as in "composition over inheritance"). In Ruby, composition is Dog.new(Woofing.new), whereas using inheritance (class hierarchies) is Dog.new after you've done "include Woofing" inside the class.
I don't know Python well, but it seems like the person in the top-level comment expressed their dislike for the second kind.
I should clarify that the elaborate class hierarchy in the book is inheritance-based. When there's one bottle of beer on the wall, she instantiates `new BottleNumber1()`, which inherits from `BottleNumber` and overrides the method `container()` to return the singular "bottle" rather than the plural "bottles" which the base class's `BottleNumber::bottle()` would return (in the javascript edition, at least).
Inheritance is not a banned practice. It should just be your second choice when there's a better path through composition. Do you see one here? Interfacing to address Liskov's substitution is a perfectly valid reason to "extend" in many older languages, since their inheritance and interface mechanism are conflated. The way it's done here is fine. Single parent, shallow and only for the purpose of overriding and specializing.
Also, the real issue SM is trying to address is actually single responsibility and open-close, which aren't just an OO thing.
As you'll design your own libraries' functional APIs, you'll have to decide whether to publish fewer functions, with a rich set of behaviors controlled through the passing of (many) parameters (and conditionals in the function body); or take a finer grain approach with multiple functions that abide as much as possible to single responsibility and only take few input about the state. I'd bet that the former will quickly raise complaints, by both maintainers and users alike, because of all the ifs and buts typically associated with it.
For the same reasons, you don't want your methods to have divergent behavior based on state. You want multiple types.
> Inheritance is not a banned practice.
Maybe it should be. Go and Rust do not provide implementation inheritance, and I think that's for the best. Few language features have led to so much spaghetti code.
> It should just be your second choice when there's a better path through composition. Do you see one here?
I don't think this logic should be split over a graph of objects at all. This is highly cohesive code; it shouldn't be factored apart. If Metz made it clear that this was an example just for the purposes of illustration, that'd be one thing. However, the stance taken is "this is good code, and you should try to write all your code like this." It's not, though, and you shouldn't.
> Maybe it should be. Go and Rust do not provide implementation inheritance, and I think that's for the best. Few language features have led to so much spaghetti code.
I agree, but there are reasons inheritance becomes problematic. Just throwing the baby is not helpful. Go, Elixir, Rust are all relatively young languages and although they did away with inheritance, they make use of interfaces/protocols/traits, hinting at that idea very much worth preserving. That is, regardless of which language you work with, if you have access to facilities that can make the concept of interface work decently, use them. Older languages (like Ruby, Python, Java) tended to use the inheritance mechanism to accomplish the same.
> If Metz made it clear that this was an example just for the purposes of illustration
She did. Perhaps read the book's introduction. She explains why she wrote a whole book that uses a banal example as a teaching tool to illustrate how SOLID should map to real world OOP. Her painstakingly going through refactoring and providing reasons for each decisions is not, in fact, to teach us to write code that generates a song.
> I don't think this logic should be split over a graph of objects at all [...] However, the stance taken is "this is good code, and you should try to write all your code like this." It's not, though, and you shouldn't.
Fair enough, but these are your very own and very isolated opinions. As an OO skeptic myself, I'll side with others like me, along with the FP enthusiasts, who originally approached this book with reserve, but came out with positive impressions.
Regarding the object graph, whether in Go, Elixir, or Rust, "No Bottle", "One Bottle", "Six Pack", and "Many Bottles" are distinct things and should be represented accordingly. Conflating them is a violation of principles that also apply in those languages. A very common, yet equally banal example that should put the debate to rest about this is the trifecta: Shape.Area(), Square.Area(), and Circle.Area(). Of course, it remains the programmer's prerogative whether they indulge with if/else in their implementation, but it should still be considered the exception rather than the rule.
> they make use of interfaces/protocols/traits
Oh I very much agree, I love interface polymorphism.
> "No Bottle", "One Bottle", [etc] are distinct things and should be represented accordingly
I completely disagree. The problem is to generate a 100-line poem, line by line. Our challenge is to express the rules which govern how to derive a line from its line number in the clearest way possible. Creating an ontology for bottle types makes that overcomplicated. What if there were a special rule for bottle numbers divisible by 3? What if there were a special rule for doubled digits? Would we need to create a DivisibleByThreeWithDoubleDigitsBottle using multiple inheritance to write a line for 66 bottles? Why?
The big gift of OOP is interface polymorphism. The big curse of OOP is the philosophy that objects should model "real distinct things." Object-oriented domain modelling often causes more problems than it solves. Clear dataflow, cohesively represented logic, and loosely coupled modules are much more important than some philosophical notion about what sorts of ideas ought to be given associated objects. That's how you get Joe Armstrong's "gorilla holding the banana and the entire jungle."
What’s funny is I did composition for a take home project when interviewing at a place and they said the approach was too complicated and failed me for it. They wanted multiple inheritance instead. Fair enough. Their codebase probably had a lot of it and my not showing it probably told them I didn’t understand it.
I used to work at a flask shop that did views with 3-5 or more inherited classes. Nobody could really follow how everything worked. It was insane.
Anyways yeah give me composition and flat classes all day long.
>I’m gonna buy the book but I prefer composition over OOP.
The GoF book (the design patterns book) says in a page right near the start, "Prefer composition over inheritance", in the middle of an otherwise blank page, presumably to emphasize the importance of that advice.
As others have replied, composition is one technique you can use in OOP, not something that is the opposite of OOP.
You can also use composition in non-OOP procedural languages like C, by having a struct within a struct.
https://www.google.com/search?q=can+you+have+nested+structs+...
These are complementary not contradictory ideas. One of the principal takeaways from the Ruby edition (and many of Sandi Metz’s conference talks) is undoubtedly a mindset of, and techniques for, writing compositional OO code.
I have seen quite a few digital books at this price point now. Personally I feel it is quite high, but I assume I am in the minority?
HN's automatic title editing strikes again. The title of this submission should presumably be: "99 Bottles of OOP now available in Python".
Note to anyone who submits an article: If the title gets mangled like this, edit it.
It definitely took me quite a bit of time from I joined HN until I learned that if you edit your submission title then you can override the automatic edits that HN makes to the title you originally submitted.
And I would guess that likewise there are still a lot of people that don’t know this.
Also, sometimes one might not realize that the title got changed until it’s too late to edit the title of the post.
I wish HN could just take the url and then fetch the title of the page.
That would save quite a hassle, especially on mobile.
That would be prone to title injection by malicious pages. ;)
More seriously, HN should instead display an instruction after submitting, informing that the title was changed and that the submitter should check the change and edit if necessary. The issue is that most submitters either don’t seem to notice that the title changed or don’t hit on the idea that they can edit it after the fact.
In this case, the missing '99' isn't a typo, it's deliberate by HN to eliminate clickbaity headlines like '7 Ways To Be A Better Programmer.'
> edit it.
How???
Same as with comments, submissions have an edit link that works for an hour or so after submission.
You can only edit the submission title. If you want to edit the link, email the mods.
Here I was assuming bottles might fit nicely into the Flask ecosystem.
There's already a bottle web framework [1]. It came out around the same time as Flask I think. It might have even started as a spoof of Flask even? Or vice versa?
[1] https://bottlepy.org/
IIRC, it's vice versa. Bottle predates Flask by a few years I think.
Edit to add: Bottle's own FAQ says so: https://bottlepy.org/docs/dev/faq.html#what-about-flask
Why does HN automatically edit titles??
It’s supposed to kill clickbait titles. In practice it just randomly mangles titles. I don’t get why they don’t remove the feature and instead rely on flagging.
> HN's automatic title editing strikes again.
Demonstrating for the umpteenth time that automated clairvoyance is an idiotic idea.
OOP is an industry of its own which generates a ton of incidental complexity. See "Object-Oriented Programming is Bad" by Brian Wills (https://www.youtube.com/watch?v=QM1iUe6IofM) and most of Rich Hickey's excellent videos, especially his keynote at Rails Conf 2012 where he basically told the Ruby crowd they're doing it wrong (https://www.youtube.com/watch?v=rI8tNMsozo0).
> OOP is an industry of its own which generates a ton of incidental complexity.
I think you're confusing "OOP is used in projects and I've seen accidental complexity in projects" with "OOP generates accidental complexity".
The truth of the matter is that developers create complexity. It just so happens that the vast majority use OOP.
I challenge you to a) start by stating what you think OOP is, b) present any approach that does not use OOP and does not end up with the same problems, if not worse.
a) I'm not sure what OOP is, and it doesn't seem like the people who tout it are either. I'm sure someone would look at code I think is good and call it OOP, and someone who wouldn't. It is so many buzzwords old at this point that using it is more a label of a viewpoint than a coding style. Combined with the book's apparent focus on TDD and carefully selecting names, it zeros me precisely in on a set of people I have worked with over the years. I don't, as a rule, like the code those people generate.
b) The best style is no style, or at least pick a more recently popular dogma like FP, at least it gets you easy/safe parallelism in exchange for throwing some of the tools out of your toolbox.
The core of OOP is encapsulation (into objects) and polymorphic interfaces. You program against interfaces with well-defined contracts. Implementation details are encapsulated behind those interfaces. The same interface can have different implementations. The same interface-typed variable can point to different implementations at different times within the same program execution. The caller who invokes operations on an interface is supposed to not care about the implementation details behind it, and thus be decoupled from them. Interfaces can have an inheritance/subtyping relationship. (Implementations may as well, but that’s optional.) This enables abstracting over the commonalities of several related interfaces without having to introduce an adapter/proxy object. That’s basically it.
After making those interfaces, the moment you need to do something that breaks those interfaces, you suddenly have a headache. I wrote a program in college that daemonizes by being an instance of a daemon class. Years later, people wanted the option to not daemonize. With C, this would be easy. With the way that I did things according to OOP principles in C++, I need to delete all of the code related to starting the program and write it from scratch.
You could say that I just did not do it right, but that is the problem. You need to know precisely what the future will want to do it right and that is never possible to know in advance. OOP encapsulation is heavily overrated. There are a ton of headaches in C++ that do not exist in C because C does not try to do these things. Ever hear of the diamond problem? It does not exist in C. Nonsensical types that span multiple lines when trying to figure out why there is a type error? Not an issue in C either.
C++ was advertised as reducing complexity, but in reality, it that encourages developers to drown themselves in complexity. If it were not for C never gaining a widespread STL equivalent, C++ would be far less popular. Sun Microsystems did make libuutil to provide such facilities, but sadly, it never caught on outside of Sun Microsystems technologies. The BSD sys/queue.h is the closest we have to it, but it is only for lists, and we need trees too to get a good equivalent to the C++ STL. That said, libuutil is available through ZFS, so it is not that people cannot adopt its awesome AVL tree implementation on other platforms. It is just that people do not know about it.
Your problem was a misuse of inheritance, not encapsulation or interfaces.
That is exactly what I said you could say:
> You could say that I just did not do it right, but that is the problem. You need to know precisely what the future will want to do it right and that is never possible to know in advance.
Every time inheritance causes a headache, you can call it a misuse of inheritance, but that is only obvious after you have been to the future.
You don't need to precisely know about the future to know that inheritance isn't for sharing behavior. Unfortunate it's not really taught properly.
Misuse of inheritance is often the biggest generator of criticism of inheritance, sadly.
People use the wrong tool for the job, or use it incorrectly, and then blame the tool. It's like using a hammer to play drums, obliterating the drum set, then ranting against hammers.
OOP is a tool that causes people to hang themselves with it by design. The only way to avoid misusing it is to be to the future to see the impact of every single decision involving it to make only the ones that do not cause problems.
A. OOP as practically implemented for the last 25 years is glueing functions to state
B. Functions and structs.
In the real world, where many people of varying backgrounds and skill levels are editing the same code, if the OOP method becomes a mess, why wouldn’t the functional approach also become a mess? I think that is more the point OP was making. In a vacuum with a single perfect Adonian programmer, seems like the OOP and functional approaches would becomes the same level of maintainable, because we’re in a vacuum of perfection anyway.
In my experience, the mess created using OOP is harder to untangle than the mess created using a functional approach. With the latter, it's simpler to replace a different function for any part of the logic, and the data is always just data; whereas with OOP the method is usually tied up with shared state and other functions.
> In my experience, the mess created using OOP is harder to untangle than the mess created using a functional approach.
OP complained about accidental complexity, not subjective takes on how hard it is to refactor code.
Even so, anyone who has any cursory experience with TypeScript projects that follow a functional style can tell you without any doubt whatsoever that functional style is incomparably harder to refactor than any "enterprise-grade" OOP.
> OP complained about accidental complexity, not subjective takes on how hard it is to refactor code.
The biggest problem with accidental complexity _is_ how hard it is to refactor code. Refactoring code is a huge part of software development.
> Even so, anyone who has any cursory experience with TypeScript projects that follow a functional style can tell you without any doubt whatsoever that functional style is incomparably harder to refactor than any "enterprise-grade" OOP.
My experience differs greatly. One can see functional style has taken off in the TS world, particularly with the popularity of React, so I suspect I'm not alone.
> A. OOP as practically implemented for the last 25 years is glueing functions to state
I see you opt to go with a huge amount of handwaving over the question.
> Functions and structs.
That's what a class is, and thus OOP, except it supports information hiding and interfaces. So your alternative to OOP is... OOP?
I suspect "functions and structs" here meant "functions and structs, separately, instead of gluing functions together with structs into unholy amalgams". Basically, Wirth's "Algorithms + Data Structures = Programs" idea.
Compare e.g. to "What should a language have instead of Lua-like tables? Maps and vectors" — "But that's what a table is, so your alternative to tables is... tables?"
>>> a) start by stating what you think OOP is
>> A. OOP as practically implemented for the last 25 years is glueing functions to state
> I see you opt to go with a huge amount of handwaving over the question.
I think the question was answered pretty clearly. You can't ask for an opinion ( "what do you think" ) and then criticize the response as 'hand-waving'.
OOP is a mental crutch that breaks complex problems down into a easy mentally discoverable world/domain model with objects that have a life of their own, that is capable to derive correct results to complex problems via the relations of the objects.
Meanwhile its creators can not hold the whole complexity in mind (often barely in spec) and still can produce a artifact that produces correct results.
> OOP is an industry of its own which generates a ton of incidental complexity.
And that "ton" is still miniscule compared to front-end development which almost completely eschews OOP and has 10x more incidental complexity.
I guess my point is that, while OOP's incidental complexity is large, it's still insignificant compared to other technology stacks which developers are showing a great appetite for anyway. Things like "incidental complexity" is irrelevant to developers anyway, today, at the tail end of 2024.
IOW, OOP introduces significantly less $BAD_THING, when the clear majority of developers don't even care about the quantity of $BAD_THING in the first place, making the whole "should we use OOP" argument moot.
Doesn't matter if you use it or not, the extra introduced incidental complexity is still going to be insignificant due to the complexity load of the entire project, more so in front-end.
Hence, there's no point in having the argument in the first place.
Is your argument that
1. its ok to add incidental and unnecessary complexity
2. so long as it's less complex than your most complex component?
Because that's a formula we can all agree leads no where good nor productive.
> Is your argument that
> 1. its ok to add incidental and unnecessary complexity
> 2. so long as it's less complex than your most complex component?
That is not my argument.
> OOP is an industry of its own which generates a ton of incidental complexity.
Code in any form can generate a ton of incidental complexity. The issue isn't the tool rather than the education to properly wield those tools. Especially when you introduce the team dynamic where everyone has varying understandings of what is being built and how it should be built.
As it pertains to Python in particular I think OOP is great for libraries but of limited usefulness at the application layer. Things like pytorch's nn.module IMO is a great abstraction, but every time I've tried to map some concrete business concept to an OOP construct I've regretted it.
[flagged]
Can you actually give an example?
I believe in mechanical empathy. I think developers should - win performance is a concern - think about how their code ends up on the CPU, what ends up on the heap, how caches are used, and so on.
But for most Python developers, and Ruby developers, it's more important, in more use cases, to have clear and readable code that is easily maintainable.
I've met Sandi, I've gone through her Ruby books, I recommend her teaching and her books to others. I don't see the problem.
If you need ASM, use ASM. If you're on a team doing OOP, maybe learn about OOP.
Look at the fastest linting/build tools/runtimes for JavaScript. They are mostly not written in JavaScript, because JavaScript is slow and no amount of hotspot profiling/optimization will fix that. This is also why Python's core data types for dictionaries and so on are not implemented in Python.
You can also look at any microbenchmark between pure Python and a native language of your choice, even one that uses GC like OCaml or Go, and unless the Python code has been written so that it spends all its time calling C code the way you sometimes can with regex-heavy code, Python will generally lose by a varying margin. These micro inefficiencies multiply at every level hundreds of times over to create needlessly slow, inefficient applications.
The truth is I can link benchmarks, case studies of app rewrites from Ruby to Go or something comparable, measurements about the results of using NIFs in Erlang/Elixir, etc. all day long but you'll ignore all of them.
A lot of time (most?) writing software isn't about writing software for the pleasure of doing so, but for building something that helps generate revenue.
For businesses, having a highly optimized web application that takes a long time to develop and doesn't allow for quick additions of new features is not worth the cost. Instead, they can have a poorly optimized web application with way more features and a faster feature production because Python/Ruby/Javascript are more approachable than other native languages.
Thank you for using snake_case.
That's all I got. Best of luck!
I'm glad my comment noting the usage of best practices when it comes to writing Python was downvoted. Says a lot about the immature user base around here.