Template Methodパターンの関係者
- 親クラスの制作者
-
親クラスは、子クラスで変更できる部分と、できない部分を規定する。よって、親クラスの制作者は、将来作られるであろう子クラスをおおよそ想定して、子クラスでオーバーライドできる抽象メソッドを用意しておく必要がある。
- 子クラスの制作者
-
子クラスには、処理が異なる部分だけが記述されているため、子クラスだけを見ても、意図や存在意義が分からないこともある。
例えば、子クラスには、下記の1行だけしか書かれていない、ということすらある。
return true;
子クラスの制作者は、親クラスの作り、特に、Template Methodはその実装まで、強く意識する。
- クラスの利用者
-
利用者は、Template Methodパターンになっていることは、ほとんど意識しない。
利用者はたいてい、子クラスを直接使う。呼び出すメソッドが、実は、処理自体は親クラスでTemplate Methodとして実装されていて、子クラスでは差異の分だけ実装されているとしても、利用者には関係のないことである。
良く使われるパターン
Template Methodパターンは、ふつうにソフトウェアを作成していると、知らぬうちに使っていることが多い。
子クラスから親クラスを作る時
ソフトウェアを作っていると、2ヶ所で同じような処理を記述しているのに気づくことがある。そういった時は、同じような処理の部分を1つにまとめ、共通化することになる。
手続き型のプログラミング言語では、同じような処理の部分を1つの関数にまとめることだろう。異なる部分は、引数に応じた場合分けで対応したりする。
オブジェクト指向言語では、同じような処理の部分を1つのクラスとしてまとめることになる。それを継承した子クラスを2つ作り、それぞれに異なる部分だけを実装する。この時、子クラスごとに異なる部分は抽象メソッドとして、親クラスで空の定義をしておくことも多い。この時、出来上がったクラスの構成は、Template Methodパターンの形になる。
親クラスから子クラスを作る時
テンプレートメソッドから呼び出される抽象メソッドには、親クラスでデフォルトの実装を用意しておいても良い。
あるクラスを継承して子クラスを作り、親クラスのprotectedメソッドをオーバーライドすると、結果的にTemplate Methodパターンの形になる。
この場合、元の親クラスでは、意識せずにTemplate Methodパターンを採用し、抽象メソッドにデフォルトの実装を用意しておいただけ、と見なされる。
極論すれば、Java言語の場合、クラスのprotectedメソッドをfinal宣言していない場合は、無条件にTemplate Methodパターンに該当してしまうことになる。C++言語の場合は、意図的にvirtual修飾子を付けないとオーバーライドできないので、意識しない限りはTemplate Methodパターンにはならない。
継承と委譲
継承の強制
将来の機能拡張に備えて、基本となるクラスでは積極的にTemplate Methodパターンを採用して、子クラスを作った時にオーバーライドできるようなメソッドをたくさん用意しているケースも見かける。
しかし、Template Methodパターンを使うと、機能の拡張を継承によって行うことが定められてしまう。場合によっては、機能の拡張を委譲によって行う方が適しているケースもある。そういったケースでは、予めTemplate Methodパターンを前提とした枠組みが組まれていると、却って弊害となることもある。
例として、Decoratorパターンの考察では、Template Methodパターンの枠組みの中でDecoratorパターンを使いたいケースについて紹介している。
設計の分かりやすさ
Template Methodパターンによって、親クラスと子クラスで処理を分担するよりも、委譲によって処理を分けた方が、クラス設計を説明しやすい。
委譲であれば、共通の処理と、その中で子クラスごとに異なる処理が、2つのクラスに分担されていることが、シーケンス図ではっきり示せる。それぞれが、異なるインスタンスに任されているためである。
一方、Template Methodパターンでは、共通の処理も、異なる処理も、結局は同一のインスタンスで行われる。よって、インスタンスどうしの動的な関係を示すシーケンス図では、処理が分担されていることが分かりにくい。
継承か、委譲か
継承によるTemplate Methodパターンか、委譲によるStrategyパターンか、どちらが良いか。それは、子クラスごとに異なる処理が、単独でも意味のあるものかどうか、で決めればよい。
単独でも意味のあるメソッドであれば、他のクラスからも呼び出せる、publicメソッドとすべきだ。即ち、子クラスごとの差異を表す抽象メソッドを、publicメソッドとして公開してもふさわしいものであれば、委譲を使った方が良い。
逆に、子クラスごとに異なる処理をするメソッドが、単独では意味を為さず、Template Methodから呼ばれるためだけに存在するような場合は、他のクラスからは隠蔽した方が良い。即ち、 Template Methodパターンの使用例として最適なケースは、子クラスごとの差異を表す抽象メソッドが、protectedメソッドであるような場合である。
Template Methodパターンの問題
クラスの数が膨大になりやすい
Template Methodパターンに凝り過ぎると、子クラスの数が膨大になってしまうことがある。場合によっては、if文やswitch文で処理を分岐するだけに留め、クラスの数を減らした方が、却って分かりやすくなることがある。
オーバーライドを忘れがち
子クラスでオーバーライドすべき抽象メソッドに対して、親クラスでデフォルト実装を用意しておくこともある。だが、デフォルト実装があると、新しい子クラスを作る際に、それらを適切にオーバーライドすることを忘れてしまうことがある。
デフォルト実装があると、それぞれの子クラスは、必要なメソッドしかオーバーライドしない。つまり、子クラスを見ても、オーバーライドすべきメソッドがすべて記述されていない。
ここで、新しい子クラスを作る際に、既存の子クラスをコピー&ペーストして作ってしまうと、本当はもっとオーバーライドしないといけないメソッドがあるのに、忘れてしまうことがある。
これを防ぐために、敢えて、親クラスではデフォルト実装を用意しない場合もある。
アスペクト指向プログラミングとの関係
あらゆる処理の前後で、必ずログを出力したい、というケースは多い。
これは、
- 開始ログを出力する。
- 何らかの処理を行う(抽象メソッド)。
- 終了ログを出力する。
というTemplate Methodを用意すれば、実現できる。だが、この方法では、どんな機能のどんな処理でも、すべて、必ず、同じクラスを継承しなければならない。これは、大きすぎる制約である。
ログ出力のように、あらゆる機能で横断的に行われる処理は、アスペクト指向プログラミングで解決したい。
Template Methodパターンの逆
いくつかクラスを作ったら、同じような処理が出てきたので、共通化して、親クラスにまとめた。これは良くある話だが、やり方によっては、Template Methodパターンになる場合と、まったく逆になる場合がある。
Template Methodパターンは、親クラスに処理の流れを書き、親クラスから子クラスのメソッドを呼び出す。だが時には、これとは逆に、子クラスに処理の流れを書いて、子クラスから親クラスのメソッドを呼び出してしまっている例も見られる。
Template Methodパターンでは、親クラスは、基本となる処理の流れを規定するもの、として、役割が明確である。一方、これが逆になってしまった例では、親クラスはたいがい、単なるいろいろな処理の寄せ集めに過ぎず、継承関係に意味がなくなっている。
子クラスから親クラスのメソッドを呼び出している時は、設計がおかしくないか、疑ってみると良い。
親クラスと子クラスの境界線
Template Methodパターンでは、親クラスと子クラスが密接に連携している。と同時に、クラス設計の考え方では、親クラスは親クラスで、子クラスは子クラスで、独立性を保っていなければならない。
親クラスでどのような部分を抽象メソッドとして切り分け、子クラスで実装を任せても、Template Methodパターンであることには変わりはない。しかし、クラス設計の考え方からは、Template Methodパターンの使い方には、良い例と悪い例がある。
例えば、N個のデータをファイルやデータベースに保存するとしよう。
ファイルに保存する処理は、次のようになる。filenameはファイル名を表すメンバー変数である。最初にディスクの空き容量を調べている。また、保存する前に、古いファイルのバックアップを残している。
if (checkDiskSpace()) return; // バックアップを残す。 copyFile(filename, filename + ".bak"); open(filename); for (int i = 0 ; i < N ; i++) { print(data[i]); } close(filename);
データベースに保存する処理は、次のようになる。
openDatabase(); for (int i = 0 ; i < N ; i++) { saveToDatabase(i, data[i]); } closeDatabase();
ここで、共通の処理を抜き出した親クラスのテンプレートメソッドと、子クラスで実装すべき抽象メソッドを、次のように作成した。
public final void saveData ( ) { if (checkDiskSpace()) return; createBackup(); open(); for (int i = 0 ; i < N ; i++) { save(i, data[i]); } close(); } protected abstract void checkDiskSpace ( ); protected abstract void createBackup ( ); protected abstract void open ( ); protected abstract void save ( int i, Data data ); protected abstract void close ( );
ここには、2つの大きな問題がある。
1つは、checkDiskSpace関数である。ディスクの空き容量を調べる、という処理は、ファイルに保存する場合でしか意味を持たない。データベースに保存する場合は、ディスクの空き容量を調べる、という概念は無い。そのため、データベースに保存する処理を記述する子クラスでは、checkDiskSpace関数の扱いに困ることになる。実際にはこういうケースは良く見られ、子クラスでは無条件にtrueを返していたりするが、ここでは、ディスクの空き容量を調べる処理も、openメソッドに含めてしまった方が良い。
もう1つの問題は、createBackup関数である。バックアップを残す、という処理は、ファイルに保存する場合しか機能していない。しかし、テンプレートメソッドを上記のように記述してしまうと、どこに保存する場合でも、バックアップが残されるかのように見えてしまう。
これらの問題を解消した親クラスのメソッドは、下記のようになる。
public final void saveData ( ) { if (isBackupSupported()) { createBackup(); } open(); for (int i = 0 ; i < N ; i++) { save(i, data[i]); } close(); } protected abstract bool isBackupSupported ( ); protected abstract void createBackup ( ); protected abstract void open ( ); protected abstract void save ( int i, Data data ); protected abstract void close ( );
isBackupSupportedという抽象メソッドを追加することによって、親クラスのコードを見た時に、バックアップを残せる場合と、残せない場合があることが、明確になる。
コンストラクタをTemplate Methodとする
インスタンスを初期化する手順を親クラスで規定するために、コンストラクタをTemplate Methodとして、コンストラクタから呼ばれるメソッドを、子クラスでオーバーライドしたいことがある。
これは、プログラミング言語によって、できる場合とできない場合がある。
C++言語では、コンストラクタから抽象メソッドを呼び出しても、子クラスでオーバーライドされた処理が実行されない。よって、コンストラクタでは、Template Methodパターンは使えない。
例えば、下記のコードを実行しても、「Parent」と出力されてしまう。
class Parent { protected: virtual void foo ( ) { puts("Parent"); } public: Parent ( ) { foo(); } }; class Child : public Parent { protected: void foo ( ) { puts("Child"); } }; int main() { Child c; }
Java言語であれば、コンストラクタからでも子クラスでオーバーライドした処理が実行される。但し、この場合も注意がある。それは、インスタンスの初期化は、親クラスのコンストラクタが実行された後で、子クラスのコンストラクタが呼ばれる、という点である。親クラスのコンストラクタから抽象メソッドが呼ばれる時点では、子クラスのコンストラクタで記述された初期化はまだ実行されていない。即ち、インスタンスとして完成していない状態でメソッドが呼び出される。
例えば、下記のコードを実行しても、Childクラスのフィールドbは2ではなく、0と出力されてしまう。
abstract class Parent { protected int a = 0; protected abstract void foo ( ); protected Parent ( ) { a = 1; foo(); } } class Child extends Parent { protected int b = 0; protected void foo () { System.out.printf("a=%d b=%d\n", a, b); } public Child ( ) { b = 2; } public static void main ( String[] args ) { Parent c = new Child(); } }
このような問題があるため、Java言語でも、子クラスでオーバーライドされ得るメソッドをコンストラクタから呼び出すことは、不適切とされている。それに従えば、コンストラクタでTemplate Methodパターンを使うのも、望ましくないことになる。
だが、インスタンスの初期化の手順を規定できる、という利点のためには、コンストラクタをTemplate Methodとして、抽象メソッドを呼び出しても良い、と思う。
「デザインパターン入門」の例について
Template Methodパターンについて、下記の本(以下「デザインパターン入門」と記す)
Java言語で学ぶデザインパターン入門
結城浩著
ソフトバンクパブリッシング株式会社
で取り上げられている例は、Template Methodパターンの例としては適切でないように思う。
「デザインパターン入門」では、テンプレートメソッドから呼び出される抽象メソッド(open/print/close)が、publicメソッドとして定義されている。
この例では、
開いて、5回出力して、閉じる。
という一連の処理を実行するdisplayメソッドを、たまたま親クラスのAbstractDisplayクラスで実装しているため、見かけ上はTemplate Methodパターンに見える。
しかし、この一連の処理は、Strategyパターンを使って、下記のように、別のクラスに実装しても構わない。
public class FifthDisplay { private AbstractDisplay display; public FifthDisplay ( AbstractDisplay display ) { this.display = display; } public final void display ( ) { display.open(); for (int i = 0 ; i < 5 ; i++) { display.print(); } display.close(); } }
この時は、AbstractDisplayはinterfaceとなる。
open/print/closeのようなpublicメソッドを組み合わせて何らかの処理を行う場合、その処理を親クラスで実装するのは、適していない場合が多い。むしろ、上記のように、その処理を表す独立したクラスとして分離する方が良いことが多い。また、このようなメソッドを親クラスに追加していると、親クラスが不要に複雑で多機能なものになってしまうことも多い。
前述したように、Template Methodパターンの使用例として最適なケースは、子クラスごとの差異を表す抽象メソッドが、protectedメソッドであるような場合と思われる。
「てるぼう」と「やん茶姫」の著作権は「まる」氏にあります。