Throwableについて本気出して考えてみた 2nd Season

1st Seasonはこちら。Throwableについて本気出して考えてみた - 都元ダイスケ IT-PRESS

以前は、何かをスローする状況を3つに分けてそれに合った設計をした例外を投げましょう、という考え方を示しました。

  • callerのバグ: RTE
  • calleeのバグ: Error
  • どちらでもない: Exception (非RTE)


まぁ詳しくはSeason1の方で。

Seasar2はRuntimeExceptionですね。2004年ぐらいからのフレームワークはRTEをスローしていると思いますよって、ひがさんから情報。

チェックされる例外とチェックされない例外について - じゅんいち☆かとうの技術日誌


ただ、上記のような考え方もあるのも事実。実際.NETやRuby, Python, 新鋭のScala等もcatchを強制する例外というものが言語仕様的に存在しません*1。逆に、チェック例外があるのはJavaくらいなんだろか? 他言語のことは余り詳しくないので、他に言語仕様でチェック例外がある言語があったら教えてくださいw

で、自分の考え方とこれらの考え方の間にどんな違いがあって、どんな理由で思想が別れているのか考えてみた、というのがこのエントリ。id:j5ik2oと色々話していたまとめです。
便宜上、Season1の考え方を「使い分け法」、他方を「フルRTE法」と呼ぶことにしますね。

この2つの考え方の差でポイントになるのは「チェック例外使うか否か」でしょう。Errorに関しては、RTEと同じようにキャッチを強制しないので、実質RTEと大差ありません。

使い分け法

私が「使い分け法」の考え方に至ったのは、Jiemamyの設計からでした。例えば以下のような状況を想定します。

  • XMLからデータを読み込もうと思ったらファイルがなかった、ファイルが壊れていた等
  • DBから情報を読み取ろうとしたら、DBに接続できなかった。パスワードが違った等


Jiemamyを設計する視点からは、これらの状況が発生した場合、「ダメでした><」っていうダイアログを出すなりの処理が必要です。そのままRTEとしてEclipseまで到達すると、「真っ白なエディタが表示される、がその先上手く動くのかは保証できない」とか「DBから情報をインポートするウィザードのウィンドウが閉じない」などの状況になるのではないか、と思います。

それは困る。ということで、こういった状況に対して、「エラー処理のロジックを書くことを強制する」という意味合いでチェック例外を使います。

RTEやErrorの場合は、ライブラリ側またはクライアント側の「バグ」が原因なので、プログラムが止まってしまうのは致し方ありません。

このようなデスクトップアプリに関しては、「使い分け法」による設計が適しているのだと思いました。

フルRTE法

対する「フルRTE法」ですが、結論から言うと、これはWebアプリに適した考え方なのかな、と思いました。

というのも、Webアプリの世界では、環境に依存した「DBに接続できなかった」という状況と「バグによって上手く動きませんできた」という状況に対する対処方法が基本的に同じ、つまり「エラーページに遷移」なんですね。

どの状況も、プログラム上で見分ける必要性が大きくない。エラーページに遷移した後は、セッションをクリアして再アクセスすれば何とかなるケースが多々あります。

大抵のWebフレームワークは、処理されないRTEをキャッチして、自動的にエラーページに遷移してくれますし。何か変なことが起ったら、そのまま上に投げてしまえば何とかなるんです。

というわけで、Webアプリを作る際には「フルRTE法」でも問題ない、と言えます。

Javadocの問題

私の考え*2では、Javadocは「仕様」です。

  • このメソッドは基本的に何をするのか。
  • 「例外的であろう(とユーザ側が思う)状況」に直面したら、どうなるのか?
    • デフォルトの値を返す、というのも「何をするのか」の一種。
    • 例外を投げる、というのも仕様。


上記の内容を全て網羅すべき。極端に言えば、

  • ある、正常に動いているアプリケーションがあるとする。
  • 思いがけない事故で、メソッドの中身*3だけが綺麗さっぱり消えてしまった。*4
  • しかし、Javadocを参照するだけで、元通り動く実装を再び作るための情報が全て手に入り、再実装することができる。

というのが理想的なJavadoc。まぁ、自分でもなかなか出来ないんですけどねんw

