読者です 読者をやめる 読者になる 読者になる

かとじゅんの技術日誌

技術の話をするところ

例外の再考

Java

愛する部下の一人がよいエントリを書いてくれたので、私も重い腰をあげてみたw
Throwableについて本気出して考えてみた 2nd Season - 都元ダイスケ IT-PRESS
短時間で、ようまとめたなーw

チェック例外は、Java以外の言語では聞いたことがないのですが。Javaが10年やってきて、これがベストプラクティスなら他の言語にも存在してもよいはず。なぜ使わないんだろう。。。ということで、例外を再考してみました。

例外をめぐる議論

何気にいろいろ調べていたら、こういうのがでてきました。2004年以降はRTE使っているよというのがこれか!?
Javaの理論と実践: 例外をめぐる議論
これはもう過去にされていた議論ですね。知らなかったですw
Sunは、文書化されない例外は投げるべきでない。つまるところ、扱う例外はメソッドのthrowsにちゃんと明記する派。APIを使う側のユーザとしてはどのような例外が飛んでくるか想定するべきということですね。

しかし、Bruce EckelやRod Johnsonなどは、チェック例外には重大な問題があるとしています。特にBruce Eckelはこの実験は失敗だったと言及している。

これを読んで個人的にチェック例外が批判される大きい要因と思ったのは、

  • メソッドのシグニチャが不安定な場合には使えない。
  • チェック例外は例外のラッピングが多すぎる。

番外編で

  • RTEが飲み込まれるというのは、ウケたw

C++/C#の例外に対するスタンス

DelphiC#の生みの親Anders Hejlsbergのインタビューから。
The Trouble with Checked Exceptions
彼はまず、Versioning、互換性の問題を言及しています。メソッドfooがA,B,Cの例外をスローしていても、ほかの要件の関係でDの例外をスローしてしまうとクライアントのコードを破壊してしまうと。
もうひとつのScalabilityがよく理解できなかったのですが、下位から上位で例外の再スローで例外のラッピング。このような複雑な例外階層を意識した構造だと拡張性が乏しくなるってことかな。これはシステムの規模に関係するかも、小規模だとあまり問題にならない。

まとめると?

  • チェック例外は、メソッドの互換性と拡張性に問題が生じる可能性が高い。(ただし、小規模システムでは問題にならない)
  • 非チェック例外は、スローされる例外の仕様を文書化できない。つまりどんな例外が飛んでくるか不明になる。また、コードを解析してJavadocに記述する場合も、システムの規模に応じて非チェック例外の数も増える。文書化は絶対したほうがいいが現実的ではない。(irenkaでRTEの@throws自動生成とか考えたがとてつもない行数となる可能性が、かえって可読性が落ちるw)

結論はどの立場をとるのか、その”開発時の設計思想”によるところになるのではないかと思いますが、上記から学べることとしては、さまざま複雑な要件を対応しなければならないシーンではメソッドのシグニチャの互換性をなるべく維持できるほうがいい、システムが大きくなりクラスの責務を分割しメソッドの呼び出し階層が増えてきた場合はその抽象度に応じた例外クラスが必要となり、設計に与えるコストが大きくなる可能性が高いというのは言えるのではないかと思います。

業務アプリケーションで考えると上記の視点は無視できないんですよねー。業種、業態、顧客、仕向、バージョンごとにさまざまな要件がありますし、規模だってどうしたって大規模になりがち。

フルRTE法ならどうすればいいか?

仮にフルRTE法を採用するとして、どういう対策があるか、

  • スローされる例外の文書化の方針はid:daisuke-mのプランがよいと思います。

すべてを書ききるのは現実的でないから。で、スローされる例外はメソッドの階層を追えばわかるようにしていればいい。IDEが助けになってくれるので、RubyPythonの例外より管理しやすい状況ではないかとw*1
さらに、

  • RuntimeExceptionをそのままスローしない。(Exceptionもスローしない)

RuntimeExceptionから具体的な例外クラスを実装しスローする。なるべくどこで何がおこったかの情報を保持させて解析しやすいようにする。例外と判断した理由や背景までわからないといけない。

  • どのような処理でもtry{}finally{}で記述する。

developerworksの記事にもさらっと書いてあるのですが、フルRTE法でリソースを解放する必要がある場合は、try{}finally{}はどんなところでも使わないいけませんと。そのメソッドを呼ぶとどんなRTEが飛ぶかわかりませんからw Seasarでもそうです。T2でもそうだと思います。

  • mainメソッドなどの最上位階層では必ずtry{}catch(Exception ex){}finally{}する

