エラーをスローすべきかどうかを示すフラグを持っている


64

私は最近、かなり古い開発者(50歳以上)がいる場所で働き始めました。彼らは、システムがダウンすることができなかった航空を扱う重要なアプリケーションに取り組んできました。その結果、古いプログラマーはこの方法でコーディングする傾向があります。

彼は、オブジェクトにブール値を入れて、例外をスローすべきかどうかを示す傾向があります。

public class AreaCalculator
{
    AreaCalculator(bool shouldThrowExceptions) { ... }
    CalculateArea(int x, int y)
    {
        if(x < 0 || y < 0)
        {
            if(shouldThrowExceptions) 
                throwException;
            else
                return 0;
        }
    }
}

(このプロジェクトでは、その時点では存在できないネットワークデバイスを使用しようとしているため、メソッドは失敗する可能性があります。エリアの例は、例外フラグの例にすぎません)

私にはこれはコードの匂いのようです。毎回例外フラグをテストする必要があるため、単体テストの作成は少し複雑になります。また、何か問題が発生した場合、すぐに知りたいと思いませんか?続行方法を決定するのは呼び出し側の責任ではないでしょうか?

彼の論理/推論は、プログラムがユーザーにデータを表示するという1つのことをする必要があるということです。それを妨げないその他の例外は無視する必要があります。私はそれらが無視されるべきではないことに同意しますが、バブルアップして適切な人によって処理されるべきであり、そのためのフラグを処理する必要はありません。

これは例外を処理する良い方法ですか?

編集:設計上の決定についてより多くのコンテキストを与えるために、このコンポーネントが失敗した場合でも、プログラムは引き続き動作し、メインタスクを実行できるためだと思います。したがって、例外をスローしたくない(そして、それを処理しない?)ので、ユーザーが正常に動作しているときにプログラムを停止させます

編集2:より多くのコンテキストを与えるために、この場合、メソッドはネットワークカードをリセットするために呼び出されます。ネットワークカードが切断および再接続され、異なるIPアドレスが割り当てられると問題が発生します。したがって、古いIPでハードウェアをリセットしようとするため、リセットは例外をスローします。


22
c#には、このTry-Parseパターンの規則があります。詳細:docs.microsoft.com/en-us/dotnet/standard/design-guidelines/… フラグはこのパターンと一致しません。
ピーター

18
これは基本的に制御パラメーターであり、メソッド内部の実行方法を変更します。シナリオに関係なく、これは悪いことです。martinfowler.com/bliki/FlagArgument.htmlsoftwareengineering.stackexchange.com/questions/147977/...medium.com/@amlcurran/...
BIC

1
:ピーターからTRY-解析のコメントに加えて、ここで難問の例外についての素晴らしい記事ですblogs.msdn.microsoft.com/ericlippert/2008/09/10/...
Linaith

2
「したがって、例外をスローしたくない(そして、それを処理しないのか?)ため、プログラムが正常に動作するときにプログラムを停止させる」-例外を正しくキャッチできることを知っていますか?
user253751

1
これは他のどこかですでにカバーされていると確信していますが、単純なエリアの例を考えれば、それらの負の数がどこから来て、そのエラー状態をどこかで処理できるかどうか疑問に思うでしょう(例えば、たとえば、長さと幅を含むファイルを読み取っていたもの)。ただし、「その時点では存在できないネットワークデバイスを使用しようとしています。」ポイントはまったく異なる答えに値する可能性がありますが、これはサードパーティのAPIですか、TCP / UDPなどの業界標準ですか?
jrh

回答:


74

このアプローチの問題は、例外がスローされることはなく(したがって、キャッチされない例外が原因でアプリケーションがクラッシュすることはありませんが)、返される結果が必ずしも正しいとは限らず、ユーザーがデータに問題があることを決して知らないことです(またはその問題とその修正方法)。

結果を正しく意味のあるものにするために、呼び出し元のメソッドは結果を特別な番号、つまり、メソッドの実行中に発生した問題を示すために使用される特定の戻り値をチェックする必要があります。正の量(面積など)に対して返される負(またはゼロ)の数値は、古いコードでのこの典型的な例です。ただし、呼び出し元のメソッドがこれらの特別な番号を確認することを知らない(または忘れる)場合、処理はミスを認識せずに続行できます。その後、データがユーザーに表示され、0の領域が表示されます。この領域はユーザーが間違っていると認識していますが、何がどこで、どこで、または何が間違っているのかはわかりません。次に、他の値のいずれかが間違っているのではないかと考えます...

例外がスローされた場合、処理が停止し、エラーが(理想的には)ログに記録され、ユーザーに何らかの方法で通知される場合があります。その後、ユーザーは問題を修正して再試行できます。適切な例外処理(およびテスト!)により、重要なアプリケーションがクラッシュしたり、無効な状態で終了したりすることがなくなります。


1
@Quirkチェンがたった3行または4行で単一責任原則に違反したことが印象的です。それが本当の問題です。さらに、彼が話している問題(プログラマーが各行のエラーの結果について考えていない)は、常に未チェックの例外の可能性であり、チェックされた例外の可能性あります。私はこれまでにチェック例外に対して行われたすべての議論を見てきましたが、それらのどれも有効ではありません。
TKK

@TKK個人的には、.NETでチェック例外が本当に好きだった場所に出くわした場合があります。スローされた例外としてAPIドキュメントが正確であることを確認できるハイエンドの静的分析ツールがあればいいのですが、特にネイティブリソースにアクセスする場合はほとんど不可能です。
jrh