つまり、仕様には「RTEが飛ぶケースについても明示」しなければならんのです。どういう状況でRTEを投げていいのか、が分からないと、Javadocから再実装はできません。使い分け法では、何とか書けますかね。この例外が起ったのは、どの部分の責任なのか、が明確になりますから。Javadocに書かれていない例外が飛んできたら、そのメソッドのバグです。Javadocの書き漏らし(仕様バグ)か、もしくは本当に意図していない例外が飛んだ(実装バグ)か。

対するフルRTE法では、エラーの責任範囲が明確にしづらいです。何でもかんでもRTEとして投げますので、例外が果てしなく伝播する。そのメソッドの内部で使ったメソッドが投げる可能性のある例外は、全て投げる可能性があります。(日本語分かりづらいw)

というわけで、@throws の項目が結構果てしなく列挙されることになるんではないかな…。洗い出すのも一苦労。ぶっちゃけ現実的ではないですね。

というわけで、メソッドという細かい粒度での仕様が明らかにできなくなる、というのがフルRTE法が持つ欠点です。

ちなみにフルRTE法は「ただ単純に楽じゃん」という理由ではなく、しっかりとしたメリットがあります。その辺りは以下のエントリを参照のこと。


で、ここで疑問点。

フルRTE法では、仕様が明確にしづらい(完全明確にするのは非現実的)訳ですが、この方針をとった時、Javadocに「例外的であろう(とユーザ側が思う)状況」に関する記述をどこまですればいいのか?

  1. 全く記述しない。とにかくこのメソッドが遂行しようとすることだけを書く。
  2. ある基準を満たしたものだけを書く。
  3. 全て書く。仕様は明確にする。 →非現実的


1つ消えるので、以上の2択になる訳ですが。1じゃあ適当過ぎる気がするw かといって2のある基準というのが思い浮かばない。

チェック例外 + 自分自身のクラス内でthrowするRTEは @throws に書く(案)

意外とバランスが取れてるかもなー。

public class Hoge {

  Fuga fuga;

  public void method1() throws FooException, BarException, QuxException {
    if (...) {
      throw new CorgeRuntimeException();
    }
    method2();
    fuga.method3();
    // ...
  }

  void method2() throws BarException {
    if (...) {
      throw new BazRuntimeException();
    }
    // ...
  }
}

public class Fuga {

  public void method3() throw QuxException {
    if (...) {
      throw new QuuxRuntimeException();
    }
    // ...
  }
}
  • method1
    • FooException (チェック例外だから)
    • BarException (チェック例外だから)
    • QuxException (チェック例外だから)
    • CorgeRuntimeException (自分自身が投げているから)
    • BazRuntimeException (投げているのはmethod2だが、自クラス内だから) ← これを書くかどうか、迷う。
    • QuuxRuntimeException 飛ぶ可能性があるが書かない。他クラスだから。
  • method2
    • BarException (チェック例外だから)
    • BazRuntimeException (自分自身が投げているから)
  • method3
    • QuxException (チェック例外だから)
    • QuuxRuntimeException (自分自身が投げているから)


まぁ一つの案の紹介でした。まだ結論出ていないので、なにかご意見などありましたら、どしどしと。

まとめ

メリット デメリット 適所
使い分け法 仕様が明確にできる 変更に弱くなる
色々考えなきゃいけない
デスクトップアプリ
使用シーンが不明確なクラスライブラリ
フルRTE法 変更に強くなる
考えることが少なくて済む
仕様が明確にしづらい Webアプリ
Webフレームワーク

Jiemamyは、デスクトップアプリに使うクラスライブラリとして設計してあります。従って、使い分け法を採用しているのは、多分正解。仮にWebアプリで使う場合も、何かをキャッチさせられたらRTEのサブクラスにラップして再スローしてやればいいのではないかと思います。

Seasarは、デスクトップアプリで使えない訳ではないですが、恐らく使いにくいです。しかしまぁ、主にWeb系で使われているフレームワークなので、特に問題はありません。Web系に限定すれば、使いやすく変更に強い、というメリットを享受できる設計になっているので、これも多分正解。

このように、そのコードが使われるシーンを想定して、例外設計の方針を決めると良いのだと思います。

*1:らしいですw

*2:使い分け法とは別次元

*3:{ から } まで。

*4:中身だけが消えるとかありえないけどw