When did people favor composition over inheritance?
Posted by AWildMonomAppears@reddit | programming | View on Reddit | 243 comments
TL;DR: The post says it came from trying to make code reuse safer and more flexible. Deep inheritance is difficult to reason with. I think shared state is the real problem since inheritance without state is usually fine.
trmetroidmaniac@reddit
This aphorism is usually used to mean implementation inheritance. Interface inheritance (
implements Interfacein Java) is inherently stateless and therefore fine.Kotlin has clearly been influenced by this principle with ideas like final-by-default and delegation but uses interface inheritance everywhere.
SanityInAnarchy@reddit
It's still something worth being cautious about, if your interfaces can have default implementations. The biggest thing that bugs me about inheritance is when a method in the child class invokes a method in the parent, that in turn invokes another method in the parent that's been overridden in the child, and so on. You don't need any state for that call stack to be a maze.
But when the article complains about this as a "thought-terminating cliche", I think the issue is people taking "Prefer X over Y" as an absolute "Y is bad, never use Y, always use X instead." I always read this as saying something closer to "When tempted to do Y, consider X, it's usually better." Sometimes you really do have a problem that fits into a hierarchy of types.
HAK_HAK_HAK@reddit
yeah don't do this kinda shit lol
SadPie9474@reddit
"this kinda shit" is like the only thing I've ever found useful about inheritance, everything else that inheritance does can be done in a simpler way, but inheritance is the only way I've ever found to do open recursion. Are you saying open recursion is never useful, or that you know of better ways to do open recursion?
nicheComicsProject@reddit
It's not that no use can be derived from it: it's that it's incredibly difficult to understand to anyone new to the project. Every language and manner of programming has "cute" things but the general consensus is to avoid them because they're devastating to maintenance, which is what most time on nearly any project will be spent doing.
SadPie9474@reddit
find me a better way to maintain tens of visitors over an AST with hundreds of types of nodes.
nicheComicsProject@reddit
I'd have to see an example but this strikes me as exactly the kind of wrong road OO pushes you down. It's probably not hundreds of nodes at the top level, it's probably a language and hierarchy of nodes which would come out of the design if you had to describe it with ADTs and pattern matching. Something like:
type AstNode =| Stmt of Statement| Expr of Expression| Decl of Declaration...type Statement =| IfStmt of ...| ForLoopStmt of ...... -- only dozens of caseslet rec evaluate node =match node with| Stmt s -> evaluateStatement s| Expr e -> evaluateExpression e| Decl d -> evaluateDeclaration dlet evaluateStatement s =match s with| IfStmt (...) -> ...| ForLoopStmt (...) -> ......SadPie9474@reddit
okay, and now how do I swap a custom function in for evaluateIfStmtGuard without having to rewrite all of its callers and callees as well? How does the types being hierarchical make it no longer the case that without open recursion I have to duplicate hundreds of lines of code for each custom visitor?
nicheComicsProject@reddit
Well, any function that has the same inputs and outputs of `evaluateIfStmtGuard` can be used in its place. I feel like the other problems you're describing come from how one tends to write OO but we're getting too far out of my expertise here. I program almost exclusively functionally but I write parsers or anything like that. Maybe this topic should be its own thread to get a real answer.
SadPie9474@reddit
what? how would I put another function in its place?
nicheComicsProject@reddit
Well, one approach might be to have your
parsefunction that takes functions for each of the different handlers. Then, at the top level you might have a variable which is calling theparsefunction with all the default handlers already passed in as parameters. Everywhere that you will want whatever is the default you call via this variable. You might have other variables that change the passed in functions somewhat and you might have some which pass most of the default functions but allow a few to be passed in for special cases.Ok-Scheme-913@reddit
ADTs are a closed hierarchy, they are not the same thing.
What if you write something like LLVM and others can write extensions?
nicheComicsProject@reddit
The above is an ADT. And if part of your tree is coming from other modules then the above strategy works if you recompile the program on changes. It might be trickier if it needs to be dynamically linked, not sure.
Ok-Scheme-913@reddit
Well that's what we call open vs closed. Of course if recompile it works, but that's not feasible
BaronOfTheVoid@reddit
That's basically the way Rust's Iterators work, and people love it.
You have dozens of "trait" methods relying just on the one next() method that may be implemented differently based on whatever the Iterable looks like.
gc3@reddit
This will happen with an old enough program that has been refactored if you are using inheritance
Intrepid_Result8223@reddit
I think this battle is not over. We simply haven't found the combination of syntax and editors/LSP to work with deeply inherited code.
But there is something really nice about implementing a parent class and having all children inherit the behavior without having to wrap it. The problem is now often it becomes mental load for the programmer to keep the parent functionality in mind when reading the child and doing this for multiple levels, mixins, etc.
trmetroidmaniac@reddit
It's the same discourse as "X is evil".
Inheritance of behaviour without state, which I think is best described as a mixin, falls between the other two in the evilness hierarchy IMO.
Bakoro@reddit
What helped me finally get this solified was writing code that interfaced with machines, for a bigger system composed of a bunch of different machines.
It was a phenomenal first job to have, because almost literally everything could have been textbook examples.
It was like, this class is literally representating the state of the physical machine. This class inherits from base class because the child class represents a machine that is literally a type of device that has all of the features of the parent device, plus other stuff.
Another class was a data orchestration class, it's not the machine itself, it has a machine that it interacts with, and manages requests from other parts of the code. Another orchestrator has multiple components, and manages and coordinates the things is has.
I needed interfaces, because the high level logic for a process was fixed, but the device we used might get swapped out for something from a different manufacturer. Programming against an interface made thousands of lines of code able to handle a ton of changes to lower level implementations, and let manufacturing be extremely flexible.
Then things got more complicated, and we started doing networking, and more complicated data processing, and then we started doing CI/CD with Jenkins.
I wish I could package that whole experience up for people, it was seriously about as perfect a case study for software development as it gets, from OOP, to version control, to CI/CD to deployment, project management... We did a speed run through like 40 years of software development growing pains and finding out why best practices are best practices, in a span of 2~3 years.
manifoldjava@reddit
Maybe...
Many languages that claim to support delegation, Kotlin included, don’t actually provide true delegation. What they really offer is call forwarding, which is a crude and incomplete approximation. Call forwarding doesn’t address key issues like the self problem or the diamond problem in multiple interface inheritance. Because of this, it’s not a realistic substitute for full implementation inheritance, and should be used carefully.
The delegation plugin for Java explores a more complete model of delegation. Its README has examples that illustrate how it resolves the self problem and related issues.
Ok-Scheme-913@reddit
Could you perhaps tell a bit more about your experience/opinion on this topic? I have seen manifold multiple times and I'm quite interested in PL design questions like this.
manifoldjava@reddit
Sure, though I’d rather expand on a specific question or area if you have something particular in mind.
Supuhstar@reddit
I think what you described by “interface inheritance” is composition at the type level
grauenwolf@reddit
The word you are looking for is "polymorphism".
devraj7@reddit
No.
Polymorphism is a runtime construct. We're strictly talking static design here.
nicheComicsProject@reddit
I don't agree with the GP but polymorphism is most definitely not runtime only. There are nearly a dozen kinds of polymorphism and some of them are static.
grauenwolf@reddit
https://en.wikipedia.org/wiki/Polymorphism_(computer_science)
Kered13@reddit
Polymorphism is an umbrella term that includes many techniques, including both static and runtime polymorphism.
ConejoSarten@reddit
That’s not what polymorphism is
Akkeri@reddit
The trade-offs depend heavily on the language, system complexity, and long-term maintainability. In some cases, inheritance is still the cleaner and more practical solution, especially in statically-typed, structured systems. Understanding the context is far more important than following the mantra blindly.
10113r114m4@reddit
I personally think readability is the biggest issue with inheritance.
Fucking overrides, and then diving into however many levels to find the actual implementor. It's ridiculous
smutaduck@reddit
I think it's more fundamental than that - inheritance assumes something similar to phylogeny - the analogy with evolutionary biology is deeply built in to the assumptions, thus inherited code has common ancestors. However code does not work like this. A classic example being that being agricultural equipment, a wheelbarrow and a tractor both have wheels, so both "inherit" the wheel. However a tractor is somewhat similar to a "car" and so they would be assumed to have some common ancestor. However a wheelbarrow doesn't really have a common ancestor with a car, so the fundamental assumptions around inheritance start falling apart in what becomes a conceptual mess.
rep_movsd@reddit
Inheriting vehicle from wheel is stupid.
smutaduck@reddit
Correct. I was just parroting a poor approximation of the contrived examples in text books, thus demonstrating the general flaw.
mark_99@reddit
If your idea of OO is "a tractor is-a wheel" then you're just doing it wrong. That's clearly a has-a relationship, which is composition.
Now try whether a tractor is-a vehicle or has-a vehicle.
There are valid criticisms of inheritance both in theoretical and practical terms, but there are also situations where it's the right choice (even if you're using composition mechanically to get the same effect).
Either way starting from an accurate understanding of the difference between inheritance and composition is kind of important.
https://en.wikipedia.org/wiki/Liskov_substitution_principle
https://en.wikipedia.org/wiki/Composition_over_inheritance
smutaduck@reddit
We're not in disagreement. However inheritance should not be the default due to this problem at the root of the conceptual model. In fact I would argue that inheritance should be quite rare, and one should be easily able to identify when it's the right choice due to having a very good reason for it to be that way.
mark_99@reddit
Sure... but nothing should really be "the default". You pick the right technique for what you are trying to achieve. Sometimes that's inheritance, sometimes it's composition, sometimes it's something else.
One criticism (as it ably demonstrated by most of the comments on this post) is that inheritance is commonly misapplied and/or designed poorly. I'm not sure that's the "fault" of inheritance as an available tool however.
smutaduck@reddit
I'm arguing that the "correct" answer is assume that composition is almost never the correct answer and when it is, confirm it with a very good reason.
propeller-90@reddit
No, I'm sure you mistyped that, you are arguing that inheritance is almost never the correct answer.
And to give a counter-example inspired by the article, algebraic data types:
datatype Tree = Leaf(X value) | Branch (X value, Tree left, Tree right)
Datatypes are a supertype (Tree) and types that inherit from the supertype (Leaf and Branch). Some "methods" are implemented on Tree (like tree.getValue) and some only on the subtype (Branch.getLeft). However, no other subclasses are allowed.
In general, I think such "sealed" superclasses are good use of "inheritance".
smutaduck@reddit
You are correct. I Mistyped. Best to assume you don’t need inheritance until you are sure that you do.
nicheComicsProject@reddit
Purists always say this but I've yet to see once, in my entire career, a place where it made the code more clear. What inheritance actually does is help with writing the code. You have to write less when you have these elaborate hierarchies and I've certainly made some large systems that took advantage of this. It was super trivial to add new sub-behaviours. But new people coming to the project would simply never achieve the level of comfort with it that I had because OO hierarchies are so impenetrable from the outside.
I've read mid sized Java programs that I knew what they did but literally couldn't find a single line of code that contributed to the action. Just machinery going through the OO hierarchy, passing off to some other location but apparently none of these sites actually doing anything either.
Contrast this with functional programming: You follow the function calls and you know everything that's happening and can happen.
Ok-Scheme-913@reddit
GUI frameworks.
They suck if you don't have inheritance.
nicheComicsProject@reddit
I'm not convinced. Most GUI frameworks are pretty bad TBH. I wouldn't know how to improve them but they're somehow clunky. The best one, IMO, was WPF which was composition based.
agumonkey@reddit
I wonder if there have been studies on the adequate depth of a class inheritance tree
mark_99@reddit
If your behaviour is "impenetrable" then yes that's a problem. I don't know Java much but anecdotally it is prone to this (although I imagine tooling can help somewhat).
I just think it's reductive to say "inheritance bad" when the actual problem is e.g. giant do-it-all classes with multiple levels of inheritance, in a poorly thought out structure.
nicheComicsProject@reddit
After 5 years of maintenance, the structure will always have been poorly thought out. I was an OO zealot for most of my career so I've done it in more languages than most people probably know. Now I'm convinced that OO is a complete dead end.
Ok-Scheme-913@reddit
I never liked these kinds of arguments.
You ain't building a knowledge graph, that's not the point of abstractions in programming languages.
The point of inheritance is basically being able to substitute one object in place of another, while potentially altering behavior.
And for GUI frameworks, there is nothing that would work as well, so there absolutely are domains where it's a very useful tool. (A Button "is-a" Node, and you want your CustomButton to also have a bunch of existing functionality like support for double click and whatnot).
(But in general, of course composition should be used first and foremost)
60hzcherryMXram@reddit
Just allow multiple inheritance and now you can represent both inheritance from a car and a wheel and also have 50 other new problems to eventually solve.
NewPhoneNewSubs@reddit
If i want to build a bunch of vehicles for my kart racing game, they might just all be karts.
If I want to sell various wheeled items on my various stores, they're absolutely all different, but probably all implementing some kind of inventory meta functionality.
The shared ancestor doesn't come from the real world. It comes from the thing you're making a bunch of.
fragglerock@reddit
What if you introduce hover karts! WHERE ARE YOUR WHEELS NOW!?!
NewPhoneNewSubs@reddit
Transparent because clearly I tied speed to the wheel rotation and not to just applying a force. So the hover cart definitely still needs them to go.
smutaduck@reddit
This is true. I think composition models this better than inheritance though as inheritance is somewhat tightly coupled to real world analogies. And as we all know, code has very little to do with the real world
fragglerock@reddit
This being flagged controversial is a laugh riot!
10113r114m4@reddit
Sure, there's plenty wrong with it, but my biggest gripe with it over composition is readability.
I think your reply is more about how to model it. Which can definitely get problematic if you are trying to force something into some inheritance model when it does not belong there. I think inheritance's main goal was preventing duplicate code and allowing other inheritors allow to take on those behaviors. However, I think there are better ways to accomplish that, and like you said, modeling it well is a challenge of its own.
nicheComicsProject@reddit
Exactly. All the "is-a"/"has-a" crap modelling is navel gazing. The only possible purpose of OO is prevention of code duplication. But it does this at the cost of readability and, much of the time (ironically), verbosity. All this establishing and wiring up of these hierarchies, injection, etc. makes code that can be easy to write but is nearly always hard to read.
No_Dot_4711@reddit
> if you are trying to force something into some inheritance model when it does not belong there.
Inheritance is more flawed than that.
Even if something you build today is actually correctly represented by inheritance, software has a strong tendency to change and make it not that way in the future and just break all invariants in your code.
When you use inheritance, you don't just need to be sure that A is-a B, you also need to be sure that this will ALWAYS be the case, which you can almost never be
CptBartender@reddit
A platypus is a mammal that lays eggs.
smutaduck@reddit
I guess that demonstrates that while the phylogeny/classification is sane, the DNA and its expression manifests as composition.
DiligentRooster8103@reddit
Compose wheels do not break when you need square ones tomorrow
smutaduck@reddit
I'm gonna use that one :D
Chii@reddit
only if your IDE sucks and cannot automatically allow you to click-thru into the implementors. I don't think the code navigation or readability has anything to do with why inheritance causes bad maintance headaches - it's the amount of context you require to understand the piece of code.
Good inheritance exists - they are designed so that you dont need to understand the context of the hierarchy. Such as java's Collections library, which has quite a deep inheritance chain (and has been extended by multiple different other libraries to provide extra features - like in ORMs to provide seamless lists etc).
However, most inheritance is poorly designed and the authors of them have not deeply considered anything much.
BlindTreeFrog@reddit
If a programming language requires an IDE to be a usable programming language, it's hardly worth considering.
Chii@reddit
an IDE strictly improves the coding experience.
It's like saying you'd prefer to use an axe over a chainsaw to chop wood.
BlindTreeFrog@reddit
No, the "Your ide sucks" stance is claiming that the wood can only be chopped by using an Chainsaw.
As said by everyone else in this thread, the amount of obfuscation that is introduced is inherit to the language. Saying that this isn't an issue because your IDE sucks and you need an IDE is not fixing the problem
nicheComicsProject@reddit
No, the point is that, in fact, Java is not OK with a text editor. Any project of reasonable size is completely opaque without a massive IDE to help you make sense of the thousands and thousands of lines of code that seemingly do nothing but defer to other lines of code found who knows where.
I can read lines of Haskell on a web page and understand everything. Rust too for the most part.
10113r114m4@reddit
And another good reason why inheritance is terrible. It requires an IDE where it specializes in resolving this. Using editors like emacs or vim makes programming in those languages much harder.
balefrost@reddit
Surely there are plugins for emacs and vim to help you navigate your codebase! Things like "Go to definition", "go to implementation", and "go to subclasses" have been staples of IDE navigation for decades. If the emacs and vim ecosystems haven't evolved plugins to make it possible to navigate around, I think that says more about emacs and vim than it does about these programming languages.
I'd go so far as to say I don't believe that these plugins don't exist. Surely somebody has made them by now.
10113r114m4@reddit
Oh they exist, but you'd be nuts to try to use vim or emacs for java professionally. I tried for 3 years, multiple plugins, to the point one engineer said you may as well just join the IDE for java club lol.
While they do provide some support, it is on a very basic level.
syklemil@reddit
Yeah, these days that functionality is provided by language servers. The language server protocol is a pretty nifty idea, which allows people to implement functionality once in some language server that can be used in pretty much any modern editor or IDE, rather than have to reimplement a bunch of stuff over and over again.
Actual language servers are in a variety of states though, so they might not have a certain capability, may have horrendous resource use, or be prone to crashing. I'd say it's a rapidly maturing field, for something that didn't exist a few years ago?
BlindTreeFrog@reddit
CScope and Ctags have existed for decades.
But if I can overload the
=or any other operator/function, I can no longer trust anything without checking if it's been redefined first.How often do you mouse over an operator to see if it does what you assume it does? I mention
=because the number of times I've been debugging someone's code and they swear that the=does a memcpy rather than an pointer assignment is more than I should have had to count.RiPont@reddit
The biggest problem is that people drastically underestimate the difficulty of filling the
is-apromise.agumonkey@reddit
it's also a lock-in
many times you run into a nice library with tons of features but your Foo class isn't part of the family so he gets to participate in none of that
Gyro_Wizard@reddit
Yep. That's where we get the classic square "is a" rectangle problem. "Behaves like a X" is usually a better paradigm.
Fast_Face_7280@reddit
It is always trickier when the invariants that must be maintained are outside of the model of the language you're working with.
Full-Spectral@reddit
It's easy to do if you are modelling a real hierarchy that's fundamentally based on is-a relationships. There are plenty of them in software world that map well to inheritance.
The gotcha is trying to apply it to things that are not strictly hierarchal, and the inconsistencies that involves. Or, even if you start our clean and pure, inheritance is so flexible that you can go forever and never really re-factor it to reflect changes. So you end up with death by a thousand cuts over time.
tcmart14@reddit
I do think that is a problem, but from working in a legacy codebase where people went ball to walls with inheritance, the biggest offender is an abuse of inheritance hierarchies. To share an example of a legacy codebase I’ve worked in.
On an invoice we have a concept of an authorized signer. It’s a person for an account who is authorized to sign for the invoice. Think of Bob the contractor who has an employee Joe who can purchase materials under the company account, but Bob wants the store to have on record Joe signed for the items. So, we have an object called authorized signers. A couple years later (and before I got into the codebase) clients had customers like Bob who also wanted to associate an invoice with a project. Bob has a project to build Jane’s house and wants some traceability in his account that materials are bought for Jane’s house project. Someone decided to create a “project” object, and it get it done “fast,” they made project inherit authorized signers. Now an invoice can have either an authorized signer or a project, not both. And because of this inheritance, to implement new features for project management, we would have to rewrite projects to safely and efficiently store and handle the newly requested project data for new project features.
watduhdamhell@reddit
And the obvious double edged sword of updating a class >> updates all objects. But, if you don't want all of the objects to have that update, or you don't want to fuck with all of them, or whatever, then composition is much easier to deal with. More updating when you change how something should work, but you have more flexibility and isolation when updating.
I don't know hardly shit about c# and python outside of some hello world scripting, but in controls, ABB 800xA is totally designed around OOP and favors inheritance, and it's fantastic when building the code and making bull updates to classes/structured data types/etc. BUT if you wanted to say, change just one module inside one large piece of code (for example, a transmitter), forget it. You have to download the entire application to the controller for the update to work, and that's risky as all hell... So you don't. You have to wait for the right time.
Meanwhile in DeltaV, which favors composition, allows me to build entire control applications out of singular modules, which is harder to look at at make sense of in the IDE, and more work to update things. But once it's there... You don't need to update often, and of course, it's totally isolated like this. I can download a transmitter if I know it's not critical at the moment, because I can download just that transmitter.
I'm sure it gets more convoluted with "real" programming but. I feel like I understand what y'all mean!
10113r114m4@reddit
Dont sell yourself short. You're still programming.
What you stated is just the various issues with inheritance. Like you said, changing something deep in the chain will require retesting, which is normal with any change. However, if any section of the code isnt well tested, it can easily bite you. However, in programming language theory there's 3 core principles: readability, writability, and reliability
readability is how easy is it to infer what is going to happen by looking at the code
writability is how easy is it to write in this language to do something
reliability is how "safe" the language is. You can think of this is type checking, error handling, etc (security restrictions also fits in this category)
Languages try to have all three. So let's take C. If we were to rank it in these three categories, it would probably be rank lowly on writability and reliability due to the lack of security restrictions. However it reads pretty simply. So Id rank that the highest of the three categories.
Now for Java, it is easy to write, but at the cost of readability. You have to look at the whole polymorphism tree to ensure you know what is going to happen. It is also very reliable compared to a lot of other languages.
So if you were to rank each category based on importance, you'd get a whole slew of tiers based on people's values. A security engineer is really going to care about reliability, for example. However, for me, readability is easily number 1. It's the one where everyone should care about because any person should be able to understand their code and any code they are reviewing. However, I would also say it's not black and white either. You could have the most readable language, but if it takes 10 years to write a simple program, it is worthless. Same for if it is completely unreliable. So, when designing a language and you want to add some paradigm, you need to ask, how will this affect those three categories
atheken@reddit
Inheritance requires the author to assume they can anticipate all future scenarios in which their code will be used, at the time they have the least information about how it will be used.
Composition allows an author to specify precisely what their component provides and under what conditions it can make those guarantees, and therefore the decision-making is based on things you can control/know at design time.
Ok-Scheme-913@reddit
Well, they don't 100% replace each other, you can't do everything with just composition that you can with inheritance, so a comparison should account for that.
atheken@reddit
I’m not sure that’s true, what’s an example?
remy_porter@reddit
Well used, inheritance allows a developer to write code which is "fill in the blank". "I'm going to use this this way, but I don't know how you'll want to do this step, so just fill in the blank (virtual function)."
Of course, at that point, it becomes indistinguishable from composition- it's just the strategy pattern with tighter coupling.
atheken@reddit
I mean, I know that’s the theory, but in practice, anything sufficiently complex to warrant inheritance is unlikely to be able to account for the variation over time, and you end up with a bunch of hacks and vestigial stuff to compensate for the polymorphism.
remy_porter@reddit
I would argue that if it’s complex, inheritance is the wrong tool. You should employ inheritance when it’s sufficiently simple. And generally hide your polymorphisms behind a non-polymorphic interface.
atheken@reddit
I think we agree? My point was that the abstraction breaks down as the model gets more complex, which is precisely why you think inheritance would have helped in the first place. “It’s bad at its job.”
Prestigious_Boat_386@reddit
Just define
invoke(type, method)
To return the method that will be used for a type.
Supuhstar@reddit
It's swizzling with an air of formality
Certain_Syllabub_514@reddit
My biggest issue with inheritance is it totally breaks the single responsibility principle.
Worst example I've seen was a Delphi app with 14 layers of inheritance in forms (the view layer). They had a "locking" form that every form descended from. A simple change to that form broke the app in unexpected ways and required dozens of files to be updated.
If they used composition, or just implemented a non-visual "locking" component, that change would've only been a component property change on any form where the change was needed.
Also unit tests were such a nightmare in that app, the devs who created it frequently commented them out rather than fix the underlying issues. We put a measure on code coverage at one point, and the devs started "hacking" it by doing things like putting a test around the application startup code.
mexicocitibluez@reddit
It does not "totally break" SRP. SRP and inheritance/composition are different concepts. You can still "break" SRP via composition, and you can still abide by SRP with inheritance.
In fact, I think SRP is one of the most misunderstood principles within the community. People want to believe it's this black and white issues, and that you can spot it without actually knowing what the code does (eg "Inheritance breaks SRP"). But you can't know whether something is breaking SRP without understanding the context it's being used in.
Sopel97@reddit
at what point do you consider those devs adversaries?
analcocoacream@reddit
I would add that it also makes code unintuitive
For instance let’s say you have a class with method A calling an abstract method B
Then if you extend the class and need to override method A for some reason and it doesn’t call B anymore you still have B to override despite it being unused
ICanHazTehCookie@reddit
I'd think that's generally the result of a bad abstraction. Not inherent (heh) to inheritance itself
nicheComicsProject@reddit
Programming has always had bad programmers. Some would say most programmers are bad. If your paradigm is defeated by bad programmers it's simply bad.
ICanHazTehCookie@reddit
I agree, the pattern should accommodate reality haha
Princess_Azula_@reddit
It's easier to read bad code using composition than bad code with inheritance. Imagine reading some godawful code written 10 years ago with no documentation, but the programmer decided to use the worst class inheritance you've ever seen for everything.
ICanHazTehCookie@reddit
That's fair!
devraj7@reddit
That's a programmer issue, not an inheritance issue.
Bad code can be written in any language, doesn't mean the language is bad.
10113r114m4@reddit
inheritance makes it very easy to shoot other devs in the foot. Similarly to void pointers in C. You could make that same argument for that, but it's a terrible language design. Language needs to be designed where it provides a best path for its users. Hence rust and go
colei_canis@reddit
It’s weird, I generally agree with this on an intellectual level but despite containing an entire arsenal of footguns, I’m still going to reach for Scala given the choice most of the time.
nicheComicsProject@reddit
When a language makes it easy to write bad code and hard to write good code, yes that absolutely means the language is bad.
syklemil@reddit
One thing I'll have to say in Javascripts defence is that its devs don't seem to engage in "skill issue! git gud!"-style machismo when someone inevitably ends up with
[object Object].nicheComicsProject@reddit
It's hilarious that we mention bad languages and you instantly jump in here defending a language literally not mentioned once in any thread from this entire post.
syklemil@reddit
I think you're misreading me; I still think Javascript is a bad language, and I'm using it as a comparison/contrast to talk about culture and attitudes towards weaknesses in language.
Javascript's users_seem to have a culture of _accepting that it absolutely has bad parts, even recommending tools like Typescript to alleviate the problem.
Unlike Java here, where people will respond "skill issue, git gud" to complaints about incredibly convoluted hierarchies.
Java isn't the only culture where we can find that sort of machismo on display either.
nicheComicsProject@reddit
I think you're taking my comment more adversarial than it is. Imagine me laughing while I write it, and not laughing at you either.
syklemil@reddit
Yeah, that's the ever-present weakness of these online discussions; we never know each other's personalities or even the tone in which something is said. :)
WarPenguin1@reddit
I remember working in a codebase where the developer drank the oop Kool-Aid. I would traverse an objects inheritance and there would be several classes called BaseClass. That is confusing as hell for anyone.
I remember trying to optimize a function and find out someone attached a ton of callback functions any time someone called a SQL script. Good luck finding all of that.
I am so happy I no longer need to maintain that monstrosity.
cheezballs@reddit
Alt + Enter in IntelliJ.
Saki-Sun@reddit
I hit myself in the skull every time I use inheritance. I mean that literally, it's a reminder that it's a bad idea... But sometimes it makes sense so I wear the pain.
valleyman86@reddit
It isn’t just readability. If your base class changes it affects everything that uses it. It’s fragile. In composition it’s easier to keep components smaller and more specific. Fine if you break a wheel but at least the whole car isn’t shit.
nazhrenn@reddit
I’m refactoring some code to try and add some new implementations of a concrete class. The inheritance is running five classes deep, across multiple files. So much repetitive changes. And I know this isn’t even the worst example.
grauenwolf@reddit
Inheritance = composition + polymorphismOnce you understand that it becomes much, much easier to know when to use inheritance.
Tubthumper8@reddit
polymorphismhere must mean specifically subtype polymorphism, as there are other kinds of polymorphism tooAhoyISki@reddit
Not really, Rust has polymorphism without having inheritance.
bleachisback@reddit
Rust has composition and polymorphism, but not inheritance. So it’s not a strict equality.
balefrost@reddit
Effective Java has a good example of the problems that inheritance can cause.
The example is: you have a List class. You want to instrument it to know how many items were ever added to the List. So it's not the same as
size; it's at least as large assize.The public API of the List class looks something like this:
So say you use inheritance to instrument the class. Your solution looks something like this:
Seems reasonable. There's just one problem. I didn't show how
addAllis implemented. It's implemented like this:Whoops! It turns out that this double-counts items. The call tree looks like this:
Because of how polymorphism works, when the base class calls a base class method, it will actually call a subclass override if it exists. This is generally desirable. But to properly instrument the List class using inheritance, we need to know how it is implemented. We have to understand under what circumstances
List.addwill be called internally byList.Consider instead composition:
Because there's no inheritance here, the call stack won't go back-and-forth between
ListandCountingList. The user will call aCountingListmethod, and that method will call aListmethod. Once it's insideList, it'll never call back into anyCountingListmethods.The lesson here is that, with composition, you really only need to know the externally-visible API of a class. Inheritance deeply connect the base class to the subclass. Going back to the inheritance-based approach, we could work around the issue by not having
addAllcount any items. But suppose thatList(which we didn't write) later changes its implementation. Maybe that future version ofList.addAlldoesn't need to calladdat all. A reasonable change made to the implementation ofListhas broken ourCountingList.You might say "this is exactly what I meant by shared state!". But you could tweak this example to be completely stateless and the problems would persist.
PogostickPower@reddit
It's a good example, but it creates a new problem. The implementation using composition does not implement the Collection interface and can not be passed as an argument instead of a List.
The addAll method in CountingList would not accept a CountingList as argument.
SerdanKK@reddit
The example is obviously simplified. In the real world there's nothing keeping it from implementing whatever interface.
Cautious_Implement17@reddit
I think the point is that you now have to write a bunch of boilerplate code to implement the rest of the List API. annoying, but imo worth it to avoid the problem you described.
PogostickPower@reddit
Yeah, there are 28 methods in the List interface. I think IntelliJ has a shortcut for auto-generating delegation methods, so it's not time consuming - just ugly.
SerdanKK@reddit
Like half of them could be implemented solely in terms of the other half. C# uses extension methods for such cases. You get all of LINQ by implementing an interface with just two members for example.
balefrost@reddit
Depends on the language. In Java, yes. Though if writing these "list wrappers" is common, you can (funnily enough) use inheritance to create a base class that by-default delegates to another list object. For example, Guava's
ForwardingListis such a base class.In Kotlin, you can delegate interface methods automatically:
There you go, you have a class that by default delegates all
Listmethod calls towrappedList. You can, of course, override methods inCountingListas appropriate.Logical_Angle2935@reddit
Or, in the real world sometimes inheritance is the best option
SerdanKK@reddit
When?
Intrepid_Result8223@reddit
Maybe your points are true for java (don't know, not a Java guy), but it certainly isn't true for other languages. Composition will often require detailed knowledge of the workings of the component since you will end up interacting with private component state or you end up adapting the component, adding new methods so it can interact with your enclosing class. Sure, there are lots of cases where the component has a neat interface that doesnt ever change but when it gets hairy it doesn't really matter to me if it's deeply inherited or composed with multiple wrappers and iffyness.
Revolutionary_Ad7262@reddit
I like the
class ConcurrentList extends ArrayList and Mutexexample. I have seen something like this in a C++ codebaseGoodie__@reddit
For me the problem is less state, and more that unless your inheritance structure perfectly matches your program structure, now and forever, you're in trouble.
For example, let's talk about a basic example using URL vars:
/burger/{burger}/filling/{one}/crud
/burger/{burger}/meat/{one}/crud
Now imagine we have a class that deals with each "thing" in that URL, that is in our object inheritance hierarchy. We have a "SaveController" that extends a "MeatProvider" that extends a "BurgerProvider". Our save method can now just call "GetMeat()" and "GetBurger" that know a lot, and figure it all out for us. Beautiful code. We can make a new "ReadController" that also extends "MeatProvider". Success! Code re-use! SOLID! DRY!
But what happens if we decide that we also want to have a lasagna endpoint? Our "MeatProvider" extends our "BurgerProvider" and not our new "LasnagaProvider". It is fundamentally not reusable by different things.
If instead these are individual classes that are injected/instantiated by the endpoint, any input that uses them, it could be injected into our new "LasangaMeatSaveController" AND our old "BurgerMeatSaveController".
VigilanteXII@reddit
Bit of a strawman, though. Don't think even the most ardent inheritance defender would claim that this is proper use of inheritance. A "SaveController" obviously isn't a "MeatProvider" by any stretch of the imagination.
What you would probably more likely find is a "Provider" base class with maybe an interface to receive URL parameters and basic support for chaining/building a tree of sub providers. Burger- and MeatProvider would then extend Provider, and you can then dynamically chain them together however works for you. Still bad?
Besides, no one says to never use any composition; even the most overwrought Java codebase is full of it. Obviously you use whatever tool makes sense for the task at hand.
Goodie__@reddit
And yet, that is a "pretty close" analogy to the current system I work on day to day. Except it has something like 5 layers, starting with Abstract controller, through a provider, through abstract base controller (I swear to god), maybe to your controller, but if your dealing with a user it's another abstract class. I think I might be forgetting one.
Oh and basically no one knows how it all holds together these days, and everyone tries to avoid touching it 🙃
VictoryMotel@reddit
Anyone who has worked with inheritance hierarchies and realized that it is all about creating dependencies and dependencies are the enemy quickly wants a way out.
Combine that with the fact that there are massive performance gains of 25x-150x when you stop using pointer chasing inheritance while being much simpler, and it there aren't many people who want to go back.
These two aspects drive people to evangelize because the difference is massive.
BigHandLittleSlap@reddit
Composition tends to use more pointer indirections than inheritence.
If you allocate an object that is derived from a long chain of sub-classes, it's still just one heap allocation, one static vtable, and that's it.
With composition, in most languages, you end up with many times more heap allocations.
VictoryMotel@reddit
This is not true.
What people mean by composition would be allocating once for a big array instead of every object individually. Then there is just one pointer to the heap from your vector and all the data is next to each other in memory. If you loop through it, the CPU will prefetch.
With typical inheritance every object is a heap allocation and every access is a pointer dereference, then maybe another for the vtable pointer.
The performance consequences of this are severe.
BigHandLittleSlap@reddit
How this works is language dependent.
But if you have:
For “wrapping” and overriding the behaviour of the Bar type in the Foo type, then in Java, C#, Python, JavaScript, etc… this is two allocations.
If you derive Foo from Bar, then in all programming languages I’ve ever used, this is one allocation.
It’s a different story with “struct” value types, C++, and Rust, which tend to merge the type such that it can be allocated in one heap object.
VictoryMotel@reddit
Your idea of composition is a scenario that no one should ever do and is not what people mean when they say composition.
Allocating single objects is the enemy and having an allocation that allocates more gets into nonsense territory.
What you actually want is simple compound data structures that contain arrays. This is what people are talking about with composition.
BigHandLittleSlap@reddit
They really aren't, except in very specific fields such as game development (RCS) and machine learning (tensors).
Business software, web apps, etc... all heavily use single objects, typically in some sort of tree structure.
VictoryMotel@reddit
This is ridiculous. Look up data oriented design, check out the talk by mike Acton.
They definitely do, that's a big part of why the software is terrible. Terrible to work on and terrible performance that could be 20x-100x faster. The java and early C++ inheritance everywhere idea that infected software for decades and never worked out.
BigHandLittleSlap@reddit
Got a direct link? I'm curious..
I absolutely agree, but very specifically object-oriented inheritance doesn't contribute to this! An inherited type is still just a type, singular. Allocating an inherited class still requires just one heap allocation, there are no additional allocations for the base type (in most mainstream languages).
Composition tends to increase allocations in OO-first languages, which is currently all of the popular ones.
VictoryMotel@reddit
You keep saying this but you never explain it because it isn't true. One allocation per object is a performance problem. With composition you allocate once for a million elements.
With the individual allocations you would allocate a data structure to hold pointers. You have to index to get the pointer then dereference the pointer. The allocations are expensive, the deallocations are expensive and the pointer dereferencing is expensive.
BigHandLittleSlap@reddit
WHAT MILLION?
You keep saying that over and over like most apps have millions of identical records and are built up with a SoA design.
Almost none are, except for special cases like simulations, games, numerical codes, etc...
Show me one (1) example of a typical business app, web app, or mobile app where composition is extensively used for millions of items in arrays.
You are confused here.
Objects are objects, by definition. Almost all commonly used programming languages allocate 1 heap entry per 1 object, no matter what.
Arrays or not doesn't make a difference.
Inheritence or not doesn't make a difference <- my point being made here.
You are conflating this argument with array based programming techniques such as structure-of-arrays, RCS, numerical codes, data-oriented programming, etc...
That has nothing to do with composition-versus-inheritence, the topic of this whole thread!
Also, in C++ and Rust, you can have your cake and eat it too. An array of classes types in C++ would use one large contiguous allocation whether or not the classes use inheritance or composition.
E.g.: https://godbolt.org/z/zGvjsjsxq
KagakuNinja@reddit
The Gang of Four patterns book, published in the early ‘90s recommended composition over inheritance. The idea is older than that.
dobkeratops@reddit
there's a point where languages didn't have inheritance .. I'd guess composition literally predates inheritance. One would have to check the timelines (what was the first language to have C-like datastructures? when did OOP appear in it's various guises? I know that C++ was inspired by 'simula')
naughty@reddit
OOP in research is 70s, mass market with C++ (early 90s) then Java (mid to late 90s).
One of the things to remember is we had Inheritance, then templates/generics, then lambdas in that order (also many languages resisted templates/generics). If they happened in a different order history would have been wildly different because the main abuses of OOP are to create poor versions of templates/generics or lambdas.
tcmart14@reddit
Depending on what you define as OO research, it can even go back to simula in 1962. Then depending on how you define OO, you could say the concepts predate that.
Blue_Moon_Lake@reddit
I never understood the "conflict" between OOP and FP.
Especially when you can almost rewrite everything OOP into FP doing equivalences like this:
But with OOP having a mechanism to neatly access all the associated methods.
Kered13@reddit
Yes, and you can write immutable objects that fit very naturally into a FP style of programming. The conflict between OOP and FP is entirely artificial.
dobkeratops@reddit
OOP was over-done ('ok we've got class based vtables but no lambdas, lets push this idea everywhere') .. then FP became mainstream as a backlash ('classes and mutability are evil, lets do everything with discriminated unions and pure functions and lambdas') .. at the same time language designers grapple with a complexity budget. I like rust for having a bit of both. (can't do FP to the extent of haskell, doesn't actually have classes but can do OOPy things with structs and trait objects, and does have discriminated unions)
vytah@reddit
Trait objects work roughly the same way existential types in Haskell do, and if you want runtime downcasting, the equivalent of Any is called Typeable.
MaxwellzDaemon@reddit
The J language, which dates from 1990, explicitly supports composition. It is descended from APL which dates to the mid-60s.
naughty@reddit
Array based programming is more function composition than what is meant by composition in the phrase "favour composition over inheritance".
MaxwellzDaemon@reddit
Thanks - I did not know that.
dobkeratops@reddit
I think all those features appeared independently earlier (which is why I wanted to check timelines), but weren't integrated into popular languages at the same time.. like lambdas were done in lisp in the late 1950s? (early 1960s) .. but it took until 2010 or so to get them in C++ . templates/generics might have been one of the later to appear but I beleive the ML-family did them before the mainstream? (wikipedia tells me ML was 1973)
naughty@reddit
Yes, all these features were present in older languages before they appeared in C++, Java or anything else with widespread adoption. There's a slight exception with templates (they are similar but not identical to generics/parametric polymorphism) but the use case for them is pretty much the same.
atxgossiphound@reddit
We’ve had all those ideas in programming since the 70/80s. If you took a course based on the wizard book (SICP, Structure and Interpretation of Computer Programs), you were exposed to them in Scheme. In its heyday, all of these techniques were used in LISP. And then there was Smalltalk.
A lot of large C code bases used them, too (generics and lambdas via macros, object via structs and “namespaces” functions). C++ was originally just a set of preprocessor scripts for C.
Java (and C#), C++, and even Python formalized what was already common practice.
Bitterbalpizza@reddit
I recommend Casey Muratori's talk called The Big OOPs. Paraphrasing from memory, But basically original OOP was literally just composition using a "sentinel" that knew about the different parts of the object and how to mutate them. The people behind early OOP said this design is great, but the sentinel has to go and we need to build the objects in such a way that they can safely share logic and data with each other without revealing any inner implementation details. That change is called OOP and the reverted version with the sentinel is now called an ECS.
dobkeratops@reddit
i haven't watched all of this but i'm familiar with a lot of his takes.
strangely he keeps lamenting that C++ doesn't have nice discriminated unions, but then trash talks the new systems language that *does* have them..
syklemil@reddit
Depending on what you're asking about here, the answer may predate computer programming languages.
roadit@reddit
Now mention Liskov and you have a summary.
AustinBenji@reddit
This is a solid observation
winky9827@reddit
Open and closed case, Johnson.
WeeklyCustomer4516@reddit
Y aun asi cada nuevo lenguaje nos tienta a crear esa clase base llamada Animal
jewdai@reddit
We care more about what things can do and less about what they are.
Composution often leads to a facade pa
elperroborrachotoo@reddit
All the time, ever since I bothered with more than just cranking out code.
Interface inheritance is the strongest coupling between two components, it's a derived-is-a-base relationship (see Liskov substitution principle).
It's correct for pure interfaces as long as the is-a relationship hold up. (Old interview question: should
circleinherit fromellipseor vice versa?)private implementation inheritance is comfortable in C++, had its uses.
Public implementation inheritance is often but not always a toxic combination of the two, leading to very stiff architectures with baroque workarounds for the inevitable changes in problem space topology.
finnw@reddit
Ellipse should inherit from circle, because
I failed this question apparently. The team were keen users of "Java Beans" and their codebase had getters on everything and used them flippantly from other packages.
Cautious_Implement17@reddit
circle does have major and minor axis. they just happen to be the same. every operation that’s valid on an ellipse is valid (if trivial) for a circle. on the other hand, it’s unclear what SetRadius should do for an ellipse.
in any reasonable program, neither would inherit from the other. they should both implement interfaces for commonly used geometric functions (eg GetArea).
maskull@reddit
There have been some programming language designs (none mainstream) that proposed conditional inheritance as a solution to this. I.e., you could express "a circle IS-A ellipse WHEN width = height". You could overload methods on the conditional subclass and they would only be called, dynamically, if the condition was met.
nicheComicsProject@reddit
This is how at least Haskell and Rust do parameterised type classes.
nightwood@reddit
You only have to debug one massive inheritance tree code-base to know why. So my guess would be: about three years after OOP became the way to go.
zackel_flac@reddit
Since forever. There is a reason why C is still taught and used wildly. Junior and intermediate dev usually think the more recent the better something gets. That's absolutely not true. OOP & Functional programming are good examples of theory not aligning with reality.
Absolute_Enema@reddit
C is taught due to inertia.
Princess_Azula_@reddit
I think they were refering to the fact that functional programming and oop are levels of abstraction above how code actually runs on a computer and because of this it makes it harder to work with in certain cases.
For example, I've never felt the need to use functional code in an embedded environment, and I've very rarely wanted to ever use OOP. Also, in many cases functional code usually takes more memory space than procedural code and using functional programming concepts, like not using mutable states, directly clashes with the registers used in a microcontroller which are literally mutable states in physical form.
SerdanKK@reddit
That's an implementation detail imo. SQL is a very high level declarative language, but no one sane would argue that it is therefore nonperformant.
FP code gives you certain guarantees that remain true when compiled. It's fine for the compiled code to mutate in-place when the compiler can prove doing so is equivalent to the human readable code.
Princess_Azula_@reddit
I was refering to the fact that embedded programmers need to read from and write to register values directly, instead of compiling code to do so implicitly like on desktop applications. For example, reading the values given by a GPIO pin, input from an I2C line, or a timer value.
SerdanKK@reddit
You can do that with any language. There's a lot of embedded Java in the world for example.
Princess_Azula_@reddit
A lot of the reasons why you would use OOP, specifically OOP and not as a part of a language that is built around using OOP, don't exist when you're working with small embedded applications. For example, there are very few reasons why I would need to instantiate multiple Objects when I just need to read some data from a sensor and do some digital filtering on it.
SerdanKK@reddit
I think OOP is a bad paradigm, so I'd argue it's always unneeded complexity.
Princess_Azula_@reddit
It has its uses, like any design pattern or paradigm.
SerdanKK@reddit
It's possible for a tool to be strictly inferior to the alternatives. If you want to hang up a picture you're not going to grab a rock from the garden to hammer in the nail. You could, but you're not going to.
Princess_Azula_@reddit
But a rock is great at being a rock. A hammer isn't a rock. You could have a hammer in a rock garden but you're not going to.
SerdanKK@reddit
I'm saying OOP is inferior to the alternatives. You can poke holes in the analogy if you want, but I don't think that's a particularly interesting tangent.
Princess_Azula_@reddit
I guess you could think this. Have fun not using OOP when its suited for certain tasks. Good luck.
SerdanKK@reddit
Oh, I will. Coding is fun.
It's kinda funny that you seem to assume my work ever involves those "certain tasks" that OOP is supposedly better suited for than FP.
Princess_Azula_@reddit
They're both tools that are useful in different situations. I realize that you're just baiting me with this falacious arguement, but I just wanted to point out that you're also making the same mistake as people who overuse OOP for anybody who decides to read this later on. Go and have fun, as you said, using a rock instead of a hammer.
zackel_flac@reddit
Let me put it the other way around. Can you prove that a better macro system is making better programs?
At the end of the day, everything boils down to assembly. Your program, how complex the initial language is, ultimately just runs on a Turing machine. Better, any program out there can be written as mov assembly instructions.
Higher level language makes devs faster, by eliminating a bunch of errors. But moving faster does not mean you are moving better.
SyntheticDuckFlavour@reddit
And that has nothing to do with "composition is le bad".
zackel_flac@reddit
If inheritance was "le best", C would have disappeared in favor of OOP already. That was my point.
syklemil@reddit
This is a frankly bizarre take in a world where C absolutely has retreated mostly to some entrenched niches, like the Linux kernel and embedded programming; the vast majority of code being written is in various hybrid languages that permit plenty of both OOP and FP.
SyntheticDuckFlavour@reddit
nah
SerdanKK@reddit
How does FP misalign with reality?
syklemil@reddit
Also, what does FP mean to them? It's one of those words where as soon as something from it enters the mainstream (like lambdas), it stops being FP and starts being "just normal programming".
These days it seems to mean something like statically typed, immutable, pure functional programming, which might not be all that recognisable for people who were talking about FP a couple of decades ago. By older measures, Java grew some FP capabilities back in Java 8, and has been getting ever more hybrid since.
Functional programming isn't some synonym for Haskell.
SerdanKK@reddit
Agreed, but even if we were talking about Haskell I just don't see how "misaligned with reality" is a meaningful critique. What does it even mean?
syklemil@reddit
I have some conjecture, but I'd ultimately just be picking apart a strawman. I will say, however, that their response to another commenter,
betrays a confusion of ideas. Assembly compiles to machine code, which runs on actual electronics, unlike the Turing machine, which is a mathematical abstraction over any sort of computation, electronic or no.
Plus, given the development and features of modern computers, ideas about "low-level" languages are quickly becoming outdated.
ClimbNowAndAgain@reddit
In the past, I think inheritance was taught wrong. Always using real-world examples. Inheritance should be about interface. Is-a. Not sharing code. The Liskov substitution principle is the most important thing. If you want to utilise my code, send me something that supports this interface and I'll deal with it.
And then there's a few patterns that use inheritance like template method etc.
Everytime I see a type-check in code, I think that's a failure. I work on, at most... interface, abstract base, concrete. There shouldn't be more levels than that.
I counted 12 in some code the other day. I despair.
I'm pretty sure I remember Scott Meyers writing that you should give yourself a kick if you find yourself checking the type of something to decide what to do with it, but I can no longer find the quote. Apologies to Scott if I misremember that. It's a good guide though.
ldrx90@reddit
I saw it gain traction in 2008-2010. So probably before that.
dwighthouse@reddit
This video goes over the long and complex history of object orientation and the usage of inheritance, as well as the motivations behind it.
https://youtube.com/watch?v=wo84LFzx5nI
TLDR: From the very, very beginning, inheritance was created to avoid extra typing. OOP was created to model systems that were already inherently structured object hierarchies. Even from very early on, as soon as they tried to use it for something else (most real life program data), it was just as big of a mess as it is today. So why didn’t other forms of behavioral sharing get used instead? There’s a video for that too:
https://youtube.com/watch?v=QyJZzq0v7Z4
TLDR for that video: It was an accident of history, and we are still dealing with the consequences, but there is light at the end of the tunnel.
firemark_pl@reddit
For several components you can write docs and unittests. With inheritance is too big risk to create a god object.
Supuhstar@reddit
Even when all of the state in the entire inheritance hierarchy is constant, you can never know if the method you’re calling does what you expect it to do, because it might be on some subclass which overrode that method to do something else
BeABetterHumanBeing@reddit
I mean, composition and inheritance aren't interchangeable. If you're "favoring" one of the other, it probably means you don't recognize which one you should be using.
wademealing@reddit
Just skip both if you dont need it. Why hide your data in objects when you can just use functions on data. Seems like extra work with a pay off in complication.
tiller_luna@reddit
straight to callback hell the moment you try to make something flexible, yay
wademealing@reddit
I don't think that functions require callbacks.
BobSacamano47@reddit
Remember that "favor composition over inheritance" is advice from the early 90s. A more modern suggestion would be "understand that composition and inheritance both exist and use what's most appropriate. Also try not to ever have inheritance more than 3 levels deep."
tiller_luna@reddit
As another person in the thread said, I've also been reading "favor Y over X" rules in engineering as "when tempted to do X, pause and consider doing Y instead".
BobSacamano47@reddit
I do like that better. Both imply that X is bad, which I think is a mistake. Certainly in this case.
Xryme@reddit
I once worked at a company where their game engine had over 35 layers of inheritance on their main game object, it sucked trying to navigate that codebase
sbrick89@reddit
in general the industry has agreed that shared state is difficult. this is leading to the rise in immutable objects and more functional programming (which is usually just an external method to calculate/operate against inputs rather than method on an object with state) - and this style is usually about a thousand times easier to test. Additionally, shared memory is faster but problematic in many ways compared to object copies, due to the same challenges of state tracking.
so composition of services is far more related to the functional programming style of isolated code that is easier to test and understand.
Full-Spectral@reddit
I've moved on to Rust, so this is a no-op for me, but there's a lot of bad takes on this whole subject. There are people who will act like OOP itself, as a concept, is completely unworkable not just in practice but in theory. This is just silly. It can be used to great effect and kept very clean, in theory, it's just the in practice parts that are hard.
I used it to very good effect in a very complex code base in the field over a couple decades. But, that's because I practiced the theory. I took the time to refactor when that was needed, I kept my hierarchies shallow and clean. I added optional functionality via virtual interfaces along the hierarchy where needed to avoid the 'God Class' problem, etc...
The problem is that, in commercial development, that's hard to do because of pressures to make minimal changes. Inheritances is such a flexible system that it will allow this to go on almost ad infinitum. In the end, you can wind up with an incomprehensible mess.
I don't find myself particularly missing it in Rust, and I'm just as able to create a clean system with composition, because I'll do the right thing. And it's at arguably harder to abuse, which makes it more practical in the real world over time.
Probable_Foreigner@reddit
The evils of inheritance are overstated. Today I see a lot of inheritance replaced with function pointers assigned at runtime (think callbacks and the likes). This is infinitely worse as compile time(inheritance) analysis is much easier than runtime(function pointers) analysis.
MrMo1@reddit
Personally I prefer composition over deep inheritance structures. Imo if you have objects that have 2,3 or more super classes in the inheritance tree it becomes difficult to maintain and write new code. I generally find it easier in such scenarios to use composition instead. Inheritance is still fine and works really good if you have only 1 super class e.g. depth of inheritance tree is 1.
anengineerandacat@reddit
Generally speaking because it's easier to read and deal with for more complex projects.
You switch the question from "is a" to "has a" and our tools work better with the "has a" relationship.
You also prevent a good class of foot guns where inheritance can cause code paths to execute that weren't immediately clear.
I think when IoC and DI became far more mainstream that sorta spelled the death of inheritance as well.
You simply have your interface (contract essentially) and then a concrete implementation and if you don't want things to execute you no-op the operation on the implementation.
This way from an implementation standpoint with other classes you simply inject the right class for the task in the workflow.
If you have complex orchestration needed this is where the facade pattern comes in as well, where you inject both implementations into the facade and pick/choose at that time what needs to be ran.
At every stage of reasoning you "know" what is going to run based on the coded conditionals.
Does get funky though once you involve things like interceptors, aspects, etc.
Testability is another important element as well, mocks become easier to create and isolating what needs to be tested is easier.
TheVenetianMask@reddit
I think the contract used to change less than the implementation. For example you'd get a bunch of UI elements and they'd be THE UI elements (as in, for like 50% of world wide computing) and you'd make child objects for specifics. Same with databases, etc. Meanwhile the components themselves were changing all the time, partly depending on hardware limitations, so the logger or cache that you'd bring in could be wildly different and a single object supporting more than one implementation made no sense.
Now the contract-giver changes every few years and there's like 70 of them but the logger and the cache have almost language agnostic APIs that haven't changed for a decade. Once you have sorted out how to compose them it's going to look nearly the same forever. If you made them an object hierarchy instead then you have to figure out anew how to jam them into whatever the framework-du-jour gives you.
arekxv@reddit
The true answer is balance. You need both. Too deep inheritance is bad because it is hard to reason about but too much composition is bad because it separates the code out too much and when not implemented well (often not, same as in OOP) its hard to follow and there are too many things to jump around.
There is a task better suited for OOP and better suited for composition, there isn't a clear winner.
Nakasje@reddit
The untold story.
"is-a" relationships are sufficient for abstractions. A square is a shape.
"has-a" relationships are necessary for dynamics. A child has parents.
We used to develop dead static digital calculators. Evolving to interactive machina has forced* developers to favor composition.
Forced*, because the education system - that train our brain in urban areas - was, and still, all about abstractions.
zvrba@reddit
Though square is not a rectangle :)
vytah@reddit
A square cannot be a mutable rectangle, but it can easily be an immutable rectangle.
shevy-java@reddit
That model only works for when there are singular features. In reality many features are not singular.
We have ... class Animal, class Whale, class Cat. All seems easy, but once you go towards more details, suddenly certain things emerge that are no longer linear. Cats have a tail. Well, not all - Manx cats are tailless. Some animals have more colours (colour range) than others. And there are albino variants. And numerous more different traits that are there or not there, depending on how one goes to the level of things. Say you have code that represents all of that; you describe a game world where these entities are representations in 3D too. Can entity1 talk; breath; swim; run; fly; carry stuff. I don't really see is-a and has-a being a useful criterium in regards to composition versus inheritance.
Well, OOP is an abstraction system too. But OOP is defined differently. Java's OOP versus Ruby's OOP are different. Ruby prefers an OOP with a stronger focus on introspection; Java is more the "oldschool" variant with a stronger focus on encapsulation and "you should not poke at the internals". Which one is better?
NotGoodSoftwareMaker@reddit
Inheritance is mostly taught in schools because its an easy way to get started with building more complex programs
In schools and academic worlds everything is discrete, follows theory and is usually one off. So everything fits into the same structure again and again ad infinitum.
Composition is just another way to do things but IMO still becomes extremely complicated, it at least evolves better.
Im more a fan of trying to keep things modular as possible. The major thing you are trying to achieve 99% of the time is human understandable code. Minimise for cognitive burden with small series of discrete inputs and outputs help with this.
leftofzen@reddit
another vibe coder blog from someone who clearly has taken the line verbatim rather than thinking about that it actually means and why such a pattern would be recommended, and applying it appropriately to their work.
ExiledHyruleKnight@reddit
Because "A Car is a Vehicle like a boat and a plane" is a great analogy for a inheritance.
But you'll (almost) never come across a problem like.
Vehicle having a type value, and a "Action functor" now, your cooking, and every time you have a new action you don't have to make a brand new object.
florinp@reddit
The rule is :
This rule is old (around 1990)
josephblade@reddit
It's rather straightforward: if you want to re-use functionality, specifically a sub-section of functionality and data, it is a waste to use your single inherit to get it. Similarly it is a waste to use c++ (and others?) headache of multiple inheritance.
Now if you want all the functionality, subclassing makes sense. But if you are just interested in a subsection, split it off into an object and incorporate it using composition.
One nice way you can still use this in a generic / isA way is by using composition and interfaces. I don't use it a lot but something like
this way you can do:
As I said I don't use this every day but if you have code that wants to operate on a composites used in disparate classes, it does the trick.
Inheriting really should be reserved for "I am part of this group" or "being X is at the core of this class"
durimdead@reddit
Unless it changed since last time I used it, C++ doesn't have the concept of interfaces, just virtual and abstract classes. So if you were to do the same thing as your comment above, you'd be required to inherit from an abstract parent class with all abstract functions (as this is basically what an interface is). This would act the same as your "AddressSource" interface above.
As much as allowing you to inherit from multiple classes sounds completely awful, it is (or at least was) the only way to implement multiple "interfaces" in C++
joahw@reddit
You could always do the old c style struct of function pointers approach, but that isn't very ergonomic to say the least.
Professor226@reddit
If you write the same code in a bunch of components, then you might need inheritance. If you are writing code that isn’t used in all the children, then you might need composition.
dkarlovi@reddit
Inheritance leads to over sharing, everything is all up in everything else's business, it's like an ever growing house of cards.
Composition leads to much tighter, more locked down code which, ironically, allows for much richer, more flexible and robust APIs.
pjmlp@reddit
For decades, one of the design decisions on Visual Basic was to only use composition, and COM only allows interface inheritace, you always need to use composition and delegation.
The problem is the amount of people that teach OOP badly, or that only learn as they go without the theory of what goes where.
danielv123@reddit
Since I first learned inheritance. I am sure there are places it makes sense. They are just few and far between.
Jonny0Than@reddit
https://thecodelesscode.com/case/83
Blothorn@reddit
It’s multiple inheritance, too—a class that inherits from multiple others can easily become a mess of unrelated aspects with little clarity. This is especially if the method/variable names from the different ancestors don’t clearly indicate what they relate to—if you have ‘Pigeon: Walks, Flies’, which behavior does the variable ‘speed’ affect? If you use composition, everything is autocratically “namespaced”. Even without multiple/nested inheritance, though, inheritance has a problem in that adding methods/variables to the superclass is always a potentially breaking change.
shevy-java@reddit
Agreed. It can apply to either variant. You could also add swimming, so we have three speed ranges here.
There is a lot of detail.
RGB755@reddit
Game engines / game dev in general often uses composition to attach scripts to GameObjects, because it makes it much easier to reason about the functionality of complex objects.
I’ve done dev work with both systems, and when you’re deep into a FlyingShootingDodgingFlappyNighttimeJumpingHumanoid inheritance, it’s mind-breaking to figure out what errors are happening at what level, particularly when dealing with code you didn’t write, or perhaps didn’t recently see.
The advantage with composition in that context is a much flatter behaviour hierarchy. Check if GameObject has-a Flappy component -> if it does, go do flappy things.
oneandonlysealoftime@reddit
Inheritance even without state is pain. Traveling through a network of inherited methods, where parental methods call methods of inheriting classes is not a fun thing, when troubleshooting an app
shevy-java@reddit
Ok but you have this in any larger code base too when that one is only using functions. So that is more a problem of organisation and complexity. About 30 years ago, single-chain inheritance was the rage. I even remember the advertisement done for Java back then, for instance (to some extent).
shevy-java@reddit
Classical inheritance is too inflexible in some cases. For instance, take the Tree of Life by Darwin. This does not reflect true inheritance in all cases; it does not work for bacteria, for instance, as they exchange a ton of genetic information to the point of no longer being able to say that this is a "species" of bacteria. some megaplasmids are larger than the smallest prokaryotic genes, for instance. This is one example of many omre. Composition simply has more flexibility to offer.
kyune@reddit
When I think about this it's hard not to laugh because there are absolutely intracacies I understand from both sides but it also makes me think of someone complaining about the differencrs between "read" and "read". And then doubling down by saying "you probably interpreted those prounounced as "reed" then "red"" lol.
When I think of it that way, it helps me to understand why we favor composition over inheritance--the letters compose the word, but only through context do we really understood what was meant. Specifically, every layer of inheritance creates layers of context, but the problem is root/parent oriented instead of branch oriented. In the above example, the root context/concept of "reading".
In that sense, coding often becomes a messy endeavor once it meets the real world, and is rarely pure. And....in that sense you cannot fully escape state, because state is always present; outside of basic CRUD activities you are always fighting with state somewhere even in a "stateless" application (i.e. the DB). So when you have multiple layers of context that could possibly change at any time, composition is a fsr more trustworthy concept as a programmer than an indefinite number of layers of abstraction (which we already have simply by writing code with dependencies)
I'd argue both shared state and deep inheritance are a problem but those too could also be contextual. Interfaces and implementations will probably be used perform different operations on common objects (shared state), but deep inheritance
gwillen@reddit
The specific inheritance-based pattern that I see making code hardest to understand (and easiest to break) is something I've sometimes heard the PL theorists refer to as "open recursion". This is the property of a system of implementation inheritance whereby, if one calls a member function from the base class, one ends up running the subclass implementation.
If one codes in such a way that this comes into play, now every single place where the implementation of one method in the base class calls another method has become part of the public interface, because anybody who inherits from the class can change the behavior of any of those callsites.
The usual result, unless this is done VERY CAREFULLY, is subtle bugs somewhere down the line.
FlyingRhenquest@reddit
https://wiki.c2.com/?CompositionInsteadOfInheritance
A lot of new-ish OO programmers make the mistake of trying to model everything exactly like it works in the real world, which is really not the correct approach no matter which one you pick. Instead of trying to model this huge thing from which you only need a tiny piece of data, think about the data that you need to accomplish your current task and the object it needs to live in. You'll find the design growing much more organically and suiting your needs much more than trying to force a hugely complex viewpoint on a system you've only barely started working on. Test first/test driven design enforces this by having you focus on just the piece the next test needs to pass.
I've implemented some hugely complex systems and when I do use inheritance hierarchies they are seldom more than two or three objects deep. But I also tend not to use a lot of object composition. Instead, my systems are frequently three or four libraries of objects communicating with each other without a lot of stuff that I was never going to need. This avoids the interface/implementation labyrinths that are very common in enterprise Java and the bizarre object relationships you see in some C++ code.
Leading-Ability-7317@reddit
No one has mentioned the main advantage for me.
Composition makes things much more testable. I can pass mock instances to my class under test. Makes testing a breeze and your tests are much clearer.
devraj7@reddit
They are not mutually exclusive.
A better way to put it is: "Implement inheritance of implementation by using composition".
bunkoRtist@reddit
When programming became more popular and less sophisticated is the answer. There is a time to use composition, a time to use inheritance, but inheritance is more complicated (including for compilers), so it has naturally grown less popular. It's harder to work yourself into a terrible situation with composition.
jonas-reddit@reddit
Maybe not only less sophisticated but we slowly realized the difference between is-a and has-a. I feel we were overdoing it a bit a few decades ago.
White_C4@reddit
There are four key advantages of interfaces over inheritance:
Interface is a pseudo multi-inheritance model by allowing the class to have multiple shared type properties. Inheritance doesn't have this luxury and only shoehorns you into one upward derived type (parent, grandparent, etc.).
Inheritance assumes that all the states and methods provided to the subclass are all necessary. There are cases where the subclass might not make sense for certain states/methods created by the parent. Subclasses being too hyper specialized can run into problems when the parent starts making changes that affect the subclasses.
Interface doesn't run into the risk of shared state problem. Since interfaces are about functions, every class that implements have to manage their own states.
Interfaces are re-usable. Multiple classes that have no similar states whatsoever may have a shared type such as Serializable or Renderable in order to achieve a goal of producing a common output. It's impossible to have a parent class in inheritance model that could be serializable and renderable without no longer making any sense because some subclasses might not want both of them.
edgmnt_net@reddit
I don't think it's fine without shared state. One underlying cause surrounds the Liskov substitution principle, as mentioned, and basically boils down to the fact that inheritance-based hierarchies are brittle: you can't really know if overriding something used by a method of your base clsss isn't going to break stuff, now or later, so inheritance-heavy code isn't very extendable especially when considering code not under your control. Another thing is inheritance does not yield composable abstractions by itself: it's easy to extend something but not in a reusable way, basically you can't just swap the base class with a different one (e.g. RateLimitedHttpServer <: HttpServer won't do you any good if you want rate limiting transplanted onto an HTTPS server class, so maybe you should have a standalone rate limiter too, not just that subclass). Thirdly there's the matter that it confuses behavior with interface, which should be distinct concerns (abstract classes are kind of an abomination). And less controversial, more legitimate uses of inheritance tend to be relatively rare, it saves you the trouble of writing code that does method forwarding but that's not very significant usually.
Revolutionary_Ad7262@reddit
This is true
However for me composition is just more elegant. It is easier to understand, it does not require any additional features in the language and in all cases leads to better and more maintainable code
Simulated_Reality_@reddit
this