7

この質問はおそらく言語に依存しませんが、指定された言語に焦点を当てます。

いくつかのレガシーコードを操作しているときに、関数の例をよく見ましたが、(私の考えでは、明らかに)関数内で多くの作業を行っています。私は5000のLoCモンスターについて話しているのではなく、それらの中に前提条件チェックを実装する関数について話している。

ここに小さな例があります:

void WorriedFunction(...) {
   // Of course, this is a bit exaggerated, but I guess this helps
   // to understand the idea.
   if (argument1 != null) return;
   if (argument2 + argument3 < 0) return;
   if (stateManager.currentlyDrawing()) return;

   // Actual function implementation starts here.

   // do_what_the_function_is_used_for
}

さて、この種の関数が呼び出されると、呼び出し元は満たされる必要のあるすべての前提条件について心配する必要がなく、簡単に次のように言うことができます。

// Call the function.
WorriedFunction(...);

さて、次の問題にどのように対処する必要がありますか?

同様に、一般的に言えば、この関数は要求されたものだけを実行し、「前提条件チェック」を呼び出し側に移動する必要があります。

if (argument1 != null && argument2 + argument3 < 0 && ...) {
   // Now all the checks inside can be removed.
   NotWorriedFunction();
}

または-前提条件の不一致ごとに例外をスローする必要がありますか?

if (argument1 != null) throw NullArgumentException;

この問題を一般化できるかどうかはわかりませんが、それでも、これについてのあなたの考えをここに示したいと思います。おそらく、私が再考できることがあるでしょう。

別の解決策がある場合は、遠慮なく教えてください:)

ありがとうございました。

4

5 に答える 5

6

すべての関数/メソッド/コード ブロックには、動作するように設計された正確な状況である事前条件と、関数が戻ったときの世界の状態である事後条件が必要です。これらは、仲間のプログラマーがあなたの意図を理解するのに役立ちます。

定義上、事前条件が false の場合、コードは機能しないと想定され、事後条件が false の場合はバグがあると見なされます。

これらを頭の中に書き留めるか、設計ドキュメントの紙に書き留めるか、コメントに書き留めるか、実際のコード チェックに書き留めるかは、一種の好みの問題です。

しかし、事前条件と事後条件を明示的なチェックとしてコーディングすると、実際的な問題、長期的な生活が楽になります。このようなチェックをコーディングする場合、そのコードは偽の事前条件で動作するとは想定されていないか、偽の事後条件でバグがあるため、事前条件と事後条件のチェックにより、プログラムが簡単にエラーを報告するようにする必要があります。障害点を発見します。あなたの例が示すように、コードがすべきでないことは、何もせずに単に「戻る」ことです。これは、何らかの形で正しく実行されたことを意味します。(もちろん、何もせずに終了するようにコードを定義することもできますが、その場合は、事前条件と事後条件にこれを反映させる必要があります。)

そのようなチェックを if ステートメントで書くことができることは明らかです (あなたの例は危険なほど近くにあります):

if (!precondition) die("Precondition failure in WorriedFunction"); // die never comes back

しかし、前条件または事後条件の存在は、 assertionと呼ばれる言語の特別な関数/マクロ/ステートメント... を定義することによってコードで示されることが多く、そのような特別な構造は通常、アサーションが実行されたときにプログラムの中止とバックトレースを引き起こします。は偽です。

コードの記述方法は次のとおりです。

void WorriedFunction(...)  
 {    assert(argument1 != null); // fail/abort if false [I think your example had the test backwards]
      assert(argument2 + argument3 >= 0);
      assert(!stateManager.currentlyDrawing());
      /* body of function goes here */ 
 }  

洗練された関数は、何らかの条件が失敗したことを呼び出し元に喜んで伝える場合があります。それが例外の本当の目的です。例外が存在する場合、技術的に事後条件は、「条件 xyz の下で関数が例外で終了する可能性がある」という趣旨で何かを言う必要があります。

于 2011-03-15T22:56:43.663 に答える
3

それは興味深い質問です。「契約による設計」の概念を確認すると、役立つ場合があります。

于 2011-03-15T22:33:41.943 に答える
3

場合によります。

ケース1、3、ケース2の間で答えを分けたいと思います。

ケース1、3

引数の問題から安全に回復できる場合は、例外をスローしないでください。良い例はTryParseメソッドです - 入力が間違ってフォーマットされている場合、メソッドは単純に を返しfalseます。もう 1 つの例 (例外) は、すべての LINQ メソッドです。 sourceis nullまたは必須のFunc<>areの場合にスローされnullます。ただし、カスタムのIEqualityComparer<T>orIComparer<T>を受け入れる場合はスローせずEqualityComparer<T>.Default、単にorによるデフォルトの実装を使用しますComparer<T>.Default。それはすべて、引数の使用状況と、引数から安全に回復できるかどうかに依存します。

ケース 2

コードがインフラストラクチャのような環境にある場合にのみ、この方法を使用します。最近、私は LINQ スタックの再実装を開始しましたが、いくつかのインターフェイスを実装する必要があります。これらの実装は、自分のクラスやメソッドの外部で使用されることはないため、それらの内部で仮定を行うことができます。外部は常にインターフェイスを介してそれらにアクセスし、それらを独自に作成することはできません。

API メソッドに対してその仮定を行うと、コードは間違った入力に対してあらゆる種類の例外をスローし、ユーザーはメソッドの内部がわからないため、何が起こっているのかわかりません。

于 2011-03-15T22:37:52.367 に答える
2

「それとも、前提条件の不一致ごとに単純に例外をスローする必要がありますか?」

はい。

于 2011-03-15T22:33:33.677 に答える
1

関数を呼び出す前にチェックを行う必要があります。関数を所有している場合は、渡された引数が期待どおりでない場合に例外をスローするようにする必要があります。

呼び出し元のコードでは、これらの例外を処理する必要があります。もちろん、渡された引数は、呼び出しの前に検証する必要があります。

于 2011-03-15T22:37:17.343 に答える