thanks いらすとや
https://www.irasutoya.com/2015/07/blog-post_993.html
前置き
この記事は僕がぼんやり考えてぼんやりこうだよなぁ、と思ったという内容です。
割と当たり前な結論なってしまったのでアレてす。これが正解というものではないですし、間違いを含んでいる可能性もあるので悪しからず。
間違いはご指摘いただけると助かります。
なお、例外機構や例外オブジェクトのない言語もまとめて考えるため、本記事では例外という言葉を使わず、
とりあえずエラーと表現しています。
実装中、デバッグしていてエラーに気づかない時
Q1.実装中、エラーが発生しているのに気づかないということがあった。
なぜか?
A1.エラーログが適切に出ていなかったから
Q2.エラー発生箇所を特定するのに時間がかかった。
なぜか?
A2. try-catchで広い範囲を囲んでいたから(どこで発生し、どこでcatchされたか分からない)
こう行ったことが発生したのは、try-catchのせいだろうか。そうではなく、try-catchを使うための最低限が足りていなかったのではないか、という話。
この2つのQ&Aをお題にして、これをを解消する方法を考えてみる。
エラー処理の方式
まず、エラーの扱いについて。
僕はJavaやGo、JavaScriptなどをボチボチと何年か書いているというのがあり、
- try-catch(Java、C#、php、JavaScript、一応Ruby Python etc)
- Goなどのエラーオブジェクトによるチェック
の2種類の方式を知っている。
Goスタイルならログが出ていなかった点が防げたかというと同じようなことは起こり得る。
ただ、スコープに関していうとエラーの発生箇所特定までは早いなと思う。どこでエラーが発生するかはコードの戻り値(error)で分かるし、errorを返す箇所で絞っていけば良い。
一方でtry-catchの場合はtryブロック内のコード全体でエラーが発生する可能性がある。
先に結論を言っておくと、
どちらの方式であっても重要な点は、エラー処理のスタイルより
- 追跡しやすさ
- ハンドリングのしやすさ
ではないかと思う。
try-catchのスコープは狭くしたいけど…?
特定の容易さを上げるため、
まずは狭くする努力は必要だろうと。
そうするとどうなのだろう、究極的には2〜3行に対してtry-catchを仕込んでいくか。それなんてGo(略)
極力スコープ狭くするにしても、可読性や使いたい変数のスコープとかの観点でどうしてもtry-catchは広めになるのではないだろうか。
例えばメソッド/関数単位でtry-catch、特定の処理境界でのtry-catchなど。
try-catchは大域脱出か
try-catchは極論go toであるみたいな主張を見ることがある。使おうと思えばそういう風にも使えることはjokeコードを書いて試したことがあるので分かる。
実際問題、大域脱出として使用されているかと言われると少し違和感がある。
曖昧なガード
try-catchは、大域脱出というより
どちらかというと、エラーの曖昧なガードとして扱われていることが多い気がする。
「何が起こるか分からないけど、catch節で何かしとけば大丈夫だろう」という保険。
曖昧と書いているのは、tryのブロックによって、ハンドリングするスコープを広めたり狭めたり出来るから。どんなエラーか、どの行からやって来るエラーなのかが明確ではないからそう書いている。
もしかしたらエラー発生しないコードに対してもtry-catchを書くことが出来る。
発生箇所が比較的曖昧であるが故に、エラーを追跡しにくいのではないかとも思う。
コード量と例外処理のジレンマ
発生箇所の曖昧なエラーを扱うのにはある程度スキルが要る気がするが
一方で、例外処理のコード数が減るという点でコード全体行数は少なくて済む。
ただし、例外処理を減らす(広くすればするほど)エラーの抽象度が上がるというジレンマがありそう。
try-catchを成立させるにはstacktraceが不可欠
try-catchはそのように曖昧なのに、様々な言語で採用されているのは
(普段当たり前すぎて考えもしていなかったけど)スタックトレースがあるからだと思う。
それにより曖昧なエラー発生を明確に追跡可能にしている。
つまりスタックトレースのないtry-catch=死。個人的には。
if err != nilというイディオム
一方、
Goをやってるとif err != nil {}
を結構な回数書くわけですが
これはかなり明確なエラーハンドリングであると思う。
どこで発生したエラーに対して何をしているか、すぐに分かるのが良い。
冗長であると言われればそうだが、Goを書いていると、このイディオムは頭の中で省略されて見える(多分みんなそう)。
分かりきってることを書かないか、分かりきってることを確実に書いて捌くかの違いがある。
怠惰なエラー処理を防ぐには
単純にtry-catchにしたら良い or Goスタイルが良いという話をしたいわけでない。
エラー追跡などの観点だと、どちらもアンチパターンに陥ることがありそう。
etc.....
結局用法容量守るのが良い。
というわけで、以下で防止策を考える。
防止策1.エラーメッセージのgrepbility
自由度の高い、エラーメッセージはプログラマのログの書き方やシステム文言(共通のエラーメッセージ)などに左右されてしまう。
自然言語に揺らぎがあるのはしょうがないと思う。
メッセージをどうするかという点は、強いて言うならgrepしやすくしたほうが良い。
防止策2.スタックトレース(コードジャンプ)
揺らぎに左右されないのはスタックトレースだなと。
エラー解析時、確実に有効なのはスタックトレースだと思う。これがあれば問題を辿っていける。
これはtry-catchがあろうとなかろうと。
握りつぶしがダメというのは多く言われるけど
何故、ダメなのかの一例としては
スタックトレースが潰れて追跡難度が上がることが1つ、大きいと思う。
防止策3.エラーハンドリングはmiddleware/interceptorあるいはテンプレートメソッドパターン
ハンドリングに関して深堀りすると
try-catchの有無に関係なく
middleware/interceptorでエラーのハンドリング処理を挟んでおけばそれで良いはず。
try-catchの場合はre throw出来るし、Goの場合はerrorをreturnして渡していけば良いだけ。
try-catchは書いたら負けな気もするのは、各末端の実装で例外処理するべきではないというところだろうか。でも、それはそういったAOPのためのコンテナがある前提でのお話。もしくはテンプレートメソッドパターン的な何かがあれば良いかとも思う。
まとめ
ということで、実装中のふとしたことから始まり、エラー処理は何が最低限必要なのかを考えた。
自分の考えた結果はこんな感じでした。では、良いエラー処理ライフを!
- try-catchのスコープはなるべく狭くする
- try-catchにスタックトレースは必須
- メッセージはgrepbilityを考える
- エラーハンドリングはmiddlewar/interceptorで出来ると楽
0 件のコメント:
コメントを投稿