例外設計について勉強
やりたいこと
あるmethodに不正な値が入力されたり、依存しているオブジェクトが不正な値を返した時に行う例外出力を設計したい。
疑問
今、Unit testとオブジェクトの実装をしている。有効同値と無効同値を書き出すまでは出来たが、無効同値の処理に対して全て例外を書いたほうが良いのか、それとも返り値としてエラーコードのようなものを返したほうが良いのか、よく分からず、悩んだ。そもそも、考えられる全ての場合を書くと、膨大な例外処理になってしまうところで「おかしい」と思って調べてみた。
※Javaやkotlinではエラーコードを使わずthrowを使うほうが良さそう[1]
そもそも例外とは、その考えられる種類
そもそも例外とはなにか。様々な考え方の記事があったので纏めてみます。これはなぜ調べたかというと、どこまでの不具合を例外として記述するのかを知りたかったからです。想定外の不具合については「Unexpected error occured」みたいなのをthrowするのだと思うのですが、先にも書いたように同値分割した時に考えられる無効同値は全てthrowするべきかということです。
例外と似た言葉にエラーがあります。神田ITSchoolによると[2]
「エラー」の場合はプログラムで対処できない致命的な例外を指し、「例外」の場合はプログラムで対処できる例外を指します。
だそうです。一方で、佐々木真さんの記事[3]では
例外:「想定できた」おかしな状態
エラー:「想定外の」おかしな状態
としています。この2つの記事は似ていますね、ところが@KyojiOsadaさんの記事[4]によれば
我々エンジニアが最低限知っておかなければならないのは「例外とエラーの違いが普遍的に成立する概念は存在しない」ということです。
だそうです。なるほど…
一応、英語でも調べてみて、Stackoverflow[5]では
An Error "indicates serious problems that a reasonable application should not try to catch."
An Exception "indicates conditions that a reasonable application might want to catch."
と書かれています。この、Stackoverflowではcatchすべきかすべきでないかと書かれていますが、この後の文を読んでいくとエラーは「回復不可能」かや「終了せざるを得ない」などがあります。例外の中でも、プログラムによって回避できるchecked exceptionと、プログラムによって回避できないunchecked exceptionがあると書かれています。この前に作られたStackoverflowのスレッド[6]ではJavadoc[7]に書かれている内容を元に議論しています。
具体的にJavadocでは
An Error is a subclass of Throwable that indicates serious problems that a reasonable application should not try to catch. Most such errors are abnormal conditions. The ThreadDeath error, though a "normal" condition, is also a subclass of Error because most applications should not try to catch it.
と書かれています。
以上のことから、つまり、エラーと例外については厳密には分けられないけど
- Error: 実行したアプリを落としたほうが良いもの。落とさざるを得ないもの。その原因となる不具合。
- Exception: エラー以外。プログラマに修正して欲しい(修正すれば直る)もの(unchecked exception)とプログラムが回避できるもの(checked exception)がある。
つまり、エラーはどうしようもないけど、例外については「プログラマに訴えかけるもの」と「ユーザー又はプログラムに訴えかけるもの」の2つかなという印象でした。
しかし、ここまでの結論は、結局は原則、考えられるものを全て書かないといけないという話ですね…
じゃあ、例外をどうやって設計するか
「例外設計」と検索するといっぱい出てくる。Takuto Wadaさんのスライド[7:1]を元に書かれたtasuwoさんの記事が分かりやすかった[8]。エラーを放置しないなど、今回の範囲外なので飛ばしますが、エラー処理をする際はとても勉強になるところが多いので、是非一読ください。また例外設計で重要な「教訓」はここでは飛ばします(記事が長くなりすぎるので)。
今回、2つ目の章で述べた「例外が多すぎる」というのに似た記述があり、それは「過剰防御」というらしいです。これを防ぐために「契約による設計」という手法を用いるそうです。詳しくは、こちらもtasuwoさんの記事[9]ですが、一読ください。自分なりにまとめると、対象のmethodに入力されるデータの型であったりフォーマットなどは事前条件として、対象のmethodを呼び出す側で保証し、対象のmethodは呼び出された後(終了後)の動作を保証すれば良い。 と言えそうです。つまり、引数にInt型のはずがStringが入ってきた場合は考慮しなくていい(言語側でチェックしてくれる)し、例えばファイルを指定して取り出すmethodがあった時にパスが絶対パスのはずが相対パスで来たときも、本来は呼び出す側で保証するということです。しかし、後者の場合は、対象のmethodで、例えば.checkPath()のように有効なパスかどうかをチェックするものが必要です。
今の自分なりの例外設計方針
以上のことから、まずはじめに
- エラー:もうどうしようもない、アプリを落とすしか無いエラー(想定できれば)
- 例外:プログラマもしくはプログラムに何とかして回避してほしいメッセージ
の2つに分け、例外については
- checked exception: try-catchを書いて回避する(プログラムが回避する)
- unchecked exception: null pointなどプログラマがプログラムを書いて修正すれば直る
- その他の例外:予測できない例外
に分ける。例外が多くなりすぎるかもしれないが、契約による設計に則り、事前条件、事後条件、不変条件(処理対象のデータなどの条件)を考慮してthrowするべきかどうかを決める。
-
例外の設計指針~歴史と分類とトレンド / @tashxii https://qiita.com/tashxii/items/573862fd432f1acab616 ↩︎
-
例外とエラーについて / 神田ITSchool https://kanda-it-school-kensyu.com/java-basic-contents/jb_ch08/jb_0801/ (2022-01-13閲覧) ↩︎
-
「エラー」と「例外」の違い / 佐々木真 https://wa3.i-3-i.info/diff78error.html (2022-01-13閲覧) ↩︎
-
例外とエラーの違い / @KyojiOsada https://qiita.com/KyojiOsada/items/4c65e3c014d27a16bb6f (2022-01-13閲覧) ↩︎
-
What is difference between Errors and Exceptions? [duplicate] / Nirmal- thInk beYond on Stachoverflow https://stackoverflow.com/questions/5813614/what-is-difference-between-errors-and-exceptions (2022-01-13閲覧) ↩︎
-
Differences between Exception and Error / Eddie on Stackoverflow https://stackoverflow.com/questions/912334/differences-between-exception-and-error (2022-01-13閲覧) ↩︎
-
例外設計における大罪 / Takuto Wada https://www.slideshare.net/t_wada/exception-design-by-contract (2022-01-13閲覧) ↩︎ ↩︎
-
例外設計における大罪 / tasuwo https://scrapbox.io/tasuwo/%E4%BE%8B%E5%A4%96%E8%A8%AD%E8%A8%88%E3%81%AB%E3%81%8A%E3%81%91%E3%82%8B%E5%A4%A7%E7%BD%AA (2022-01-13閲覧) ↩︎
-
契約による設計 / tasuwo https://scrapbox.io/tasuwo/%E5%A5%91%E7%B4%84%E3%81%AB%E3%82%88%E3%82%8B%E8%A8%AD%E8%A8%88 (2022-01-13閲覧) ↩︎
Note(実装について)
不正な値だった時にnullを返すようにしようかと思ったが、それは良くないので、nullは本当にnullを返すべきときだけにして(nullが有効同値である場合)、それ以外は面倒でもthrowする。そして、nullであるかどうかをチェックするmethodを実装する。