http://martinfowler.com/bliki/CallSuper.html

親メソッド呼び出しは、オブジェクト指向フレームワークのなかで 不吉な匂い(アンチパターンと言っても良いでしょう)を出しています。 その症状は簡単に見つけることができます。 フレームワークを使うために、スーパークラスを継承しているとしましょう。 ドキュメントには、おそらくこのようなことが書かれているでしょう。 「サブクラスを作成して処理メソッドをオーバーライドしてください。 ただし、メソッドの先頭でスーパークラスの呼び出しを行ってください。」 例えばこんな感じです。

 public class EventHandler ...
   public void handle (BankingEvent e) {
    housekeeping(e);
   }
 
 public class TransferEventHandler extends EventHandler...
   public void handle(BankingEvent e) {
     super.handle(e);
     initiateTransfer(e);
   }

毎回、何かを忘れずにやらないといけないなら、それは悪いAPIの兆候です。 ここでは、APIがhousekeepingの呼び出しを行うべきです。 よくやるのは、handleメソッドをテンプレートメソッドにすることです、 こんな風に。

 public class EventHandler ...
   public void handle (BankingEvent e) {
     housekeeping(e);
     doHandle(e);
   }
   protected void doHandle(BankingEvent e) {
   }
 
 public class TransferEventHandler extends EventHandler ...
   protected void doHandle(BankingEvent e) {
     initiateTransfer(e);
   }

ここでスーパークラスは、publicメソッドとサブクラスがオーバライドするためのメソッド(フックメソッドと呼ばれます)を定義しています。 サブクラスを書く人は、これで親クラスのメソッドを呼び出すことについて頭を悩ませる必要はありません。 さらに、スーパークラスの作成者はサブクラスのメソッドの後に処理の呼び出しを追加することもできます。

フックメソッドを定義する方法はいくつかあります。 ここでは特に何も実装していませんが、サブクラスで何か振る舞いを追加する必要がなければこれでよいでしょう。 多くのサブクラスで同じことをする必要があるなら、デフォルト実装を検討することができます。 その場合これは、共通の枠組みを設けて自由に実装させるテンプレートメソッドになります。 もし全てのサブクラスで独自の振る舞いを追加するのであれば、スーパークラスのフックメソッドをabstractにします。

この手法の問題点のひとつは、どのメソッドがフックメソッドなのか知る方法がないという点です。つまり、フレームワークの作者がオーバーライドして欲しいと思っているのはどのメソッドなのかが分からないのです。 handleやdoHandleといった名前を付けるという慣習もありますが、やはりどう動くのかについてはどこかで説明する必要があります。説明する際は、サンプルを添えるとよいでしょう。 私がこのような場合に遭遇したときは、既存のサブクラスを見て、それが何をしてるか調べています。

先ほどのケースでは、フックメソッドはひとつしかなく、比較的分かりやすいです。 しかし、サブクラスで制御する度合いによって、多くのメソッドがフック可能になっていることがあります。 私のHTMLレイアウト用の抽象クラスは、ちょっと見ただけで半ダースのメソッドがオーバライド可能です。 サブクラスでやりたいことは簡単なことで、小さいフックをオーバライドしています。 中には手の込んだサブクラスもあり、より大きなスコープのメソッドをオーバーライドしています。

サブクラスがオーバーライドできないように、ハンドルメソッドをシールできる言語もあります。 私は委任主義なので気が進みませんが——特に継承が公布済みのインターフェースなら絶対にやりません。 シール派は、サブクラスがスーパークラスを壊すことができなくするためだと主張します。 私はオーバーライドが「スーパークラスを壊す」ような悪いことだとは思いません。 サブクラスとスーパークラスは密接に協調しなくてはいけません。 継承というのはそれほど密接な関係なのです。 継承とは、すべてが動くか、すべてが動かないかのいずれかです。 シールしておくと、そのメソッドはオーバーライドしてはいけないと警告を出すことができます。 インターフェースを公布していないならば(サブクラスが同じコードベースにあるならば)、私もシールを使ってみようかなと思います。 シールの問題点は、サブクラスが何か特別なことを行うときに、handleメソッドを呼び出せない点です。 このサブクラスの要求を阻むことはできません。 ですから「どうぞご自由に。ただし、自己責任でお願いします。」と言うでしょう。 同一のコードベースにあれば、必要なときにシールを剥がすこともできます。

