I frequently hear people malign inheritance, and while it can obfuscate code in some circumstances, it can also produce code that is easily and clearly extendable. For example, a class with a static method that uses class properties to control behavior is cleaner than a function factory that takes a config object. Interface inheritance is also quite useful.
> it can also produce code that is easily and clearly extendable
It can also produce code that is not easily and clearly extendable, if the behavior you're trying to extend is buried a layer or two further down the inheritance hierarchy.
Unfortunately, due to the First Law of Kipple, the rate at which you run into this problem is proportional to the age of the code base. And so we grow frustrated with implementation inheritance. Other extensibility mechanisms probably have similar pitfalls, but they haven't been the dominant way of doing things for long enough to accumulate the same volume of clutter.
Don Knuth has mentioned in a few interviews that he isn't so hot on code reuse, and prefers code that's designed to be easy to edit over code that's designed to be easy to extend. I'm starting to see some wisdom in that idea. With the one, being able to keep things tidy is a primary goal. With the other, eventually tidying becomes a frightening enterprise, because you have to avoid upsetting the inheritance hierarchies that are precariously balanced on top of the code you're trying to tidy up.
I think the practice of not using a tool because people could potentially misuse it is misguided in this day and age. Linting and static code analysis are quite capable of enforcing good usage patterns.
My concern is not exactly that it could potentially be misused. It's more that it seems to set up forces that subtly push projects toward becoming resistant to change over time. And, while it's possible for individual programmers to exercise discipline in order to push back against these sorts of forces, at a larger scale the guiding principle seems to be, "water flows downhill." So we should seek to find ways of building things that generally set up forces guiding us toward good design in the long run, without having to drill people on large laundry lists of best practices. That approach only works for as long as the code is owned by a sort of benevolent dictator who is able to, by hook or by crook, keep all their teammates on (their version of) the righteous path. And that approach itself is unstable; it has a tendency to degrade rapidly whenever the leader decides to spend a week at the beach. If they should ever leave the company, it's likely to be lost forever.
I can't say that I know a software development idiom that reliably creates a more stable equilibrium point. But I don't think we'll ever find one unless we're willing to examine the failure modes of existing paradigms.
The problem is "developer" spans a gigantic range of capabilities. Someone who wrote Hello World in Chrome's Console is a developer, and someone writing kernel code professionally is also a developer.
It's hard to define a "good practice" for this wildly heterogenous group. What's good for a beginner (training wheels on a child's bike), is completely counter-productive to a pro (motorcycle sports driver).
I think thanks to Java opting to use "implements" for interfaces, people no longer associate "inheritance" as the thing we do when we write a fully abstract class (i.e. interface) and then "inherit" this abstraction to implement it. Interfaces are, of course, crucial.
Not sure I understood your example about the static class vs. function factory tbh though.
The class can scale to multiple methods sharing parameters, but the semantics of the factory fall apart if you want to return more than one parameterized function.
But I don't think a "fully abstract class" is the same thing as an "interface", at least in Java? As far as I know, you can implement multiple "interfaces" but you can still only "extend" one abstract class, even if it is "fully abstract" in that it has no concrete member variables and all methods are abstract.
In my experience, whether you use an abstract class or an interface class as your reference depends very much on the problem you are trying to solve - specifically, is there common code shared across all implementations?