6

免責事項: 私は現在プログラミングを学んでいる素人です。プロジェクトに参加したことも、500 行を超えるものを書いたこともありません。

私の質問は次のとおりです。防御的プログラミングは、自分自身を繰り返さないという原則に違反していますか? 私の防御的プログラミングの定義が正しいと仮定すると (呼び出し関数が逆ではなく入力を検証するようにする)、それはあなたのコードに有害ではないでしょうか?

たとえば、これは悪いですか:

int foo(int bar)
{
    if (bar != /*condition*/)
    {
        //code, assert, return, etc.
    }
}

int main()
{
    int input = 10;
    foo(input); //doesn't the extra logic
    foo(input); //and potentially extra calls
    foo(input); //work against you?
}   

これと比較して:

int main()
{
    if (input == /*condition*/)
    {
        foo(input);
        foo(input);
        foo(input);
    }
}

繰り返しになりますが、素人の私には、単純な論理ステートメントがパフォーマンスに関してどの程度不利になるかわかりませんが、防御的なプログラミングはプログラムや魂にとって良くないことは確かです。

4

6 に答える 6

9

DRYの原則に違反すると、次のようになります。

int foo(int bar)
{
    if (bar != /*condition*/)
    {
        //code, assert, return, etc.
    }
}

int main()
{
    int input = 10;
    if (input == /*condition*/)
    {
       foo(input);
       foo(input);
       foo(input);
    }
}

ご覧のとおり、問題はプログラムで同じチェックを2回行うことです。したがって、条件が変更された場合は、2つの場所で変更する必要があり、そのうちの1つを忘れて、奇妙な動作を引き起こす可能性があります。DRYは、「同じコードを2回実行しない」という意味ではなく、「同じコードを2回記述しない」という意味です。

于 2009-06-07T06:38:52.553 に答える
6

それはすべて、インターフェースが提供するコントラクトに帰着します。これには、入力と出力という 2 つの異なるシナリオがあります。

入力 (つまり、基本的には関数へのパラメーターを意味します) は、原則として実装によってチェックする必要があります。

少なくとも私の意見では、出力(結果が返される)は基本的に呼び出し元によって信頼されるべきです。

これはすべて、次の質問によって緩和されます。一方の当事者が契約を破るとどうなりますか? たとえば、次のインターフェースがあるとします。

class A {
  public:
    const char *get_stuff();
}

そのコントラクトでは、null 文字列が返されないことが指定されている (最悪の場合、空文字列になる) 場合は、これを実行しても安全です。

A a = ...
char buf[1000];
strcpy(buf, a.get_stuff());

なんで?あなたが間違っていて、呼び出し先が null を返した場合、プログラムはクラッシュします。それは実際には問題ありません。何らかのオブジェクトがその契約に違反した場合、一般的に言えば、結果は壊滅的なものになるはずです。

過度に防御的であると直面するリスクは、不要なコードを大量に記述したり (バグが増える可能性があります)、実際にすべきではない例外を飲み込んで深刻な問題を実際に隠したりする可能性があることです。

もちろん、状況によってこれは変わる可能性があります。

于 2009-06-07T06:07:13.683 に答える
4

最初に、盲目的に原則に従うことは理想主義的で間違っていることを述べさせてください。達成したいこと (アプリケーションの安全性など) を達成する必要がありますが、これは通常、DRY に違反することよりもはるかに重要です。GOOD プログラミングでは、ほとんどの場合、意図的な原則違反が必要です。

例: 私は重要な段階で二重チェックを行います (例: LoginService - LoginService.Login を呼び出す前に最初に入力を検証し、次に内部でもう一度検証します)。ただし、すべてが 100% 機能することを確認した後で、外側のものを再度削除する傾向があります。 、通常は単体テストを使用します。場合によります。

ただし、二重の条件チェックでイライラすることはありません。一方、それらを完全に忘れることは、通常、数倍悪いことです:)

于 2009-06-07T06:00:25.783 に答える
3

防御的なプログラミングは、言葉の多いコードや、さらに重要なことに、エラーを処理するなど、望ましくないことを行うため、一種の悪いラップになると思います。

ほとんどの人は、プログラムがエラーに遭遇したときにプログラムが速く失敗することに同意しているようですが、ミッションクリティカルなシステムは失敗しないことが望ましく、代わりにエラー状態に直面し続けるために多大な努力をします。

もちろん、そのステートメントには問題があります。一貫性のない状態にあるときに、プログラムがミッションクリティカルであっても、どのように続行できるか。もちろん、それはできません。

あなたが望むのは、何か奇妙なことが起こっていても、プログラムが正しいことをするためにあらゆる合理的なステップを踏むことです。同時に、プログラムは、そのような奇妙な状態に遭遇するたびに、大声で文句を言う必要があります。また、回復不能なエラーが発生した場合は、通常、HLT命令の発行を回避する必要があります。正常に失敗し、システムを安全にシャットダウンするか、バックアップシステムが利用可能な場合はアクティブにします。

于 2009-06-07T07:10:54.673 に答える
1

アレックスが言ったように、それは状況に依存します。たとえば、私はほとんどの場合、ログインプロセスのすべての段階で入力を検証します。

他の場所では、それはすべて必要ありません。

ただし、あなたが与えた例では、2番目の例では、複数の入力があると想定しています。そうしないと、同じ入力に対して同じ関数を3回呼び出すのが冗長になるため、次のようになります。条件を3回書き込みます。今ではそれは冗長です。

入力を常にチェックする必要がある場合は、関数に含めるだけです。

于 2009-06-07T06:43:36.663 に答える
1

あなたの単純化された例では、はい、2番目の形式がおそらく望ましいです。

ただし、それは実際には、より大規模で、より複雑で、より現実的なプログラムには当てはまりません。

「foo」がどこでどのように使用されるかを事前に知ることはできないため、入力を検証して foo を保護する必要があります。入力が呼び出し元によって検証される場合 (たとえば、例では「main」)、「main」は検証ルールを認識して適用する必要があります。

実際のプログラミングでは、入力検証規則はかなり複雑になる場合があります。呼び出し元にすべての検証規則を知らせ、それらを適切に適用することは適切ではありません。どこかの発信者は、検証ルールを忘れたり、間違ったルールを実行したりします。したがって、繰り返し呼び出される場合でも、検証を「foo」内に配置することをお勧めします。これにより、呼び出し元から呼び出し先に負担が移ります。これにより、呼び出し元は「foo」の詳細について考える必要がなくなり、抽象的で信頼性の高いインターフェイスとして使用できるようになります。

「foo」が同じ入力で複数回呼び出されるパターンが本当にある場合は、検証を 1 回実行するラッパー関数と、検証を回避する保護されていないバージョンをお勧めします。

void RepeatFoo(int bar, int repeatCount)
{
   /* Validate bar */
   if (bar != /*condition*/)
   {
       //code, assert, return, etc.
   }

   for(int i=0; i<repeatCount; ++i)
   {
       UnprotectedFoo(bar);
   }
}

void UnprotectedFoo(int bar)
{
    /* Note: no validation */

    /* do something with bar */
}

void Foo(int bar)
{
   /* Validate bar */
   /* either do the work, or call UnprotectedFoo */
}
于 2009-06-07T06:08:33.807 に答える