- cross-posted to:
- rust@programming.dev
- cross-posted to:
- rust@programming.dev
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 yourBicycle
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.
I think part of the problem isn’t just bad hierarchies, it is that they are so hard to fix.
Bad OOP code gets its fingers everywhere, and tearing out a bad hierarchy can be downright impossible.
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
Inheritance is useful.
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.
Inheritance here is limiting. Composition creates a far nicer API. Take both go and rust, they use a set of different interfaces/traits that cover, Reader, Writer, Closer, Seeker. Not everything implements all of these and can implement any set of them. For instance, a File implements them all, as it can be read from, be written to, closed and arbitrarily seeked. But a TCP Stream cannot random seek to other locations inside it - you cannot suddenly reset a network connection to the start again, those bytes are lost. So even if they are both file deciphers they do have different behaviours.
And these can be used for more than just files and network connections - you can write into or read from a vector/list of bytes, even seek randomly into it, but cannot close it as that does not make any sense.
Most languages cannot have multiple inheritance - so you are stuck with a abstract class with all the functions on it with blank implementations so the unsupported functions become no-ops. But IMO that is just a hack to workaround the lack of being able to split these different types up.
You’re not describing composition.
Go Files do not “hasa reader”. You don’t do file.reader.read(), you just do file.read(), that’s inheritance as file has inherited the read() method.
You’re confusing polymorphism for inheritance.
read
is a method on an interface thatFile
implements - it is not inherited from a base class. You can use thatFile
directly, or wherever aReader
interface (or whatever the name is, idk I don’t really do Go) is expected.Composition do not necessitate the creation of a new field like x.reader or x.writer, what are you on?
https://www.infoworld.com/article/3409071/java-challenger-7-debugging-java-inheritance.html#toc-2
composition is literally the “has a” relationship. That’s how its always been taught.
Man, I honestly have no idea why they are downvoting you. Composition literally means taking common behavior and placing it in external objects behind interfaces using a has-a relationship.
No idea why they are denying this. Inheritance vs composition is the same as “is-a” vs “has-a”. In composition re usability isn’t done via inheritance but code encapsulation.
Saying that in Go objects can implement many interfaces is the Interface Segregation principle from SOLID. Basically having small interfaces instead of a big interface with methods that may not be implemented by all implementors.
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.
What’s a good alternative to inheritance in your Animals example?
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.
Thanks for the explanation! I haven’t worked with Rust before, would these traits be an equivalent of C# interfaces?
Traits are similar to an interface, but with some differences. Here is a comparison with Java interfaces https://stackoverflow.com/a/69485860
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.
I remember the old times where every class in my codebase extended
Database
and I thought how clever that was and how cool inheritance is.So, what would you do instead in this case, use a DI framework to pass the
Database
capability around?(Edit: I do realize the answer is likely to be language dependent)
That would be a good solution most of the time, yes
Yeah, I find DI really scary. It practically shifts the complexity elsewhere, out of the programming language and program, and into configuration and classloading magic, that you may eventually discover at runtime.
I think I would prefer languages to embark proper abstractions for that. Scala has implicits which are not as refined as some DI frameworks I’m sure, but do the job in practice. In general, there should be a way for programs to delineate portions of code that expose/depend on certain capabilities (use a database, or more generally run asynchronous, frees its own memory, is a pure calculation), and this is an avenue being researched in capabilities-based languages.
I hear your concerns about DI frameworks and I agree it would be preferable if their config could somehow be validated at compile time instead of runtime. That being said, in my experience, runtime issues are fairly rare. Some DI frameworks even provide a simple method you can call at runtime to validate config at startup. Furthermore, you can use the DI pattern without a framework— design your classes accordingly, then create and inject the dependencies yourself instead. The point is to program against abstractions to make your code more testable, and while a framework can automate away some of the bookkeeping, you can dispense with using a framework for any of a number of valid reasons.
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.
In my experience, inheritance is really handy in the right situation. But far more often, inheritance is just a quick way to code yourself into a corner or just a waste of mental energy in the wrong place.
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…).
Good riddance.
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.
Composition ftw.
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.
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.
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.
In Python, everything is a Dictionary.
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.
Great read!
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.
Well written article that mirrors exactly what I’ve discovered over the years as well.
Typo in one part if the author is reading:
compilated
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
andstd::iter::Iterator
will give you a good idea of what composition can do.Thanks for the info! I have been very slowly making my way through the Rust book, hardly made a dent so far but am enjoying it.
I don’t know of any. Give Rust a try. Traits and trait bounds will change the way you think about code.