Good Code 書籍の例外処理とフェールセーフ思想の違い

新人教育に「Good Code ~」を使うのだけど、第4章の「エラー」の部分だけは、ちょっと私の思想とは異なる。一般的に言えばそういう書き方が推奨されるのだが、実務的にはフェールセーフな動きが求められることが多い。ゆえに、ある程度のコードを書かずに「Good Code ~」を先に読んでしまうと変なところに陥る(たびたび原著が抜けてている点も含めて)の難点・・・なのだけど、まあ、新人教育には便利な教材である。

例外を呼び出し元に通知するべきか?

15年前位に、アプリケーション例外とシステム例外の区別をすべきかどうか?というのが流行った時期がある。システム例外はオペレーションシステムが発生する例外、アプリケーション例外はシステム以外全般のユーザーが使うという意味でのアプリケーション層ということになる。

一般的にユーザーが使うアプリケーションは、何か問題が起こったときにはユーザーに対して「予期せぬエラーが起こりました!」でアプリケーションが落ちる。「予期せぬエラー」というのは、アプリケーションにとって予期せぬエラーであって、回復不能なのでエラーダイアログには「OK」ボタンしかない。ユーザーには、何らかのエラー番号が伝えられることもあれば伝えられないこともない。予期しないのは、ユーザーにとって予期しない動作だったかもしれないが、アプリケーションを作った開発者にとっても予期しないエラーだったのかもしれない。ともかく、エラーメッセージを出して落ちる。

この「予期せぬエラー」は、たいていの場合はシステム例外が起因となる。アプリケーション内のロジック的なエラーではなく、システム側でHDDが書き込めなくなった、LANケーブルが抜けた、CPUが熱暴走した、という際のハードエラーも含め、オペレーションシステム開発者が出す例外も含める。よくあるのは、システム側がヌルポインターを返す場合もこれにあたる。これの場合、アプリケーションが対処できるかといえば、できる場合もあるしできない場合もある。回復可能かと言えば、再試行できる場合もあれば(Wi-Fiの接続がわるいとか、輻輳とか)、回復できない場合もある。そんなアプリケーション開発者としてはレアなケースでもあり、ユーザーにとってもレアなケースである。

一方で、アプリケーション例外は、アプリケーションの中で何らかのロジック的なエラーが発生した場合と言える。なので、完全なエラーのないプログラムであれば、アプリケーション例外は一切発生しない!のかもしれないし、別の意味での「例外」を発生させる場合もできる。「Good Code ~」の中でいればユーザーが入力する電話番号にアルファベットが紛れ込んだときの場合にもアプリケーション例外は使える。

歴史的な視点でいえば、結果的にアプリケーション例外とシステム例外はあまり区別しなくてよいという結論に至っている。アプリケーション例外、システム例外の2点で考えるとエンドポイントがクライアントとサーバーの2つのように見えるのだが、オペレーションシステムだとしてもはハードウェアに対してはクライアントになるし、アプリケーション内でもライブラリとユーザーインターフェースの区切りがあれば、ライブラリはシステムサイドと言える。なので中間層があるわけで、決定的な区別はない。特に、Java や C# のように VM を使う場合は、VM 内部での発生がシステム例外なのかアプリケーション例外なのか分離しづらいところもあるので、まあ、雰囲気的に例外を扱うという着地点に至っている。

そんな中で、システム例外をアプリケーションの表層(ユーザーインターフェース)に押し出すのがよいのか、という疑問がでてきる。「Good Code ~」で扱う Java の場合、呼び出す関数に throws を付けて「無視してはいけない例外」を明記することができるのだが、現在、他のプログラム言語で流行らなかったところを見ると例外の情報は呼び出し元にとってあまり重要ではないということらしい。端的に言えば、システム内部で発生した例外を細かく呼び出し元に通知していると、実に膨大な例外の数が渡ってくるために現実的に処理しきれなるためだろう。

そう、Google が開発した Go には例外がない。まあ、そういうことだ。

なので、なんらかの例外を呼び出し元に通知する場合は、

  • 有限である例外の種類

がひとつの基準になるだろう。例外の種類が5種類ぐらいならばいいけど、100種類ぐらい飛んでくるのであれば、それは何かクラス構造とかそもそも例外を飛ばす必要があるのか?というのを考え直した方がいい。この視点は必要になる。