ウェブアプリではFilterでキャッチすることが多いですが、それ以外のアプリでは最上位のmainメソッド内でRTEをキャッチして、"処理できないエラーが発生しました"などとログなり、UIなりにエラー用の表示する処理が必要。たとえば、XMLが壊れているとか、DBのパスワード間違いは、”使い分け法”では強制的にcatchだけど、”フルRTE法”の場合は、プログラマが意識してcatchしてエラーメッセージを表示してリカバリするなどしないと、エラー用の表示に遷移されてしまう。

例外のハンドリングがクライアント任せになるので、そのクライアントのテストケースが肝になる。ないとカオスw

これらはウェブアプリでもそれ以外でも同じ考え方になるのではないかと。Seasarってウェブだけでなくバッチ処理でも使っているので、気をつけたいところになるかな。

あわせて読みたい
http://d.hatena.ne.jp/asakichy/20091215/1260838490
http://d.hatena.ne.jp/asakichy/20091216/1260932997
http://d.hatena.ne.jp/kmaebashi/20100101/p1
http://pub.ne.jp/tb.php/1129006
http://d.hatena.ne.jp/SiroKuro/20090809/1249838522
http://fieldnotes.sytes.net/wiki/index.jsp?pid=Exception


追記:
>下位から上位で例外の再スローで例外のラッピング
これちょっとわかりにくい表現と思ったので追記。
Effictive Java(初版)なら43項で、第二版なら61項.

その抽象化に適切な例外を投げる。言い換えるとメソッドが投げる例外は、メソッドが何を行うのかと一貫した形で、抽象化レベルで定義すべきであり、必ずしもそのメソッドがどう実装されるかという低レベルの詳細で定義する必要はありません。例えばファイルやデータベース、あるいはJNDIからリソースをロードするメソッドは、リソースが見つからない場合には、何らかのResourceNotFound例外を投げるべき(一般的には下にある原因を保存するために例外チェーンを使います)であって、低レベルのIOExceptionやSQLException、NamingExceptionを投げるべきではありません。

というのがあります。
たとえば、ユーザ情報をストレージからユーザ名で取得するようなメソッドの場合。

public UserInfo getUserInfo(String userName) throws SQLException,IOException,NamingException{
    return dao.selectByUserName(userName);
}

この実装だとDaoを使っているのでDBから取得しますが、そのDBにつながらない場合はSQLExceptionが発生することが考えられます。その場合でもgetUserInfoはSQLExceptonではなくResourceNotFoundExceptionをスローすべきです。

public class UserService{
    public UserInfo getUserInfo(String userName) throws ResourceNotFoundException{
        try{
            return dao.selectByUserName(userName);
        }catch(SQLException ex){
            throw new ResourceNotFoundException(ex);
        }catch(IOException ex){
            throw new ResourceNotFoundException(ex);
        }catch(NamingException ex){
            throw new ResourceNotFoundException(ex);
        }
    }
}

なぜなら、メソッドの内部の仕様を公開してしまっているのでカプセル化が弱いというのと、getUserInfoがDBの場合はSQLExceptionでもよいですが、/etc/passwdなどのファイルベースになった場合はどうでしょうか?SQLExceptionはスローされないのにメソッドのシグニチャのthrowsは?これでは例外クラスの抽象度があっていないということになります。
抽象度を合わせるために例外をラップしてスローします。例外連鎖ともいいます。
勘のいい人はわかると思いますが、抽象度のレイヤー数に応じて例外連鎖が増えていき、チェック例外クラスも増えます。

追記:
ひがさんのコメントみっつけたw

検査例外と実行時例外のどちらを使うべきか、
いろいろな意見があると思いますが、
私は、catchしたければそうできるし、
catchしたくなければ何もしなくていい、
実行時例外のほうが、結局良いのではという
考えに変わり始めています。
検査例外をラップして例外を投げたりすると
それがいくつもネストしたりするんですよね。
悩んだんですが、Seasar V2では、すべて実行時例外で
行くことにしました。
ただし、実行時例外でもthrowsには記述します。

最後の実行時例外でもthrowsには記述するのは結局クライアントのコードを破壊するので自分はよくないと思っています。Seasar2のコードみたけどthrowsにRTE書かれてなかったと思います。

*1:Effective Javaの初版 44項, 第二版 62項ではすべての例外を文書化しろとしているが、非チェック例外の場合は現実的でない場合もありうる。