1
@jrhはい、TypeScriptがJSに安全性を入力する方法と同様に、何かが.NETに例外の安全性を詰め込んでくれるといいと思います。
TKK

47

これは例外を処理する良い方法ですか?

いいえ、これはかなり悪い習慣だと思います。例外をスローすることと値を返すことは、APIの根本的な変更であり、メソッドのシグネチャを変更し、インターフェイスの観点からメソッドの動作をまったく異なります。

一般に、クラスとそのAPIを設計するときは、次のことを考慮する必要があります。

  1. 同じプログラム内で異なる構成が同時に浮かんでいるクラスの複数のインスタンスが存在する場合があります。

  2. 依存関係の注入や他の多くのプログラミング手法により、ある消費クライアントがオブジェクトを作成し、別のクライアントがそれらを使用して使用することがあります。

ここで、計算メソッドの呼び出しなど、渡されたインスタンスを使用するためにメソッド呼び出し元が何をしなければならないかを考えてみましょう。テストの考慮事項は、クラス自体だけでなく、呼び出し元のエラー処理にも当てはまります...

消費側のクライアントにとっては、できる限り簡単にできるようにする必要があります。インスタンスメソッドのAPIを変更するコンストラクター内のこのブール構成は、消費クライアントプログラマー(おそらくあなたまたは同僚)を成功の落とし穴に陥らせるのとは反対です。

両方のAPIを提供するには、2つの異なるクラス(エラー時に常にスローするクラスとエラー時に常に0を返すクラス)を提供するか、単一クラスで2つの異なるメソッドを提供する方がはるかに優れています。このようにして、消費クライアントはエラーをチェックして処理する方法を簡単に正確に知ることができます。

2つの異なるクラスまたは2つの異なるメソッドを使用すると、IDEでメソッドユーザーの検索や機能のリファクタリングなどを使用できます。コードの読み取り、書き込み、保守、レビュー、およびテストも同様に簡単です。


別の注意として、私は個人的に、実際の呼び出し元がすべて単にconstantを渡すブール構成パラメーターを取るべきではないと感じています。このような構成パラメーター化により、2つの別個のユースケースが制約され、実際のメリットは得られません。

コードベースを見て、コンストラクターのブール値構成パラメーターに変数(または非定数式)が使用されているかどうかを確認してください!疑わしい。


さらに考慮すべき点は、面積の計算に失敗する理由を尋ねることです。計算できない場合は、コンストラクターをスローするのが最善です。ただし、オブジェクトがさらに初期化されるまで計算が可能かどうかわからない場合は、異なるクラスを使用してそれらの状態を区別することを検討してください(面積の計算準備ができていないか、面積の計算準備ができていないか)。

あなたの失敗状況はリモーティングに向けられているので、当てはまらないかもしれません。ちょっと考えてみてください。


続行方法を決定するのは呼び出し側の責任ではないでしょうか?

はい私は同意する。呼び出し先がエラー状態で0の領域が正しい答えであると判断するのは時期尚早のようです(特に0は有効な領域であるため、アプリには適用されないかもしれませんが、エラーと実際の0の違いを見分ける方法はありません)。


メソッドを呼び出す前に引数を確認する必要があるため、実際に例外を確認する必要はありません。結果をゼロと照合しても、有効な引数0、0と不正な負の引数を区別しません。APIは本当に恐ろしい私見です。
ブラックジャック

C99およびC ++ iostreamのAnnex K MSプッシュは、フックまたはフラグが障害に対する反応を根本的に変更するAPIの例です。
デデュプリケーター

37

彼らは、システムがダウンすることができなかった航空を扱う重要なアプリケーションに取り組んできました。結果として ...

これは興味深い導入です。この設計の背後にある動機は、「システムがダウンする可能性があるため」一部のコンテキストで例外をスローしないようにすることです。しかし、システムが「例外のためにダウンする可能性がある」場合、これは明確な兆候です

  • 例外は、少なくとも厳格に適切処理されません。

そのため、を使用するプログラムにAreaCalculatorバグがある場合、同僚はプログラムを「早期にクラッシュさせる」のではなく、間違った値を返すことを望みます(誰も気付かないか、誰も重要なことをしないことを望みます)。これは実際にはエラーを隠しており、私の経験では、遅かれ早かれ、根本的な原因を見つけることが難しくなるフォローアップバグにつながります。

どんな状況でもクラッシュしないが、間違ったデータや計算結果を表示するプログラムを書く私見は、通常、プログラムをクラッシュさせるよりも良い方法ではありません。唯一の正しいアプローチは、呼び出し元にエラーに気づき、対処し、ユーザーに間違った動作について通知する必要があるかどうか、処理を続行しても安全か、または安全かを判断する機会を与えることです。プログラムを完全に停止します。したがって、次のいずれかをお勧めします。

  • 関数が例外をスローする可能性があるという事実を見落とすことを困難にします。ドキュメントとコーディング標準はここであなたの友人であり、定期的なコードレビューは、コンポーネントの適切な使用と適切な例外処理をサポートする必要があります。

  • 「ブラックボックス」コンポーネントを使用するときに例外を予期して対処するようにチームをトレーニングし、プログラムのグローバルな振る舞いを念頭に置いてください。

  • 何らかの理由で、呼び出しコード(またはそれを記述する開発者)が例外処理を適切に使用できない場合、最後の手段として、明示的なエラー出力変数を使用して、例外をまったく使用しないAPIを設計できます。

    CalculateArea(int x, int y, out ErrorCode err)

    そのため、呼び出し側が関数が失敗する可能性を見落とすのは本当に難しくなります。しかし、これはC#では非常に見苦しいです。これはCからの古い防御的なプログラミング手法であり、例外はありません。通常、そのような作業は通常必要ありません。


