Replace Conditional With Polymorphism

A Refactor from RefactoringImprovingTheDesignOfExistingCode.

From the XpMailingList:

...It turns out that the only way to replace duplication with readable code is to introduce the kinds of OO abstractions that directly enable extensibility.

Which kinds of OO abstraction do you mean?

If I say,

    if (fileHandle == STDOUT)
        printf ("hi");
        fprintf (fileHandle, "hi");
I just committed an egregious OO sin. (Even an old-school structural C programmer could spot it.)

But the situation may be more obscure, such as:

    switch (myTie.getType())
        case CRAVAT:  myTie.tieCravatOn(neck);  break;
        case STRING:  myTie.tieStringOn(neck);  break;
        case WIDE  :  myTie.tieHalfWindsorOn(neck);  break;
Both those situations call for the Refactor "Replace Conditional with Polymorphism".

The first situation becomes simply:

    fprintf (fileHandle, "hi");
It has less duplication. A file handle >IS< a polymorphic object that the OS supports. It could be a file, the console, a printer, a device, etc.

The second situation becomes:

Of course it looks like there's less duplication. But we might have moved the 'switch' inside the tieOn.

If we did not - if myTie were a reference to an abstract base class - then each of its derived types would tie the different kind of knot. The only 'switch' statements would be at "boundary" situations, where we convert inputs to types.

So ultimately we have the minimum duplication possible.

Doesn't this just move the choice further back in the code, choosing which subclass is instantiated? WayneMack's comment below implies this, but it would mean that you don't *really* end up with less conditionals.

{It means there's perhaps one explicit conditional to choose which subclass is instantiated. That's typically far fewer than the explicit conditionals that would necessarily have to be appear throughout the code in the absence of polymorphism.}

Programmers even try to assault their 'if' statements with this technique. The code gets better.

How can such abstractions help to handle the situation of switching to a new architecture (similar to switching to an application layer)?

Imagine finding every 'switch' statement in a program and editing it. With fewer conditionals and more polymorphism, you change implementation by changing the concrete classes. The abstract classes, and their clients, avoid changes: OpenClosedPrinciple.

This is the heart of why ObjectOrientedProgramming is better. Replacing a flat file with a real database should be as easy as redirecting a stream from the console to a printer. -- Phlip

Increased polymorphism allows an increase in the quantity of changes in functional requirement to be achieved by configuration management rather than code changes. a GoodThing. -- MartinSpamer.


If a language supports optional named parameters:

  print("foo", destination=STDOUT)   // same as first, defaults to STDOUT if no handle
  print("foo", destination=COLORPRINTER)
This is undesirable since the switch statement is still inside of print switching on the handle argument, print must be aware of everything it can print too, it must change each time a new thing is added to the system. That's still what OO is trying to avoid. We don't want a print statement that has to know everything it can print on, we want a print statement that can print to any file object, which all other objects can pretend to be. This makes print more flexible, it can potentially print to anything, even things created years after it, without ever having to change it.

This gets into the age-old adding-new-subtypes-versus-adding-new-operations change-impact HolyWar. It has already been discussed (but not settled). See SwitchStatementsSmell for comparisons.

I would note however that this is an issue only for the implementor to worry about, not the API user. It is a black box from the API user's standpoint.

How Far to Take it?

How many of the conditionals (IF/case) should be converted to polymorphism? The opinions seem to vary widely on this. Is an application that has only 30% of its conditionals converted to polymorphism only "30 percent OO"? If you are not a purist on this matter, then what are the rules of thumb for determining what to make polymorphism and what to make conditionals?

Or should the SmallTalk technique be adopted, wherein every conditional is polymorphic call? And what does that buy you? Is it a common SmallTalk idiom to make other objects (besides true and false) respond to ifTrue: and similar methods?

Or is that just the OO equivalent of automatic-conversions-to-bool that C/C++ embrace and Java abolishes--a bit of SyntacticSugar that is frequently useful, seldom necessary, and sometimes harmful?

As a blanket guideline, I find this to be bad advice bordering on the absurd. It really only makes sense if the same or very similar conditional tests are repeated often. For simple, seldom-repeated tests, replacing a simple conditional with the verbosity of multiple class definitions, and likely moving all this far from the code that actually requires the conditionally required activity, would result in a textbook example of code obfuscation.

Prefer clarity over dogmatic purity. -- DanMuller

Any dogmatic approach produces inferior results - taste and experience always beat sound bytes. Anyway, the intent behind this advice is to reduce code and increase resilience and flexibility - if the code is getting worse, it's not the right approach for that particular case. The best candidates are type tests as in the examples above.

I don't know if this is true. Some people may simply think better if pure concepts are used. They have less variations to consider when thinking about something. Barring any objective evidence, if person X claims that some form of EverythingIsa works for them, it is hard to say otherwise. The problem seems to be when they extrapolate their preferences to other people.

My view is that the frequency of use of the conditional is not the relevant criteria, rather it is the chronological relationship between the decision and the action taken on the decision. If one takes a value, saves it, and then later selects a specific operation based upon the value (conditional approach), then it is more complex than if one takes a value, uses a class factory to select a subclass, and then later performs the operation associated with the subclass. The decision is more closely bound in time and code between receiving the discriminator and acting upon it. It is very difficult to isolate problems when the error occurs long after the reception of the (probably bad) data that caused the error, especially if the method is rarely called. -- WayneMack

I suppose then we can group opinions on this into these:

(There are actually 2 or more factors here, but this is de-normalized for illustrative purposes.)

This situation:
    switch (myTie.getType())
        case CRAVAT:  myTie.tieCravatOn(neck);  break;
        case STRING:  myTie.tieStringOn(neck);  break;
        case WIDE  :  myTie.tieHalfWindsorOn(neck);  break;
is where JavaLanguage 1.5 is so useful:

    enum TieType {
        CRAVAT { void tieMe(Neck neck) { start(neck); up(); down(); left(); done(); } },
        STRING { void tieMe(Neck neck) { start(neck); left(); left(); up(); done(); } },
        WIDE { void tieMe(Neck neck) { start(neck); down(); up(); down(); down(); done(); } };

abstract void tieMe(Neck neck); } .... myTie.getType().tieMe(myNeck);
This is assuming that one is using a mutually-exclusive or hierarchical taxonomy. Plus, it hints of OverUsedOopExamples. Polymorphism doesn't scale to sets, and this is the main reason I recommend against it. Sets fit my domain better than hierarchical taxonomies.

See also: SwitchStatementsSmell PolymorphismVsSelectionIdiom

View edit of May 25, 2012 or FindPage with title or text search