例外に関するAPIデザインのベストプラクティス

例外処理に関して教育用のいいドキュメントがないかを探していたところ、以下のページにたどり着きました。 URLから推察するに、かなり古いドキュメントではありますが、とてもよく整理されていると思ったので、紹介したいと思います。

Best Practices for Exception Handling

http://onjava.com/onjava/2003/11/19/exceptions.html

引用しながら要点だけをまとめて紹介させていただこうと思いますが、法律的にいいのかどうなのか定かではないので、もし問題ありという指摘をうければ公開を停止します。
問題がありそうであればぜひご指摘いただけますと幸いです。斜体部分は引用部分です。

Best Practices for Designing the API(APIデザインのベストプラクティス)


1. When deciding on checked exceptions vs. unchecked exceptions, ask yourself, "What action can the client code take when the exception occurs?" 
(検査例外か非検査例外かを決めるときには、「その例外が発生した時にクライアントコードがどんなアクションをするかを自問しよう)

ここでは、APIをデザインする再に、検査例外を使うべきか、非検査例外を使うべきかということについて述べられています。
  • クライアントコードが何もできない場合 → 非検査例外
  • クライアントコードが意味のある回復処理を行える場合 → 検査例外
本文中ではこのようにまとめられています。
回復処理とはログを書くなどは含まれません。ログはプログラムのもっとも上位レイヤーで、まとめて捕捉して記述されるべきです。

また、
"prefer unchecked exceptions for all programming errors (プログラミングエラーには非検査例外の方が好ましい)
と述べられています。

検査例外はクライアントコードに対して、明示的に例外を取り扱うことを強制します。もし、その結果、有効な回復処理が行えないにも関わらず、検査例外がスローされてきたとすると、そのハンドリングのためのコードは無駄ということになります。

クライアントコードが例外を受け取ったとき、何かシステムの回復のために、代替手段をとることができるのであれば、検査例外を使うべきです。もし、クライアントコードが例外を受け取った後、ログを出力することぐらいしかできず、復旧のために何もできないのであれば、検査例外を使うべきではなく、非検査例外を使うべきです。

2. Preserve encapsulation.
(カプセル化を維持しよう)

カプセル化と書きましたが、ここで述べられているのは例外の翻訳に関してです。
例としてあげられているのはSQLExceptionの取扱いに関してです。

For example, do not propagate SQLException from data access code to the business objects layer. Business objects layer do not need to know about SQLException. 
(例えば、SQLExceptionをデータアクセスレイヤーからビジネスレイヤーに伝播させるべきではありません。ビジネスレイヤーはSQLExceptionに関して知る必要はないからです。)

データアクセスがSQLを用いて行われたかどうかはビジネスレイヤーの知ることではありません。
SQLを使用しているという実装の詳細は、データアクセスレイヤーの内部に隠蔽されるべきです。

では、データアクセスレイヤーでSQLExceptionを補足した場合にどのような対応方法がありえるかと言えば、これも前述した検査例外をつかうか非検査例外を使うかという議論に帰着して、
  • クライアントコードが何もできない → 別の非検査例外に変換する
  • クライアントコードが有効な回復処理ができる → 別の検査例外に変換する
ということになります。

参考としてこの様なコードがあげられています。

public void dataAccessCode(){
    try{
        //..some code that throws SQLException
    }catch(SQLException ex){
        throw new RuntimeException(ex);
    }
}


SQLExceptionをRuntimeExceptionに変換する例です。
こうすることで、何かしらの異常によって、SQLExceptionが発生した時、そのスレッドは中断され、その例外はRuntimeExceptionとして報告されます。

さらに、この検査例外を非検査例外に変換したおかげで、このAPIを利用するクライアントコードに無意味な例外のハンドリングのためのコードを書かせることもありません。

また、付け加えると、このとき、RuntimeExceptionのコンストラクタにキャッチしたSQLExceptionのインスタンスを渡すことを忘れてはいけません。
最後に例外オブジェクトのスタックトレースを確認すると、以下のような、"Caused by:"という形で、その例外の原因となったSQLExceptionのスタックとレースも合わせて報告してくれます。この情報はデバッグのために非常に有効なものです。


java.lang.RuntimeException: java.sql.SQLException
at Main.dataAccess(Main.java:25)
at Main.execute(Main.java:17)
at Main.main(Main.java:9)
Caused by: java.sql.SQLException
at Main.query(Main.java:31)
at Main.dataAccess(Main.java:23)
... 2 more



3. Try not to create new custom exceptions if they do not have useful information for client code.
(もしクライアントコードにとって有益な情報を持っていないのであれば、新たなカスタム例外を作らないようにしよう)

ここでは、自作のカスタム例外を作る意味が述べられています。

例外もJavaクラスですので、自分でメソッドやフィールドを追加できます。そうすることでクライアントにより具体的な情報を伝えることができます。しかし逆にいえば、そういう必要がない場面でカスタム例外を作るべきではないということです。


4. Document exceptions.
(例外をドキュメントに記そう)

Javadocには、検査例外、非検査例外両方共@throwsを使って書きましょう。
さらに、ユニットテストもあるとなおよい、ということが述べられています。


疲れてきたので今回はここまでにしようと思います。
次回は、「例外の取扱いに関するベストプラクティス」を書きたいと思います。

コメント