呼び出し元の入力パラメーターの検証:コードの重複?


16

関数の入力パラメーターを検証するのに最適な場所はどこですか:呼び出し元または関数自体ですか?

コーディングスタイルを改善したいので、この問題のベストプラクティスまたはいくつかのルールを見つけようとします。いつ、何がより良い。

私の以前のプロジェクトでは、関数内のすべての入力パラメーターをチェックして処理していました(たとえば、nullでない場合)。ここで、入力パラメーターの検証は呼び出し側の責任であることをいくつかの回答とPragmatic Programmerの本で読みました。

つまり、関数を呼び出す前に入力パラメーターを検証する必要があるということです。関数が呼び出されるすべての場所。そして、それは1つの質問を提起します:それは、関数が呼び出されるすべての場所でチェック条件の複製を作成しませんか?

null条件だけに興味はありませんが、入力変数(sqrt関数への負の値、ゼロ除算、状態と郵便番号の間違った組み合わせ、またはその他)の検証に興味があります

入力条件をチェックする場所を決定する方法はありますか?

私はいくつかの議論について考えています:

  • 無効な変数の処理が異なる場合、呼び出し側で検証するのが適切です(たとえば、sqrt()関数-場合によっては複素数を処理したいので、呼び出し側で条件を処理します)
  • チェック条件がすべての呼び出し元で同じ場合、重複を避けるために関数内でチェックすることをお勧めします
  • 呼び出し元の入力パラメーターの検証は、このパラメーターを使用して多くの関数を呼び出す前に1回だけ行われます。したがって、各関数のパラメーターの検証は効果的ではありません
  • 適切なソリューションは、特定のケースに依存します

この質問が他の質問と重複していないことを願っています。この問題を検索し、同様の質問を見つけましたが、彼らは正確にこのケースに言及していません。

回答:


15

場合によります。検証を配置する場所の決定は、メソッドによって暗示される(または文書化される)契約の説明と強度に 基づいている必要があります。検証は、特定の契約の遵守を強化するための良い方法です。何らかの理由でメソッドが非常に厳密なコントラクトを持っている場合、はい、呼び出す前に確認するのはあなた次第です。

パブリックメソッドを作成する場合、これは特に重要な概念です。なぜなら、何らかのメソッドが何らかの操作を実行することを基本的に宣伝しているからです。あなたが言ったことをする方がいい!

例として次の方法を使用します。

public void DeletePerson(Person p)
{            
    _database.Delete(p);
}

暗示される契約とは何DeletePersonですか?プログラマPersonは、渡されたものがあれば削除されるとのみ想定できます。ただし、これが常に真実であるとは限りません。値pはどうなりnullますか?pデータベースに存在しない場合はどうなりますか?データベースが切断された場合はどうなりますか?したがって、DeletePersonは契約を十分に履行していないようです。人を削除する場合もあれば、NullReferenceExceptionまたはDatabaseNotConnectedExceptionをスローする場合もあれば、何もしない場合もあります(人が既に削除されている場合など)。

このようなAPIは使用が難しいことで有名です。メソッドのこの「ブラックボックス」を呼び出すと、あらゆる種類の恐ろしいことが起こる可能性があるためです。

契約を改善する方法は次のとおりです。

  • 検証を追加し、契約に例外を追加します。 これにより、コントラクトがより強力になりますが、呼び出し元が検証を実行する必要があります。ただし、違いは、要件を認識できるようになったことです。この場合、これを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を削除しようとしているシナリオを考えてください。どうしてこうなりました?nullのPersonはどこから来たのですか?たとえば、これがUIの場合、現在の選択がない場合にDeleteキーが処理されたのはなぜですか?既に削除されている場合、既にディスプレイから削除されていませんか?明らかにこれには例外がありますが、プロジェクトが成長するにつれて、バグがシステムの奥深くに浸透するのを防ぐためにこのようなコードに感謝することがよくあります。

  • 検証とコードを防御的に追加します。これにより、このメソッドは単に人を削除する以上のことを行うため、契約が緩くなります。これを反映するためにメソッド名を変更しましたが、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);
    }
    

私の経験では、厳しいルールではなく、あなたが暗示した契約に集中するのが最も効果的です。防御コーディングは、呼び出し側が操作が有効かどうかを判断するのが困難または困難な場合に、よりうまく機能するようです。厳密なコントラクトは、呼び出し元が本当に、本当に理にかなっている場合にのみメソッド呼び出しを行うことを期待する場合、よりうまく機能するように見えます。


例で非常に良い答えをありがとう。私は「防御的」および「厳格な契約」アプローチのポイントが好きです。
srnka

7

それは慣習、文書化、ユースケースの問題です。

すべての機能が同等というわけではありません。すべての要件が同じというわけではありません。すべての検証が等しいわけではありません。

