C++でデザインパターンを実装する:テンプレートメソッド


事の起こり

オブジェクト指向のメリットを理解するために、C++でデザインパターンを実装しています。今回は、第3回目、テンプレートメソッドの実装です。(本記事では、Javaで学ぶデザインパターン入門(著 結城 浩,2001年発行,第3版)を参考にしています。ですので、実装もJavaライクなものとなっています)

デザインパターンについて

デザインパターンについては前記事C++でデザインパターンを実装する:イテレータ編で説明しているので良かったら見てやってください

テンプレートメソッドとは

テンプレートメソッドは「具体的な処理の流れはスーパークラス」で、「その処理を構成する関数の実装はサブクラス」で行うというデザインパターンの一種です。
例えば、「1.初めの挨拶をする」、「2.アラートを鳴らす」、「3.終わりの挨拶をする」と言う3つの関数を使った処理があったとします。テンプレートメソッドではこのような場合、「1〜3と言う処理の流れをスーパークラス」で「1〜3の各関数の実装をサブクラス」で行います。
この段階では、ピンとこない方もいらっしゃると思うので、実装しながら説明していきます。

テンプレートメソッドを用いたサンプル:①クラス図

今回、実装したテンプレートメソッドのクラス構成は以下の通りです。

各クラスを説明していきます。

Reminderクラス

①begin_greet:仮想関数(実装はサブクラスで)
②alert:仮想関数(実装はサブクラスで)
③end_greet:仮想関数(実装はサブクラスで)
④remind:①から③の順番に並べて処理の流れを記述したメソッド(仮想関数ではない)

MornigReminderクラス

朝専用のリマインドを行うクラス。各メソッドとして...
①begin_greet : 朝の初めの挨拶を行う
②alert:朝専用のアラートを鳴らす
③end_greet:朝の終わりの挨拶を行う

NightReminderクラス

夜専用のリマインドを行うクラス。各メソッドとして...
①begin_greet : 夜の初めの挨拶を行う
②alert:夜専用のアラートを鳴らす
③end_greet:夜の終わりの挨拶を行う

このテンプレートメソッドの主役はReminderクラスのremindメソッドです。このremindメソッドで「1.初めの挨拶をする」、「2.アラートを鳴らす」、「3.終わりの挨拶をする」と言う一連の処理の流れ(テンプレート)を実装します。テンプレートメソッドではこのテンプレートを変更すると言うことは原則しません。テンプレートの実挙動を変更する際には、そのテンプレート構成するメソッドの中身を変更することで対応していきます。詳しいことは実際のコードを見て理解していきましょう。

テンプレートメソッドを用いたサンプル:②実装

1.Reminderクラス

Reminderクラスの定義は下記のようになっています。

sub.hpp
class Reminder
{
public:
  //これらの仮想関数はremindを構成するための部品
  virtual void begin_greet(void) = 0; //①初めの挨拶
  virtual void alert(void) = 0;//②アラートを鳴らす
  virtual void end_greet(void) = 0;//③終わりの挨拶
  //今回の主役テンプレート
  void remind(void); //仮想関数でないことに注意

  virtual ~Reminder() {} //仮想デストラクタ
};

remindメソッドの実装は次の通り。このコードを見てもらうとremindメソッドではbegin_greet、alert、end_greetを用いて処理の流れのみを記述していることが分かりますね。これがテンプレートです。。このようにremindでは処理の流れのみを記述し、実際にどのような動作になるのかはbegin_greet、alert、end_greetで決めていきます。

sub.cpp
void Reminder::remind(void)
{
  begin_greet();//①初めの挨拶
  for (int i = 0; i < 10; i++)
  {
    alert();//②アラート
  }
  end_greet();//③終わりの挨拶
}

2.MorningReminderクラス

MorningReminderクラスの定義は下記のようになっています。

sub.hpp
class MoningReminder : public Reminder
{
public:

  void begin_greet(void); //朝の初めの挨拶を行う
  void alert(void);//朝専用のアラートを鳴らす
  void end_greet(void);//朝の終わりの挨拶を行う

  explicit MoningReminder() {}//コンストラクタ
  ~MoningReminder() {}//デストラクタ
};

各メソッドの実装は下記の通りです。各メソッドは朝専用の処理を行うようにしています。(夜におはようは言いませんよね。)。こんな感じで、サブクラスでテンプレート(remind)を構成するメソッドの挙動を変えていきます。こうすることで、MorningReminderから継承したremindを呼び出すと朝専用のリマインドを行なってくれるようになります。

sub.cpp
//朝の初めの挨拶を行う
void MoningReminder::begin_greet(void)
{
  cout << "おはようございます。朝です" << endl;
}

//朝専用のアラートを鳴らす
void MoningReminder::alert(void)
{
  cout << "ビープ音1" << endl; //私の環境では音が出ないので代わりに
}

//朝の終わりの挨拶を行う
void MoningReminder::end_greet(void)
{
  cout << "いってらっしゃい" << endl;
}

2.NightReminderクラス

