Everyone likes to give inheritance a hard time these days. Compose, don't inherit, so they say. Well, time for me to jump onto the bandwagon. Here is reason #6701 why inheritance is a problem.
Inheritance in traditional OOP introduces implicit single-dispatch polymorphism
at every call site. When you call .myMethod()
on SomeClass someObject
, you
may think you know what behaviour you're eliciting, but you actually don't.
Because if SomeChildClass extends SomeClass
, and later you or someone else
decides to @override myMethod()
, then the caller of .myMethod()
no longer
has any control over what happens.
Contrast this with generics-based polymorphism. When I call a generic
myProc<T>()
, at the call site, I have full control over where control flow
goes. I know at compile time exactly what will be executed. This makes
myProc<T>()
nice and usable from the caller's perspective.
Same thing for ad hoc polymorphism. When I call myProc(float x)
, I know that
it's not myProc(String x)
that will get run.
(Not all inheritance has this problem, you specifically need dynamic dispatch, i.e.: virtual methods. So this isn't as much of a problem in C++ for example, since you know at the call site that unpredictable runtime polymorphism might occur because you know can see the method is virtual.)
Of course, the fact that you don't know where control flow is the whole point of OOP, right? Hide the implementation. Yes! Absolutely. Information hiding is awesome. Modularity all the way. But, as has been pointed out by many before me, the object just never seems to be the right scale at which to create an interface-implementation split. Modules are.
This ends up incentivising code which is really hard to understand, where you
have to jump around between classes a lot. Because let's be honest: most
methods or procedures just don't have the kind of in-depth documentation of
exactly what it's supposed to do and which post-conditions need to be
maintained, information which is critical for a person extend
ing your class
in the future. So TheirChildClass
ends up violating that implicit
post-condition which callees of the method are quietly depending on.
This reminds me of nothing less than the dreaded goto
. goto
is bad because
it creates an unstable relationship between the goto
er and the goto
ee. The
goto
er gets no guarantees about if, when, nor how execution will return to
it. This is implicit control flow, because the goto
could send you anywhere
compared to the current statement, before or after, creating the potential for
complicated unexpected dependencies.
With inheritance, you have implicit polymorphism. Sure, you're still doing structured programming, so at least you know the method will return. So there's no uncertainty in the caller control flow. But there is unpredictability in the behaviour of the method call, which leaks upwards to make the caller unpredictable as well.