Menu

Document

art

What Is The Problem?

GoF Template Method Pattern

How Is It Useful?

GoF's template method pattern is a useful pattern to free subclass implementers from concerns about execution sequence and restrictions and rules brought by the sequential concerns.
For example, when we initialize a class that has a base class, outside of their constructors (this is often time due to needs of implementation detail), the code would look like this:

Base class:
:::C++
void Base::Init() {
  if (initialized_) return;
  DoMyInit();  // Base class initialization first
  DoInit();  // virtual function to invoke subclass initialization after base class's
  initialized_ = true;
}
void Base::DoMyInit() {
  // Base class initialization code
}
Subclass:
:::C++
void Subclass::DoInit() {  // virtual method override
  // do whatever the subclass needs to do for initialization
}

So far so good. The subclass developer does not have to know about the guard (initialized_ flag) and the order (base class first), which are required to correctly initialize these two classes.

We Got A Problem

However when it comes to a class hierarchy more than two levels, this technique does not work well.
The problem is because the mechanism I showed above is based on virtual function override.
If Base derives Sub1, then Sub1 derives Sub2, the virtual function call made by the base class calls Sub2's override implementation, skipping Sub1's.
Therefore, Sub2's implementer needs to manually call Sub1's method in the right order:

Sub2:
:::C++
void Sub2::DoInit() {  // virtual method override
  Sub1::DoInit();  // Sub2 writer needs to be careful calling this before Sub2's initialization.
  // do whatever Sub2 needs to do for initialization
}
Sub1:
:::C++
void Sub1::DoInit() {  // virtual method override
  // do whatever Sub1 needs to do for initialization
}

This fact impairs the smart solution, we took using the template method pattern in the first example, where there were only two level of class hierarchy.
The error-prone-ness came back in the code Sub2::DoInit(). It needs to know it has to call Sub1::DoInit() 'before' it does its own initialization.

Method Chain Framework

Above was the motivation I wanted to come up with a helper mechanism that frees sub class writer completely from anything other than its own class's concern.

With using the mechanism, the initialization codes of the three classes (Base, Sub1, Sub2) look like these:

Base:
:::C++
Base::Base()
  // sets up its own initialization method
 : MethodChainerTop<InitCaller>(std::bind(&Base::DoMyInit, this));
{}
void Base::Init() {
  if (initialized_) return;
  MethodChainerTop<InitCaller>::InvokeChainMethod();  // this starts the hierarchical call chain
  initialized_ = true;
}
void Base::DoMyInit() {
  // Base class initialization code
}
Sub1:
:::C++
Sub1::Sub1()
  // sets up its own initialization method
 : MethodChainer<InitCaller, Sub1>(this, std::bind(&Sub1::DoInit, this));
{}
void Sub1::DoInit() {
  // do whatever Sub1 needs to do for initialization
 }
Sub2:

Sub2 looks exactly like Sub1. It does not need to care about calling Sub1's initialization.

:::C++
Sub2::Sub2()
  // sets up its own initialization method
 : MethodChainer<InitCaller, Sub2>(this, std::bind(&Sub2::DoInit, this));
{}
void Sub2::DoInit() {
  // do whatever Sub2 needs to do for initialization
}

Usage Variation

You may have already noticed, the method chain is established by the MethodChainer[Top] constructor that takes std::function.
This means DoInit no longer needs to be a virtual method, so each subclass can use its uniquely named method, or even more, lambda can be used.
Again, the emphasis here is the fact that Sub2 writer no longer has to be careful and remember he needs to call its superclass's initializer in its initializer method.
There are two call chain sequences.
One is, as described above, descending sequence which calls superclass's method before subclass's.
This direction of call sequence is usually required for initialization.
The other is ascending sequence which calls subclass's method before superclass's.
This direction of call sequence is usually required for shutdown.

Example With Variations

Here are complete example codes that have these two sequence directions.
In addition to that, the shutdown sequence (Stop in the example below) takes one argument.
The framework allow each implementation method take any number of arguments, as long as they are consistent through the method chain.
In this example, class Parent derives SubParent, SubParent derives Child and Child derives GrandChild:

:::C++
struct InitCaller : public DescendingMethodChain<> {}; // This specifies call chain order and id.
struct StopCaller : public AscendingMethodChain<int> {}; // ditto, and specifies the chained method takes one 'int' arg.

class Parent
    : protected MethodChainerTop<InitCaller> // attaches the mechanism for Init
    , protected MethodChainerTop<StopCaller> { // ditto for Stop
 public:
  Parent()
    : MethodChainerTop<InitCaller>(
        // Its own init method is this lambda
        [this]() { std::cout << "Parent::DoMyInit\n"; }
      )
    , MethodChainerTop<StopCaller>(
        // Its own stop method is this lambda
        [this](int i) { std::cout << "Parent::DoMyStop " << i << std::endl; }
    ) {
  }
  void Init() { // public method to start initialization sequence
    if (initialized_) return;
    MethodChainerTop<InitCaller>::InvokeChainMethod(); // this starts the call chain
    initialized_ = true;
  }
  void Stop(int code) { // public method to start shutdown sequence
    if (stopped_) return;
    MethodChainerTop<StopCaller>::InvokeChainMethod(code); // this starts the call chain
    stopped_ = true;
  }
 private:
  bool initialized_ = false;
  bool stopped_ = false;
};

class SubParent
  : public Parent
  , protected MethodChainer<InitCaller, SubParent>
  , protected MethodChainer<StopCaller, SubParent> {
 public:
  // no initialization needed if this class has no method to chain
};

class Child
    : public Parent
    , protected MethodChainer<InitCaller, Child> // attaches the mechanism for init
    , protected MethodChainer<StopCaller, Child> { // ditto for stop
 public:
  Child()
    : MethodChainer<InitCaller, Child>(this, std::bind(&Child::DoInit, this))  // specifies a method to chain
    , MethodChainer<StopCaller, Child>(this, std::bind(&Child::DoStop, this, std::placeholders::_1)) {
  }
  void DoInit() {
    // Child's own initialization implementation
    std::cout << "Child::DoInit\n";
  }
  void DoStop(int code) {
    // Child's own shutdown implementation
    std::cout << "Child::DoStop " << code << std::endl;
  }
};

class GrandChild
    : public Child
    , protected MethodChainer<InitCaller, GrandChild>
    , protected MethodChainer<StopCaller, GrandChild> {
 public:
  GrandChild()
    : MethodChainer<InitCaller, GrandChild>(this,
        // inits by lambda
        [this](){ std::cout << "GrandChild::DoInit\n"; }
      )
    , MethodChainer<StopCaller, GrandChild>(this,
        // stops by lambda
        [this](int code){ std::cout << "GrandChild::DoStop " << code << std::endl; }
      ) {
  }
};

Test Run:
int main() {
  GrandChild gc;
  gc.Init();
  std::cout << std::endl;
  gc.Stop(123);
  return 0;
}

The code above outputs:

Parent::DoMyInit
Child::DoInit
GrandChild::DoInit
GrandChild::DoStop 123
Child::DoStop 123
Parent::DoMyStop 123


MongoDB Logo MongoDB