I l@ve RuBoard Previous Section Next Section

Solution

graphics/bulb_icon.gif

Let's consider the questions one by one.

  1. This is a good design and implements a well-known design pattern. Which pattern is this? Why is it useful here?

    This is the Template Method (Gamma95) pattern (not to be confused with C++ templates). It's useful because we can generalize a common way of doing something that always follows the same steps. Only the details may differ and can be supplied by a derived class. Further, a derived class may choose to reapply the same Template Method approach梩hat is, it may override the virtual function as a wrapper around a new virtual function梥o different steps can be filled in at different levels in the class hierarchy.

    (Note: The Pimpl Idiom is superficially similar to Bridge, but here it's only intended to hide this particular class's own implementation as a compilation dependency firewall, not to act as a true extensible bridge. We'll analyze the Pimpl Idiom in depth in Items 26 through 30.)

    Guideline

    graphics/guideline_icon.gif

    Avoid public virtual functions; prefer using the Template Method pattern instead.


    Guideline

    graphics/guideline_icon.gif

    Know about and use design patterns.


  2. Without changing the fundamental design, critique the way this design was executed. What might you have done differently? What is the purpose of the pimpl_ member?

    This design uses bools as return codes, with apparently no other way (status codes or exceptions) of reporting failures. Depending on the requirements, this may be fine, but it's something to note.

    The (intentionally pronounceable) pimpl_ member nicely hides the implementation behind an opaque pointer. The struct that pimpl_ points to will contain the private member functions and member variables so that any change to them will not require client code to recompile. This is an important technique documented by Lakos (Lakos96) and others, because while it's a little annoying to code, it does help compensate for C++'s lack of a module system.

    Guideline

    graphics/guideline_icon.gif

    For widely used classes, prefer to use the compiler-firewall idiom (Pimpl Idiom) to hide implementation details. Use an opaque pointer (a pointer to a declared but undefined class) declared as "struct XxxxImpl; XxxxImpl* pimpl_;" to store private members (including both state variables and member functions)梖or example, "class Map {private: struct MapImpl; MapImpl* pimpl_; };".


  3. This design can, in fact, be substantially improved. What are GenericTableAlgorithm's responsibilities? If more than one, how could they be better encapsulated? Explain how your answer affects the class's reusability, especially its extensibility.

GenericTableAlgorithm can be substantially improved because it currently holds two jobs. Just as humans get stressed when they have to hold two jobs梑ecause that means they're loaded up with extra and competing responsibilities梥o too this class could benefit from adjusting its focus.

In the original version, GenericTableAlgorithm is burdened with two different and unrelated responsibilities that can be effectively separated, because the two responsibilities are to support entirely different audiences. In short, they are:

  • Client code USES the (suitably specialized) generic algorithm.

  • GenericTableAlgorithm USES the specialized concrete "details" class to specialize its operation for a specific case or usage.

Guideline

graphics/guideline_icon.gif

Prefer cohesion. Always endeavor to give each piece of code梕ach module, each class, each function梐 single, well-defined responsibility.


That said, let's look at some improved code:



//--------------------------------------------------- 


// File gta.h


//---------------------------------------------------


// Responsibility #1: Providing a public interface


// that encapsulates common functionality as a


// template method. This has nothing to do with


// inheritance relationships, and can be nicely


// isolated to stand on its own in a better-focused


// class. The target audience is external users of


// GenericTableAlgorithm.


//


class GTAClient;





class GenericTableAlgorithm


{


public:


  // Constructor now takes a concrete implementation


  // object.


  //


  GenericTableAlgorithm( const string& table,


                         GTAClient&    worker );





  // Since we've separated away the inheritance


  // relationships, the destructor doesn't need to be


  // virtual.


  //


  ~GenericTableAlgorithm();





  bool Process(); // unchanged





private:


  struct GenericTableAlgorithmImpl* pimpl_; // MYOB


};


//---------------------------------------------------


// File gtaclient.h


//---------------------------------------------------


// Responsibility #2: Providing an abstract interface


// for extensibility. This is an implementation


// detail of GenericTableAlgorithm that has nothing


// to do with its external clients, and can be nicely


// separated out into a better-focused abstract


// protocol class. The target audience is writers of


// concrete "implementation detail" classes which


// work with (and extend) GenericTableAlgorithm.


//


class GTAClient


{


public:


  virtual ~GTAClient() =0;


  virtual bool Filter( const Record& );


  virtual bool ProcessRow( const PrimaryKey& ) =0;


};





//---------------------------------------------------


// File gtaclient.cpp


//---------------------------------------------------


bool GTAClient::Filter( const Record& )


{


  return true;


}


As shown, these two classes should appear in separate header files. With these changes, how does this now look to the client code? The answer is: pretty much the same.



class MyWorker : public GTAClient 


{


  // ... override Filter() and ProcessRow() to


  //     implement a specific operation ...


};





int main()


{


  GenericTableAlgorithm a( "Customer", MyWorker() );


  a.Process();


}


While this may look pretty much the same, consider three important effects.

  1. What if GenericTableAlgorithm's common public interface changes (for example, a new public member is added)? In the original version, all concrete worker classes would have to be recompiled because they are derived from GenericTableAlgorithm.

    In this version, any change to GenericTableAlgorithm's public interface is nicely isolated and does not affect the concrete worker classes at all.

  2. What if GenericTableAlgorithm's extensible protocol changes (for example, if additional defaulted arguments were added to Filter() or ProcessRow())? In the original version, all external clients of GenericTableAlgorithm would have to be recompiled even though the public interface is unchanged, because a derivation interface is visible in the class definition. In this version, any changes to GenericTableAlgorithm's extension protocol interface is nicely isolated and does not affect external users at all.

  3. Any concrete worker classes can now be used within any other algorithm that can operate using the Filter()/ProcessRow() interface, not just GenericTableAlgorithm. In fact, what we've ended up with is very similar to the Strategy pattern.

Remember the computer science motto: Most problems can be solved by adding a level of indirection. Of course, it's wise to temper this with Occam's Razor: Don't make things more complex than necessary. A proper balance between the two in this case delivers much better reusability and maintainability at little or no cost梐 good deal by all accounts.

Let's talk about more generic genericity for a moment. You may have noticed that GenericTableAlgorithm could actually be a function instead of a class (in fact, some people might be tempted to rename Process() as operator()(), because now the class apparently really is just a functor). The reason it could be replaced with a function is that the description doesn't say that it needs to keep state across calls to Process(). For example, if it does not need to keep state across invocations, we could replace it with:



bool GenericTableAlgorithm( 


  const string& table,


  GTAClient&    method


  )


{


  // ... original contents of Process() go here ...


}





int main()


{


  GenericTableAlgorithm( "Customer", MyWorker() );


}


What we've really got here is a generic function, which can be given "specialized" behaviour as needed. If you know that method objects never need to store state (that is, all instances are functionally equivalent and provide only the virtual functions), you can get fancy and make method a nonclass template parameter instead.



template<typename GTACworker> 


bool GenericTableAlgorithm( const string& table )


{


  // ... original contents of Process() go here ...


}





int main()


{


  GenericTableAlgorithm<MyWorker>( "Customer" );


}


I don't think that this buys you much here besides getting rid of a comma in the client code, so the first function is better. It's always good to resist the temptation to write cute code for its own sake.

At any rate, whether to use a function or a class in a given situation can depend on what you're trying to achieve, but in this case writing a generic function may be a better solution.

    I l@ve RuBoard Previous Section Next Section