Most of us have bad memories of over-complex hierarchies we regret seeing, but this is probably due to the dominance of OOP in recent decades.
This sentence here is why inheritance gets a bad reputation, rightly or wrongly. Inheritance sounds intuitive when you're inheriting Vehicle in your Bicycle class, but it falls apart when dealing with more abstract ideas. Thus, it's not immediately clear when and why you should use inheritance, and it soon becomes a tangled mess.
Thus, OO programs can easily fall into a trap of organizing code into false hierarchies. And those hierarchies may not make sense from developer to developer who is reading the code.
I'm not a fan of OO programming, but I do think it can occasionally be a useful tool.
Yeah that's a good point. It's telling that inheritance is by design difficult to change unless you follow very specific rules of good OO design patterns.
I guess it's easy to write bad code in any programming paradkgm but inheritance makes it easy to screw up.
Lots of people equate OOP with inheritance, and I think that does a disservice to OOP code style in general. The original definitions of OOP had nothing to do with inheritance at all and this relationship was only popularized by a few languages (which others then copied due to aggressive marketing).
IMO the more important parts of OOP that are still useful are encapsulation and message passing which can be done in languages without inheritance.
I would caution against throwing out a paradigm before of one aspect of of that languages added onto it after the fact.
There are some infamous cases of OOP abuse in the 7 digit LOC embedded system codebase I work in. I have known several developers that created these sorts of overly-engineered inheritance hierarchies seemingly just for their own sake. It's awful, and it's even worse when the original author leaves the company and leaves these sorts of unmaintainable blocs in our applications.
But I think there are definitely places where OOP is not only beneficial but just the correct solution for a part of the application. GUI for instance--all the various widget types and how they plug in to the UI system to handle mouse events and get drawn in the correct Z-order--it's very intuitive. Of course you will find a subclass of a subclass of a subclass of Button every once in a while, and in these cases I do look for opportunities to use composition over inheritance.
By the way - can we talk about the author's weird definition of composition? I've always defined composition as a class with a "has-a" relationship with other classes. But this author still defines it as classes with a "is-a" relationship to other classes, or in this case a single "generation" of inheritance. That seems bizarre to me, especially when they give this example:
In composition, this is a simple task: go to Dog class, add getTreat call in between eating and pooping. Done. 💪
And yet, their example of a Dog class following composition literally inherits from the Animal interface and does not have a point in which Dog can, internally, insert this getTreat() call:
class Dog implements Animal {
doStuff() {
consumeEnergy()
println("woof")
rest()
}
}
Lol... anyway, I like the sentiment of the article but I lean much farther toward just picking the correct tool for the job. Inheritance can still be the right solution in many cases and blaming a paradigm for code readability problems doesn't seem as apt as, say, realizing that even in codebases where every commit has to be reviewed, at the end of the day you still have to ship product and there is never time to keep all code in perfect order, especially in large legacy systems*.
note this may not apply to some open source projects or other projects where True Idealism can be/is exercised
However, "Dog isa Animal" is worse than useless, it actively hampers your code and makes your life worse.
Useful inheritance patterns are all over the place in GUI / Model View Controller code however. "Button isa Window", and "FileStream isa Stream", and "StringStream isa Stream" in C++. If you stick with SOLID principles, inheritance helps your code significantly.
But even there a TcpStream, a FileStream and a StringStream might have quite differen behaviour, since they all abstract very different things. So, even this simple example may fall apart very fast if you need to care about those. I'm not saying that Inheritance is always bad, but it is quite rigid and might cause problems in a large codebase if you suddenly run in to the corner cases where the assumptions the abstraction is based upon is no longer upheld.
But the fact that TCPStreams isa file-descriptor, Files isa file-descriptor, Pipes isa file-descriptor, and other such "stream-like objects" in the Linux kernel proves that the read/recv and write/send system calls are generic enough to work in a wide variety of circumstances.
Yeah, they're all different. But as far as the Linux API goes, they're all file descriptors under it all and have abstractions that work well for inheritance in practice. In many cases, inheritance doesn't work. But in many cases, it works. And works well, for decades.
The core problem I see with Inheritance is that the abstractions tend to fall apart and no longer be true. Lets use the Animal example. It is easy, when you have Animal -> Cat and Animal -> Dog. But as soon as we become more specific like Animal -> Mammal -> Cat, Animal -> Fish -> Hammerhead Shark, Animal -> Bird -> Bald eagle, we risk of getting in trouble. Because now for all purposes we assume things about the Fish, Birds and Mammals, like fish is in the sea and mammals are live on land. We know that this is not strictly true, but for everything we need it works. Later we need to handle a dolphin.... should that be a fish, or do we need to restructure the whole program. If we treat it like a fish, then we might be even deeper in trouble later if we would need to handle birth. And even if we restructure our program to be correct to handle birth, we might stil forget that some mammals lay eggs like the Platypus, so then things break again if we need to handle that.
We tend to see Inheritance as a rigid fact based structure, but the core problem is that it is just a bunch of assumptions we dictate in a very rigid way that is hard to change.
Composition have no problem with specifying the platypus as a mammal that lays eggs and have a beak.
If you look at Rust for example, then you specify the Traits instead. So, you could define a trait that defines the properties for birth, another to define if the animal have a beak, and another one to define the number and type of legs. The each animal implement these traits, which then properly can define a duck, cat and a platypus.
I also used to love inheritance. That is, right up until I started dabbling with game development and started hitting the limitations. Then I discovered ECS systems and everything changed.
Conpsition is better in every case. I know some people will say inheritance is still good for things like GUIs, but I'm not buying in. I'm now firmly in the Rust camp, and there's nothing someone could do with inheritance that I couldn't do with traits and trait bounds.
It's still cool. Ain't nothing wrong with what ain't broke. Imagine programming without inheritance in many languages. Inheritance is another tool, to perform another task. Don't be afraid to learn any tool in your toolbox. And don't sweat about problems you may never have either. And have some things inherit, you're in good company still
I've been senior for quite some time now, I know when to use what (most of the time) and yeah, inheritance has its place, though I don't use it often because most of the time it simply isn't the right thing to do.
I can think of maybe 1 or 2 places where inheritance makes sense, and I haven't encountered these in the last 5 years... (not counting implementing interfaces of course, which definitely makes sense). In all other cases inheritance is IMHO a bad decision (why I think it should not be a thing in programming languages, because it leads someone to write bad code, as it seemed to be the right thing to do...).
Inheritance is a fine abstraction. Easy to understand, but can't bring you very far. It's like a necessary evolutionary niche. It has its places, but it's most important as a gateway to get us to better abstractions.
6First off, Inheritance is not "dead". We all just learned to favour interface inheritance over class inheritance.
Secondly, class inheritance is not bad or useless, it's just poorly taught and wildly overused. The fact that the article still uses the bullshit Animal kingdom example is indicative of that. There's no value in trying to model cats and dogs in OOP.
Inheritance is pretty useful in niche scenarios, mostly involving polymorphism and probably in the context of Library or Framework code. Trying to re-use code between classes with inheritance is always the incorrect approach. Two classes that are unrelated but have similar properties don't actually need a common base class, they can each have their own version of the data.
The big, big, big problem outside the education system comes back to the top down, design-first approach. There's still this strange practice of trying to model class hierarchies and abstract classes in neat little diagrams before you've actually started writing code. No UML type document has ever survived contact with the real world. If you need any form of inheritance, it will become obvious as you build out your system.
I find that inheritance isn't that useful when it comes to helping me reason about programs, which is more important to me than avoiding code duplication. The ability to create subclasses can be convenient for adding new code but the same extensibility also means that I have to consider all potential future uses of a class when I'm using one. It's easy to make assumptions when writing code that uses a class than can then be broken by future subclasses. You can try to mitigate this by writing clear invariants in advance and updating them when you find that you need to make more assumptions, but then this means that you have to give up the flexibility of inheritance.
There are many reasons one might use inheritance. The one which is often first introduced by OOP proponents is creating a “is-a” relationships in some domain model where the goal is to somehow model “real life” with classes and objects.
This always ends badly in my experience. Not worth doing. Just consider the “rectangle square controversy” https://henrietteharmse.com/2015/04/18/the-rectanglesquare-controversy/. Inheritance as a domain modelling tool doesn’t really mean anything outside of textbooks talking about animals and vehicles.
One should understand how computers and programming work and model data in a way which makes sense for the concrete scenario. Look in any algorithms and data structures book. Nothing is ever explained and presented through object models. No UML diagrams needed.
But inheritance also bring actual concrete functionality. In particular dynamic single dispatch. If you need dynamic single dispatch, inheritance is good. That being said, I think that parametric polymorphism (“generics” in OOP terminology) is a better solution than subtype polymorphism in almost all cases. Although languages build around OOP and inheritance sometimes make it too inconvenient to use.
Great article. I think it's been a long time coming that OOP gets a serious second look, especially regarding the blind adoption of design patterns that add complexity for the sake of code isolation and segmentation.
I think a bonus point in favour of composition here is the power of static typing. Introducing advanced features like protocols can bring back some of that safety that this article describes as being exclusive to inheritance.
Overall, I think composition will continue to be the future going forward, and we'll find more ways to create that kind of compilation-time safety without binding ourselves into too restrictive or complicated models.
Take Rust as an example, composition implemented in modern languages is really a mix of both paradigms.
In the Advantages of Inheritance section, we can emulate the inheritance way by adding a IndefinitelyDoStuff type class (or interface). The type's capability is extent by type class hierarchy and code duplication is mitigated by default implementation.
Inheritance provides a safer experience due to compile time guarantees [...] the code simply doesn’t compile until you’ve updated all child classes
Similarly, adding that to the type class declaration will enforce implementation for each instance to be added at compile time.
C++ dev with minimal experience of composition here, but this is a great article and it's something I've been trying to play with more and more. Out of curiosity, does anyone know if there are any known big pitfalls to avoid with composition (thinking of things like the diamond problem)?
The only thing I can think of: you often need to strike a balance between having interfaces that are too generic/large or too specific/small. However, you already have to worry about that in OOP (God Object vs ravioli code) so it's not a huge deal.
Another reply suggested giving Rust a try, and I do second that. But if it's not your cup of tea, at least take a look at some of the standard library traits - std::io::Read/Write and std::iter::Iterator will give you a good idea of what composition can do.