5 ms·
After reading all the comments of many confused and curious, here is when inheritance is bad and why so _in the absence of any performance considerations_. Fir
by valenterry 6y ago
After reading all the comments of many confused and curious, here is when inheritance is bad and why so _in the absence of any performance considerations_.
First, inheritance from an interface/trait is totally okay. The problem is class inheritance, meaning implementation inheritance.
There are two cases:
1) you inherit from a class and only add methods but don't overwrite anything. This is the good case, you can do that
2) you inherit from a class and overwrite one ore more of its methods. This is the bad case, no matter how you look at it. And here is why.
Look at the following classical simple Stack class:
class Stack(...) {
def push(element) returns nothing = ...
def pushAll(elementlist) returns nothing = ...
def pop() returns element = ...
}
Now imagine you want to keep a count of the number of elements, so you decide to extend the class:
class CountingStack(...) extends Stack {
override def push(element) returns nothing =
count += 1; super.push(element)
override def pushAll(elementlist) returns nothing =
count += elementlist.size; super.pushAll(elementlist)
override def pop() returns element =
count -= 1; super.pop(element)
}
Looks good no? Can you spot the problem?
If the Stack class implements pushAll as iterating over the elementlist and calling push for each element, then the CountingStack will count twice!
We could try to just not overwrite pushAll in CountingStack - but what if the Stack class is later changed to implement pushAll without calling push? Then CountingStack would now miss counts.
This problem is impossible to fix and it is a general problem - when overwriting a method, you can never be sure that semantics could break when the base class is changed.
So what is the solution here? Simple: use composition over inheritance.
class CountingStack(underlyingStack) {
def push(element) returns nothing =
count += 1; underlyingStack.push(element)
override def pushAll(elementlist) returns nothing =
count += elementlist.size; underlyingStack.pushAll(elementlist)
override def pop() returns element =
count -= 1; underlyingStack.pop(element)
}
Problem solved - no matter how Stack is implemented, our CountableStack will always be correct. The drawback (if you want to call it one) is that we need some common interface/trait that both Stack and CountableStack will implement.
I hope that sheds some additional light on why (implementation) inheritance is often considered a bad practice.
- titzer 6y agoThere are lots of bad uses of implementation inheritance, but it's not all bad. One pattern I use a lot is the "just these 5 missing methods". The base class might be complicated and large, with a lot of logic driving the process, but it needs to have 5 specific functions that it calls. One way is to have that big base class have almost all the logic and then 5 abstract methods and expect a subclass to implement those 5 methods and not override anything else. While the base class could in theory accept 5 first-class functions, or an object with 5 methods, in some cases those 5 methods are intricately tied to the operation of the whole thing and it doesn't make sense to separate them. That's a perfectly safe and clean use case of implementation inheritance.
- valenterry 6y agoI think for the usecase you mention, there is a different solution that I personally prefer. Optimally, if your language supports it, just define an interface with these 5 methods and then define extension methods that work on any type that implements the interface. The reason why this works is that all the other functions are usually helper functions / convenience functions and they only need the other 5 functions to work, so no need to access any inner/private properties. That is by far the most lightweight solution, and it works even for 3rd-party libraries where you can't control the types. If your language does not have extension methods, then you can still use composition just like in the example I gave and delegate to the base class. That is a bit more code to write (because you have to delegate to the non-5 methods if you language doesn't automate that for you, some do) but I find it cleaner than hoping that no one overwrites any non-5 method. But yeah, essentially what you are saying here is to ask people to follow the rule I gave by convention - depending on that context that might work as well.
- titzer 6y agoYeah, I generally like composition better, but sometimes the coupling between the base class and those subclasses that provide the 5 methods is just too strong to ignore, and if you break out the methods into another interface, then you are struggling to find a place to put the shared logic (which you proposed to do with extension methods). Your example with the counting stack made use of "super", which is always a red flag to me. "super" is such a bad smell to me, that I literally never use it. In fact, in Virgil, my language project (http://github.com/titzer/virgil http://github.com/titzer/virgil), there is no super construct at all, nor static methods or interfaces for that matter. 15 years of writing it, 200k lines later, and I can say that personally, having delegates, first-class functions, partial application, and tuples go way further than more complex trait/interface/extension method madness.