7-2 エラー処理
最近は会社の仕事も忙しく、日本語試験の準備で……。
日本語試験が終わったので、また再開します。
4. 例外に意味を与えよ
例外を投げる際には、その前後状況を十分に補足することで、エラーが発生した原因や位置を簡単に特定できるようになります。
Java をはじめとしたほとんどの言語は、すべての例外に呼び出しスタックを提供しますが、失敗したコードの意図を理解するには呼び出しスタックだけでは不十分です。
例えば、以下のように Java でデフォルト提供されている例外をコード内で直接使用する場合、どのロジックで発生したエラーなのか分かりづらく、スタックトレースを確認する必要があります。
User user = userService.getUser();
if(user == null ){
throw new NullPointerException();
}
しかし、以下のように識別可能な例外クラスを定義し、その中でエラーメッセージを渡してやると、どこでエラーが発生したのか簡単に推測できます。
User user = userService.getUser();
if(user == null ){
throw new CustomUserException("Not Selected User");
}
5. 呼び出し側を考慮して例外クラスを定義せよ
エラーを分類する方法は非常に多岐にわたります。発生箇所やタイプによって様々な分類が可能ですが、私たちにとって最も重要なのは、エラーを見つけやすくすることです。
以下の例で、Aimyon オブジェクトの song メソッドは 三つのエラーが発生する可能性があり、エラー処理のために三つの catch 文を記述する必要があります。
Aimyon aimyon = new Aimyon();
try {
aimyon.song();
} catch (NotAimyonException e) {
reportSongError(e);
logger.log("She is not Aimyon", e);
} catch (TestException e) {
reportSongError(e);
logger.log("Test exception", e);
} catch (MarigoldException e) {
reportSongError(e);
logger.log("Marigold Exception", e);
} finally {
...
}
しかし、もし aimyon.song()メソッドを呼び出すコードがあちこちに存在し、そのたびに catch 文を 3 回も書かなければならないならば、何かが間違っていると感じるでしょう。
特に上記の例では reportSongError とロギング処理だけが全てです。
そこで、以下のように Aimyon クラスが発生させる例外をひとまとめにするラッパークラス(Wrapper クラス)を定義し、新たな例外で統一して扱うようにします。
public class AimyonWrapper {
private Aimyon aimyon;
public AimyonWrapper(Aimyon aimyon) {
this.aimyon = aimyon;
}
public void song() {
try {
singer.song();
} catch (NotAimyonException e) {
throw new SongFailure(e)
} catch (TestException e) {
throw new SongFailure(e)
} catch (MarigoldException e) {
throw new SongFailure(e)
}
...
}
}
こうすることで、song メソッドを呼び出す部分は以下のように簡潔になります。
AimyonWrapper aimyon = new AimyonWrapper(new Aimyon());
try {
aimyon.song();
} catch (SongFailure e) {
reportSongError(e);
logger.log(e.getMessage(), e);
} finally {
...
}
Wrapper を利用することで、例外処理の利点以外にも、外部ライブラリを利用する際に自分のサービスに合わせてカスタマイズできるという利点もありますが、これは本題から外れるので省略します。
6. 正常なフローを定義せよ
ここまでの内容を守れば、ビジネスロジックとエラー処理が分離された良いコードを書けるようになります。
しかし、その結果、コードが却って複雑になる場合もあります。
以下の例をご覧ください。findById メソッドを使ってユーザー情報を ID から取得し、ユーザー情報があれば getLevel を、なければ BASIC を返すロジックですが、try/catch で書かれているため、コード理解の流れを妨げています。
public UserLevel getUserLevel(Long id) {
try {
User user = userRepository.findById(id);
return user.getLevel();
} catch (UserNotFoundException e) {
return UserLevel.BASIC;
}
}
以下のように、例外を try/catch で無条件に処理するのではなく、正常なフローで読めるように修正すると理解しやすくなります。
public UserLevel getUserLevelOrDefault(Long id) {
User user = userRepository.findById(id);
if (user == null) {
return UserLevel.BASIC;
} else {
return user.getLevel();
}
}
7. NULL を返すな
エラー処理を論じる章であれば、我々がつい陥りがちなエラー原因を誘発する行為にも触れるべきでしょう。
その一つが null を返す習慣です。
以下のコードを見てください。
public void registerItem(Item item){
if(item != null){
ItemRegistry registry = peristentStore.getItemRegistry();
if(registry != null){
...
}
}
}
このように null を返すコードを書くと、必然的に null チェックが必要になります。
そして、もし上記のコードで peristentStore が null だったらどうなるでしょうか? NullPointerException が発生します。
この関数を呼び出したどこかで NullPointerException をキャッチするかもしれないし、しないかもしれません。
このコードの問題点は null チェックが抜け落ちていることだと考えることもできますが、実際の問題は null チェックが多すぎることです。もし、メソッド内で null を返したくなる誘惑に駆られたら、その代わりに例外を投げたり、null ではなくプロジェクト内で定義した特殊なオブジェクトを返すことが良い方法です。