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