GranularityOfVariation is the granularity of the pattern of difference between two or more things that are similar. For example, does the entire block (function, class, method, etc.) differ between the items being compared, or only part of the block?
I've learned to be hesitant to hard-wire or heavy-wire the granularity of variation into a design. Although there may be a general tendency to the pattern (at least up front), exceptions can and do often pop up (EightyTwentyRule). It is one of the things that bothers me about PolyMorphism. IF statements seem easier to adjust in this regard. They are like a caliper tool that can widen or narrow as needed. Polymorphism, on the other hand, often requires overhauling or splitting the polymorphic interface, requiring roughly 4 times as much code rework.
Of course, it can be partially solved by duplicating the portions that are similar rather than refactor to a smaller polymorphic granularity, but of course this is a violation of OnceAndOnlyOnce. A small amount may be acceptable, but this is hardly the ideal and the granularity lapses may continue into the future.
--top
'if' statements do provide you higher-precision control than parametric or inheritance-based polymorphism. It does come at a cost, of course. It leads to a rather 'organically grown' codebase (hard to verify with analysis or code review, but organically grown unit tests provide a solution there), a possibly extreme cost to add new objects or dispatch decisions ('extreme' if you don't own the module or library), and poor code mobility (extremely high unnecessary coupling; every dispatch-point needs the whole damn namespace of objects). I.e. it isn't very nice to use when writing libraries or components for use by other people. Other than that, if it works for you... then feel free to do it.
- Messy layers of polymorphism could be called "organic" also. I will agree that polymorphism may be a better way to package closed code for far-off users even if it does make code maintenance more expensive for the library author. But most actual "open" code I encounter in custom applications does not fit into this.
- I don't disagree that polymorphism can also be "organic". However, the injection of new and more conditions into "if" statements distributed throughout the code tends to lead much more readily in that direction; when using "if" statements, it takes more discipline to avoid the code becoming "organic" (especially if you have several developers). This is because there always exists a potential to add more and more 'special cases' at various dispatch points buried deep in the program logic. This is as opposed to dispatch based upon polymorphic interfaces, which will apply said interfaces universally, with special cases being fewer, more visible, and more predictable.
- Of course, organic but working is better than 'clean' but broken, and so long as there are only a few authors who know their own little home-grown 'jungle' well, maintenance will not be too much an issue. Organic is mostly a problem when introducing new developers or maintainers (who must stumble upon, discover, and learn many of those special "if" cases). 'Organic' also makes difficult any major refactoring... people making changes become afraid (for good reason) to do more than 'trim' here and there. (It is for this reason that people will often 'start afresh' rather than attempt big changes.) (shall move to OrganicallyGrownCode?)
- [If you believe you'll never add a new developer or maintainer you're either deluding yourself or working on a doomed project.]
The 'overhaul cost' for polymorphism is high when you decide you need to modify the polymorphic interface (cost is linear with number of unique objects unless you can have a reasonable default behavior). The 'overhaul cost' for the if-statement mechanism is high if you have a lot of decision points where you need to inject code (but, again, is reduced if you have reasonable default behaviors) - and a great deal higher if you don't control the code in which the dispatch decision is made. You can just as easily end up violating
OnceAndOnlyOnce with the 'if' based solution because, all too often, you'll find yourself duplicating the exact same conditions-checks to every decision point.
- Well-formed IF statements are often not a lot of code, sometimes less than class and method declarations. One may argue that polymorphism makes such decisions "automatic", but the setup for it is about the same amount of code if not more than IF statements. Measuring "decision points" and measuring code volume may not be the same thing.
- Whether the well-formed if statements will be "a lot of code" will depend on how many "well-formed if statements" you're talking about compared to how many "class and method declarations". With polymorphism, you declare the case once and it spreads to all parts of the code; with if statements, you declare the case once and it applies to exactly that piece of code. As far as setup cost for polymorphism: the volume of code depends largely upon the language you're using, and how much it supports 'accidental' polymorphism (e.g. where dynamic objects can possess the same interface just by having the same method-names with the same arity, or similar things with the C++ templates). In my own experience, setting up polymorphism is often more difficult, but the cost isn't about amount or volume of code; rather, it just takes more thinking to 'get it right'. If I were to put words to it: good polymorphic interfaces are difficult mostly because good interfaces are difficult.''
- Polymorphic interfaces are difficult because they require a rather specific pattern of change or variation which is often uncertain or unrealistic. Real-world requirements are often "organic" in shape whether we want them to be or not. Forcing order where there isn't any or only partial order is asking for trouble down the road. Perhaps another way of saying this is that some people appear to be "order addicts" and pound square pegs into round holes to obtain short-term order or perceived order regardless of the practical fit. I've made these kinds of mistakes before myself, and had to learn when to let go and embrace organics.
- [Polymorphic interfaces are only difficult when they are misused. Polymorphism is a tool, not a philosophy or a religion. When 'if' based solutions are appropriate, they should be used. When polymorphism-based solutions are appropriate, they should be used. Real applications tend to benefit from a mix of both. The absence of polymorphism (and 'if's, obviously!) can hurt when you need it. As a case in point, some years ago I was responsible for developing and maintaining two Canadian payroll systems, one implemented in C++ and one in a procedural language. In C++, the federal and provincial income tax calculations turned into a rather neat system consisting of base class that mainly defined federal tax calculations and the tax engine interface, and a set of derived classes that defined provincial tax calculations and province-specific factors that affected federal calculations. Maintaining and testing this was very straightforward. Occasionally, significant provincial tax changes meant re-working a whole province's class, and maybe a bit of the base class, but this was minor compared to the dire 'if'-surgery that was needed whenever we had to make changes in the procedural system. That festering oh-god-we-need-polymorphism-desperately monster was a nightmare to maintain and debug, and I used to wake up sweating in the wee hours, wondering if somewhere in that ghastly tangle of conditionals I forgot an 'if (province == PQ) v1 = 0.151;' or something that would condemn some poor sot to a lifetime of Revenue Canada audits. With the OO implementation that was never a worry, as each class mapped neatly to the published Revenue Canada formulae, and so could be trivially eyeball-verified and tested independently of the rest. Eventually, we re-worked the procedural implementation to use the OO version's tax engine, deployed as a DLL. I slept better thereafter.]
- But you don't know ahead of time what will neatly chunk-ify into poly and what won't beyond the immediate requirements. Perhaps there was a way to clean up the procedural version, but I cannot see your code to suggest ways. I cannot tell if it is the best procedural can possibly be, or if you were a crappy proceduralists (they do exist). If the calculation path tends toward a DAG or graph instead of a tree, then polymorphism will generally not be any cleaner than IF statements. Related: PayrollExample.
- [In Canadian payroll systems, we do know what will "neatly chunk-ify into poly and what won't". The formulae are contained in an official published document. Experience shows how the formulae and factors tend to change over time, and these tend to fall into clear categories: There are either simple changes to existing factors (e.g., in Ontario, change factor V5 from 0.51 to 0.56), or changes to formulae (e.g., change V2 = V3 + V4 to V2 = V3 + V4 + V5, and introduce a new factor V5), or changes to base calculation method (e.g., New Brunswick changes from using the calculation structure employed by Manitoba and Saskatchewan to using the calculation structure employed by Newfoundland and P.E.I.), or some combination of these. Furthermore, the calculation path tends toward a tree, rather than a graph, and the code was well-reviewed by multiple developers of varying background -- hence our annoyance with the procedural implementation was unlikely to be some shared lack of procedural coding ability, especially as most of us had long experience with both procedural and OO approaches. There are certain problems, such as this one, where use of OO inheritance and polymorphism provides a clear advantage over procedural solutions. Likewise, there are certain problems where use of OO inheritance and polymorphism are clearly overkill or over-complication. This strikes me as being a fork vs. spoon or hammer vs. screwdriver debate. Simply pick the right tool for the job, and recognise that this isn't a religious or philosophical issue. Like any other language feature, polymorphism isn't something to be categorically embraced or rejected; it's merely a handy tool to be used where appropriate.] (HorsesForCourses)
- You may be right. Such up-front-clearity and a documented history of changes does occassionally happen and poly may be the best way to go in that case. However, I would venture to say that is the exception instead of the rule and I applaud the Canadian gov't for keeping the rules organized as a fairly nice tree. Perhaps the US is more influenced by capitalism, which tends to change in an "organic" fashion.
- I imagine that if you start focusing on "the best" way to distribute procedural "if" statements throughout the code, it suddenly will be just as difficult as doing polymorphism well... and just as disciplined, ordered, and subject to problems involving future changes.
- True. In that case the choice would come down to personal preference or estimated future change patterns. If I smell organic change or organic business philosophy, I may side with IF statements because of their flexible granularity calipers, unless possibly they have extra staff to help in the polymorphic refactorings.
In my own experience, the number of decision points and number of object-variants can vary quite a bit between projects. I'd not be comfortable saying that one is more or less expensive than the other. However, I do tend to combine the two techniques a great deal (e.g. have a few high-abstraction if/enum-based variants that are implemented with low-level polymorphism).
There are not a lot of reliable guidelines for when to use one or the other, other than "rely on experience". Paying attention to the patterns of GranularityOfVariation in general and in a domain is one way to learn.
SeptemberZeroSeven
CategoryConditionalsAndDispatching, CategoryPolymorphism