misc.log

日常茶飯事とお仕事と

プログラムにおける制御構造やエラー処理の「まとめ方」

内部向け説明資料をここで推敲しますwww
--------------
論点を分けましょう。多分ごちゃごちゃになってます

Try/Catchによる構造の構成方法について

VB6.0以前のOn Error Gotoと異なり、.NETでのTry/Catchはあくまで「例外的状況を制御できる制御構造」です。If/Then/ElseやWhile/End While、For/Nextなどと同じと考えてください。

主たる処理で発生した「想定外/規定外の状況」を捕まえて何かしらの処理を行う構造です。IF文で戻り値をみて処理を分岐させるのと何ら変わりません。また、例外に対する対処は必ずしもエラー処理とは限りません。それを踏まえた上で「必要であれば使ってください。ネストにせよ、大きく囲うにせよ、そうしないといけない理由があるならそうすべきです」が答えになります。

例外をThrowするタイミングとどこで受けるか

例外による呼び元への通知は、従来の戻り値での結果処理と何ら変わりません。ただ、「1段上じゃなくても、数段上まで一気に飛ばせる」「バリエーションが多く、自分でも種類を増やせる」「中に複雑な情報を保持させられる」などの特徴があるだけです。ですので、「途中でキャッチするか、一番上でキャッチするか」についても、たとえば戻り値でOK/ERRを返す処理が3段の多段呼び出しになっている場合、「どの戻り値で最終的な『答え』と見なすか」というのはプログラムを書く人が都度考えることです。

たとえば…例外を使わなくても今までも同じような状況はあったと思います。

FunctionMain
  If FunctionA=OK Then
   (Aがうまくいった場合)
  Else 
   (Aが失敗した場合)
  End If
End Function

FunctionA As OK/ERR
 If FunctionB=OK Then
  (Bがうまくいった場合)
  Return OK
 Else
  (Bが失敗した場合)
  Return ERR
 End If
End Function

FunctionB As OK/ERR
 Dim result = (DB処理)
 Return result
End Function

上記のような場合

  • FunctionBでログを出すべきですか?
  • FunctionAでログを出すべきですか?
  • FunctionMainでログを出すべきですか?
  • FunctionMainを呼んだ側でログを出すべきですか?

と聞かれても、一概に答えは言えないと思います。DB処理の失敗原因を詳細にログに出すには、FunctionB(3段目、最下層)でログを出すのが一番簡単ですが、それをやらないなら、失敗情報を上位まで伝達する必要があります。そんなレアケースの為に、グローバルな変数や複雑な戻り値を工夫するくらいなら、最下層でログを出して、上位は「OK/ERR」だけを取るようにするのも手でしょう。
ですが、最下層の処理が、「まぁ失敗は想定のうち」であれば、せいぜいFunctionA(2段目)で正常系ログとして「試したけどダメだった」というログをだすかどうか、くらいが悩みどころになります。
結局、「一連の処理は何をやる処理で、どれくらい重要で、どの部分の失敗は許されないのか?」といったことに大きく依存します。

いや、というよりも、上記の悩みを持った時点で「なぜ別のFunctionに分割したのか?」を考えるべきだと思います。エラー処理などがひとまとめにできるということは、業務機能的には1つにできるものなはず。それを分割する理由としては

  1. 他でも使う処理だから
  2. 長い処理中で何度も登場するから(ループ内で使うなども含む)
  3. 長すぎるので

あたりがありがちです。上記の1の場合は、以下の制約を他の利用者に適用できるのであれば、エラー処理などは行わなくても良いと思います。

エラー処理は一切行わず、発生した例外もそのまま素通りさせます。

それ以外の場合は、作り手や作り手の管理上の都合な訳で、そうであれば「まとめるかどうか」なんてのは作った本人しか判りません。というわけで、やはり「作り手が自分で、その時どうすべきかを考えるべきこと」というのが答えですね。

うまく説明できてないかもしれませんが、私が回答に困っていたのはそうした考えがあるためです。

まとめて大きくエラー処理するメリット、デメリット

最上位構造でTry/Catchしたりエラー処理しておけば、とりあえず全ての例外をキャッチできるので、エラーで止まらない、という要件は満たしやすくなります。が、上記のQA回答にあるような「エラー時の後片付け」を実装する場合、考えなければいけない状況の組み合わせが増えるため、逆に、片付け処理で「Nothingなオブジェクトのメソッドを呼んでさらにエラー」などという事態が発生しやすくなります。

であれば、ログを出す/出さないなどとは別の流れで、片付け処理などを個々の段階で細かく行う為にTry/Catchで囲み、受け取った例外はそのままThrow。最上位構造でもTry/Catchで囲い、片付けは端折って(下位構造でやっていることは自分が保障する)本当のエラーメッセージや業務的なエラー処理は最上位で、なんてやり方もあります。

細かくエラー処理することのメリット、デメリット

細かくエラー処理すると、コーディング分量は確実に増えますし、ログなども冗長に書き出される可能性があります。

が、たとえば関数単位で独立してエラー処理すると、テストの手間が若干楽になります。1つの関数が他の関数との構成や構造に依存しない、「疎な状況」になっていれば、その関数に絞ってテストすればそこは単体テストレベルでの動作を保証できるわけです。その関数内でログやメッセージがきちんと出ることが確認できれば、その部分は動作確認OKです。

一方で、大きく複数の関数を組み合わせた構成でエラー処理してしまうと、最上位でのエラー処理の網羅性をみる場合に、下位の関数の結果組み合わせも含めた再現を行う必要があるため、テストケースの生成やテストデータ、状況の再現が大変になる場合があります。また、「この関数はここからこういう風に呼ばれ、結果はこうでなくてはならない」といった暗黙のルールが生じる場合もあります。

テストをやりやすくするために、各関数(メソッドなど)を互いに依存しないように作るという観点から、どうするかを決めるアプローチもあります。

まとめ

こちら(弊社)としては、実装やテストの手間(コスト)が少なくて、確実に動くことを確認できるのであれば、どちらでも良いと思います。規模や処理内容で決まることですので、規定できません。

ものすごく長くなりましたが、こんな感じでどうでしょうか?

参考

@IT/制御構造としての例外(Javaですが基本は同じです)
http://www.atmarkit.co.jp/fjava/rensai2/javaent13/javaent13.html