Implementation inheritance is often justified as a way of "reusing" code, but as it turns out, we've merely introduced undesired coupling instead of the seamless reuse we might have expected.
Implementation inheritance is often justified as a way of "reusing" code, but as it turns out, we've merely introduced undesired coupling instead of the seamless reuse we might have expected.
You've touched on a whole host of performance issues here too. Not only are we making indirection after indirection when accessing virtual methods and data (my poor cache), but invariably these are all stored in heap memory, so we get the worst possible performance profile baked in.
> the actual code that gets executed depends on whether the object is one of a "derived" class, where that method might have been changed in ways that might break any amount of expected invariants.
On top of that inheritance makes the concrete type unknowable at compile time just to add to the pain.
> Implementation inheritance is often justified as a way of "reusing" code, but as it turns out, we've merely introduced undesired coupling instead of the seamless reuse we might have expected.
Yes this 100%. Inheritance actually stands in the way of code reuse. If I have a class that is kind of similar to another, but different enough to "require" being another class, oh dear they're now incompatible. So you try to kind of fit it into a subclass, but it makes more sense in another, and now you're spending mental effort on a problem created by architecture and not the actual thing you're trying to solve.
This is really clear in the game development world, where game objects often just don't fit into the inheritance format, even with multiple inheritance. This is why most complex games are built with the entity component system pattern as it focuses entirely on composition.
Perhaps the crux of the issue is that inheritance encourages design by "identity" (mental concept) rather than "attribute" (data). Thus to solve problems we must mentally classify them into some taxonomy as if they are animal species before we even write any code. This is just extra work in exchange for all the issues mentioned.
It's not supposed to, and if it does, the design is clearly broken. Now the (rhetorical) question is: does it happens often. The not so rhetorical question is: is it possible to make it happen rarely. The even more interesting question is: is it easy / more practical compared to alternative approaches, and if not then what is the point.
My opinion on SOLID is that there is precisely one hard "principle", and it is worthwhile: the LSP (it derives directly from logic, that's why). I believe that Open-close is even borderline insane, and that if there is a crazy way to make it not insane, this way is probably applied by so few people that it may well be irrelevant -- most people will try to apply it in ways that will quickly put them at risk for LSP violation, but LSP is far more important (or they will try to apply it in bad places, but that is another story). Plus programs designed with complex hierarchies are often missing the point of execution contexts, and then understanding them is absolute hell -- their original authors sometimes do not understand them themselves. (The rest of SOLID are soft attempts at fixing self-inflicted wounds, sometimes even reasonable if you insist on doing Javaesque / old-C++-esque OOO, but I digress.)
I'm not sure why anybody thought that kind of OOO was a good idea, or that the main characteristic in interesting big programs was the usage of "classes". I even find the suggestion of causation instead of mere correlation dubious; they were already quite a good number of big programs before, and what permitted the explosion of program size was more the ever growing capabilities of computers, that happened, in affordable versions, during the Java-like OOO hype. Besides the simplistic reductionism (we can start with: which classes model entities, which classes model values, which classes are controllers, etc.) which is not too much a big deal in practice, modeling with class diagrams is often missing the river in the middle of the forest for some groups of intertwined trees.
The Smalltalk-80 container hierarchy demonstrates that you can get a lot of mileage out of simple single-dispatch virtual methods with inheritance. The Smalltalk-78 system, which you can try at https://lively-web.org/users/bert/Smalltalk-78.html (although it's not working for me at the moment), got a multiwindow GUI with an IDE running usably on an Intel 8086 with 256KiB of RAM, in only about 100 classes and 2000 methods totaling 200 KiB of code. This is not what I would describe as a "big program", but it is a fairly impressive program nonetheless. Seeing that kind of thing is what led people to adopt object-oriented programming.
In principle, with a sufficiently-robust type system, the invariants expected by the base class would be encoded in the typing, and the derived class would only be able to narrow them.
(Of course, OOP languages tend to either be untyped or have insufficiently robust type systems for this; robust type systems are more frequently associated with functional languages.)
why would you make internal methods virtual though?
So, for example, Java's Map is an interface type with, for example, .get() and .put() methods; TreeMap and HashMap implement those methods differently. There are similar things in Smalltalk-80 and the C++ STL.
So clearly you think Java, Smalltalk, and C++ are "a bass-ackward parody of proper OO design." I'm interested to hear what systems you consider paragons of proper OO design.
You might find the book Program Development in Java: Abstraction, Specification, and Object-Oriented Design by Barbara Liskov and John Guttag very useful for understanding OOD/OOP.