関数の入力パラメーターを検証するのに最適な場所はどこですか:呼び出し元または関数自体ですか?
コーディングスタイルを改善したいので、この問題のベストプラクティスまたはいくつかのルールを見つけようとします。いつ、何が良いか。
以前のプロジェクトでは、関数内のすべての入力パラメーターをチェックして処理していました(たとえば、それがnullでない場合)。ここで、いくつかの回答とPragmatic Programmerの本でここを読みました。入力パラメーターの検証は呼び出し元の責任であるということです。
つまり、関数を呼び出す前に入力パラメーターを検証する必要があります。関数が呼び出されるすべての場所。そして、それは一つの質問を提起します:それは、関数が呼ばれるあらゆる場所で条件をチェックすることの重複を作成しませんか?
私はnull条件だけではなく、入力変数の検証(sqrt
関数の負の値、ゼロで除算、状態と郵便番号の誤った組み合わせ、またはその他)に関心があります。
入力条件をどこで確認するかを決めるルールはありますか?
私はいくつかの議論について考えています:
sqrt()
関数-場合によっては複素数を処理したい場合があるため、呼び出し側で条件を扱います)この質問が他の質問と重複していないことを願っています。この問題を検索しましたが、同様の質問が見つかりましたが、彼らはこのケースについて正確に言及していません。
状況によって異なります。検証をどこに置くかは、説明と契約の強さに基づいて決定する必要がありますメソッドによって暗黙(または文書化)。検証は、特定の契約の遵守を強化するための良い方法です。なんらかの理由でメソッドが非常に厳密なコントラクトを持っている場合、はい、呼び出し前に確認するのはあなた次第です。
これは、パブリックメソッドを作成する場合に特に重要な概念です。 それはあなたが言うことをする方が良いです!
例として次のメソッドを考えてみます。
public void DeletePerson(Person p)
{
_database.Delete(p);
}
DeletePerson
が暗示する契約とは何ですか?プログラマは、Person
が渡された場合、それが削除されると想定することができます。ただし、これが常に正しいとは限りません。 p
がnull
値の場合はどうなりますか? p
がデータベースに存在しない場合はどうなりますか?データベースが切断された場合はどうなりますか?したがって、DeletePersonは契約を適切に満たしているようには見えません。個人を削除することもあれば、NullReferenceExceptionまたはDatabaseNotConnectedExceptionをスローすることもあります。それは何もしません(その人がすでに削除されている場合など)。
このようなAPIは、メソッドのこの「ブラックボックス」を呼び出すと、あらゆる種類のひどい事態が発生する可能性があるため、使用が難しいことで有名です。
契約を改善する方法をいくつか紹介します。
検証を追加し、契約に例外を追加します。これにより、契約がstrongerになりますが、呼び出し元が検証を実行します。ただし、違いは、彼らが自分の要件を知っているということです。この場合、C#XMLコメントでこれを伝えていますが、代わりにthrows
(Java)を追加するか、Assert
を使用するか、コード契約などの契約ツールを使用できます。
///<exception>ArgumentNullException</exception>
///<exception>ArgumentException</exception>
public void DeletePerson(Person p)
{
if(p == null)
throw new ArgumentNullException("p");
if(!_database.Contains(p))
throw new ArgumentException("The Person specified is not in the database.");
_database.Delete(p);
}
補足:このスタイルに対する反対意見は、すべての呼び出しコードによる過剰な事前検証を引き起こすことが多いですが、私の経験では、多くの場合そうではありません。 nullのPersonを削除しようとしているシナリオを考えてみてください。どうしてこうなりました?ヌルの人はどこから来たのですか?たとえば、これがUIである場合、現在の選択がない場合にDeleteキーが処理されたのはなぜですか?すでに削除されている場合は、すでに表示から削除されていませんか?明らかにこれには例外がありますが、プロジェクトが成長するにつれて、バグがシステムの奥深くに侵入するのを防ぐために、このようなコードに感謝することがよくあります。
検証とコードを防御的に追加します。これにより、契約がlooserになります。これは、このメソッドが単に人を削除します。 これを反映するようにメソッド名を変更しましたが、APIに一貫性がある場合は必要ないかもしれません。このアプローチには長所と短所があります。すべての種類の無効な入力を渡してTryDeletePerson
を呼び出すことができ、例外を心配する必要がないというプロです。もちろん、欠点は、コードのユーザーがこのメソッドを過度に呼び出すか、pがnullの場合にデバッグが困難になる可能性があることです。これは 単一の責任の原則 の軽度の違反と見なされる可能性があるため、炎上戦争が発生した場合はその点に注意してください。
public void TryDeletePerson(Person p)
{
if(p == null || !_database.Contains(p))
return;
_database.Delete(p);
}
結合のアプローチ。両方を少し必要とする場合があり、外部の呼び出し元が規則に厳密に従う(責任のあるコードを強制するため)必要がありますが、プライベートコードを柔軟にしたい。
///<exception>ArgumentNullException</exception>
///<exception>ArgumentException</exception>
public void DeletePerson(Person p)
{
if(p == null)
throw new ArgumentNullException("p");
if(!_database.Contains(p))
throw new ArgumentException("The Person specified is not in the database.");
TryDeletePerson(p);
}
internal void TryDeletePerson(Person p)
{
if(p == null || !_database.Contains(p))
return;
_database.Delete(p);
}
私の経験では、ハードルールではなく、暗黙の契約に集中することが最も効果的です。防御的なコーディングは、呼び出しが操作が有効かどうかを判断することが困難または困難な場合に、より効果的に機能するようです。厳密なコントラクトは、呼び出し元がメソッド呼び出しを行うのは、本当に意味のある場合にのみ期待できる場合に、よりうまく機能するように見えます。
これは、慣例、ドキュメント、およびユースケースの問題です。
すべての機能が同じというわけではありません。すべての要件が同じというわけではありません。すべての検証が同じというわけではありません。
たとえば、Javaプロジェクトが可能な限りnullポインターを回避しようとする場合(たとえば、 (グアバスタイルの推奨事項 を参照))、すべての関数引数を検証して、それがnullではない?おそらく必要ではないかもしれませんが、バグを見つけやすくするために、まだそうしている可能性があります。ただし、以前にNullPointerExceptionをスローした場所でアサートを使用できます。
プロジェクトがC++の場合はどうなりますか? C++の慣習/伝統は前提条件を文書化することですが、(もしあれば)デバッグビルドでそれらを確認するだけです。
どちらの場合も、関数に文書化された前提条件があります。nullの引数はありません。代わりに、関数のドメインを拡張して、動作が定義されたnullを含めることもできます。 「いずれかの引数がnullの場合、例外をスローします」。もちろん、これはここでも私のC++の遺産です-Javaでは、このように前提条件を文書化するのに十分一般的です。
しかし、すべての前提条件がcanであっても合理的にチェックされるわけではありません。たとえば、バイナリ検索アルゴリズムには、検索するシーケンスをソートする必要があるという前提条件があります。しかし、それが間違いなくそうであることを確認することはO(N)操作であるため、すべての呼び出しでそれを行うと、ちょっとO(log(N))アルゴリズムを使用するポイントが無効になります。最初の場所。防御的にプログラミングしている場合は、より少ないチェック(たとえば、検索するパーティションごとに、開始値、中間値、終了値がソートされていることを確認する)を実行できますが、すべてのエラーをキャッチできるわけではありません。通常は、満たされている前提条件に依存する必要があります。
明示的なチェックが必要な実際の場所は境界です。プロジェクトへの外部入力?検証、検証、検証。灰色の領域はAPI境界です。それは、クライアントコードをどれだけ信頼したいか、無効な入力がどれほどの損害を与えたか、そしてバグを見つけるためにどれだけの支援を提供したいかによって、実際に異なります。もちろん、特権の境界はすべて外部としてカウントする必要があります。たとえば、syscallsは昇格された特権コンテキストで実行されるため、検証は慎重に行う必要があります。もちろん、そのような検証はすべてsyscallの内部で行う必要があります。
パラメータの検証は、呼び出される関数の問題である必要があります。関数は、有効な入力と見なされるものとそうでないものを認識している必要があります。呼び出し元は、特に関数が内部でどのように実装されているかを知らない場合、これを知らない可能性があります。関数は、呼び出し元からのパラメーター値の任意の組み合わせを処理することが期待されます。
この関数はパラメーターの検証を行うため、この関数に対する単体テストを作成して、有効なパラメーター値と無効なパラメーター値の両方で意図したとおりに動作することを確認できます。
関数自体の中。関数が複数回使用されている場合、すべての関数呼び出しのパラメーターを確認する必要はありません。
さらに、パラメーターの検証に影響を与えるような方法で関数が更新された場合は、呼び出し元の検証をすべて検索して更新する必要があります。それは素敵ではありません:-)。
Guard Clause を参照してください。
更新
あなたが提供した各シナリオについての私の返事を見てください。
無効な変数の扱いが変わる可能性がある場合は、呼び出し側で検証することをお勧めします(例:sqrt()
関数-場合によっては複素数を処理したい場合があるため、呼び出し側で条件を扱います)
回答
プログラミング言語の大部分は、複素数ではなく整数と実数をデフォルトでサポートしているため、sqrt
の実装は負でない数のみを受け入れます。複素数を返すsqrt
関数がある唯一のケースは、- Mathematica のように、数学指向のプログラミング言語を使用する場合です。
さらに、ほとんどのプログラミング言語のsqrt
はすでに実装されているため、変更できません。実装を置き換えようとすると(モンキーパッチを参照)、共同作業者はsqrt
は突然負の数を受け入れます。
必要な場合は、負の数を処理して複素数を返すカスタムsqrt
関数をラップすることができます。
チェック条件がすべての呼び出し元で同じである場合、重複を避けるために、関数内でチェックすることをお勧めします
回答
はい、これは、コード全体にパラメーターの検証を分散させないための良い方法です。
呼び出し元の入力パラメーターの検証は、このパラメーターを使用して多くの関数を呼び出す前に1つだけ行われます。したがって、各関数のパラメーターの検証は効果的ではありません
回答
呼び出し元が関数であればいいでしょうね。
呼び出し元内の関数が他の呼び出し元によって使用されている場合、呼び出し元によって呼び出された関数内のパラメーターを検証できないのはなぜですか。
適切なソリューションは特定のケースに依存します
回答
保守可能なコードを目指します。パラメーター検証を移動することで、関数が受け入れることができるかどうかについての真実の1つの情報源が保証されます。
関数はその事前条件と事後条件を述べる必要があります。
前提条件とは、関数を正しく使用し、入力パラメーターの有効性を含めることができる(そしてしばしば行う)前に、満たす必要がある条件呼び出し元によってです。
事後条件は、関数が呼び出し元に対して行う約束です。
関数のパラメーターの有効性が前提条件の一部である場合、それらのパラメーターが有効であることを確認するのは呼び出し側の責任です。ただし、すべての呼び出し元が呼び出しの前に各パラメーターを明示的に確認する必要があるわけではありません。ほとんどの場合、呼び出し元の内部ロジックと事前条件により、パラメーターが有効であることはすでに確認されているため、明示的なテストは必要ありません。
プログラミングエラー(バグ)に対する安全対策として、関数に渡されたパラメーターが実際に指定された前提条件を満たしていることを確認できます。これらのテストはコストがかかる可能性があるため、リリースビルドではテストをオフにできるようにすることをお勧めします。これらのテストが失敗した場合、プログラムはおそらくバグにぶつかったため、プログラムを終了する必要があります。
一見すると、呼び出し元のチェックはコードの重複を招くように見えますが、実際にはその逆です。呼び出し先のチェックにより、コードが重複し、多くの不要な作業が行われます。
それについて考えてみてください。どのくらいの頻度で関数の複数のレイヤーを介してパラメーターを渡し、途中でそれらのいくつかに小さな変更のみを加えますか。 check-in-calleeメソッドを一貫して適用する場合、これらの各中間関数は、各パラメーターのチェックを再実行する必要があります。
次に、これらのパラメータの1つがソートされたリストであると想定されていると想像してください。
呼び出し元でのチェックにより、リストが実際にソートされていることを確認する必要があるのは最初の関数だけです。他のすべての人は、リストがすでに並べ替えられていることを知っており(それが前提条件で述べたとおり)、それ以上のチェックなしでリストを渡すことができます。
ほとんどの場合、誰が、いつ、どのようにして、作成した関数を呼び出すかがわかりません。最悪の場合を想定するのが最善です。無効なパラメーターで関数が呼び出されます。だから、あなたは間違いなくそれをカバーするべきです。
それでも、使用する言語が例外をサポートしている場合は、特定のエラーをチェックせず、例外がスローされることを確認できますが、この場合は、ドキュメントにケースを説明する必要があります(ドキュメントが必要です)。例外は、何が起こったかに関する十分な情報を呼び出し元に提供し、無効な引数に注意を向けます。