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

リソースを保持するクラスの設計指針 (案)

Java

例外について色々と考えてみた - ぐるぐる〜
「例外と RAII イディオム」での考えた、「tryOpen 方式」をもう少しだけ煮詰めてみる。

リソースを保持するクラスはコンストラクタで例外を投げない

Java では、リソースを確実に解放するために、try-finally を用いる必要がある。

FileInputStream fin = null;
try {
    fin = new FileInputStream(fileName);
    ...
} finally {
    if (fin != null) fin.close();
}

これは、コンストラクタが例外を送出した場合でもリソースを解放する必要があるためだが、コンストラクタで例外を投げないようにしておけば、以下のように記述することができる。

FileInputStream2 fin = new FileInputStream2(fileName);
try {
    ...
} finally {
    fin.close();
}
リソースを保持するクラスではコンストラクタでリソースを取得しない

上のように記述できるのが一番いいのだが、リソースの取得は失敗する可能性のある操作である場合が多い。
そのため、

  • コンストラクタでリソースを取得し、失敗したらその後のメソッド呼出しが常に失敗する
  • コンストラクタではリソースは取得せず、リソース取得用のメソッドを用意する
  • コンストラクタではリソースは取得せず、実際にリソースを使用するメソッドが初めて使用された際にリソースを取得する

のうち、いずれかの方法をとることになるだろう。
この中で、一番下の方法は、初めて使用するまで成功か失敗か分からず、前提を置きにくいという問題点を持つ。
一番上の方法は使用する前にリソースの取得が成功したか問い合わせない限り、メソッドの呼出しが成功するか失敗するかが分からない。
これでは、真ん中の方法と何らかわりがないが、メソッド名が違ってくる。

// 一番上の方法
FileInputStream2 fin = new FileInputStream2(fileName);
try {
    if (fin.isOpen()) {
        // ここではメソッドの呼出しは成功するものとして
        // コードを記述できる
        // この際、実際は失敗する可能性はあるが、
        // 失敗した場合は例外が投げられる
    } else {
        // ここではリソースを使用するメソッドの呼出しは失敗するものとして
        // コードを記述できる
    }
} finally {
    fin.close();
}

//------------------------------------------------------------------------------

// 真ん中の方法
FileInputStream2 fin = new FileInputStream2(fileName);
try {
    if (fin.tryOpen()) {
        // ここではメソッドの呼出しは成功するものとして
        // コードを記述できる
        // この際、実際は失敗する可能性はあるが、
        // 失敗した場合は例外が投げられる
    } else {
        // ここではリソースを使用するメソッドの呼出しは失敗するものとして
        // コードを記述できる
    }
} finally {
    fin.close();
}

個人的な感覚としては、2 番目のコードの方が分かりやすい。
isOpen メソッドを呼び出さなくても動作する可能性もしない可能性もあると言うのも何か気持ち悪い。


これらのことより、ここではコンストラクタでリソースを取得せず、リソース取得用のメソッドを使用する方法を取る。

static メソッドとして自身を返すメソッドも用意する

ただし、try の中で常に if 文を記述しなければならないのは、どう考えても面倒なので、自身を返すリソース取得用メソッドも用意しておくといいだろう。

FileInputStream2 fin = FileInputStream2.open(fileName);
try {
    ...
} finally {
    fin.close();
}

open で例外を投げるというのも考えたのだが、ここで例外を投げてしまうと close が必ず呼び出されるものではなくなってしまう。
代替案として、open が内部でリソース取得に失敗した場合は Null Object を返すというのはどうだろう?
そうすることで、close は必ず呼び出されるし、try ブロック内部で使用するメソッドの細かいカスタマイズも可能となる・・・そのようなカスタマイズが必要か、という疑問は残るが。

リソースを保持するクラスはリソース解放時に例外を投げない

リソースの解放時に例外を投げることを許すと、あまり嬉しくないケースを考慮する必要が出てくる。

FileInputStream2 fin = new FileInputStream2(fileName);
try {
    ...
} finally {
    fin.close();
}

例えば上のようなコードで、try ブロック中で例外が発生したとする。
finally は実行されるが、close でも例外が発生したとすると、try ブロックで発生した例外はなかったことにされてしまう。
常に catch を書けば回避できるが、面倒なので、できれば close では例外を投げないほうがいい。


既存のクラスで close で例外を投げない確率を少しでも上げるために、try の最後で flush を明示的に呼んでおく、という方法がある。
こうすることで、close 内部で flush されることは (おそらく) なくなり、もし close で例外が発生したとしてもそれは (おそらく) 致命的ではない。
また、例外を送出したオブジェクトはまだ生存中なので、close 内部で例外やその他の問題が発生したとしても、問い合わせメソッドにより確認することができる。

FileOutputStream2 fout = new FileOutputStream2(fileName);
try {
    ...
    fout.flush();
} finally {
    if (!fout.close()) {
        // fout.errorObject()でエラーオブジェクトにアクセス
    }
}

結論

こんな interface

public interface ResourceHolder<E> {
    boolean tryOpen();
    boolean close(); // tryCloseの方がいいのかも・・・?
    E errorObject();
}