例外通知はプログラムのバグ修正のためにあるのか?

これは C++ で例外処理が入った頃から思っていたことなのだけど、例外の通知は、誰のためにあるのだろうか?「Good Code ~」や当時の上司には「例外を隠蔽してしまうと、コードのバグが隠蔽されてしまう」ので、例外を隠さないに呼び出し先の関数では例外を呼び出し元に通知するという方針をとることが多い。

が、そもそも、実運用で動いているコードに対して「バグが隠蔽されるから」という理由で例外処理を潜ませておいて、例外が発生したらアプリが停止する(バグの特定のために?)というのはユーザーにとっては変な話ではないだろうか?

なので、私の方針としては、

  • 関数内で例外が発生したときは、エラーログを出力し、デフォルト値を返す(知らん顔をする)
  • ユーザー入力やデータ値でエラーになったときは、デベロッパー環境以外では異常値が入力されたときと同じように、通常のエラーメッセージと同様にユーザーに通知する(ユーザーの異常値なのかシステムの異常なのかは判断がつかない)

というコードにするのがベターと考える。もちろん、システムの方針によっては例外を発生してアプリケーションを停止させることもあるのだが、たいていの場合は停止させない。というか停止できない場合が多い。

  • 組み込みシステムの場合は、内部エラーが発生してもリセットする「人間」が不在のことが多いので、ひとまず動作を続けるパターンが多い。
  • 基幹システムの場合、止めることができない場合が多いので、勝手にアプリケーションを落とさない。応答はするものの、エラーメッセージを大量に吐き出すという対処で、デベロッパーに通知を送る。あるいは異常な状態に陥っても、メールを送る、通知を送る等の手段を残す。
  • ロボット系の緊急停止は、動作プログラムは別につくらないといけない。異常が発生した場合は、アームの位置を危険ではない状態に戻したのちに、停止するというスタイルになる。
  • もし、原子力関係のソフトウェアでバグが発生した場合、緊急停止するのではなく、現状維持がベターである。本当の緊急停止は監視者が手動で行えるので、ソフトウェアは閾値の加減となる。

このように製造業のソフトウェアシステムの場合は、プログラムのバグの追求よりも、安全にシステムが停止するという意味での「例外処理」が優先される。例外に対処するのは、利用者を守るために安全に停止する(あるいは動作を継続する)のが目的のなる。これは「フェールセーフ」の考え方になる。

xUnit 以前ならば、バグが隠蔽されるという意味もわからなくもないが、実運用しているソフトウェアに対して開発者の利点のためだけに「例外」を使うのはどうかと思われる。利用者の利点を最大限にするなれば、

  • プログラム内の回復不可能な異常発生時には、ユーザーに通知をして判断を仰ぐ。
  • プログラム内の回復可能な異常発生時には、エラーログを出力しユーザーには通知しない。

という使い分けが必要と思われる。「Good Code ~」では、後者の部分が省かれてしまっている問題がある。

bool login( string username, string password ) {
    
    if ( username == null ) return false;
    if ( password == null ) return false;

    // ログインチェック
    ...
}

たとえば、login という関数で、username と password が渡されて bool を返すとする。

このとき、何らかのプログラムミスで、username や password に null が入っていてた場合は、例外を発生してプログラムの開発者に「login 関数の呼び出し元が異常なことをやった!」と通知したほうがいいだろうか?

私はそうは思わない。プログラムのコードのミス(それが login 関数を作った開発者ではなく、login 関数を呼び出す別の開発者であったとしても)を運用時にユーザーに通知するのは不親切であろう。もともと、login 関数の目的はユーザー名とパスワードのチェックなのだから、ユーザー名が null の人はいないので、しれっと false を返して、ログインできないようにすればいいだけだ。呼び出し元がバグのままかどうかなんてユーザーには知ったことではない。login 関数は例外を発生するのではなく、エラーメッセージを吐く位がせいぜいだろう。あるいは、C言語の assert 関数のように、リリースビルドをするときには影響しないものを考えるべきだろう。login 関数には bool 値(ログインできるかできないか?)の判断を任せているだけで、ユーザー名が null かどうかは尋ねていないのだから。

カテゴリー: 開発 パーマリンク