3
「どのような状況でもクラッシュせず、間違ったデータや計算結果を表示するプログラムを書くことは、通常、プログラムをクラッシュさせることよりも良い方法ではありません。」飛行機のコンピューターをシャットダウンするために、計器が間違った値を示していることに変わりはありません。重要度の低いすべてのアプリケーションでは、エラーをマスクしない方が間違いなく優れています。
トライラリオン

18
@Trilarion:フライトコンピューターのプログラムに適切な例外処理が含まれていない場合、コンポーネントが例外をスローしないようにすることで「これを修正する」ことは非常に誤ったアプローチです。プログラムがクラッシュした場合、引き継ぐことができる冗長なバックアップシステムが必要です。たとえば、プログラムがクラッシュせず、間違った高さを示した場合、飛行機が次の山に突入している間、パイロットは「すべてがうまくいっている」と考えるかもしれません。
ドックブラウン

7
@Trilarion:飛行コンピューターが間違った高さを示し、これが原因で飛行機がif落した場合、それは役に立ちません(特に、バックアップシステムが存在し、引き継ぐ必要があると通知されない場合)。飛行機のコンピューターのバックアップシステムは新しいアイデアではありません。「飛行機のコンピューターのバックアップシステム」はGoogleです。世界中のエンジニアは、実際の生活に不可欠なシステムに冗長システムを常に組み込んでいると確信しています。保険)。
ドックブラウン

4
この。プログラムがクラッシュする余裕がない場合、それを黙って間違った答えをする余裕はありません。正しい答えは、すべての場合に適切な例外処理を行うことです。Webサイトの場合、予期しないエラーを500に変換するグローバルハンドラーを意味します。1つの要素が失敗した場合に処理を続行する必要がある場合、ループ内にtry/ を含めるなど、より具体的な状況用のハンドラーを追加することもcatchできます
jpmc26

2
間違った結果を取得することは、常に最悪の失敗です。それは、最適化に関するルールを思い出させます。「最適化する前に正しいことをしてください。間違った答えをより速く得ることは、まだ誰にも利益をもたらさないからです。」
トビースパイト

13

毎回例外フラグをテストする必要があるため、単体テストの作成は少し複雑になります。

n個のパラメーターを持つ関数は、n-1 個のパラメーターを持つ関数よりもテストが難しくなります。それを不条理に拡張すると、引数をテストするのが最も簡単になるため、関数にはパラメーターがまったくないという議論になります。

テストしやすいコードを書くことは素晴らしいアイデアですが、それを呼び出さなければならない人々に役立つコードを書くことよりも、テストをシンプルにすることはひどい考えです。質問の例に例外がスローされるかどうかを決定するスイッチがある場合、その動作を望む番号の呼び出し元が関数にそれを追加することに値する可能性があります。複雑なものと複雑すぎるものの間の境界線は、判断を促すものです。すべての状況に当てはまる明るい線があることを伝えようとする人は、疑いを持って目を向ける必要があります。

また、何か問題が発生した場合、すぐに知りたいと思いませんか?

それはあなたの間違った定義に依存します。質問の例では、「ゼロ未満の次元が与えられ、shouldThrowExceptions真である」と間違っています。ゼロより小さい場合にディメンションが与えられてもshouldThrowExceptions、スイッチが異なる動作を引き起こすため、falseの場合は間違っていません。それは、非常に単純に、例外的な状況ではありません。

ここでの本当の問題は、スイッチが関数に何をさせるのかを説明していないため、スイッチの名前が不適切であったことです。のようなより良い名前が付けられていたらtreatInvalidDimensionsAsZero、この質問をしたでしょうか?

続行方法を決定するのは呼び出し側の責任ではないでしょうか?

呼び出し元続行方法を決定します。この場合、設定またはクリアすることで事前に行われshouldThrowExceptions、関数はその状態に従って動作します。

この例は、単一の計算を行って戻るため、病理学的に単純な例です。数字のリストの平方根の合計を計算するなど、少し複雑にする場合、例外をスローすると、呼び出し元が解決できない問題が発生する可能性があります。のリストを渡して[5, 6, -1, 8, 12]、関数がを介して例外をスローした-1場合、関数は既に中止されて合計を破棄しているため、続行するように関数に指示する方法がありません。リストが膨大なデータセットである場合、関数を呼び出す前に負の数値なしでコピーを生成することは実用的ではない可能性があるため、「単に無視する」という形式で、無効な数値の処理方法を事前に言わざるを得ませんその決定を行うために呼び出されるラムダを切り替えるか、多分提供します。

彼の論理/推論は、プログラムがユーザーにデータを表示するという1つのことをする必要があるということです。それを妨げないその他の例外は無視する必要があります。私はそれらが無視されるべきではないことに同意しますが、バブルアップして適切な人によって処理されるべきであり、そのためのフラグを処理する必要はありません。

繰り返しになりますが、万能のソリューションはありません。この例では、関数はおそらく、負の次元を処理する方法を示す仕様に書かれています。最後にしたいことは、ログに「通常は例外がスローされますが、呼び出し側は気にしないように」というメッセージをログに書き込むことにより、ログの信号対雑音比を下げることです。

そして、それらのずっと古いプログラマーの一人として、私はあなたが親切に私の芝生から出発するようお願いします。;-)


