[ Team LiB ] Previous Section Next Section

Gotcha #96: Type-Based Control Structures

We never switch on type codes in object-oriented programs:



void process( Employee *e ) { 


   switch( e->type() ) { // evil code!


   case SALARY: fireSalary( e ); break;


   case HOURLY: fireHourly( e ); break;


   case TEMP: fireTemp( e ); break;


   default: throw UnknownEmployeeType();


   }


}


The polymorphic approach is more appropriate:



void process( Employee *e ) 


   { e->fire(); }


The advantages of this approach are enormous. It's simpler. The code doesn't have to be recompiled as new employee types are added. It's impossible to have type-based runtime errors. And it's probably faster and smaller. Implement type-based decisions with dynamic binding, not with conditional control structures. (See also Gotchas #69, #90, and #98.)

The substitution of dynamic binding for conditional code is so effective that it often makes sense to recast a conditional as a type-based question to employ it. Consider code that simply wants to process a Widget. The Widget has a process function in its public interface, but, depending on where the Widget is located, additional work must be performed before the process function can be called:



if( Widget is in local memory ) 


   w->process();


else if( Widget is in shared memory )


   do horrible things to process it


else if( Widget is remote )


   do even worse things to process it


else


   error();


Not only can this conditional code fail ("I want to process the Widget, but I don't know where it is!") but it may be repeated many times in the source. All these independent sections of conditional code must be maintained in sync as the set of possible locations of Widgets grows or shrinks. A better approach might encode the location of a Widget in its type, as in Figure 9-12.

Figure 9-12. Avoiding conditional code; use of the Proxy pattern to encode an object's access protocol as part of its type

graphics/09fig12.gif

This situation is so common that it has a name. This is an instance of the Proxy pattern. The different mechanisms for accessing Widgets according to location are now encoded in each Widget's type, and a simple virtual function call is all that's required to distinguish them. Further, this code is not repeated, and the virtual call can't fail to know how to access the Widget:



Widget *w = getNextWidget(); 


w->process();


Another important benefit of avoiding conditional code is so obvious that it can be easily overlooked: one way to avoid making an incorrect decision is to avoid making any decision at all. Simply put, the less conditional code you write, the less likely it is you'll have incorrect conditional code.

One manifestation of this advice is the Null Object pattern. Consider a function that returns a pointer to a "device" that must be "handled":



class Device { 


 public:


   virtual ~Device();


   virtual void handle() = 0;


};


// . . .


Device *getDevice();


The Device class is an abstract base class for a number of different device types. It's also possible that getDevice could fail to return a Device, so our code for getting and handling a Device looks like this:



if( Device *curDevice = getDevice() ) 


   curDevice->handle();


That's pretty simple code, but we're making a decision. In this case, we might worry about maintenance that neglects to check the return value of getDevice before attempting to handle it.

The Null Object pattern suggests that we create an artificial type of Device that satisfies all the constraints on a Device (it can be handled) but that does nothing. Essentially, it does nothing in exactly the right way:



class NullDevice : public Device { 


 public:


   void handle() {}


};


// . . .


Device &getDevice();


Now getDevice can never fail, we can remove some conditional code, and we circumvent a potential future bug:



getDevice().handle(); 


    [ Team LiB ] Previous Section Next Section