March 6, 2007, 1:55 p.m.
posted by nogood
Item 30. Factory MethodA high-level design often requires the creation of an object of the "appropriate" type, based on the type of an existing object. For example, we may have a pointer or reference to an Employee object of some kind, and we need to generate the appropriate kind of hrInfo object for that type of Employee, as shown in Figure. Pseudoparallel hierarchies. How should we map an employee to its corresponding human resources information?
Here we have almost parallel Employee and hrInfo hierarchies. Salary and Hourly employees require the generation of an StdInfo object whereas a Temp requires a TempInfo object. The high-level design is simple: "Create the appropriate type of record for this employee." Unfortunately, programmers often see such a requirement as an excuse to engage in runtime type queries. That is, the code that implements this requirement simply asks a series of questions about the exact type of Employee in order to determine the type of hrInfo object to generate. One common approach that is always wrong is to use a type code and a switch-statement: class Employee { public: enum Type { SALARY, HOURLY, TEMP }; Type type() const { return type_; } //... private: Type type_; //... }; //... HRInfo *genInfo( const Employee &e ) { switch( e.type() ) { case SALARY: case HOURLY: return new StdInfo( e ); case TEMP:return new TempInfo( static_cast<const Temp*>(e) ); default: return 0; // unknown type code! } } Nearly as bad is the use of dynamic_cast to ask a series of personal questions of the Employee object: HRInfo *genInfo( const Employee &e ) { if( const Salary *s = dynamic_cast<const Salary *>(&e) ) return new StdInfo( s ); else if( const Hourly *h = dynamic_cast<const Hourly *>(&e) ) return new StdInfo( h ); else if( const Temp *t = dynamic_cast<const Temp *>(&e) ) return new TempInfo( t ); else return 0; // unknown employee type! } The major flaw with both of these implementations of genInfo is that they are coupled to all the concrete types derived from both Employee and hrInfo, and they must be familiar with the mapping from each employee type to its appropriate hrInfo type. Any change in the set of Employees, in the set of HRInfos, or in the mapping from one to the other requires maintenance of the code. In the likely event that different groups will be adding (and removing) new types from these hierarchies on a continuing basis, it's unlikely that this maintenance will always be correctly performed. Another problem is that either approach can fail to identify the exact type of the Employee argument, which will require the code that calls genInfo to make provision to handle the error. The correct approach is to consider where the mapping from each Employee type to the appropriate hrInfo type should reside. Put another way, who knows best what type of HRInfo object a Temp employee requires? The Temp employee itself, of course:
class Temp : public Employee {
public:
//...
TempInfo *genInfo() const
{ return new TempInfo( *this ); }
//...
};
We still have a problem in that we may not know that we are dealing with a Temp employee rather than some other type of employee. But that's easy to fix with a virtual function:
class Employee {
public:
//...
virtual HRInfo *genInfo() const = 0; // Factory Method
//...
};
This is an instance of the Factory Method pattern. Rather than ask a series of blunt personal questions of an employee, we are, in effect, saying, "Whatever type of employee you are, generate the appropriate type of information for yourself." Employee *e = getAnEmployee(); //... HRInfo *info = e->genInfo(); // use Factory Method The essence of Factory Method is that the base class provides a virtual function hook for generating an appropriate "product." Each derived class may override that inherited virtual function to generate an appropriate product for itself. In effect, we have the ability to use an object of unknown type ("some type of employee") to generate an object of unknown type ("the appropriate type of information"). Use of a Factory Method is often indicated when a high-level design requires generation of the "appropriate" object based on the exact type of another object, in the case of parallel or almost parallel hierarchies, and is often the cure for a series of runtime type queries. |
- Comment