私が命名し、意図が非常に重要であり、この場合には、適切なパラメータ名が実際にテーブルを回すことができることに同意し、その1が、1 our program needs to do 1 thing, show data to user. Any other exception that doesn't stop us from doing so should be ignored、実際にプログラムを行う必要があるので、考え方は間違ったデータ(に基づいて利用者の意思決定につながる可能性1つのこと-ユーザーが情報に基づいた意思決定を行うのを支援bool ExecuteJob(bool throwOnError = false)するため)
ユージンポッドスカル

@EugenePodskal「データを表示する」は「正しいデータを表示する」という意味だと思います。質問者は、完成した製品が機能しないとは言っておらず、「間違っている」と書かれているかもしれないというだけです。2番目のポイントでいくつかのハードデータを確認する必要があります。私の現在のプロジェクトには、スロー/ノースローのスイッチがあり、他のどの関数よりも推論するのが難しくない少数の頻繁に使用される関数がありますが、それは1つのデータポイントです。
Blrfl

良い答えです。このロジックは、OPだけでなく、はるかに広範な状況に適用されると思います。ちなみに、cppの新しいバージョンには、まさにこれらの理由でスロー/ノースローバージョンがあります。私はいくつかの小さな違いが、...がいることを知っている
drjpizzle

8

安全性が重視される「通常の」コードは、「グッドプラクティス」がどのように見えるかという非常に異なるアイデアにつながる可能性があります。多くの重複があります-いくつかのものは危険であり、両方で避けるべきです-しかし、まだ大きな違いがあります。応答性を保証する要件を追加すると、これらの偏差はかなり大きくなります。

これらは多くの場合、あなたが期待するものに関連しています:

  • gitの場合、間違った答えは次の場合に比べて非常に悪い可能性があります:ロングテイク/アボート/ハングまたはクラッシュ(これは事実上、たとえばチェックインコードを誤って変更することに関連する問題ではありません)。

    ただし、g-force計算が停止し、対気速度計算が行われないようにするインストルメントパネルでは、受け入れられない場合があります。

一部はそれほど明白ではありません:

  • あなたがテストしている場合は多くのことを、(正しい答えのような)一次の結果は心配が比較的話していないとして大きいです。あなたのテストがこれをカバーすることを知っています。ただし、隠された状態または制御フローがあった場合、これがさらに微妙な原因にならないことはわかりません。これをテストで除外するのは困難です。

  • 明らかに安全であることは比較的重要です。購入しているソースが安全かどうかを理由に座っている顧客は多くありません。あなたが他方で航空市場にいるなら...

これはあなたの例にどのように適用されますか:

知りません。安全性が重要なコードで採用される「ノープロダクションコードをスローしない」などのリードルールを持つ可能性のある多くの思考プロセスがあり、通常の状況ではかなりばかげています。

いくつかは組み込みに関連し、いくつかは安全性、そして多分他のものです...いくつかは良い(厳しいパフォーマンス/メモリの限界が必要でした)いくつかは悪いです(私たちは例外を適切に処理しないので、リスクを冒さないでください)。ほとんどの場合、彼らがそれをした理由を知っていても、実際には質問に答えることはありません。たとえば、コードをより簡単に監査することと実際にコードを改善することとの関係がある場合、それは良い習慣ですか?本当にわかりません。彼らは異なる動物であり、異なる治療が必要です。

言ったことのすべてが、それは私には少しの容疑者を探しますBUT

安全性が重要なソフトウェアおよびソフトウェア設計の決定は、おそらくソフトウェアエンジニアリングのスタック交換に関する見知らぬ人が行うべきではありません。たとえそれが悪いシステムの一部であっても、これを行う正当な理由があるかもしれません。「思考の糧」として以外に、これのいずれにもあまり読まないでください。


7

例外をスローすることが最良の方法ではない場合があります。特にスタックの巻き戻しが原因ではありませんが、特に言語やインターフェースの継ぎ目に沿って、例外をキャッチすることが問題になることがあります。

これを処理する良い方法は、強化されたデータ型を返すことです。このデータ型には、すべての幸せなパスとすべての不幸なパスを記述するのに十分な状態があります。ポイントは、この関数(member / global / otherwise)を操作すると、結果を処理することを余儀なくされることです。

つまり、この強化されたデータ型は、アクションを強制するものではありません。あなたの地域の例で何かを想像してくださいvar area_calc = new AreaCalculator(); var volume = area_calc.CalculateArea(x, y) * z;。役に立つようにvolume深さで乗算された領域を含める必要があります-それは立方体、円柱などである可能性があります...

しかし、area_calcサービスがダウンした場合はどうなりますか?次にarea_calc .CalculateArea(x, y)、エラーを含むリッチデータ型を返しました。それを掛けることは合法zですか?いい質問です。ユーザーにチェックをすぐに処理させることができます。ただし、これはエラー処理でロジックを分割します。

var area_calc = new AreaCalculator();
var area_result = area_calc.CalculateArea(x, y);
if (area_result.bad())
{
    //handle unhappy path
}
var volume = area_result.value() * z;

var area_calc = new AreaCalculator();
var volume = area_calc.CalculateArea(x, y) * z;
if (volume.bad())
{
    //handle unhappy path
}

本質的なロジックは2行に分散され、最初のケースではエラー処理によって分割されますが、2番目のケースでは1行に関連するすべてのロジックがあり、その後にエラー処理が続きます。

