I have inherited old Java, PHP, and C# code bases with more than a thousand classes, lots of inheritance, and no composition. It can be so difficult to answer the question, "What happens when I call this method on this instance?" that a 10-min edit takes a full day. The code is basically frozen for fear of breaking something
At some point, you see enough of this that you realize the tool is more dangerous than helpful. The makers of Kotlin and many other newer languages seem to know this and have disabled inheritance as a default or left it out entirely.
I've seen C# interfaces and base classes that were committed years ago and never got a second implementation. This essentially makes the interface and base class a pointless ritual in hindsight.
It's always easy to spot in hindsight, but TBH I think the threshold for creating interfaces or base classes should be a bit higher - likely when you know you will need 3 or more implementations. It's always felt like premature optimization to me, unless something is hard coded to only accept an interface like using Type.IsInterface or something.
Concepts like "Product" are almost always going to have multiple implementations though, so it's easy to judge in that case to make an interface/base class at the beginning. The real magic is guessing what other concepts satisfy this requirement.
I had this same belief but there is one other useful situation where an interface plus just one implementation class helps, and that's when you want to hide the noise of implementation details from another part of the program.
This can happen between different layers of app. For example you might have a persistence layer under an API or UI, and want to expose a nice "clean" interface to the API / UI layer.
To add to that, in C++ that approach often saves non-trivial amount of compilation time. Consumers of the object only need to #include the interface, while the implementation of the object, both code and data, stays private and is not needed to compile the consuming code.
> It can be so difficult to answer the question, "What happens when I call this method on this instance?" that a 10-min edit takes a full day. The code is basically frozen for fear of breaking something
That's not a OO problem, that's a software engineering and code quality problem.
Modularity/compartmentalization and the obligation of managing complexity are basic age-old principles. You'll arrive to a big ball of mud if you are oblivious to these basic principles, whether you do inheritance or composition.
Don't pin the blame on a programming paradigm if your developers are clueless about basic software engineering principles.
> Don't pin the blame on a programming paradigm if your developers are clueless about basic software engineering principles.
You can write OO code without inheritance. Inheritance is syntactical sugar, essentially. You can accomplish the same things with interfaces and composition.
The difference between composition and inheritance is that a composed method is fully discoverable. I can see every statement. There's no secret parent method running before or after.
At some point, you see enough of this that you realize the tool is more dangerous than helpful. The makers of Kotlin and many other newer languages seem to know this and have disabled inheritance as a default or left it out entirely.