😵

例外処理 俺の指針

2023/07/16に公開

前置き

  • 「例外処理むっず...何を指針にして実装すりゃええねん...」ってなったので、調査して思考して記事書きました。
  • めちゃくちゃ予防線張りますが、知識理解や思考深度が甘かったりする可能性あるので、そこはお手柔らかにお願いしますm

1. 契約

「いや例外はどこ行ったんじゃい」と思うかもしれませんが、例外を考える上で一番基礎になる知識がこの「契約」です。
以下の引用がすごく的を得た説明になってます。

もしそちらが事前条件及びクラス不変条件1を満たした状態で私を呼ぶと約束して下さるならば、お返しに、事後条件及びクラス不変条件を満たす状態を最終的に実現することをお約束します。(1

まずは、この契約を各ルーチン(≒ 関数やメソッドなど)全てで定義することが大事です。
詳細はググってみてくださいまし。

2. 「例外」とは何か

ではでは本題に入っていきます。「例外」とはなんでしょうか。
ここの定義についてはネットで調べても様々な意見が出てくると思うのですが、筆者的には「契約」の視点で考えるととてもシンプルな答えになると思っています。

  • 例外とは「システム実行中に発生した契約違反」であり「システムを異常に導く災いの火種」です

システム作成の際は、各ルーチンの契約を信じてそれらを組み合わせることになります。
そのため、システムが稼働した際、各ルーチンが契約を守った動作をしてくれれば、システムも想定内の動きしかしないはずです。

しかし、たとえば、どこぞのルーチンAが契約違反を犯したらどうなるでしょうか?
そのルーチンAのせいで、そのルーチンAを使用してる別のルーチンBも契約を満たせなくなります。
そして、さらにルーチンBを使用しているルーチンCも...(略
この連鎖が続いた結果、最終的にはシステムが正常動作しなくなっちゃいます。

システムは各ルーチンが契約通り動いてくれると思ってたのに、どこぞのルーチンが契約を守らない事で、システムが想定してない動きになっちゃったり、異常停止しちゃったりする。災いが起きる。
この契約を守らない事象が「例外」ってわけですね。そして、この例外はシステムを異常に道くと。

3. 契約違反って具体的に何

「例外」=「ルーチンの契約違反」てことは分かりましたが、具体的に契約違反ってなんなんだ?って話ですよね。
これは「契約」を調べればすぐにわかるのですが、以下3つになります。

  1. 事前条件が満たされてない
  2. 不変条件が満たされてない
  3. 事後条件が満たされてない

(詳細としてはもう少し違う可能性ありますが、イメージとしては以下かなと)

事前条件の違反は、ルーチンが定義してる引数(インプット)の条件が守られなかったことを指します。
たとえば、ルーチンAが「引数aは、必ず1以上をくれ」て言ってるのに、使う側がaに0を与えたりしますと、ルーチンAは動作できません。
契約としては「aに1以上をくれればこういう動作をします」てなってるわけで、それ以外の値を与えられるとルーチンAは約束を守れません。

不変条件の違反は、ルーチン実行前、実行完了時に発生し得ます。
調べるとクラスの不変条件というワードが出てきますが、まあそういう定義じゃなくて、普通にルーチンが使うデータが条件通りになってるかってな感じの認識でいいかなと思ってます。
ルーチン実行前の時点で条件通りになってるか?ルーチンが実行完了した時点で条件通りになってるか?ってのを見る感じです。
ルーチン実行前の時点で不変条件が守られてないとその時点でルーチンは約束通りの動きをしないことが確定しますし、ルーチン完了時に不変条件が守られてないなら、ルーチンは約束を守らなかったってことになります。

事後条件の違反は、ルーチンが実行完了時に返すデータが約束通りじゃない時のことを指します。
ルーチンの約束事として「引数a, bを渡したらその足し算した結果を返してくれる」って話だったのに、a=1,b=1渡したら返ってきた値が200だった!!!
みたいな状況ですね、これは明らかにルーチンが約束を守らなかったのですね。

という感じで上記3つの約束事・条件が守られないことが契約違反となります。

4. 例外(契約違反)を完全に防ぐことは現実的に不可能

システムを災いに導き得るこの「例外」ですが、完全に発生を防ぐというのは無理です。

たとえば事前条件の違反。
「いやルーチンを使う側が気をつければいい話やん」てなりそうですが、そう簡単な話ではないです。
システムを使うのはユーザーです。開発者ではありません。ユーザーはシステムを完全に正しく利用することはできません。普通に事前条件を満たさないデータとか入力してきます。
なので、開発者がシステムを組む際にいくら事前条件に気をつけても、最終的な利用者のシステム利用行動を制御することはできないことから、普通に事前条件の違反は起きえます。

次に事後条件や不変条件の違反。
これは開発者の努力次第で頑張って抑えることは可能です。テストを徹底的にすればいいのです。
開発者は、各ルーチンを契約通りに実装する必要があります。
このインプットが来たらこのアウトプットを出力する、利用データはこういう状態にするっていう約束を守るように実装すべきです。
基本的に事後条件による契約違反(例外)の発生は、開発者の怠慢で起きます。

ただし、基本的にはそうなのですが、開発者がどう頑張っても発生を抑えれない状況もあります。
それは外部システム(外部API、データベースなど)と関わる時ですね。
システム側で動作を制御できないものがルーチンに関わってたら、どうテストを頑張っても事後条件の違反が起き得てしまいます。
たとえば以下のような状況が考えられますね。

  • Databaseとの通信間のネットワークが壊れた
  • 外部APIのレスポンス内容が変わった
  • 指定のファイルが存在しない

なので、例外の発生を完全に抑えるのは到底無理だと覚えておいてください。

5. 例外処理とは

「例外」についてなんとなくわかってきたところで、「例外処理」の話に入っていきましょう。

くどいですが、この記事では例外を以下のように定義してます。

  • 例外とは「システム実行中に発生した契約違反」であり「システムを異常に導く災いの火種」

てことは例外処理は、簡単に言ってしまえば火種を消すなり何なりして災いを防ぐ、もしくは災いが拡大するのを防ぐ処理のことですね。

では災いを防ぐために例外処理はどういうことをすべきなのでしょうか。
以現状の筆者の思いつく具体的な処理は以下4つです。

  1. 例外発生の原因調査ができるよう、例外の詳細を記録する。
  2. 可能なら例外発生原因の処置をして、そのまま処理を続行させる。
  3. 大災害にならないよう、例外発生によって起きたデータ不整合などを修正する。
  4. ユーザー起因の例外なら、そのことをユーザーに伝えて正しい使い方をしてもらう。

「1.」ができてないと、例外発生の原因が一生解消できずに、永遠に同じ例外が繰り返されてしまう可能性があります。
なので、発生した例外はログに記録できるようにしておき、例外が発生した原因を見つけて解消できる状態にしておきましょう。
少なくとも想定できてなかった例外、つまり、開発者が見たことも聞いたこともないような新種の例外の詳細くらいは記録する状態にしておきましょう。でないと修正できません。

可能なら「2.」のように、例外の発生要因をどこかのルーチンで修正して、再度正規処理に復帰させれるといいですね。無理して頑張らなくてもいいですが、可能なら検討してください。

「3.」は絶対に必要です。例外が起きると「データの更新が途中の状態になったまま」なんてことも考えられます。
そのまま去っちゃうと、システムが永遠に壊れたままになるなんてことも起き得ます。
なので、例外が起きてデータが不整合になったら、ちゃんと元に戻して去るようにしてください。
具体的な例で挙げると、データベースの更新作業をしている際に例外が発生したら、ロールバックという処理をすることもあると思います。そのような感じです。

「4.」に関しては、前節でもお伝えしたようにユーザーはシステムを完全に正しく使うことはありません。
なのでユーザーが間違った使い方をした場合は、そのことをちゃんと伝えてあげる必要があります。
ユーザー起因の例外は、ユーザーにそのことを伝えて行動を正してもらいましょう。

おまけ: 例外と言っても色んな見方があるよね

例外と言っても見方がいくつかあると筆者は考えてます。
その見方を知っておくことで、適切な例外処理ができるのかなと思ったり。

「復帰させる例外/させない例外」

あるルーチンで例外が発生した際に、その例外をキャッチして復帰させるかどうかって話があります。
復帰させる場合は、対象ルーチンを含めた伝播先のどこかでキャッチして復帰処理を実装してあげましょう。
例えば、そのルーチンが外部サービスAPIを叩いているのだが、、一時的なネットワークエラーが発生して実行結果を取得できなかったみたいな場合は、そのルーチンで例外キャッチしてAPIを再実行するという復帰処理が考えられます。
復帰させない、させれない例外はもう上位ルーチン(もっと言うと最上位ルーチン)まで伝播させちゃいます。どうしよもうないので。

「回復必要な例外/不要な例外」

ここで言う「回復」とは、例外発生によるシステム(データ)の不整合状態を正すことをイメージしてください。
一番わかりやすい例がデータベース操作の際のロールバック処理がまさに回復処理です。
ロールバックしないとテーブルの状態に不整合が起き得ますよね。
不整合が起きるとたちまちシステムは挙動不審になっちゃいます。なので、例外が起きても不整合が起きないよう回復処理してあげてください。
回復不要なら何もしなくていいです。

「想定できてる例外/できてない例外」

これは開発者が想定できてるか/できてないのかって話です。
はっきり言って発生しうる全ての例外を開発時に予測するのは不可能です。
なので、想定できてなかった例外は、システム運用時に必ず出てきます。
ただ、想定できてないことは仕方ないと思っています。発見ベースで「想定できている例外」に変えていくしかありません。
この対応ができるように、想定できてない例外もちゃんとログに記録できる仕組みはリリース前に整えてあげた方がいいかと思います。

最後に

今回は、筆者なりに調べたり思考した「例外」「例外処理」の内容をまとめてみました。
皆さんの理解の助けになれば幸いです。
また、皆さん自身でも調べる、思考を重ねるなどして、ぜひ「俺の指針」を作ってみてください。

なお、こういう風に実装したらいんじゃない?っていう指針もボヤッとあるのですが体力切れですゴメンナサイ。
筆者のScrapboxに適当に残した内容があるので、もし参考になれば。
https://scrapbox.io/onigiri-it-tips/例外処理_指針#630a3dddb3641f00006a5862

引用

  1. 契約による設計、例外、表明の関係について個人的なまとめ

参考

Discussion