2番目のケースでvolumeは、リッチデータ型です。その数だけではありません。これにより、ストレージが大きくvolumeなりますが、エラー状態については引き続き調査する必要があります。さらにvolume、ユーザーがエラーを処理することを選択する前に他の計算をフィードして、複数の異なる場所でエラーを明示することもできます。これは、状況の詳細に応じて、良い場合も悪い場合もあります。

あるいはvolume、単なるデータ型、つまり単なる数字でもかまいませんが、エラー状態はどうなりますか?幸せな状態にある場合、値は暗黙的に変換される可能性があります。不幸な状態にある場合、デフォルト/エラー値を返す可能性があります(エリア0または-1の場合は妥当と思われるかもしれません)。あるいは、インターフェース/言語境界のこちら側で例外をスローする可能性があります。

... foo() {
   var area_calc = new AreaCalculator();
   return area_calc.CalculateArea(x, y) * z;
}
var volume = foo();
if (volume <= 0)
{
    //handle error
}

... foo() {
   var area_calc = new AreaCalculator();
   return area_calc.CalculateArea(x, y) * z;
}

try { var volume = foo(); }
catch(...)
{
    //handle error
}

悪い値、またはおそらく悪い値を渡すことにより、データを検証するためにユーザーに多くの責任を負わせます。コンパイラーに関する限り、戻り値は正当な整数であるため、これはバグの原因です。何かがチェックされなかった場合、物事がうまくいかないときにそれを発見するでしょう。2番目のケースでは、例外が不幸なパスを処理できるようにすることで両方の長所を組み合わせていますが、幸福なパスは通常の処理に従います。残念ながら、例外を賢く処理するようユーザーに強制しますが、これは困難です。

不幸なパスを明確にすることは、ビジネスロジック(例外のドメイン)に不明なケースであり、検証に失敗すると、ビジネスルール(ルールのドメイン)でそれを処理する方法を知っているため、幸せなパスです。

最終的な解決策は、すべてのシナリオを(理由の範囲内で)許可するものです。

  • ユーザーは、悪い状態を照会し、すぐに処理できる必要があります
  • ユーザーは、ハッピーパスをたどってエラーの詳細を伝播するかのように、強化されたタイプを操作できる必要があります。
  • ユーザーは、キャスト(適切な場合は暗黙的/明示的)によってハッピーパス値を抽出し、不幸なパスに対して例外を生成できる必要があります。
  • ユーザーは、ハッピーパス値を抽出できるか、デフォルト(提供されているかどうか)を使用できる必要があります。

何かのようなもの:

Rich::value_type value_or_default(Rich&, Rich::value_type default_value = ...);
bool bad(Rich&);
...unhappy path report... bad_state(Rich&);
Rich& assert_not_bad(Rich&);
class Rich
{
public:
   typedef ... value_type;

   operator value_type() { assert_not_bad(*this); return ...value...; }
   operator X(...) { if (bad(*this)) return ...propagate badness to new value...; /*operate and generate new value*/; }
}

//check
if (bad(x))
{
    var report = bad_state(x);
    //handle error
}

//rethrow
assert_not_bad(x);
var result = (assert_not_bad(x) + 23) / 45;

//propogate
var y = x * 23;

//implicit throw
Rich::value_type val = x;
var val = ((Rich::value_type)x) + 34;
var val2 = static_cast<Rich::value_type>(x) % 3;

//default value
var defaulted = value_or_default(x);
var defaulted_to = value_or_default(x, 55);

@TobySpeight結構、これらのことはコンテキスト依存であり、範囲があります。
Kain0_0

ここでの問題は「assert_not_bad」ブロックだと思います。これらは、元のコードが解決しようとした場所と同じ場所になると思います。テストではこれらに注意する必要がありますが、実際に主張する場合は、実際の航空機で生産する前にそれらを削除する必要があります。そうでなければ、いくつかの素晴らしい点。
drjpizzle

@drjpizzleテスト用のガードを追加することが重要であれば、実稼働環境でガードを配置したままにしておくことが重要だと思います。警備員の存在自体は疑念を意味します。テスト中にコードを保護するのに十分なコードを疑う場合、技術的な理由で疑います。すなわち、条件は現実的に発生する可能性があります。テストを実行しても、本番環境でこの条件に決して到達しないことは証明されません。つまり、発生する可能性のある既知の条件があり、それをどこかで処理する必要があるということです。それがどのように処理されるかが問題だと思います。
Kain0_0

3

C ++の観点から答えます。すべてのコアコンセプトがC#に移行可能であると確信しています。

あなたの好みのスタイルは「常に例外を投げる」ようです:

int CalculateArea(int x, int y) {
    if (x < 0 || y < 0) {
        throw Exception("negative side lengths");
    }
    return x * y;
}

例外処理は重いため、これはC ++コードの問題になる可能性があります。これにより、エラーケースの実行が遅くなり、エラーケースにメモリが割り当てられ(場合によっては利用できないこともあります)、一般的に予測が難しくなります。EHのヘビーウェイトは、「制御フローに例外を使用しないでください」などと言っている人がいる理由の1つです。

そのため、一部のライブラリ(など<filesystem>)は、C ++が「デュアルAPI」と呼ぶもの、またはC#がTry-Parseパターンと呼ぶものを使用します(Peterにヒントをありがとう!)

int CalculateArea(int x, int y) {
    if (x < 0 || y < 0) {
        throw Exception("negative side lengths");
    }
    return x * y;
}

bool TryCalculateArea(int x, int y, int& result) {
    if (x < 0 || y < 0) {
        return false;
    }
    result = x * y;
    return true;
}

