エラー値をあつかうべきかどうか?はプログラムの重要性や
エラーの致命度によると思います。
プログラム自体が内部メンバだけが使うテストプログラムであれば
エラーをほっておいてもいいかもしれないし、また逆に、一般ユーザが
使うのであれば、きっちり何が起きているのか教えてあげる必要があると
思います。また、そのエラーが発生して、それ以降の処理を継続しては
いけない(例えば過去のデータを空白で上書きして消す結果になる)場合は
ちゃんと中断処理をする必要があると思います。さらに例えると、
オーダ処理でエラーになったら、課金処理をロールバックさせるとか、、
逆に、軽微なエラーであって、一度発生してもまたリトライするタイミングが
あって、その時にうまくいけばいいのであればほっといてもいいし。。
一般的に、エラー処理はまじめにやりだすと、コードが肥大化して読みにくく
なるし、さらに、サブルーチンの深い所でエラーが発生すると、大域ジャンプに
近いようなことをしないと処理を中断できないとか、とにかく頭を悩ませる
問題とは思います。(品質向上のためには必要なコードではありますが)
今風に実装するなら、例外(Exception)を投げる実装がすっきいりまとまって
いいのではないか?と思います。
テストの段階では、現実から離れたデータでもテストをすると思いますので必要だと思います。
でも、実用のレベルになってきたときに、そこまでプログラムの中でチェックをする必要があるかどうかは、その関数がどのくらいプログラムの中で重要なポジションだったり、イレギュラーなデータが来る可能性が高いかによると思います。
テストを繰り返して、ほぼ問題なく稼動しているのであれば、何か問題が起きたときにすぐにチェックできる手法で行われていれば、チェックはいらないと思いますよ。
なるほど。至極もっともな考えだと思います。ただ、一般論の域を出ていないもどかしさも感じます。こちらの質問もひじょーに抽象的な書き方をしているので、どうしても突っ込んだ意見になりにくいのは、仕方がないですかね。
私の個人的な考え、やり方ですが、関数が意味のある値を返すなら全てチェックするコードを書きます。でないと次に何が起こるか予測がつかないと思うので。あとは運用中に不具合が発生したとき、エラートラップをカッチリやってログなりなんなり残さないと後で原因分析できなくなるのが恐ろしいです。
結局のところ、その関数の結果値を判断していないモノに対してどの程度の信頼性が求められているのか? という、その信頼性の高低によって答えは変わってしまうんですよね。
非常に大雑把にあいまいな表現で説明すると、モノは高信頼性が要求されるシステムの一部です。作ってくれている人には少なくないお金を払っているし、これを安くないお値段でお客様に買っていただく。そーゆー状況で考えたとき、ちょっと困ってしまうわけです。
メモリアクセス違反(ポインタの戻り値がNULL)でオチるのは恥ずかしいし
他の処理(関数)でも、どこが発生原因かわからないのは、不具合調査で自分の労力が増すばかり。
PGの誤りだけではなくて、動作環境や渡される引数、入力ファイルが原因の場合にも処理異常の理由を追えますから。(むしろ、リリース後は、この用途のほうが多いかも)
どの関数(必要なら呼び出し時引数の値も)で、どういった想定外あるいはエラー応答があったか、必要に応じてログにも吐き出すようにしています。
自前でエラーハンドル用(エラーだけでなくチェックポイントのログ出力、呼び出し履歴の記録なども)クラスを用意しておくと、使いまわしができて楽ですよ。
ただ、エラー処理クラスのエラーをこのクラス自身でハンドルすると無限ループ(ひいてはスタック領域枯渇)につながるのでそこだけ注意です。
そもそも質問のコードが何の言語かにもよるのですが…。
自分は基本は過剰にならないように、必要な部分だけチェックを行うタイプです。
仕事では組み込みが多いのでCが中心です。
printf()とかならまずチェックはしないし、malloc()系(というかメモリ系)の関数ならばチェックは必須ですし。
当然、自分の作った関数でもすべてチェックしています。
逆に家で使っているRubyなんかだと、わざと例外には対処せずにエラーを生のままで出すようにしています。
まあ理由としては、被害が少ないというのも有りますが言語側のエラーを生で見たいので。
個人的にはこのほうがバグがあっても修正がしやすいです。
また簡単な動作を見るためのテストプログラムならばエラーチェックはほとんどしません。
ただし、テストプログラムでもドライバの動作をチェックするユーザランド側のプログラムだとエラーチェックはしっかり行います。
どんなシステム開発でも、それが仕事なら、単体テストで戻り値のチェックはしますよ。たとえ、99.9%のケースが正常系で動いており、エラー戻り値の影響を受けることは皆無だったとしても、関数仕様書に書いてあることはすべて単体テストします。
私がプロジェクトマネージャだったら、単体テスト結果報告書が出ていない関数を結合することはさせません。
>一般論の域を出ていないもどかしさも感じます
そうですね。すみません。(会社でも突っ込みが甘いと言われたり、、)
もう少しフォローのリプライ書きます。
大昔、すこしまともなソフトを組んだ頃は、
チーム全体で、エラーハンドリングのルールも決めて
統一的に処理していました。(開発者が都度判断していると
異常時の動作が開発者間で不統一になるし、マニュアルに
エラーコードを掲載する必要があるので)
まったくもって同意見… なんですが、私のいる世界は果てしなく醜く、トラップされないエラーが山のようにあります。品質というのは常に時間(=コスト)とのトレードオフで、かけたコストに見合った品質のものしか出来上がってこない。まあ、もっとコストを投入すれば、id:pahooさんのように作れるんでしょう。今プログラム製造をお願いしている所にいくらお支払しているのか私は知りえる立場にはないんですが、ものすごく値切った結果として「結果値を判断しない」という事につながったのかもしれません。
でも製造でケチってしまったら、節約した額の数倍の額が不具合対応で消えると思うんですけどね。そこを冷静に見積もれば、製造の時点でもっとコストかけないと(品質を上げないと)保守で結局損をすると、サイフを持っている人たちを説得することができるかもしれません。
あとはまあ、これはあんまり言っちゃいけないのかもしれないけど、プログラマとしての気概というか、心意気、美意識に負う部分も個人の資質として持っていないと、より完璧に近い結果値判断はできないのかもしれません。
>自分は基本は過剰にならないように、必要な部分だけチェックを行うタイプです。
現実的な落とし所として、まあ、そうするしかないよね?という共感を覚えます。結局プログラムの中で100回関数を呼び出してそれぞれ個別にチェックをやっていたら大変なコストがかかる。チェックをまとめられるような実装上の工夫ができれば、それで実務上十分だと思います。個人の感覚として「満足」はできないけど、「納得」はできる、そんな感覚です。
あとはプログラムの用途でチェックの度合を変えるのもごもっとも。コンセプトを実証するだけのデモプログラムなら、正常ケースだけが網羅されていれば十分ですね。
あと言語に関してですが、実務上「関数の結果値」という概念がサポートされている言語であれば同じ悩みがある、と考えています。なので、いろんな人のいろんな考えがききたいなあと。
自分が作ったシステムを自分自身が使うとしたら――私は、いつも、そう考えて仕事をしています。
たしかに、テスト工数はコストとのトレードオフですが、単体テストを全くやらないようなプロジェクトは欠陥品を産み出すだけです。単体テストを無視してお客様に欠陥品を納品する仕事なら、最初からやらない方がいい(キッパリ)。
とはいえ、醜いプロジェクトは少なくないですよね。正常化できればいいんですが、コストが全然見合わなくて、プロジェクトを止めなければならないことがあります。
C標準関数に対して考えるならば、fopenのように外的要因によりエラーとなる可能性のあるものと、memcpyのように外的要因の無いものとに分けられると思います。
前者でのエラーはエラー処理が必要。
後者でのエラーはバグでしか起こりえないためエラー処理は不要です。
しかし、前者でのエラーは正常動作中に十分起こり得るので、そのエラー処理自体が正常処理の一種でありこれが無いのは欠陥と考えられます。
外的要因を検討し、それに対する処理さえ行えばエラー処理は全く必要無いと考えられます。
標準関数ではなく、自作の関数に対して考える場合は、話は全く逆になります。
考えるべきは、もし戻り値がエラーだった場合に、他への影響が有るか無いかです。
例えば、ログ出力用の自作関数がエラーとなった場合、ログが出力されないだけでプログラム本来の動作には全く影響しないためエラー処理はいらないです。
影響があると考えた場合は、きちんとしたエラー処理が必要です。
そのエラーをリカバリーするコードを記述するわけですが、リカバリーできない場合は、プログラムを停止するなどのコードを記述する場合もあります。
ではバグ対策はどうするか?
単体テストである程度潰したとしてもバグは必ず残ります。
よって、何処まで正常に動作したか、どこからおかしくなったかを事後に確認できるよう、ポイント、ポイントで動作情報のログを出力するように作るのがいいと考えています。
余談ですが、上記の単体テストの『ある程度』とは、試験員が『完璧です』と報告するレベルを言っています。
よく『テスト結果が完璧なんて有り得ません』(正論)という人がいますが、その場合、どの辺りが不安かを尋ねると完璧じゃない理由がぼろぼろ出てきます。
つまり、ちゃんとテストしてないってことです。
単体テストとしてやるべきことを全て行い不安が無くなれば『完璧』と言えるはずであり、これが品質を決めると私は考えています。
エラー処理とは、ちゃんとテストするのが前提にあってこそのものであり、単体テスト不足を補うためのものでは無いと考えています。
非常にボリュームのある内容ありがとうございます!
ほかのエントリも読んでなんとなく思うことですが、全てはどこまでうまくいかなかった原因を追い詰めるか、そのポリシー次第で、結果値判断(エラートラップ)をどの程度の粒度でやるかが決まるな、と感じます。
たとえば、私の感じだとid:tezuboaさんの
C標準関数に対して考えるならば、fopenのように外的要因によりエラーとなる可能性のあるものと、memcpyのように外的要因の無いものとに分けられると思います。
前者でのエラーはエラー処理が必要。
後者でのエラーはバグでしか起こりえないためエラー処理は不要です。
というのは大いに違和感を感じるところです。どんな関数でも、必要だから呼び出しているわけで、呼び出した結果が期待通りでなかったら、後続処理は続けられない。連鎖的にエラーが生じるか、一見うまく言っているようで実は本来の期待結果と異なるものが最終的な出力として得られる「誤動作」ということになる。
でも
影響があると考えた場合は、きちんとしたエラー処理が必要です。
そのエラーをリカバリーするコードを記述するわけですが、リカバリーできない場合は、プログラムを停止するなどのコードを記述する場合もあります。
ではバグ対策はどうするか?
単体テストである程度潰したとしてもバグは必ず残ります。
よって、何処まで正常に動作したか、どこからおかしくなったかを事後に確認できるよう、ポイント、ポイントで動作情報のログを出力するように作るのがいいと考えています。
こーゆー、意味のある結果が得られる単位、リカバリーポイントを考慮したポイントで一括してエラーの有無をチェックすればいい、という考えですね。これは私も賛成です。
エラー処理とは、ちゃんとテストするのが前提にあってこそのものであり、単体テスト不足を補うためのものでは無いと考えています。
そこは多分memcpy関数に代表される信頼性の高い枯れた関数がエラー値を出すのは単体テストにおいてだけでそこでバグを摘出しきれる!しきるべき!という考えですかね。
でもここはちょっと違うと思うんですよ。テストは不具合=要件と違う動作をするかどうかを検査するもので、エラー処理は要件そのものです。要件なんだから、エラー処理だって単体テストするんですよ。
他のエントリでも書きましたが、「プログラマとしての気概」と後で苦労したくないという実利が原動力ですね。実利がチェックを実装するという行為に結びつくには、当事者であるという認識がないといけない。作っている人の立場や気持ちが、仕様やコードにあらわれてくる。コードを延々と見ていますが、やっぱりその人がそのコードを書いている時の気持ちみたいなものは、感じられるような気がしますね。気のせいかもしれませんがw
自前でエラーハンドル用(エラーだけでなくチェックポイントのログ出力、呼び出し履歴の記録なども)クラスを用意しておくと、使いまわしができて楽ですよ。
ここはね、お手本を用意して「こうやってやればラクですからマネしてください。」といってお願いすれば、みんながやってくれるようになる。と甘い期待をしています。個人でスキルある人は、自前の便利コードの断片をいっぱいもっているし、それがその人のスキルだと思う。なので、そーゆー努力をしない人にまでため込んできたナレッジを開示するのは、抵抗があるかもしれない。でもソレをしないと組織としてスキルアップにつながらない… すいません、このへんは私の自問自答です。
でも、気概も持たず、工夫もしない人にやってもらうようにするにはどうしたらいいのか? というのは、ナレッジ開示よりも難しい問題かもしれません。
memcpy()のmanページを見ていただければ分かりますが、戻り値は有りますが、エラーかどうかは分かりませんのでエラーチェックは必要有りません(というかチェックしようがないです)。
このような場合の自分の対処方法ですが、
memcpy()に限らず標準関数で戻り値以外のチェックを行いたい場合は、関数をフックして自作の関数を呼び、この自作した関数ないで引数のチェック等を行うようにしています。
これでmemcpy()で書き込み不可能なメモリを指定されるなどしても問題はありません。
あと関数の戻り値以外でも以外でもチェックするべき箇所としては、0除算とか色々有ります。
元々の質問の関数チェックが9割方無いと言う状況では他にも問題は多数有りそうなのが心配ですね。
コメントありがとうございます。
長文なので読んでもらえないのではと心配してました。
まず、お詫びとして、memcpyを例にしたのはメモリー破壊の代表であり、皆がきらう関数No1だろうという理由からでした。
id:longicornさんの仰る通りmemcpyはvoid型であり、例としては不適切だったかもしれません。
『memcpyのエラー』=『期待通りのコピーが行われない』と読み替えて頂ければと思います。
さて、私とid:Rintaさんとでお互いに単体テストの定義(フェーズ)が食い違っているのかもしれませんね。
私の考える単体テストの主なポイントは以下です。
・1ライン毎に、意図した動作をするか確認する。
・ロジック上取りうる入力値全てでの動作を確認する。
(それ以降のテストは結合/総合テストフェーズとなります)
それを前提で考えると、単体テストを行ったのにも関わらずmemcpyでエラー(=期待通りのコピーが行われない)となるようなコードは作成できないと考えています。
エラーとするには、memcpyに渡す値がおかしいことしか考えられなく、これは、memcpyを呼び出す前のコードに問題がある、つまりはテスト漏れと考えられます。
>エラー処理は要件そのものです。
呼び出す前のチェック処理もエラー処理の1つだとのお考えならid:Rintaさんの意見に大賛成です。
ただ、自前の関数の場合は、呼び出す前にチェックしても期待通りの動作をするとは限らないので呼出し後のチェックも必要と考えます。
(その時点では良くても1年後に関数が改造されて意図しない動作となる可能性が大いにあるという意味です)
また、id:longicornさんの言うように1枚被せるのも有効だとは思います。が、将来改造される危険性が増える点は注意が必要だと思います。
呼び出し後のエラー処理については、過度のエラー処理、意味の無いエラー処理かどうかの見極めが大変なので、C標準関数は信用し、自前の関数は信用しないと考えるとポリシー的にもすっきりするような気がします。
どうでしょうか?(また長文ですみません)
ですが、エラーの状況によりしないで済ませることもあります。
また、質問のような状況(90%ほど、つまりほとんどがエラー処理していない)で、エラーが発生した場合に
致命的なことが起こるようならば、エラーを返す関数側でエラー処理(ログ吐いて終わっちゃうとか)することもあります。
また、エラーのハンドリングを入れたラッパー関数を書いて、まとめてラッパー関数を使うように修正しちゃうこともあります。
今回の質問では、
>「開発途上のプログラムの品質が心配なので検査して欲しい」という依頼を受け・・・
>
と言うことなので、どういう方法かでエラー処理を入れておかないと後のにトラブルに発展しそうな予感がしますので、時間が許すならなにがしかの手段を講じておくのが吉だと思います。
具体的な開発言語の表記がないので違うかもしれませんが、
「正常に処理を完了できたか、何らかのエラーが発生したかを整数型の値で返す関数」
という内容から単純に推測すると、
何らかの処理(ファンクション)でエラーが発生したときに「そのエラー内容に応じてエラーコードを返す処理」を各ファンクション内に記入しており、それが殆どのエラーケースに対して未チェックだという事ではないですか?(VBのように)
(VBを例にとると)
通常、定義済み関数やプログラム自身が返すエラーコード自体は、当然チェックする必要はありませんが、そのエラー(コード)によって、どういう処理を行うか?つまり、
・どういうコメントを表示するか?
・処理を中止する/そのまま継続する?
・何か特別なファンクションを実行する?
は、プログラマーが仕様に基づき(重要度が低い未定義ケースは個人的裁量)で決めるので、まず、その仕様を確認するのが先決ではないでしょうか?
仕様が決まっていれば、当然それに基づきチェックを行います。ただし、100ファンクションあっても、通常は共通で使用できるロジックもあるので、全部は必要ないはずです。
あと、「未チェックのまま正常処理を続行」の状態ですが、ファンクション内にエラーが無ければ当然未チェックというか、エラー処理は行われませんが、そのことを指しているのか、エラーが生じていても引っかからないということでしょうか?
前者をチェックするなら、色々なケースを想定してテストデータを作成し、エラーを発生させたときに意図どおりにエラー処理がされるかをチェックします。(通常テスト仕様に基づく)
後者の場合は、そのファンクションでどういうエラーが生じたときが致命的(計算結果が異なるとか、動作に障害が生じるとか)かを把握した上で、最低限、その対応は行いチェックします。それ以外は、よく生じるケースがわかっていればその分を追加し。あとは余裕のある範囲内でチェックするのが通常です。
気になるのは「開発途上」ということなので、全体が完成したときに、どこがどういう影響を及ぼすかが不安です。結合テストもやるのでしょうが、最低限、各ファンクションおよびモジュール単体レベルで、動作障害のチェックとエラー発生時の適切な処理が行われているかということでしょうか。
基本的なことですが、参考になればよいのですが。
だいたいプログラム規模の2/3はエラー処理になるとおもっております。
戻り値の判定が無い場合、確かにコーディングや単体テスト等の工数は減りますが、
後工程で戻り値を見ていない関数で問題が発生した場合、問題解析が困難になります。
その場合や発生頻度の低い場合とう原因の追究に時間がかかり結局のところ
上位工程楽した分苦労し顧客からの信用度も低下してしまいます。。ちなみに実体験です。。
単体試験環境と結合試験環境が違い場合等もあるので単体でうまくいっていても
結合で問題が発生する場合がよくあります。
コーディング+単体テストの工数 < 以降のテストや運用での解析工数と
思います。