NightReminderクラスの定義は下記のようになっています。MoningReminderと見比べると、クラスの構成が完全に一致しています。

sub.hpp
class NightReminder : public Reminder
{
public:
  void begin_greet(void);//夜の初めの挨拶を行う
  void alert(void);//夜専用のアラートを鳴らす
  void end_greet(void);//夜の終わりの挨拶を行う

  explicit NightReminder() {}
  ~NightRemind() {}
};

しかし、各メソッドの実装は夜専用のものに変えてあります。NightReminderから継承したremindを呼び出すとこの夜専用の処理が呼び出されます。こうすることでremindの処理の流れを変えずに、夜専用のremindが実現できます。

sub.cpp
//夜の初めの挨拶を行う
void NightReminder::begin_greet(void)
{
  cout << "寝る時間です" << endl;
}
//夜専用のアラートを鳴らす
void NightReminder::alert(void)
{
  cout << "ビープ音2" << endl;
}
//夜の終わりの挨拶を行う
void NightReminder::end_greet(void)
{
  cout << "おやすみなさい" << endl;
}

テンプレートメソッドを用いたサンプル:③動作確認

実装したテンプレートメソッドの動作確認をしていきます。テスト用のコードはこちらです。

main.cpp
int main(void)
{

  Reminder *m = new MoningReminder();
  cout << "MoningReminderで実施" << endl;
  m->remind();
  delete (m);

  cout << endl;

  Reminder *n = new NightReminder();
  cout << "NightRemindで実施" << endl;
  n->remind();
  delete (n);

  return 0;
}

動作結果はこちら、(コンソール画面に表示されます)

MoningReminderで実施
おはようございます。朝です
ビープ音1
ビープ音1
ビープ音1
ビープ音1
ビープ音1
ビープ音1
ビープ音1
ビープ音1
ビープ音1
ビープ音1
いってらっしゃい

NightRemindで実施
寝る時間です
ビープ音2
ビープ音2
ビープ音2
ビープ音2
ビープ音2
ビープ音2
ビープ音2
ビープ音2
ビープ音2
ビープ音2
おやすみなさい

生成するインスタンスがMoningReminderかNightReminderかで、remindメソッドの挙動が変化していることが確認できました!!!

テンプレートメソッド実装の注意点

テンプレートメソッドを実装する上では注意点があります。それは、テンプレートを構成するメソッドの名前の付け方です。 テンプレートを構成するメソッドの名前には極力、固有名詞や固有の処理を使わないで下さい

例えば、今回の例のreminderを構成するメソッドの名前を次のように変えます。(全ての関数の頭にmornig_を付けます)

sub.hpp
class Reminder
{
public:
  //これらの仮想関数はremindを構成するための部品
  virtual void mornig_begin_greet(void) = 0; //①初めの挨拶
  virtual void morning_alert(void) = 0;//②アラートを鳴らす
  virtual void morning_end_greet(void) = 0;//③終わりの挨拶
  //今回の主役テンプレート
  void remind(void); //仮想関数でないことに注意

  virtual ~Reminder() {} //仮想デストラクタ
};

これをしてしまうと、リマインドの処理ではなく朝専用のリマンドしか見えませよね。せっかく、リマンド処理を一般化しているのにその意図がコードの読み手には伝わらなくなります。それに、NightReminderでmornig_begin_greetを実装すると言うとても気持ち悪いことになります。

このようなことを防ぐためにも、テンプレート構成する関数名になるべく一般化した名前にしましょう

ex)
画像処理の前処理テンプレートだったら、
いい例
open(),trim(),close()で構築。
悪い例
png_open(),png_trim(),png_close()で構築。png画像しか処理できないように見える

テンプレートメソッドの利点

テンプレートメソッドを利用し、各クラスに共通のロジックを抜き出しておくことで変更に強いコードを書くことができます。例えば、共通のロジックを持つクラスが100個あるとして、その共通のロジックに誤りが見つかったとします。ここで、テンプレートメソッドを利用していなかった場合、100個のクラスの共有のロジックを書き換えが必要になり、非常に手間です。ここで、テンプレートメソッドを利用して共有のロジックをクラス化し100個のクラスに継承させておけば、100個のクラスの書き換える代わりに、共通ロジック持つクラスを書き換えるだけで済むので非常に楽です
また、共通ロジックを広くプログラマに周知できるので、各クラスの実装を担当するプログラマが共通ロジックを各々実装して、統一感のないコードになってしまうことを防ぐこともできます。

まとめ

率直な感想として、テンプレートメソッドを使いこなすことは難しいと感じます。テンプレートというからには定型業務に利用する機会が多いでしょう。ですが、テンプレート化するほど同じような業務がいくつも転がっている職場はあまりないような気がします。どちらかと言えば、業務そのものというよりも、業務を細分化した処理から共有項を抜き出して、適宜テンプレート化するといった活用方法になるのではないかと思います
どちらにせよ、テンプレートメソッドを使いこなすにはプログラミング能力よりも業務理解能力の方が必要なんじゃないかと感じました。