int a1 = CalculateArea(x, y);
int a2;
if (TryCalculateArea(x, y, a2)) {
    // use a2
}

「デュアルAPI」の問題をすぐに見ることができます。多くのコードの複製、どのAPIを使用するのが「正しい」かに関するユーザーへのガイダンスはなく、ユーザーは有用なエラーメッセージCalculateArea)とスピードTryCalculateArea)は、より高速なバージョンが有用な"negative side lengths"例外を取得し、それを役に立たないfalseものにフラット化するためです。(一部のデュアルAPIには、次のような、より表現のエラータイプを使用するint errnoC ++のかstd::error_code、それはまだあなたを教えてくれないところエラーが発生した-それだけということでしたどこかで発生します。)

コードの振る舞いを決定できない場合は、いつでも呼び出し元に決定を委ねることができます!

template<class F>
int CalculateArea(int x, int y, F errorCallback) {
    if (x < 0 || y < 0) {
        return errorCallback(x, y, "negative side lengths");
    }
    return x * y;
}

int a1 = CalculateArea(x, y, [](auto...) { return 0; });
int a2 = CalculateArea(x, y, [](int, int, auto msg) { throw Exception(msg); });
int a3 = CalculateArea(x, y, [](int, int, auto) { return x * y; });

これは基本的に同僚が行っていることです。ただし、彼は「エラーハンドラ」をグローバル変数に分解しています。

std::function<int(const char *)> g_errorCallback;

int CalculateArea(int x, int y) {
    if (x < 0 || y < 0) {
        return g_errorCallback("negative side lengths");
    }
    return x * y;
}

g_errorCallback = [](auto) { return 0; };
int a1 = CalculateArea(x, y);
g_errorCallback = [](const char *msg) { throw Exception(msg); };
int a2 = CalculateArea(x, y);

重要なパラメーターを明示的な関数パラメーターからグローバル状態に移行することは、ほとんど常に悪い考えです。お勧めしません。(あなたのケースではグローバルな状態ではなく、単にインスタンス全体のメンバー状態であるという事実は、悪さを少しだけ軽減しますが、それほど多くはありません。)

さらに、同僚は、考えられるエラー処理動作の数を不必要に制限しています。エラー処理ラムダを許可するのではなく、彼は2つだけを決定しました。

bool g_errorViaException;

int CalculateArea(int x, int y) {
    if (x < 0 || y < 0) {
        return g_errorViaException ? throw Exception("negative side lengths") : 0;
    }
    return x * y;
}

g_errorViaException = false;
int a1 = CalculateArea(x, y);
g_errorViaException = true;
int a2 = CalculateArea(x, y);

これはおそらく、これらの可能な戦略のうちの「サワースポット」です。厳密に2つのエラー処理コールバックのいずれかを使用することをエンドユーザーに強制することにより、エンドユーザーから柔軟性をすべて取り去りました。そして、あなたは共有グローバル状態のすべての問題を抱えています。そして、あなたはまだどこでもその条件分岐のために支払っています。

最後に、C ++(または条件付きコンパイルを使用する任意の言語)の一般的なソリューションは、コンパイル時にユーザーにプログラム全体をグローバルに決定させ、未使用のコードパスを完全に最適化することです。

int CalculateArea(int x, int y) {
    if (x < 0 || y < 0) {
#ifdef NEXCEPTIONS
        return 0;
#else
        throw Exception("negative side lengths");
#endif
    }
    return x * y;
}

// Now these two function calls *must* have the same behavior,
// which is a nice property for a program to have.
// Improves understandability.
//
int a1 = CalculateArea(x, y);
int a2 = CalculateArea(x, y);

このように動作し、何かの例があるassertマクロた条件プリプロセッサマクロにその動作をCやC ++で、NDEBUG


代わりにstd::optionalfrom TryCalculateArea()を返す場合、compile-time-flagを使用して単一のfunction-templateにデュアルインターフェイスの両方の部分の実装を統合するのは簡単です。
デデュプリケーター

@Deduplicator:たぶんstd::expected。ただでstd::optional私はあなたの提案されたソリューションを誤解しない限り、それはまだ私が言ったことに苦しむでしょう:ユーザーが便利なエラーメッセージと速度のハードな選択をしなければならない、より高速なバージョンは、私たちの便利かかるため"negative side lengths"、例外をし、無用にそれをダウン平らにfalse- 」何かがうまくいかなかったので、何をどこで聞かないでください。」
Quuxplusone

それが、libc ++ <filesystem>が実際にOPの同僚のパターンに非常に近いことをする理由です。つまりstd::error_code *ec、APIのすべてのレベルをパイプダウンし、下部でに相当する道徳的なことを行いif (ec == nullptr) throw something; else *ec = some error codeます。(実際のifを、というものに抽象化しますがErrorHandler、基本的な考え方は同じです。)
Quuxplusone

まあ、それはスローせずに拡張されたエラー情報を保持するオプションになります。適切であるか、潜在的な追加コストに見合わない場合があります。
デデュプリケーター

1
この答えに含まれている非常に多くの良い考え...間違いなくより多くの賛成票が必要です:-)
cmaster

1

あなたの同僚が彼らのパターンをどこから得たのかを言及すべきだと思います。

現在、C#にはTryGetパターンがありpublic bool TryThing(out result)ます。これにより、結果を取得することができますが、その結果が有効な値であるかどうかを確認できます。(たとえば、すべてのint値はの有効な結果ですMath.sum(int, int)が、値がオーバーフローすると、この特定の結果はゴミになる可能性があります)。ただし、これは比較的新しいパターンです。

