Do polymorphism or conditionals promote better design?

13,452

Solution 1

Actually this makes testing and code easier to write.

If you have one switch statement based on an internal field you probably have the same switch in multiple places doing slightly different things. This causes problems when you add a new case as you have to update all the switch statements (if you can find them).

By using polymorphism you can use virtual functions to get the same functionality and because a new case is a new class you don't have to search your code for things that need to be checked it is all isolated for each class.

class Animal
{
    public:
       Noise warningNoise();
       Noise pleasureNoise();
    private:
       AnimalType type;
};

Noise Animal::warningNoise()
{
    switch(type)
    {
        case Cat: return Hiss;
        case Dog: return Bark;
    }
}
Noise Animal::pleasureNoise()
{
    switch(type)
    {
        case Cat: return Purr;
        case Dog: return Bark;
    }
}

In this simple case every new animal causes requires both switch statements to be updated.
You forget one? What is the default? BANG!!

Using polymorphism

class Animal
{
    public:
       virtual Noise warningNoise() = 0;
       virtual Noise pleasureNoise() = 0;
};

class Cat: public Animal
{
   // Compiler forces you to define both method.
   // Otherwise you can't have a Cat object

   // All code local to the cat belongs to the cat.

};

By using polymorphism you can test the Animal class.
Then test each of the derived classes separately.

Also this allows you to ship the Animal class (Closed for alteration) as part of you binary library. But people can still add new Animals (Open for extension) by deriving new classes derived from the Animal header. If all this functionality had been captured inside the Animal class then all animals need to be defined before shipping (Closed/Closed).

Solution 2

Do not fear...

I guess your problem lies with familiarity, not technology. Familiarize yourself with C++ OOP.

C++ is an OOP language

Among its multiple paradigms, it has OOP features and is more than able to support comparison with most pure OO language.

Don't let the "C part inside C++" make you believe C++ can't deal with other paradigms. C++ can handle a lot of programming paradigms quite graciously. And among them, OOP C++ is the most mature of C++ paradigms after procedural paradigm (i.e. the aforementioned "C part").

Polymorphism is Ok for production

There is no "subtle bugs" or "not suitable for production code" thing. There are developers who remain set in their ways, and developers who'll learn how to use tools and use the best tools for each task.

switch and polymorphism are [almost] similar...

... But polymorphism removed most errors.

The difference is that you must handle the switches manually, whereas polymorphism is more natural, once you get used with inheritance method overriding.

With switches, you'll have to compare a type variable with different types, and handle the differences. With polymorphism, the variable itself knows how to behave. You only have to organize the variables in logical ways, and override the right methods.

But in the end, if you forget to handle a case in switch, the compiler won't tell you, whereas you'll be told if you derive from a class without overriding its pure virtual methods. Thus most switch-errors are avoided.

All in all, the two features are about making choices. But Polymorphism enable you to make more complex and in the same time more natural and thus easier choices.

Avoid using RTTI to find an object's type

RTTI is an interesting concept, and can be useful. But most of the time (i.e. 95% of the time), method overriding and inheritance will be more than enough, and most of your code should not even know the exact type of the object handled, but trust it to do the right thing.

If you use RTTI as a glorified switch, you're missing the point.

(Disclaimer: I am a great fan of the RTTI concept and of dynamic_casts. But one must use the right tool for the task at hand, and most of the time RTTI is used as a glorified switch, which is wrong)

Compare dynamic vs. static polymorphism

If your code does not know the exact type of an object at compile time, then use dynamic polymorphism (i.e. classic inheritance, virtual methods overriding, etc.)

If your code knows the type at compile time, then perhaps you could use static polymorphism, i.e. the CRTP pattern http://en.wikipedia.org/wiki/Curiously_Recurring_Template_Pattern

The CRTP will enable you to have code that smells like dynamic polymorphism, but whose every method call will be resolved statically, which is ideal for some very critical code.

Production code example

A code similar to this one (from memory) is used on production.

