Gotcha #98: Asking Personal Questions of an Object
This item considers a commonly abused capability in object-oriented design: runtime type information. The C++ language has standardized the form of runtime type queries, effectively legitimizing their use with an implicit seal of approval. But while it's true that runtime type queries have legitimate uses in C++ programming, these uses should be rare and should almost never form the basis for a design. Regrettably, much of the wisdom the C++ community has accumulated about proper and effective communication with hierarchies of types is often jettisoned in favor of underdesigned, overly general, complex, unmaintainable, and error-prone approaches using runtime type queries.
Consider the venerable employee base class below. Sometimes features must be added after a significantly large subsystem has been developed and tested. For instance, the Employee base class interface has a glaring omission:
class Employee {
public:
Employee( const Name &name, const Address &address );
virtual ~Employee();
void adoptRole( Role *newRole );
const Role *getRole( int ) const;
// . . .
};
That's right. We have to be able to rightsize these assets. (We also have to pay these assets, but that can wait until a future release.) Our management tells us to add the capability to fire an employee, given only a pointer to the employee base class and without recompiling or otherwise changing the Employee hierarchy. Clearly, salaried employees must be fired differently from hourly employees:
void terminate( Employee * );
void terminate( SalaryEmployee * );
void terminate( HourlyEmployee * );
The most straightforward way to accomplish this is to hack. We'll simply run down a list of questions about the precise type of employee:
void terminate( Employee *e ) {
if( HourlyEmployee *h = dynamic_cast<HourlyEmployee *>(e) )
terminate( h );
else if( SalaryEmployee *s = dynamic_cast<SalaryEmployee *>(e) )
terminate( s );
else
throw UnknownEmployeeType( e );
}
This approach has clear problems in terms of efficiency and the potential for runtime error in the case of an unknown employee type. Generally, because C++ is a statically typed language and because its dynamic binding mechanism (the virtual function) is statically checked, we should be able to avoid this class of runtime errors entirely. This is reason enough to recognize this implementation of the terminate function as a temporary hack rather than as the basis of an extensible design.
The poverty of the design is perhaps even more obvious if the code is back-translated into the problem domain it's supposedly modeling:
The vice president of widgets storms into her office in a terrible rage. Her parking space has been occupied for the third time this month by the junk heap driven by that itinerant developer she hired the month before. "Get Dewhurst in here!" she roars into her intercom.
Seconds later, she fixes the hapless developer with a gimlet eye and intones, "If you're an hourly employee, you're fired as an hourly employee. Otherwise, if you're a salaried employee, you're fired as a salaried employee. Otherwise, get out of my office and become someone else's problem."
I'm a consultant, and I've never lost a contract to a manager who used runtime type information to solve her problems. The correct solution is, of course, to put the appropriate operations in the Employee base class and use standard, type-safe, dynamic binding to resolve type-based questions at runtime:
class Employee {
public:
Employee( const Name &name, const Address &address );
virtual ~Employee();
void adoptRole( Role *newRole );
const Role *getRole( int ) const;
virtual bool isPayday() const = 0;
virtual void pay() = 0;
virtual void terminate() = 0;
// . . .
};
… she fixes the hapless developer with a gimlet eye and intones, "You're fired!"
Runtime type queries are sometimes necessary or preferable to other design choices. As we've seen, they can be used as a convenient and temporary hack when one is faced with poorly designed third-party software. They can also be useful when one is faced with an otherwise impossible requirement to modify existing code without recompilation when that code wasn't designed to accommodate such modification. Runtime type queries are also handy in debugging code and have rare, scattered uses in specific problem domains like debuggers, browsers, and the like. Finally, if the problem domain being modeled has an intrinsic lack of orthogonality, that intrinsic glitch may well show up as a runtime type query glitch in the code.
Since the standardization of runtime typing mechanisms in C++, however, many designers have employed runtime typing in preference to simpler, more efficient, more maintainable design approaches. Typically, runtime type queries are used to compensate for bad architecture, which typically arises from compounded hacks, poor domain analysis, or the mistaken notion that an architecture should be maximally flexible.
In practice, it should rarely be necessary to ask an object personal questions about its type.
|