たとえば、Javaプロジェクトが可能な限りnullポインターを回避しようとする場合(たとえばGuavaスタイルの推奨事項を参照)、すべての関数引数を検証して、nullでないことを確認しますか?おそらく必要ではありませんが、バグを見つけやすくするために、あなたはまだそれを行う可能性があります。ただし、以前にNullPointerExceptionをスローした場所でアサートを使用できます。

プロジェクトがC ++の場合はどうなりますか?C ++の慣習/伝統は、前提条件を文書化することですが、デバッグビルドで(もしあれば)検証するだけです。

どちらの場合でも、関数には文書化された前提条件があります。引数はnullにできません。代わりに、関数のドメインを拡張して、定義された動作を持つヌルを含めることができます。たとえば、「引数がヌルの場合、例外をスローします」。もちろん、それはここでも私のC ++の遺産です-Javaでは、この方法で前提条件を文書化するのに十分です。

ただし、すべての前提条件を合理的にチェックできるわけではありません。たとえば、バイナリ検索アルゴリズムには、検索するシーケンスを並べ替える必要があるという前提条件があります。しかし、それが間違いなくそうであることを確認することはO(N)操作なので、すべての呼び出しでそれを行うと、そもそもO(log(N))アルゴリズムを使用するポイントが無効になります。防御的にプログラミングしている場合、より少ないチェック(検索するパーティションごとに、開始値、中間値、終了値がソートされていることを確認するなど)を行うことができますが、すべてのエラーをキャッチするわけではありません。通常、満たされる前提条件に依存するだけです。

明示的なチェックが必要な実際の場所は、境界です。プロジェクトへの外部入力?検証、検証、検証。灰色の領域はAPIの境界です。それは、クライアントコードをどれだけ信頼したいか、無効な入力がどれだけのダメージを与えたか、そしてバグを見つける際にどの程度の支援を提供したいかに大きく依存します。もちろん、特権の境界は外部としてカウントする必要があります。たとえば、syscallは昇格した特権コンテキストで実行されるため、検証には非常に注意する必要があります。もちろん、そのような検証はすべてsyscallの内部である必要があります。


ご回答有難うございます。グアバスタイルの推奨事項へのリンクを教えてください。Googleで検索することはできません。境界を検証するために+1。
srnka

リンクを追加しました。実際には完全なスタイルガイドではなく、null以外のユーティリティのドキュメントの一部にすぎません。
セバスチャンレッド

6

パラメータの検証は、呼び出される関数の懸念事項である必要があります。関数は、有効な入力とみなされるものとそうでないものを知る必要があります。特に、関数が内部的に実装されている方法を知らない場合、呼び出し元はこれを知らない場合があります。この関数は、呼び出し元からのパラメーター値の任意の組み合わせを処理することが期待されます。

この関数はパラメーターの検証を行うため、この関数に対して単体テストを記述して、有効なパラメーター値と無効なパラメーター値の両方で意図したとおりに動作することを確認できます。


ご回答ありがとうございます。そのため、この関数はすべての場合で有効な入力パラメーターと無効な入力パラメーターの両方をチェックする必要があります。Pragmatic Programmer bookの断言とは異なるもの:「入力パラメーターの検証は呼び出し側の責任です」。「関数は有効と見なされるものを知っている必要があります...発信者はこれを知らないかもしれません」...それで、前提条件を使用したくないですか?
srnka

1
必要に応じて前提条件を使用できます(セバスチャンの回答を参照)が、私は防御的であり、あらゆる種類の可能な入力を処理することを好みます。
バーナード

4

関数自体の中。関数が複数回使用される場合、すべての関数呼び出しのパラメーターを検証する必要はありません。

さらに、パラメーターの検証に影響するような方法で関数が更新された場合、呼び出し元の検証が発生するたびに検索して更新する必要があります。それは素敵ではありません:-)。

ガード条項を参照できます

更新

提供した各シナリオに対する私の返信をご覧ください。

  • 無効な変数の処理が異なる場合、呼び出し側で検証するのが適切です(たとえば、sqrt()関数-場合によっては複素数を処理したいので、呼び出し側で条件を処理します)

    回答

    プログラミング言語の大半は、デフォルトで複素数ではなく整数と実数をサポートしているため、それらの実装はsqrt負でない数のみを受け入れます。あなたが持っている場合のみsqrt、あなたのような数学に向けて指向プログラミング言語で、使用時に戻っ複素数であるという機能をMathematicaを

    さらに、sqrtほとんどのプログラミング言語では既に実装されているため、修正することはできません。実装を置き換えようとすると(モンキーパッチを参照)、協力者はsqrt突然負の数を受け入れる理由にまったくショックを与えます。

    必要な場合sqrtは、負の数を処理して複素数を返すカスタム関数をラップできます。

  • チェック条件がすべての呼び出し元で同じ場合、重複を避けるために関数内でチェックすることをお勧めします

    回答

    はい、これはコード全体にパラメーター検証が散らばらないようにするための良い方法です。

  • 呼び出し元の入力パラメーターの検証は、このパラメーターを使用して多くの関数を呼び出す前に1回だけ行われます。したがって、各関数のパラメーターの検証は効果的ではありません

    回答

    呼び出し元が関数であるといいですね。

    呼び出し元の関数が他の呼び出し元によって使用されている場合、呼び出し元によって呼び出された関数内のパラメーターの検証を妨げるものは何ですか?

  • 適切なソリューションは、特定のケースに依存します

    回答

    保守可能なコードを目指します。パラメータ検証を移動することで、関数が受け入れることができるかどうかに関する真実の1つのソースが保証されます。


