There 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.
I 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.
Yeah, 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), 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.
Not sure if I understand you correctly here. With "the coupling ... is just too strong to ignore" you mean that if someone has one of the subclasses at hand, they should automatically have all the extension methods at hand not having to look for them somewhere?
> 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.
I think that's a sign that you have developed a good intuition of that it can lead to problems! :)