デザインパターンとしての例外ハンドラ - オブジェクト指向と型システムの狭間で例外を考える その4
例外考察シリーズ。
- オブジェクト指向と型システムの狭間で例外を考える - プログラマーの脳みそ
- 契約書に捨印を押す - オブジェクト指向と型システムの狭間で例外を考える その2 - プログラマーの脳みそ
- try-catch方式・ハンドラ方式 - オブジェクト指向と型システムの狭間で例外を考える その3 - プログラマーの脳みそ
前回はプログラム言語の例外処理機構としてtry-catch方式の他に、ハンドラによる例外処理方式を考えることができる、という話をした。「考えることができる」がこの2010年現在にそういった例外処理機構をもった言語があるかというと僕は寡聞にして知らない。ああ、僕は本当に寡聞なのでただの無知の可能性のほうが高い。メジャーどころではなさそうなんだけどどうだろう。
プログラム言語の機能として、という話だと、プログラム言語を作ろうという人とか、あるいは将来にハンドラ式の例外処理機構をもった言語に触る人にしか意味のない話になってしまうわけだけど、言語レベルでサポートせずとも実装は可能であったり、思想が役に立つことはある。
というわけで、今回はデザインパターン的に例外ハンドラを扱う設計について考えてみようか。これから先の話は実行時の話。非検査例外、Java的にはRuntimeExceptionの話になる。処理の流れの話、と言えばいいかな。コンパイル時に例外処理がちゃんと行われているかという静的チェックとは別の話だ。まずはおいておく。
例外ハンドラ
ハンドラ(handler)は「取り扱う人」ぐらいの意味合いで、何かのイベントが発生したときにイベントを取り扱うモノをイベントハンドラと呼ぶし、例外が発生したときに例外を取り扱うモノが例外ハンドラだ。
どうもググるとJavaにおけるcatch節のことを例外ハンドラと呼んでいる例もあるようだ。なんとなくニュアンスとしてこういう不動のハードコーディングされたコードブロックをハンドラと呼ぶのに僕は違和感を感じるのだけど。
さて、Javaの例外はメソッドの呼び出し階層を遡り、発生した例外の型をcatchするcatch節を探す。そして最後までcatchされなかった例外はコンソールにスタックトレースを書き出してThreadを停止させてしまう。
と、思ってる人が多いかもしれないが、実はそれだけではない。続きがあってcatch節がなかった場合、Threadに設定された例外ハンドラに処理が委譲される。Threadに例外ハンドラを設定するにはThreadクラスのsetUncaughtExceptionHandler()を使う。この引数に渡すThread.UncaughtExceptionHandlerインターフェースの実装クラスこそが例外ハンドラの典型例だ。
void uncaughtException(Thread t, Throwable e) 指定されたキャッチされない例外により、指定されたスレッドが終了したときに呼び出されるメソッドです。 このメソッドによりスローされる例外は、Java 仮想マシンにより無視されます。 パラメータ: t - スレッド e - 例外Oracle Technology Network for Java Developers | Oracle Technology Network | Oracle
この例外ハンドラは、そのThreadで発生した例外すべてを最上位で受け止める。システム設計としては簡素なケースではこのような例外ハンドラを、高々ひとつ設定できるようにしておくだけで事足りることもある。どこか比較的グローバルなところにハンドラを置いておくというシンプルな設計だ。
JavaのThreadの場合は例外ハンドラを設定するメソッドがこのsetUncaughtExceptionHandler()の他にもあって、staticなsetDefaultUncaughtExceptionHandler()メソッドでは、個別のThreadでハンドラが設定されてなかった場合に呼び出されるデフォルトのハンドラを設定する。個別Threadごとの設定とデフォルト設定という2段階構成になっている。
さらにいえばThreadGroupのuncaughtException()が呼ばれるとかあるんだけど、割愛しよう。
メソッドスコープの例外ハンドラ
さて、Thread.UncaughtExceptionHandlerはThreadに設定する例外ハンドラだった。システムというものを見た場合に、その頂点部分に存在し、管轄下すべての例外を受け止める存在だった。
ここで、あるメソッド呼び出しに際して、そのメソッド内だけで発生した例外を受け止める例外ハンドラを考えてみよう。とはいってもメソッド名-例外ハンドラといった対応付けのMapを用意するというわけではなく、メソッド呼び出しの際にThread.UncaughtExceptionHandlerのようなインターフェースの実装クラスを引数で渡すという方法論だ。
とはいえUncaughtExceptionHandlerという名前はちょっと用途と違うので*1ここではExceptionHandlerというインターフェースを作るとしよう。*2
public interface ExceptionHandler { void handleException(RuntimeException e); }
なんだかメソッド名がアレな気もするが.NETもそんな名前だしまあいいか。
んでこれをとあるメソッドの引数として受け取る。
public void hoge(ExceptionHandler eh) { ... }
このhoge()メソッド内で発生した例外はこの例外ハンドラに処理が委譲される。まあ実態はというと
public void hoge(ExceptionHandler eh) { try { // ... } catch (RuntimeException e) { eh.handleException(e); } }
といった具合なのだけど。
階層呼び出し
このhogeが内部でメソッドpiyo()を呼び出すとして、そのpiyo()も例外ハンドラを受け付けるとする。
public void piyo(ExceptionHandler eh) { ... }
といった具合だ。ここで、hoge()の実装としては、外から受け取った例外ハンドラをそのまま下位に受け渡すか、下位には別のハンドラを渡すかの二択がある。
/** 例1 : 外から受け取った例外ハンドラをそのまま下位に受け渡す */ public void hoge(ExceptionHandler eh) { piyo(eh); } /** 例2 : 下位には別のハンドラを渡す */ public void hoge(ExceptionHandler eh) { // 新しいハンドラを作って呼び出し元からのハンドラには伝えない piyo(new ExceptionHandler(){ public void handleException(RuntimeException e) { // 例外処理 } }); }
下位に例外ハンドラを投げずに分断するのはどういう時かというと、piyo()メソッドの仕様としては例外だとしても、それを使うhoge()メソッドの階層では想定された事項で、例外として扱いたくないようなケースだ。メソッドの実装方法として0件のときの戻り値が空の配列にしたり例外にしたりするようなものだと思って貰えればいいかな。
これは、通常のtry-catch型の例外処理機構ではcatchしてthrowし直さないようなケースに相当する。try-catch型では呼び出し階層の深い方から浅い方に向かって例外が登っていくのに対し、ハンドラ方式だと浅い方から深い方にハンドラが沈んで行くことになる。対照的で面白い。