The easier solution revolved around a the procedure called by message loop (a WinProc in Win32, but I wrote a simplier version, for simplicity's sake). So summarize, it was something like:

void MyProcedure(int p_iCommand, void *p_vParam)
{
   // A LOT OF CODE ???
   // each case has a lot of code, with both similarities
   // and differences, and of course, casting p_vParam
   // into something, depending on hoping no one
   // did a mistake, associating the wrong command with
   // the wrong data type in p_vParam

   switch(p_iCommand)
   {
      case COMMAND_AAA: { /* A LOT OF CODE (see above) */ } break ;
      case COMMAND_BBB: { /* A LOT OF CODE (see above) */ } break ;
      // etc.
      case COMMAND_XXX: { /* A LOT OF CODE (see above) */ } break ;
      case COMMAND_ZZZ: { /* A LOT OF CODE (see above) */ } break ;
      default: { /* call default procedure */} break ;
   }
}

Each addition of command added a case.

The problem is that some commands where similar, and shared partly their implementation.

So mixing the cases was a risk for evolution.

I resolved the problem by using the Command pattern, that is, creating a base Command object, with one process() method.

So I re-wrote the message procedure, minimizing the dangerous code (i.e. playing with void *, etc.) to a minimum, and wrote it to be sure I would never need to touch it again:

void MyProcedure(int p_iCommand, void *p_vParam)
{
   switch(p_iCommand)
   {
      // Only one case. Isn't it cool?
      case COMMAND:
         {
           Command * c = static_cast<Command *>(p_vParam) ;
           c->process() ;
         }
         break ;
      default: { /* call default procedure */} break ;
   }
}

And then, for each possible command, instead of adding code in the procedure, and mixing (or worse, copy/pasting) the code from similar commands, I created a new command, and derived it either from the Command object, or one of its derived objects:

This led to the hierarchy (represented as a tree):

[+] Command
 |
 +--[+] CommandServer
 |   |
 |   +--[+] CommandServerInitialize
 |   |
 |   +--[+] CommandServerInsert
 |   |
 |   +--[+] CommandServerUpdate
 |   |
 |   +--[+] CommandServerDelete
 |
 +--[+] CommandAction
 |   |
 |   +--[+] CommandActionStart
 |   |
 |   +--[+] CommandActionPause
 |   |
 |   +--[+] CommandActionEnd
 |
 +--[+] CommandMessage

Now, all I needed to do was to override process for each object.

Simple, and easy to extend.

For example, say the CommandAction was supposed to do its process in three phases: "before", "while" and "after". Its code would be something like:

class CommandAction : public Command
{
   // etc.
   virtual void process() // overriding Command::process pure virtual method
   {
      this->processBefore() ;
      this->processWhile() ;
      this->processAfter() ;
   }

   virtual void processBefore() = 0 ; // To be overriden
   
   virtual void processWhile()
   {
      // Do something common for all CommandAction objects
   }
   
   virtual void processAfter()  = 0 ; // To be overriden

} ;

And, for example, CommandActionStart could be coded as:

class CommandActionStart : public CommandAction
{
   // etc.
   virtual void processBefore()
   {
      // Do something common for all CommandActionStart objects
   }

   virtual void processAfter()
   {
      // Do something common for all CommandActionStart objects
   }
} ;

As I said: Easy to understand (if commented properly), and very easy to extend.

The switch is reduced to its bare minimum (i.e. if-like, because we still needed to delegate Windows commands to Windows default procedure), and no need for RTTI (or worse, in-house RTTI).

The same code inside a switch would be quite amusing, I guess (if only judging by the amount of "historical" code I saw in our app at work).

Solution 3

Unit testing an OO program means testing each class as a unit. A principle that you want to learn is "Open to extension, closed to modification". I got that from Head First Design Patterns. But it basically says that you want to have the ability to easily extend your code without modifying existing tested code.

Polymorphism makes this possible by eliminating those conditional statements. Consider this example:

Suppose you have a Character object that carries a Weapon. You can write an attack method like this:

If (weapon is a rifle) then //Code to attack with rifle else
If (weapon is a plasma gun) //Then code to attack with plasma gun

etc.

With polymorphism the Character does not have to "know" the type of weapon, simply

weapon.attack()

would work. What happens if a new weapon was invented? Without polymorphism you will have to modify your conditional statement. With polymorphism you will have to add a new class and leave the tested Character class alone.

Solution 4

I'm a bit of a skeptic: I believe inheritance often adds more complexity than it removes.

I think you are asking a good question, though, and one thing I consider is this:

Are you splitting into multiple classes because you are dealing with different things? Or is it really the same thing, acting in a different way?

If it's really a new type, then go ahead and create a new class. But if it's just an option, I generally keep it in the same class.

I believe the default solution is the single-class one, and the onus is on the programmer proposing inheritance to prove their case.

Solution 5

Not an expert in the implications for test cases, but from a software development perspective:

  • Open-closed principle -- Classes should be closed to alteration, but open to extension. If you manage conditional operations via a conditional construct, then if a new condition is added, your class needs to change. If you use polymorphism, the base class need not change.

  • Don't repeat yourself -- An important part of the guideline is the "same if condition." That indicates that your class has some distinct modes of operation that can be factored into a class. Then, that condition appears in one place in your code -- when you instantiate the object for that mode. And again, if a new one comes along, you only need to change one piece of code.

Share:
13,452
Nik Reiman
Author by

Nik Reiman

I develop audio and music software. I started Teragon Audio, and have produced a number of AU/VST plugins, hosts, and other stuff. I am not seeking employment; please do not contact me with job offers, project proposals, and the like.

Updated on June 25, 2022

Comments

  • Nik Reiman
    Nik Reiman almost 2 years

    I recently stumbled across this entry in the google testing blog about guidelines for writing more testable code. I was in agreement with the author until this point:

    Favor polymorphism over conditionals: If you see a switch statement you should think polymorphisms. If you see the same if condition repeated in many places in your class you should again think polymorphism. Polymorphism will break your complex class into several smaller simpler classes which clearly define which pieces of the code are related and execute together. This helps testing since simpler/smaller class is easier to test.

    I simply cannot wrap my head around that. I can understand using polymorphism instead of RTTI (or DIY-RTTI, as the case may be), but that seems like such a broad statement that I can't imagine it actually being used effectively in production code. It seems to me, rather, that it would be easier to add additional test cases for methods which have switch statements, rather than breaking down the code into dozens of separate classes.

    Also, I was under the impression that polymorphism can lead to all sorts of other subtle bugs and design issues, so I'm curious to know if the tradeoff here would be worth it. Can someone explain to me exactly what is meant by this testing guideline?