outキーワードの前に、値を表すために各結果に対して例外をスローする必要があり(高価で、呼び出し側がそれをキャッチするか、プログラム全体を強制終了する必要があります)、特別な構造体(クラスまたはジェネリックよりも前のクラス)を作成する必要がありましたエラーの可能性(ソフトウェアの作成と膨張に時間がかかる)、またはデフォルトの「エラー」値(エラーではなかった可能性があります)を返します。

同僚が使用しているアプローチにより、新しい機能をテスト/デバッグする際に例外の早期特典が得られ、同時にデフォルトのエラー値を返すだけの実行時の安全性とパフォーマンス(〜30年前は常にパフォーマンスが重要な問題でした)が与えられます。これはソフトウェアが記述されたパターンであり、予想されるパターンは前進しているため、より良い方法はありますが、このように続けるのは自然です。ほとんどの場合、このパターンはソフトウェアの時代から継承されたものであるか、大学が成長したことのないパターンでした(古い習慣を破るのは困難です)。

他の答えは、これがなぜ悪い習慣であると考えられるのかをすでにカバーしているので、TryGetパターンを読むことをお勧めします(オブジェクトが呼び出し元に対して何を約束すべきかについてもカプセル化するかもしれません)。


outキーワードの前boolに、結果へのポインター、つまりrefパラメーターを取る関数を作成します。1998年のVB6でそれを行うことができました。outキーワードは、関数が戻るときにパラメーターが割り当てられるというコンパイル時の確実性を単に買います。それだけです。ただし、これ便利で便利なパターンです。
マチューギンドン

@MathieuGuindon Yeay、しかしGetTryはまだよく知られた/確立されたパターンではありませんでした、そして、たとえそうであったとしても、それが使用されたかどうかは完全にはわかりません。結局のところ、Y2Kまでの道のりの一部は、0〜99より大きいものを保存することは受け入れられないということでした。
テズラ

0

あなたが彼のアプローチをしたい時がありますが、私はそれらを「通常の」状況だとは思わないでしょう。どのケースにいるかを判断するための鍵は次のとおりです。

彼の論理/推論は、プログラムがユーザーにデータを表示するという1つのことをする必要があるということです。それを妨げないその他の例外は無視する必要があります。

要件を確認してください。実際に、ユーザーにデータを表示するという1つの仕事があるとあなたの要件が言っている場合、彼は正しいです。ただし、私の経験では、ほとんどの場合、ユーザーは表示されるデータ気にします。彼らは正しいデータを求めています。一部のシステムは静かに失敗し、ユーザーに何かがうまくいかなかったことを理解させたいだけなのですが、私はそれらをルールの例外と考えています。

失敗した後に尋ねる重要な質問は、「システムはユーザーの期待とソフトウェアの不変条件が有効な状態にあるのか?」です。もしそうなら、どうしても必ず戻ってきてください。実際には、これはほとんどのプログラムで起こることではありません。

フラグ自体に関しては、関数がどのように動作するかを理解するためにユーザーが何らかの方法でモジュールがどのモードにあるかを知る必要があるため、例外フラグは通常コード臭と見なされます。それはになら!shouldThrowExceptionsモードは、ユーザがいることを知っている必要があり、彼らがエラーを検出し、それらが発生したときに期待と不変量を維持する責任があります。それらはまた、関数が呼び出される行で、その場で責任を負います。通常、このようなフラグは非常に紛らわしいです。

しかし、それは起こります。多くのプロセッサでは、プログラム内の浮動小数点の動作を変更できることを考慮してください。より緩和された標準にしたいプログラムは、単にレジスタを変更するだけで実現できます(これは事実上フラグです)。秘Theは、他のつま先を誤って踏まないように、非常に慎重でなければならないということです。多くの場合、コードは現在のフラグを確認し、目的の設定に設定し、操作を実行してから、元に戻します。そうすれば、誰も変更に驚かないでしょう。


0

この特定の例には、ルールに影響を与える可能性がある興味深い機能があります...

CalculateArea(int x, int y)
{
    if(x < 0 || y < 0)
    {
        if(shouldThrowExceptions) 
            throwException;
        else
            return 0;
    }
}

ここにあるのは前提条件の確認です。前提条件チェックの失敗は、呼び出しスタックの上位のバグを意味します。したがって、このコードは他の場所にあるバグを報告する責任がありますか?

ここでの緊張の中には、このインターフェイスが示すことに起因している原始的な強迫観念を - xy、おそらく長さの実測値を表すことになっています。ドメイン固有のタイプが合理的な選択であるプログラミングコンテキストでは、実際には前提条件チェックをデータのソースの近くに移動します。言い換えると、コンテキストのより良い感覚。

そうは言っても、失敗したチェックを管理するための2つの異なる戦略を持つことで根本的に悪いことは何もありません。私の好みは、構成を使用して、どの戦略が使用されているかを判断することです。機能フラグは、ライブラリメソッドの実装ではなく、コンポジションのルートで使用されます。

// Configurable dependencies
AreaCalculator(PreconditionFailureStrategy strategy)

CalculateArea(int x, int y)
{
    if (x < 0 || y < 0) {
        return this.strategy.fail(0);
    }
    // ...
}

彼らは、システムがダウンすることができなかった航空を扱う重要なアプリケーションに取り組んできました。

全国交通安全委員会は本当に良いです。私は灰色ひげに代わる実装技術を提案するかもしれませんが、エラー報告サブシステムで隔壁を設計することについて彼らと議論するつもりはありません。

