And fundamentally, inheritance is the wrong tool for reuse. Providing lots of small methods to make for improved reuse through inheritance is an equally wrong approach.
I wanted to write about that a little more.
Cases where inheritance has worked out well: when I can arrange for a base class to provide a lot of functionality, and one or maybe two abstract methods overridden in the subclass. Essentially, this comes down to “using inheritance as a mechanism to implement a strategy for some small, but critical, polymorphic bit.” Or perhaps, “making a DSL by providing operations in a base class, then implementations in child classes.”
Probably the most accessible example of this is PHP’s FilterIterator which does little more than wrap a regular Iterator with an
Notably, it doesn’t extend anything itself to do this. It composes another iterator and exposes that through the OuterIterator interface.
I’ve also been able to use inheritance effectively to establish protected variables that influence a parent’s decisions, and mostly reuse all the code. In this case, the inheritance isn’t entirely OO window dressing around feature flags and procedural code, due to one key case: that my problem is broken down into steps. The core algorithm has the skeleton:
def deploy(self): if os.path.exists(self.tmpdir): git_pull(self.tmpdir) else: git_clone(self.repo, self.tmpdir) self.build()
Inheritance lets this base class call “back” into child classes to handle the
Things inheritance has not worked out well for: anything where I’m tempted to violate Liskov substitutability. Any interface that relies on another class’s constructor accepting a specific signature. (It turns out the dependency injection crowd was onto something; if you want testable code, you don’t want things constructing other things.) Providing ‘extension points’ by splitting algorithms across smaller methods, because because maybe someone might eventually someday want to override some small portion of it.
This last bit especially gets layered up… if there are seven steps, that’s eight methods, and anyone who wants to reorganize the driving method has to handle calling all seven. Why not have methods A, B, and C each cover calling two or three of them, then call those from the top?
Because it makes code a nightmare to read. This one function now comes in eleven pieces preparing for an uncertain future that will probably not fit those neat boundaries when it arrives. Or worse, there actually is a subclass, and it now comes in anywhere from 12 to 22 pieces in two files.
That last bit, which I may have written separately about, is something I have been calling “the plugin problem.” When providing extensibility, there are two kinds of locality that are important—most of these systems keep “plugin code” all together in a single file or directory, but it can be important sometimes to see the execution path, the way control flows through the main program and all the loaded plugins, in sequence.
Breaking things apart, especially in a language with dodgy debugger access like PHP, adds a certain amount of cost to reading the code. If it’s not necessary, it shouldn’t actually be done. Especially since breaking control flow up across subclasses seems to result in spaghetti code more often than other design approaches.
My intuition, though I haven’t used them much, is that Traits won’t save us—it seems like they cover the same uses as multiple inheritance, but inheritance is the problem. (And the old school programmer in me has to ask, if you just want a Trait to be some functions in your class, why aren’t they… functions… that you just call?)
Post a Comment