web-dev-qa-db-ja.com

エラーをスローする必要があるかどうかを示すフラグがある

私は最近、かなり古い開発者(約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でハードウェアをリセットしようとするため、リセットは例外をスローします。

64
Nicolas

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

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

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

73
mmathis

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

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

一般に、クラスとそのAPIを設計するときは、

  1. 同じプログラム内で同時に異なる構成を持つクラスのインスタンスが複数存在する可能性があります。

  2. 依存関係の注入やその他のプログラミング手法により、1つの消費クライアントがオブジェクトを作成し、別のクライアントがそれらを使用して他のクライアントに渡す場合があります。そのため、多くの場合、オブジェクト作成者とオブジェクトユーザーが分離されます。

次に、渡されたインスタンスを使用するためにメソッドの呼び出し側が何をしなければならないかを考えてみましょう。計算メソッドを呼び出す場合:呼び出し元は、面積がゼロであることを確認するだけでなく、例外をキャッチする必要があります。テストの考慮事項は、クラス自体だけでなく、呼び出し元のエラー処理にも当てはまります...

消費するクライアントにとって、私たちは常に物事を可能な限り簡単にする必要があります。インスタンスメソッドのAPIを変更するコンストラクター内のこのブール構成は、使用しているクライアントプログラマー(多分あなたまたはあなたの同僚)を成功へと導くのとは逆です。

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

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


別の注記では、個人的にはブール構成パラメーター実際の呼び出し元はすべて定数を渡すだけをとるべきではないと感じています。このような構成パラメーター化は、2つの個別のユースケースを統合し、実際のメリットはありません。

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


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

あなたの失敗の状況はリモート処理に向けられているので、当てはまらないかもしれません。思考のためのほんの一部の食べ物。


続行する方法を決定するのは呼び出し側の責任ではありませんか?

はい私は同意する。エラーの状況では、呼び出し先が0のエリアが正しい答えであると判断するのは時期尚早のようです(特に0は有効なエリアなので、エラーと実際の0の違いを判別する方法はありませんが、アプリには当てはまらない場合があります)。

47
Erik Eidt

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

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

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

したがって、AreaCalculatorを使用するプログラムにバグがある場合、同僚はプログラムを「クラッシュさせる」のではなく、誤った値を返すことを望んでいます(誰もそれに気づかないように、または誰もそれで重要なことをしないように) )。これは実際にはエラーのマスキングであり、私の経験では、遅かれ早かれ、根本的な原因を見つけることが困難になるフォローアップバグが発生します。

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

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

  • 「ブラックボックス」コンポーネントを使用し、プログラムのグローバルな動作を念頭に置いている場合は、例外を予期して対処するようにチームをトレーニングします。

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

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

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

37
Doc Brown

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

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

テストが簡単なコードを書くことは素晴らしいアイデアですが、コードを呼び出す必要がある人にとって有用なコードを書くよりも、テストをシンプルにするのはひどい考えです。問題の例に例外がスローされるかどうかを決定するスイッチがある場合、その動作を必要とする発信者が関数に例外を追加した方がよい場合があります。複雑なものと複雑すぎるものとの間の境界線は、判断の呼びかけです。すべての状況に当てはまる明るい線があるとあなたに伝えようとしている人は誰もが疑いの目を向けるべきです。

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

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

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

続行する方法を決定するのは呼び出し側の責任ではありませんか?

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

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

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

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

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

13
Blrfl

安全性が重要な「通常の」コードは、「良い習慣」がどのように見えるかについて、非常に異なる考えにつながる可能性があります。オーバーラップはたくさんあります-いくつかのものは危険であり、両方で避けるべきです-しかし、まだ大きな違いがあります。応答を保証する要件を追加すると、これらの偏差はかなり大きくなります。

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

  • Gitの場合、間違った答えは次の場合に比べて非常に悪い可能性があります。時間のかかる/中止/ハングする、またはクラッシュすることさえあります(これは、たとえば、チェックインされたコードを誤って変更することに関しては実質的に問題ありません)。

    ただし、g力計算が停止していて、対気速度計算が行われないようになっているインストルメントパネルの場合、許容できない場合があります。

いくつかはそれほど明白ではありません:

  • lotをテストした場合、最初の注文の結果(正解など)は、比較的言えばそれほど大きな問題ではありません。あなたのテストがこれをカバーすることを知っています。ただし、隠された状態または制御フローがあった場合、これがはるかに微妙な原因ではないことはわかりません。これをテストで除外するのは難しいです。

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

これはあなたの例にどのように当てはまりますか?

知りません。安全性が重要なコードでは、 "No-one throw in production code"のような主なルールがあり、より通常の状況ではかなりばかげていると思われる多くの思考プロセスがあります。

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