より広範:ビジネスのコストはいくらですか?Webサイトをクラッシュさせる方が、ライフクリティカルなシステムよりもはるかに安価です。


私は、まだ近代化しながら望ましい柔軟性を保持する代替方法の提案が好きです。
drjpizzle

-1

メソッドは例外を処理するか、処理しません。C#のような言語ではフラグは不要です。

public int Method1()
{
  ...code

 return 0;
}

...コードで何かがおかしくなったら、その例外を呼び出し側で処理する必要があります。誰もエラーを処理しない場合、プログラムは終了します。

public int Method1()
{
try {  
...code
}
catch {}
 ...Handle error 
}
return 0;
}

この場合、... codeで問題が発生した場合、Method1が問題を処理しており、プログラムを続行する必要があります。

例外を処理する場所はあなた次第です。キャッチして何もしないことで、確かにそれらを無視することができます。ただし、発生が予想される特定の種類の例外のみを無視するようにします。exception exメモリ不足などのシステム例外のような無視したくない例外があるため、無視()は危険です。


3
OPが投稿した現在の設定は、例外を喜んでスローするかどうかの決定に関係しています。OPのコードは、メモリ不足例外などの望ましくない嚥下を引き起こしません。どちらかといえば、例外がシステムをクラッシュさせるという主張は、コードベース例外をキャッチしないため、例外を飲み込まないことを意味します。OPのビジネスロジックによって意図的にスローされたものとスローされなかったものの両方。
フラット

-1

このアプローチは、「早く失敗し、激しく失敗する」という哲学を打ち破ります。

なぜ早く失敗したいのか:

  • 障害が早く発生するほど、障害の実際の原因が障害の目に見える症状に近くなります。これによりデバッグがはるかに簡単になります。最良の場合、スタックトレースの最初の行にエラー行があります。
  • 失敗が速く(エラーを適切にキャッチ)するほど、プログラムの残りの部分を混乱させる可能性は低くなります。
  • 失敗が難しくなる(つまり、 "-1"コードなどを返す代わりに例外をスローする)ほど、呼び出し側が実際にエラーを気にし、間違った値を処理し続ける可能性が低くなります。

高速かつハードに失敗しないことの欠点:

  • 目に見える障害を回避する場合、つまりすべてが正常であるふりをする場合、実際のエラーを見つけるのを非常に難しくする傾向があります。あなたの例の戻り値は、100個のエリアの合計を計算するルーチンの一部であると想像してください。つまり、その関数を100回呼び出し、戻り値を合計します。エラーを静かに抑制すると、実際のエラーが発生した場所を見つける方法まったくありません。そして、以降の計算はすべて暗黙のうちに間違っています。
  • (エリアに「-1」のような不可能な戻り値を返すことによって)失敗を遅らせると、関数の呼び出し元がそれを気にせず、エラーの処理を忘れる可能性が高くなります。障害に関する情報は手元にありますが。

最後に、実際の例外ベースのエラー処理には、「アウトオブバンド」エラーメッセージまたはオブジェクトを指定できるという利点があり、ドメインコードに1行追加することなく、エラーのロギングやアラートなどを簡単にフックできます。 。

そのため、単純な技術的理由だけでなく、迅速に失敗することが非常に役立つ「システム」理由もあります。

結局のところ、例外処理が軽量で非常に安定している現在の時代に激しく失敗することはありません。例外を抑制するのが良いという考えがどこから来ているのかを完全に理解していますが、それはもはや当てはまりません。

特に、あなたも例外をスローするか否かについてのオプションを提供し、あなたの特定のケースで:これは、呼び出し側が決めなければならないことを意味するとにかく。したがって、呼び出し元に例外をキャッチさせ、適切に処理させることには、まったく欠点はありません。

コメントの中で1つのポイントが来る:

他の回答では、実行しているハードウェアが飛行機の場合、重要なアプリケーションで高速かつハードに失敗することは望ましくないことが指摘されています。

高速かつハードに失敗しても、アプリケーション全体がクラッシュするわけではありません。エラーが発生した時点で、ローカルで障害が発生していることを意味します。OPの例では、一部の面積を計算する低レベルのメソッドは、エラーを間違った値で静かに置き換えるべきではありません。明確に失敗するはずです。

チェーンの呼び出し元の一部は、明らかにそのエラー/例外をキャッチし、適切に処理する必要があります。この方法が飛行機で使用された場合、おそらく何らかのエラーLEDが点灯するか、少なくとも間違ったエリアではなく「エラー計算エリア」を表示するはずです。


3
他の回答では、実行しているハードウェアが飛行機の場合、重要なアプリケーションで高速かつハードに失敗することは望ましくないことが指摘されています。
HAEM

1
@HAEM、それは失敗することが速くて難しいことの意味に関する誤解です。これに関する段落を回答に追加しました。
AnoE

意図が「それほど難しい」失敗ではない場合でも、その種の火で遊ぶのは危険であると見られています。
drjpizzle

@drjpizzleという点です。あなたがハードフェイルファーストに失敗することに慣れているなら、それは私の経験では「危険」でも「その種の火で遊ぶ」ことでもありません。反対。「ここで例外が発生した場合に何が起こるか」について考えることに慣れることを意味します。ここで、「ここ」とはどこでもという意味です。その場合は何でも飛行機のcrash落事故)。...火と遊ぶ疑いで、ほとんどはOKだろうすべてのもの、およびすべてのコンポーネントを期待するだろう、すべてが正常であることをふり
AnoE
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.