Javaの例外設計について、自分が考えている指針などを書いてみる。
正常系は例外にしない。異常系を例外にする。
ユーザーが使ってはいけない文字を入力欄に記入した
正常系。例外にしない。 ユーザーは入力を間違えるものなので。 それを想定してバリデーション機能がアプリケーションには機能として含まれている。 バリデーションは誤った入力を検出する「機能」で、それが正常に動作している状態なので、例外としては扱わない。
利用している外部のAPIがドキュメントに記載のない値を返してきた。
異常系。例外にする。 こっちはわざわざ規定されていないことに対応するための機能を用意していない。機能外の事象が発生したので例外として扱う。
Networkエラー・IOエラー
ちょっと悩みどころだが異常系。例外にする。 一定の確率で発生しうるし、こっちもそれを想定してリトライとか入れてるので少し悩むが、ライブラリでも大抵は例外として扱われているのでそれに倣っている。まぁ「ネットワークが通じない」というのは明らかに異常ではあるかなとは思う。
キャッチするのとしないのと
例外にはなんとかなるやつと、なんともならないやつがある。 なんとかなるやつはキャッチしてもらって、なんとかしてもらいたい。だから関数のインターフェースにthrowsを書いてcatchを強制する。
public void exampleMethod() throws SomeException { // メソッドの処理 if (someCondition) { throw new SomeException("エラーが発生しました"); } }
なんともならないやつはRuntimeExceptionを使う。 そうしないと、関数の呼び出し経路全てにthrowsを書かなくてはならなくなる。
その例外に対してなにかcatchで引っ掛けたい場合や、特殊な処理をさせたい(複数箇所で発生して、メッセージフォーマットを揃えたいなど)ときはRuntimeExceptionを継承した独自例外を定義する。 そうでなければ面倒なのでRuntimeExceptionを直接使う。 (ライブラリとかであれば全て独自例外にすべきなのかもしれない。これについてはあまりわかってない)
独自例外を定義する場合、必要な情報は内部にパラメータで持たせてgetMessage()でいい感じにメッセージが出力されるようにする
悪い例のイメージ
var e = LimitExceedExcaption("最大10の想定のところを、1超過しました。合計11") e.getMessage(); // "最大10の想定のところを、1超過しました。合計11"
良い例のイメージ
var e = new LimitExceedExcaption(max: 10, actual: 11) e.getMessage(); // "最大10の想定のところを、1超過しました。合計11"
エラーメッセージは「何が、どこで起きたかわかるように」する。可能なら、何をすべきかもわかるように。対応するときのことを考える
例えば「ネットワーク疎通エラー」とだけエラーメッセージがログで出力されていても何が起きたのかわからない。 「〇〇処理が失敗した。原因は、ネットワーク疎通エラーにより、外部API〇〇との疎通が失敗した為である。その時のリクエスト内容・レスポンス内容はこれである〜〜」みたいな感じがいい。 これは全て文章でなくても、スタックトレースとその時のデータのdump等を見たときに、上記の状況であったと分かるのであればなんでもいい。
キャッチすべき例外がレイヤーを跨ぐ時はラップして投げ直す
アプリケーションにはリポジトリレイヤーの向こうを意識させない。 例えば、アプリケーションからデータ保存のために、HogeRepositiry.insert(data) を実行する際に、以下のような例外をキャッチするような構造にしない。
try { hogeRepository.insert(data); } catch(FileIoException e) { // 例外処理 }
データ保存場所をファイルからDBに変更すると、本来はRepository内部だけを書き換えればよいところが、アプリケーション側にも変更が必要になる。
以下の感じにする。
try { hogeRepository.insert(data); } catch(HogeRepository.FailedInsertException e) { // 例外処理 }
HogeRepository.insertは以下のようになっている。
HogeRepository { public void Insert(Data data) Throws HogeRepository.FailedInsertException { try { // ファイルを使った保存処理。 } catch(FileIoException e) { throw new FailedInsertException(e) // causeにFileIoExceptionを渡してFailedInsertExceptionとして投げ直す } } }
まとめ
その異常について、その機能を作ってる人・作ってる時が一番理解している。 後になって、作った以外の人がその異常を見て、どのように対応すべきなのかをノーヒントで理解するのはかなり厳しい。 だから機能を作っている人が、適切な情報を例外クラスに込めておく必要がある。