例外処理 指針
概要
システムを作る際に重要になる「例外処理」を設計/実装できるようになりたい
そう言う思いで「例外とは?」「例外処理とは?」そう言った疑問を調査した
1. 契約プログラミング
「例外どこ行ったんじゃい」と思うかもしれないが、今回の例外を考える上で一番基礎になる知識が契約プログラミング。 詳しくはググって調べてみて欲しい。以下の引用が凄く的を得ている
「もしそちらが事前条件及びクラス不変条件を満たした状態で私を呼ぶと約束して下さるならば、お返しに、事後条件及びクラス不変条件を満たす状態を最終的に実現することをお約束します。」という契約を結ぶこと
ルーチンは、契約として定義してる事前条件・不変条件を満たした状態で呼び出された場合、事後条件・不変条件を守った状態で処理を行う
このように「呼び出し先」と「呼び出し元」の間で契約を結んでる
code: add.py
# 簡単な例
def add(a: int, b: int) -> int:
"""契約内容
2つの引数に整数を与えてくれるなら、その2つの値を足して整数で返しますよ
"""
return a + b
凄く当たり前のように思うかもしれないが、この当たり前をしっかり定義することが大事
「こういう値をくれたら、必ずこういう風な処理をしますよ!」という取り決め
この取り決めを、1つ1つのルーチンで正確に定義する
2. 何が例外なのか
本題に入っていくわけだが、例外って何だろう。
様々な意見が出てくると思うが、onigiri.w2.icon的には契約プログラミングの観点で考えるとすごくシンプルになる。 例外とは、システム実行中に発生した「契約違反」のことである。
テストもちゃんとしてね。
各ルーチンが契約通りに動き続けてる限りは、システムが失敗することはない。
逆に、どこか1つのルーチンで契約違反が起きると、途端にシステムは失敗してしまう。
契約違反ということは、そのルーチンが想定通りの動きをしない
そうなるとそのルーチンを呼び出してたルーチンも契約を満たせなくなる
その連鎖が続いて最後にはシステムが正常動作しない状況を生み出す
このようにシステム実行中に起きた「契約違反(契約とは異なる動き)」を例外と呼ぶ。
契約違反とは具体的に何か
以下3つの条件に反することが起きたら違反である
1. 事前条件が成り立たない
呼び出す元が、呼び出し先で定義されてる引数の条件を満たさなかった時
2. 事後条件が成り立たない
呼び出し元が事前条件を満たしたにも関わらず、呼び出し先が指定の返り値を返さなかった時
3. 不変条件が成り立たない
呼び出し元が呼び出し前のデータ条件を満たさず呼び出した時
呼び出し先が処理完了後のデータ条件を満たさなかった時
これらの違反が発生した時こそ、例外が発生する時
3. 例外が発生したとは
「2.」でも説明したように、契約違反が発生することを例外が発生したと言う。
事前条件が満たされない時
code: sample.py
# 事前条件が満たされない
def devide(a: int, b: int) -> float:
"""
事前条件
b ≠ 0
"""
if b == 0:
raise ValueError("bは0以外を与えてください")
return a / b
事前条件が満たされないことから、ルーチンが責任を持って「契約違反です!」と声を出す。例外を発生させてる。
事後条件が満たされない時
事後条件が満たされない状況は基本的に避けられるはず。
なぜなら、事後条件に関してはテストで徹底的に守られることを確認できるから。
にも関わらず事後条件起因の例外が発生するのは、テストの怠慢てことになる。
ただし、基本的には避けるものだが、避けられないものもある。
それは外部システムを利用してるとき(Database、外部API、ストレージ、など)
こちらで制御できないものが関わってる時は、事後条件を満たせないことがある
例えば、、、
Databaseとの通信間のネットワークが壊れた
外部APIのレスポンス内容が変わった
指定のファイルが存在しない
など
code: sample2.py
# 事後条件が満たされない時
def save(msg: str) -> None:
database = db.connect()
# dbが壊れてたりすると、saveできなくなる。
# そうなると、指定のデータを保存するという契約を満たせない。
database.save({'msg': msg})
4. 例外処理とは
例外処理とは、文字通り例外が発生したらその例外を持って何かしら処理を行うこと。
例外発生の詳細を記録する
何かしらの回復処理を行って処理を続行できるようにする
例外に対処せず、そのまま処理を失敗させる
例外が起きたことをエンドユーザーに知らせる
など
結局どういう処理すればいいの!?となるが、例外処理の目的を設定すればやりやすい
目的は場合によって変わるかもしれないが、onigiri.w2.iconは基本的に以下だと思ってる
1. 発生した例外の原因調査・特定を容易にするため
2. 例外原因を適切にユーザーに伝えることで、ユーザーが次の行動を決めれるようにするため
3. 発生した例外に対する復帰を行い、処理を続行させるため
4. 例外発生時の不整合を正すため
上の目的から考えると以下のような例外処理は必要になるだろう
「1.」の目的に対して
例外発生のスタックトレースをログに記録しておく
例外発生時のルーチンへの入力値や関係データを記録しておく
「2.」に対して
エラーの原因を適切にエンドユーザーに伝える
エラー原因がユーザーにあるなら、ユーザーに対して行動を促す
エラー原因がシステムにあるなら、ユーザーには「ごめんなさい」と伝える
「3.」「4.」に対して
例外が発生したらキャッチして、復帰のため処理を行い、例外発生元のルーチンを再呼び出しする
例外が発生したらキャッチして、不整合などへの回復処理を行い、復帰せずに失敗させる
復帰不可能な例外だが失敗で終わらす前に整合性を正しておくべき、ってものがある
ロールバック処理などがここに当たる
例外と言っても観点が3つくらいある気がしてる
「復帰させる例外/させない例外」
例外が発生した場合、その例外をどこかのルーチンでキャッチして戦線復帰させるかどうか
復帰させるなら、キャッチした上で再度適切な値を持って処理を続行する
復帰させないなら何もしない。そのままユーザーまでエラー発生として伝わるだろう。
「回復必要な例外/不要な例外」
ここで言う回復とは...例外によって起きたシステム(データ)の不整合を正す行為のイメージ。
具体的には、データを例外発生前の状態に戻したりする
これからの後続のシステムに悪影響が出ないよう、「立つ鳥跡を濁さず」の精神で回復処理を行う
別に回復するものがないなら、何もしなくていい
「想定できてる例外/できてない例外」
想定できてる例外というのは、こういう例外が発生するだろうと予測できてることを言う。
予測できてるなら、その例外に対する処理方針も決まってることだろう。
回復処理が必要なのか、復帰させれるのかなどなど
ただ、想定できない例外ももちろん出てくる。
リリース前には予想だにしなかった例外が発生することもある。
これらの想定できない例外に関しては、正直なところ見つからなかったなら仕方ないと思う。
見つかったベースで対処していくしかない。
こればっかりは仕方がない。
ちなみに想定できてる例外でも、「回復しない」「復帰させない」なら、何の処理もしないことになる。
ログ記録、ユーザーに伝えるくらいはやる。
5. 例外処理の設計/実装指針
1. 原則、そのアプリケーション専用のルート例外クラスを用意しておくこと
Todoて言うアプリなら、TodoExceptionみたいな。
このアプリで定義する例外は全てこのルート例外を継承させると良い。
上手く言語化できないけど、、、
実装が綺麗になるはず。
想定できてる例外/できてない例外を区分できて、なんか想定できてない例外発生時の修正処理が楽になるかもw
2. 復帰処理が実施可能な例外以外は、全て最上位ルーチンまで流す
どのルーチンにも契約が存在する。最上位ルーチンにも。
最上位ルーチンの内部のどこかで例外が発生すると言うことは、基本的に最上位ルーチンが失敗することを意味する。
つまりこれは、最上位ルーチンの契約違反ってことになり、ちゃんと最上位ルーチンでも例外が発生したことにならないといけない。
ってことで、内部で起きた例外は基本的に最上位まで流すこと。
ただし!途中のルーチンで復帰処理ができるなら、そこから親ルーチンに例外を流す必要はない。
理由:
復帰処理したってことは、まだ上位ルーチンの失敗にはなってないってこと。
まだ上位ルーチンにて契約違反にはなってない。途中で復帰したことで、エンド側から見れば契約通りの動きに見えてるはず。
3. 少なくとも想定できてない例外の詳細はスタックトレースまで全部ログにのこす。
4. 回復させる or 復帰させる例外のみ途中ルーチンでキャッチする
ただし「回復させる例外」は、あくまで不整合を直すだけであって、たぶん復帰はできないので、復帰処理はせずに結局最上位ルーチンまで回されることになるでしょう。
5. できれば、例外発生時の引数や周辺データの値を記録しておく
6. エラーメッセージは端的かつ具体的にする
6. API、Webサービス、デスクトップアプリなどの追加指針
これらのシステムは基本指針に加えて、以下のことも必要だと思う
1. リクエストIDを生成し、そのリクエスト関連のログには全てこのIDを付与する
2. エンドユーザのリクエストパラメータを全て記録しておく(少なくとも例外発生時)
3. エンドユーザがエラー原因を適切に把握できるよう「エラーメッセージ」「エラーコード」を例外発生時には返す
4. エラーコードは連番でいい(と思う)。グルーピングとかは気にせず、発見したベースでどんどん追加していく。
エラーコードの存在理由:
ユーザーがエラー発生時に対処法がわかるようにする
開発者がエラー理由をパッと索引できるようにする
フロントエンド開発のエラーによる分岐処理を行いやすくする
エラーコードが整理されてる必要はない。なので適当に連番で追加していくだけでいい(と思う)
5. ログはJSON形式で記録しておくほうが、分析処理などに利用しやすい(らしい)