複数レベルのフック

以上のような状況で、親メソッド呼び出しに頼る必要がないことを理解していただけたでしょうか。 ただし、複数レベルのフレームワークを扱うときには複雑になります。

ここで実際の例に切り替えます。しばしば登場するJUnitです。 JUnitはテストケースを走らせることの総合的なコントロールにテンプレートメソッドを使っています。こんな感じです。

 public abstract class TestCase
   public void runBare() throws Throwable {
     setUp();
     try {
       runTest();
     }
     finally {
       tearDown();
     }
   }
   protected void setUp() throws Exception {
   }
   protected void tearDown() throws Exception {
   }

これは一般的なテンプレートメソッドと、オーバーライド用の2つのフックメソッドです。 ここまではいいですね。 setUpとtearDownに独自のコードを追加すればいいのです。

複雑になるのは、ユーザがJUnitから別のフレームワークを派生しようとしたときです。他に沢山の例がありますが、プロジェクト固有の規約を持っている簡単なケースを用います。 こんな感じです。

 public class AlphaTestCase extends TestCase
   protected void setUp() throws Exception {
     alphaProjectSetup();
   }

親メソッド呼び出し問題に出くわしました。 なので、先のアドバイスに従ってこう再定義します。

 public class AlphaTestCase extends TestCase...
   final protected void setUp() throws Exception {
     alphaProjectSetup();
     doSetUp();
   }    
   protected void doSetUp() throws Exception {        
   }

一応これでも機能しますが、JUnitに馴れた人を困惑させるという問題に出くわします。 これまで携わってきたのプロジェクトや、これまで読んできた本は、 新しいdoSetUpではなくsetUpをオーバーライドすべきだと言っています。 人々が混乱する可能性が非常に高いので、これはfinalを使う良いケースです。 しかし、finalを使っても、「setup」メソッドが違うのは混乱の元です。

他にも選択肢があります。 第二レベルのフレームワークは、 setUpの呼び出し側メソッドをオーバーライドできます。

 public class AlphaTestCase extends TestCase...
   public void runBare() throws Throwable {
     alphaProjectSetup();
     setUp();
     try {
       runTest();
     }
     finally {
       tearDown();
     }
   }
   protected void setUp() throws Exception {
   } 

これだと誰でもsetUpを普通と同じように使うことができます。 フレームワーク作者が振る舞いを追加したいのであれば、他にも選択肢はあります。 これは検討する価値のある選択です——もしテンプレートメソッドがあなたのいる場所で機能しないなら、レベルを上げることを検討してください。

もちろん自由なテンプレートのようなものはありません。 ソースが利用できないために、自由にできないこともあります。 他にもフレームワークの作者が直轄主義なので、オーバーライドできないように、呼び出し側のメソッドをシールすることがあります。

もしそれができるとしても、注意することがあります。 それはもし上位のフレームワークの作者が、あなたがオーバーライドしているメソッドを変更する必要がでることです(JUnitが2004/8/9にそれを行いました)。 継承には責任が伴います。 スーパークラスに何が起こるか、あなたは目を光らせる必要があります。

現在では、アノテーションが利用可能になりつつあります。 JUnitとその他のJavaベースフレームワークは、NUnitに続いて、この数ヶ月でアノテーションが機能するようになりました。 アノテーションはメソッド名以上のメタデータを与えることを許し、この種の状況にさらなる選択を許します。しかしそれは別の日のテーマです。

  • 2005-08-31 (水) 20:22:53 kameda : とても読みやすくなりました。ありがとうございました。
  • kdmsnr : 少し変更しました。
  • 2005-08-30 (火) 23:18:39 kdmsnr : ありがとうございます。
  • 2005-08-30 (火) 23:07:34 kameda : 初めて投稿します。拙い訳文で申し訳ありません。ビシバシツッコミのほど、よろしくお願いしますm(__)m