Call Super(親クラスの呼び出し)
2005年8月11日
Call Super(親クラスの呼び出し)は、OOフレームワークで時々見られる小さな臭い(あるいは、もしあなたがそうしたいなら、アンチパターン)です。その症状は非常に見つけやすいです。あなたは、何かのフレームワークに組み込むために、スーパークラスから継承しています。ドキュメントには、「独自のことをするためには、processメソッドをサブクラス化してください。ただし、メソッドの開始時にスーパークラスを呼び出すことを忘れないことが重要です」といったことが書かれています。例としては、次のようなものがあります。
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がハウスキーピング呼び出しを覚えておく必要があります。これを行う一般的な方法は、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メソッドを定義し、サブクラスがオーバーライドするための別のメソッド(しばしばフックメソッドと呼ばれる)を提供します。サブクラスの作成者は、スーパークラスの呼び出しについて頭を悩ませる必要がなくなりました。さらに、スーパークラスの作成者は、必要に応じてサブクラスのメソッドの後に呼び出しを追加することができます。
フックメソッドを定義する方法はいくつかあります。この例では、空の実装を示しました。これは、多くのサブクラスが独自の追加動作を提供する必要がない場合に役立ちます。多くのサブクラスが同じことをする必要がある場合は、共通スキーム内のバリエーションを可能にするために、デフォルトの実装を検討できます。すべてのサブクラスが一意の動作を提供する必要がある場合は、スーパークラスでフックメソッドを抽象化できます。
これの問題点の1つは、特定のメソッドがフックメソッドである、つまりフレームワークの作成者がオーバーライドすることを期待しているメソッドであることを示す方法が通常ないことです。慣例(handle/doHandleが一般的なものの1つ)を見ることはできますが、ほとんどの場合、それがどのように機能するかを説明する必要があります。それを説明する最良の方法の1つは、例を使用することです。これらのケースの1つを見るとき、私が通常行う最良の方法は、既存のサブクラスを見てそれが何をするかを確認することです。
この場合、単一の、そして比較的明白なフックメソッドがあります。しかし、多くの場合、サブクラスがどれだけの制御を必要とするかに応じて、すべてがフックになり得る多くのメソッドがあります。私のHTMLレイアウトクラスの抽象基本クラスをざっと見ると、オーバーライド可能なメソッドが6つほどあります。一部のサブクラスは単純なことを行い、小さなフックをオーバーライドします。他のサブクラスはより高度なことを行う必要があり、より大きなスコープのメソッドをオーバーライドします。
一部の言語では、サブクラスがそれをオーバーライドするのを防ぐためにhandleメソッドをシールできます。私はEnablingAttitudeを持っているため、通常、これを行うのに気が進みません。特に継承が事実上公開されたインターフェースである場合はそうです。シールを支持する議論は、サブクラスがスーパークラスを壊すことができないということです。「スーパークラスを壊す」こととして、間違ったものをオーバーライドしているとは思いません。結局のところ、サブクラスとスーパークラスは緊密に連携する必要があり、継承は非常に親密な関係です。全体が機能するか、機能しないかのどちらかです。シールは、間違ったものをオーバーライドしていることを誰かに示すのに良い方法であり、そのため、公開されていないインターフェース(つまり、サブクラスが同じコードベースの一部である場合)に使用する傾向があります。シーリングに関する私の問題は、サブクラスがhandle呼び出しをオーバーライドして、特に派手なことをしたい場合があることです。サブクラスのニーズを予測することはできないため、「やってもいいが、責任は自分で負う」と言う方が、「ダメ」と言うよりも良いでしょう。それがすべて1つのコードベースである場合は、必要に応じていつでもシールを解除できます。
マルチレベルフック
この種の状況でcall superに頼る必要がないことがおわかりいただけたかと思います。しかし、フレームワークが複数レベルある場合は、複雑になります。
ここでは、実際によく発生する例である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つの空のフックメソッドを持つおなじみのテンプレートメソッドです。今のところ、順調です。必要に応じて独自のセットアップとティアダウンコードを簡単に追加できます。
複雑になるのは、ユーザーがJUnitから別のフレームワークを派生させたい場合です。例はたくさんありますが、プロジェクト固有の慣例がある簡単なケースを考えてみましょう。たとえば、次のようなものです。
public class AlphaTestCase extends TestCase protected void setUp() throws Exception { alphaProjectSetup(); }
ここで、call superの問題が発生します。そこで、以前のアドバイスに従うと、次のように再定義できます。
public class AlphaTestCase extends TestCase... final protected void setUp() throws Exception { alphaProjectSetup(); doSetUp(); } protected void doSetUp() throws Exception { }
これは機能しますが、JUnitに精通している人を混乱させるという問題が発生します。彼らがいたすべてのプロジェクト、読んだすべての本は、この新しい doSetUp ではなく、setUp をオーバーライドする必要があると言っています。これは、人々が混乱する可能性が非常に高いため、final を使用するのに良いケースです。しかし、final を使用しても、異なるセットアップメソッドが引き起こす混乱は非常に苦痛です。
別のオプションがあります。2番目のレベルのフレームワークは、setUpの呼び出し側をオーバーライドできます。
public class AlphaTestCase extends TestCase... public void runBare() throws Throwable { alphaProjectSetup(); setUp(); try { runTest(); } finally { tearDown(); } } protected void setUp() throws Exception { }
これで、誰もが通常どおりにsetUpを使用できます。フレームワークの作成者は、興味深い場所に他の動作ビットを追加したい場合に、より多くのオプションを持つこともできます。テンプレートメソッドが現在の場所で機能しない場合は、1つ上のレベルに移動することを検討してください。これは常に検討する価値のあるオプションです。
もちろん、無料のテンプレートなどありません。ソースが利用できないために何をすべきかわからないため、これができない場合があります。他のケースでは、フレームワークの作成者がDirectingAttitudeを持ち、呼び出しメソッドをシールして、オーバーライドを阻止しています。
たとえそれを行うことができたとしても、注意すべきことがあります。プライマリフレームワークの作成者が、オーバーライドしたメソッドを変更する必要がある場合(実際、JUnitは2004年10月9日に行った)はどうなるでしょうか。いつものように、継承の力には責任が伴います。スーパークラスで発生する変更を監視する必要があります。
現在利用可能になっている別のオプションは、アノテーションを使用することです。JUnitやその他のJavaベースのフレームワークは、NUnitが設定したリードに従い、ここ数か月でアノテーションに取り組んでいます。アノテーションを使用すると、メソッドに名前だけでなく、より多くのメタデータを付与できるため、この種の状況でより多くのオプションを使用できます。しかし、それは別の日のトピックです。