ご回答ありがとうございます。sqrt()は単なる例であり、入力パラメーターと同じ動作を他の多くの関数で使用できます。「パラメータの検証に影響するような方法で関数が更新された場合、呼び出し元の検証が発生するたびに検索する必要があります」-私はこれに同意しません。戻り値についても同じことが言えます。関数が戻り値に影響するような方法で更新された場合、すべての呼び出し元を修正する必要があります...関数には、明確に定義されたタスクが1つ必要です...とにかく発信者の変更が必要です。
srnka

2

関数は、事前条件と事後条件を記述する必要があります。
事前条件が満たされなければならない条件である、呼び出し側が、それは正しく機能を使用することができますし、(および頻繁に行う)ことができ、入力パラメータの妥当性を含ん前に。
事後条件は、関数が呼び出し元に対して行う約束です。

関数のパラメーターの有効性が前提条件の一部である場合、それらのパラメーターが有効であることを確認するのは呼び出し側の責任です。ただし、すべての呼び出し元が呼び出しの前に各パラメーターを明示的にチェックする必要があるわけではありません。ほとんどの場合、呼び出し元の内部ロジックと前提条件によりパラメーターが有効であることを既に確認しているため、明示的なテストは必要ありません。

プログラミングエラー(バグ)に対する安全対策として、関数に渡されたパラメーターが指定された前提条件を実際に満たしていることを確認できます。これらのテストはコストがかかる可能性があるため、リリースビルドでテストをオフにできるようにすることをお勧めします。これらのテストが失敗した場合、プログラムはバグに遭遇した可能性があるため、プログラムを終了する必要があります。

一見、呼び出し元のチェックはコードの重複を招いているように見えますが、実際には逆です。呼び出し先でのチェックの結果、コードの重複と多くの不要な作業が行われます。
考えてみてください。どのくらいの頻度でパラメーターをいくつかの関数のレイヤーに渡し、途中で一部のパラメーターにわずかな変更を加えるだけです。check-in-calleeメソッドを一貫して適用する場合、これらの各中間関数は各パラメーターのチェックを再実行する必要があります。
そして、これらのパラメーターの1つがソート済みリストであると想定されていることを想像してください。
呼び出し元のチェックでは、最初の関数のみがリストが実際にソートされていることを確認する必要があります。他のすべての人は、リストが既に並べ替えられていることを知っており(それが前提条件で述べているとおりです)、さらにチェックすることなくリストを渡すことができます。


+1回答ありがとうございます。良い反省:「呼び出し先でのチェックは、コードの重複と多くの不要な作業が行われる結果になります」。そして、文の中で:「ほとんどの場合、内部ロジックと呼び出し元の前提条件がすでに保証されているため、明示的なテストは必要ありません」-「内部ロジック」という表現はどういう意味ですか?DBC機能?
srnka

@srnka:「内部ロジック」とは、関数の計算と決定を意味します。それは本質的に関数の実装です。
バートヴァンインゲンシェナウ

0

ほとんどの場合、作成した関数を誰が、いつ、どのように呼び出すかを知ることができません。最悪の事態を想定するのが最善です。無効なパラメーターで関数が呼び出されます。だから、あなたは間違いなくそれをカバーすべきです。

それにもかかわらず、使用する言語が例外をサポートしている場合、特定のエラーをチェックせず、例外がスローされることを確認するかもしれませんが、この場合はドキュメントでケースを説明する必要があります(ドキュメントが必要です)。例外は、呼び出し元に何が起こったかについて十分な情報を提供し、無効な引数にも注意を促します。


実際には、パラメーターを検証する方が適切な場合があります。パラメーターが無効な場合は、自分で例外をスローします。理由は次のとおりです。有効なデータを提供するように気にせずにルーチンを呼び出すピエロは、無効なデータを渡したことを示すエラー戻りコードを確認することを気にしない人と同じです。例外をスローすると、修正する問題が発生します。
ジョンR.ストローム
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.