多重tryブロックの除去

nullによる初期化回避の解答例 - @katzchang.contextsに対してコメントをいただいた件。

id:Nagise 二重tryブロックになるととたんに見通しが悪くなる…。完璧な例外処理の例を誰か教えてほしい。
はてなブックマーク - nullによる初期化回避の解答例 - @katzchang.contexts

もっともな指摘なので、多重tryブロックを除去してみました。

方針

要するに単純にメソッドによって切り出しただけです。ポイントは

    static int sum(File file) throws FileNotFoundException, NumberFormatException, IOException

この記述でFileNotFoundExceptionとNumberFormatExceptionを明示してあるところでしょうか。動作としては

    static int sum(File file) throws IOException

でも事足りるんですが、「どんな例外処理を投げますよー、これはあなたが処理するんですよー」っていうことをコードに記述することによって、仕様を伝えたい意思を表しています。Eclipseなんかだとcatch句の自動生成にも使えるし、自己満足以上の意味があるはず。

多重tryブロック除去

import java.io.*;

public class Foo {
    public static void main(String[] args) {
        try {
            System.out.println(sum(new File("hoge.txt")));
        } catch (FileNotFoundException e) {
            System.err.println("ファイルがないようです。");
        } catch (NumberFormatException e) {
            System.err.println("ファイルの形式が変です。");
        } catch (IOException e) {
            System.err.println("入出力に問題があるようです。");
            e.printStackTrace();
        }
    }
    
    static int sum(File file) throws FileNotFoundException, NumberFormatException, IOException {
        FileInputStream stream = new FileInputStream(file);
        try {
            return sum(new BufferedReader(new InputStreamReader(stream)));
        } finally {
            stream.close();
        }
    }
    
    static int sum(BufferedReader reader) throws NumberFormatException, IOException {
        int sum = 0;
        String line;
        while((line = reader.readLine()) != null) {
            System.out.println("adding..." + line);
            sum += Integer.valueOf(line);
        }
        return sum;
    }
}

例外処理設計の基本の話

メソッドに切り出した場合、誰がその例外をcatchするのかってのが悩みどころになると思いますが、「例外状態から回復できるメソッドがcatchする」のが基本だと思います。

上記の場合、NumberFormatExceptionは誰が処理するのか、sum(reader)/sum(file)/main(args)の三通りの選択肢があり、main(args)で初めてcatchしています。「ファイルの形式が変」って場合には、ファイルを渡したメソッドじゃないと回復できない、引数で渡されたファイル・オブジェクトを処理しているsum(file)の問題じゃないわけです*1。sum(file)は「いや、俺はこのファイルって聞いてたんだけど…」としか言えないわけで。つまり、どのメソッドが何に関して責任を持っているかを明示し、責任を持つ例外に関してのみcatchするっていう方針で、間違いないと思います。何でもかんでも独自の例外でラップするような設計*2もたまにあるけど、ありゃ間違いです。面倒なら、すべてのメソッドを「throws Exception」とか「throws Throwable」にする方がマシです。

責任を持つ例外…ということを考えると、main(args)は実行に対して全責任を負っているとも考えられます。上記のコードのような小さいコードの場合、どこかでRuntimeExceptionが起こってもそのまま落ちるだけでも構わないけど、それ以上の規模や遠くにいる誰かが使うようなmain(args)の場合だと、main(args)(またはそこに至るまでの誰かが)RuntimeExceptionまで補足して、適切に回復させる(何らかの形でログを残して保守担当者に回復させる運用を含む)ことが重要です。サーバやJVM、外部ライブラリの設定まで含んだシステム運用であれば、Errorを含めたThrowableをどこかで処理するようにした方がいいでしょうね。Errorの回復もしなきゃいけないわけだし。フレームワーク上の実装なら、フレームワークに応じた責任の切り分けができると思います。

他人様に売る商品としてのシステムではここまで考えるのは必要だと思いますし、システムやその設計者はどこまで責任を負うお仕事なのかを確認するのも、結構重要だと思うんですよね。(と、愚痴っぽいのは毎度のこと。)

もう少し進めて考えれば、Javaで例外処理をException、RuntimeException、Errorの3系統に分けているのも、実はあんまり意味わかんないよねってことにもなったり。

*1:厳密にいえば、NumberFormatExceptionじゃなくIllegalFileFormatExceptionとかにラップして投げさせるべきかも。

*2:独自のエラーコードを付番したりとか。せめてcauseに発生元の例外を入れておけば…。