そうは言っても、私には少し疑わしいようです[〜#〜] but [〜#〜]

安全性が重要なソフトウェアとソフトウェア設計の決定は、おそらく、ソフトウェアエンジニアリングのstackexchangeの見知らぬ人が行うべきではありません。それが悪いシステムの一部であっても、これを行う正当な理由があるかもしれません。 「思考のための食物」として以外にこれの多くに多くを読んではいけません。

8
drjpizzle

例外をスローすることは、最善の方法ではない場合があります。特にスタックの巻き戻しによるものですが、特に言語またはインターフェイスの継ぎ目で例外をキャッチすることが問題になる場合もあります。

これを処理する良い方法は、強化されたデータ型を返すことです。このデータ型には、すべてのハッピーパスとすべてのアンハッピーパスを記述するのに十分な状態があります。重要なのは、この関数(メンバー/グローバル/その他)を操作すると、結果を処理する必要があることです。

とはいえ、この強化されたデータ型はアクションを強制すべきではありません。あなたの地域の例で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
}

vs.

... 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);
7
Kain0_0

C++の観点からお答えします。すべてのコアコンセプトがC#に転送できると確信しています。

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

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

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

したがって、一部のライブラリ( <filesystem> など)は、C++が「デュアルAPI」と呼ぶもの、またはC#が Try-Parseパターン と呼ぶものを使用します(ありがとう- ピーター チップの場合!)

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が「正しい」APIであるかについてユーザーにガイダンスがなく、ユーザーは有用なエラーメッセージCalculateArea)およびspeedTryCalculateArea )より高速なバージョンは、便利な"negative side lengths"例外を受け取り、それを役に立たないfalseに平坦化するため、「問題が発生しました。何がどこであるかを尋ねないでください。」 (一部のデュアルAPIは、int errnoやC++のstd::error_codeなど、より表現力のあるエラータイプを使用しますが、それでもwhereエラーが発生しました—それだけdidどこかで発生します。)

コードの動作を決定できない場合は、いつでも呼び出し元に決定をキックすることができます。

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);

重要なパラメータをexplicit function parametersからglobal stateに移動することは、ほとんど常に悪いアイデア。お勧めしません。 (それはあなたのケースではglobal状態ではないが、単にinstance-wide加盟国はその悪さを少しは軽減しますが、多くはしません。)

さらに、同僚が、可能なエラー処理動作の数を不必要に制限しています。 anyエラー処理ラムダを許可するのではなく、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つのエラー処理コールバックのいずれかを使用するように強制することで、エンドユーザーからすべての柔軟性を取り除きました。 and共有グローバル状態のすべての問題があります。 and条件付きブランチのどこでもまだ料金を支払っています。

最後に、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);

このように機能する例として、CおよびC++の assert macro があります。これは、プリプロセッサマクロNDEBUGでの動作を条件付けます。

3
Quuxplusone

私はあなたの同僚がどこから彼らのパターンを得たかについて言及されるべきだと思います。

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

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

同僚が使用するアプローチは、新しい機能のテスト/デバッグ中に例外の初期の失敗に失敗し、デフォルトのエラー値を返すだけのランタイムの安全性とパフォーマンスを提供します(パフォーマンスは常に約30年前に重大な問題でした)。これが、ソフトウェアが記述されたパターンであり、予想されるパターンが前進するため、現在より良い方法があるとしても、このように続けるのは自然なことです。おそらくこのパターンはソフトウェアの時代から受け継がれたものか、大学が成長しなかったパターンです(古い習慣を破ることは難しい)。

他の回答はすでにこれが悪い習慣と見なされている理由をカバーしているので、TryGetパターンを読むことをお勧めします(おそらく、オブジェクトが呼び出し元に対して行うべき約束をカプセル化します)。

1
Tezra

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

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

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

ここでの緊張の一部は、このインターフェースが primitive obsession --xおよびyが長さの実際の測定値を表すと考えられるという事実に起因します。ドメイン固有のタイプが合理的な選択であるプログラミングコンテキストでは、実質的に前提条件チェックをデータのソースに近づけます。つまり、データの整合性の責任をコールスタックの上位に移動します。コンテキストに対するより良い感覚。

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

// Configurable dependencies
AreaCalculator(PreconditionFailureStrategy strategy)

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

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

国家交通安全委員会は本当に良いです。グレイビアードに代替の実装手法を提案するかもしれませんが、エラー報告サブシステムでの隔壁の設計について彼らと議論する傾向はありません。

より広義:ビジネスのコストはいくらですか?生命にかかわるシステムよりも、Webサイトをクラッシュさせる方がはるかに安価です。

0
VoiceOfUnreason

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

彼の論理/推論は、私たちのプログラムがデータをユーザーに示す1つのことを行う必要があるということです。それを妨げないその他の例外は無視してください。

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

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

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

しかし、それは起こります。多くのプロセッサがプログラム内の浮動小数点の動作を変更できることを考慮してください。基準をもっと緩和したいプログラムは、レジスター(実際にはフラグ)を変更するだけで、そうすることができます。トリックは、誤って他のつま先を踏まないように、非常に注意する必要があることです。多くの場合、コードは現在のフラグをチェックし、目的の設定に設定し、操作を実行してから、元に戻します。そうすれば、誰もその変化に驚かないでしょう。

0
Cort Ammon