The myth of polymorphism
I remember reading about polymorphism for the first time. I was in high school, and boy it sure looked cool! Too bad I didn’t realize that the myth of polymorphism was a bunch of poppy cock.
You see, polymorphism never seems to be presented in its real state. Instead, we get this goofy, toy-app type presentation. Does Shape -> Rectangle -> Square ring a bell?
The fallacy of geometrical shapes being polymorphic
One of the simplest ways people seem to introduce polymorphism is using geometric shapes. We all know that a Square is a Rectangle. We covered that nicely in geometry, right? Problem is, geometry doesn’t equal software.
When discussing things in light of geometry, the reason we value this relationship is because we are looking at things like angles, parallelism, vertices, and intersections. Hence, squares carry all the attributes of rectangles. They simply have the same width and height.
But software isn’t geometry. The things we construct we must also interact with. The shapes must afford us operations to grab them, interrogate them, draw them, and manipulate them. A rectangle has two attributes: width and height. A square has one: width.
If we grabbed a square, set its width, then set its height, the assumptions of what would happen is unclear. Should a square morph into a rectangle? Or should setting the height induce the side effect of also updating the width? Either way is bad form. Hence, its best to break apart this faulty geometric relationship and realize that squares are NOT rectangles.
The fallacy of inheriting behavior
So shaking off the trivial example of geometric shapes, another common example is to talk about the glorious ability to reuse code. With polymorphism, it will be SO easy to code something once, and then reuse it across dozens if not hundreds of classes! And with late binding options, gobs of 3rd party libraries can go forth and reuse your code.
The problem is, no programming language has adequately been invented to gather ALL the semantic nuances of code. As more and more classes extend a given class, they all either realize EXACTLY what the abstract parent class does and agree with it, or they discover some new wrinkle not quite handled. The API may be supported, but some underlying assumption is buried that requires an update.
As the tail of inheritance grows, maintainers are less likely to accept new changes to the shared code. The risk of breaking someone else grows, because everyone knows not the ENTIRE nature of the code can be captured.
Some of the avenues to remedy this involves opening up the API a bit more. Perhaps a private utility method is needed by a new extender. But opening it up introduces more maintenance down the road. Or more opportunities for others to abuse things that used to be kept tightly controlled.
History has proven that composition beats inheritance for sustainability. Raise your hand if maintenance, not new development, doesn’t encompass much of your work.
The alternative are more sophisticated languages where you can capture more of the concepts. Yet these languages come across as too complex to many, arguably because CAPTURING all semantics is inherently challenging. And more often than not, we don’t KNOW all these semantics on the first round.
The myth of polymorphism vs. the reality
One thing that has emerged is programming to interfaces. Interfaces provide a nice contract to work against. Naturally, we can’t capture everything. But at least every “Shape” can institute the defined methods. In Java, interfaces can be combined, allowing multiple behaviors to be pulled together.
So when it comes to abstract and refactor, think about extracting interfaces when possible and delegating solutions.
Strangely enough, despite my consternation with static variables, I’ve come to appreciate static methods in the right scenario. Factories that support interfaces can be quite handy dandy. But a well modeled, inheritance hierarchy is to hard to accomplish. If possible, try to avoid multiple layers, and see if it’s not possible to extract share code into various utility classes.
And when the entire hierarchy is nicely contained INSIDE your framework, and not as an extension point to consumers, the fallacies of polymorphism can be contained. But watch out! It takes much effort to put this together. Never think it will be easy.
Of course, I could be entirely wrong. If you have some keen examples, please feel free to share!