web-dev-qa-db-ja.com

メソッドが不正な入力を返すことができないことがわかっている場合でも、メソッド呼び出しの戻り値を検証する必要がありますか?

私が呼び出しているメソッドがそのような期待に応えることがわかっている場合でも、それらが私の期待に応えることを検証することによって、メソッド呼び出しの戻り値を防御する必要があるかどうか疑問に思っています。

与えられた

User getUser(Int id)
{
    User temp = new User(id);
    temp.setName("John");

    return temp;
}

私はすべきです

void myMethod()
{
    User user = getUser(1234);
    System.out.println(user.getName());
}

OR

void myMethod()
{
    User user = getUser(1234);

    // Validating
    Preconditions.checkNotNull(user, "User can not be null.");
    Preconditions.checkNotNull(user.getName(), "User's name can not be null.");

    System.out.println(user.getName());
}

これは概念的なレベルでお願いします。私が呼び出しているメソッドの内部の仕組みを知っている場合。私が書いたからか、調べたからだ。そして、それが返す可能な値の論理は私の前提条件を満たしています。検証をスキップするのが「より良い」または「より適切」ですか、それとも常にパスする必要がある場合でも、現在実装しているメソッドを続行する前に、誤った値を防ぐ必要がありますか?.


すべての回答からの私の結論(ご自由にどうぞ):

  • この方法は過去に正しく動作しないことがわかっています
  • この方法は信頼できないソースからのものです
  • このメソッドは他の場所から使用され、事後条件であることを明示的に示していません
  • この方法はあなたの方法に近いものです(詳細については選択した回答を参照してください)
  • メソッドは、適切なドキュメント、タイプセーフティ、ユニットテスト、事後条件チェックなどとの契約を明示的に定義します
  • パフォーマンスは重要です(この場合、デバッグモードアサートはハイブリッドアプローチとして機能します)
30
Didier A.

これは、getUserとmyMethodが変更される可能性に依存し、さらに重要なのはそれらが互いに独立して変更される可能性がどの程度あるかです。

GetUserが将来決して変更されないことを何らかの方法で知っている場合は、はい、それはそれを検証する時間の浪費です。iが_i = 3;_ステートメント。実際には、あなたはそれを知りません。ただし、知っていることがいくつかあります。

  • これら2つの機能は「共存」していますか?言い換えれば、彼らは彼らを維持している同じ人がいますか、彼らは同じファイルの一部ですか、それゆえ彼らは彼ら自身でお互いに「同期」し続ける可能性が高いですか?この場合、2つの関数が変更されるか、異なる数の関数にリファクタリングされるたびに変更(および場合によってはバグを生成)する必要のあるコードが増えるため、検証チェックを追加するのはおそらくやり過ぎです。

  • GetUser関数は、特定のコントラクトを持つドキュメント化されたAPIの一部であり、myMethodは、別のコードベースのAPIのクライアントにすぎませんか?その場合は、そのドキュメントを読んで、戻り値を検証する必要があるか(または入力パラメーターを事前に検証する必要があるか)、またはハッピーパスを盲目的にたどることが本当に安全かどうかを確認できます。ドキュメントでこれが明確になっていない場合は、メンテナに修正を依頼してください。

  • 最後に、この特定の関数が過去に突然かつ予期せずにその動作を変更した場合、コードを壊すような方法で、その関数について偏執するすべての権利があります。バグは密集する傾向があります。

上記のすべては、両方の関数の最初の作成者であっても当てはまります。これらの2つの関数が残りの人生で「一緒に生きる」ことが期待されているのか、それともゆっくりと別々のモジュールに分かれていくのか、それとも何らかの形で古いバグを抱えて足を踏み入れたのかはわかりませんgetUserのバージョン。しかし、おそらくかなりまともな推測をすることができます。

42
Ixrec

Ixrecの答えは良いですが、検討する価値があると思うので、別のアプローチをとります。このディスカッションでは、アサーションについて説明します。これは、Preconditions.checkNotNull()が伝統的に知られている名前だからです。

私が提案したいのは、プログラマが何かが特定の方法で動作することが確実である程度を過大評価し、そのように動作しないことの結果を過小評価していることです。また、プログラマはアサーションの力をドキュメントとして理解していないことがよくあります。その後、私の経験では、プログラマーが主張するよりもはるかに少ない頻度で物事を主張します。

私のモットーは:

あなたがいつも自分に問うべき質問は、「これを断言すべきか」ではありません。しかし、「私が主張するのを忘れたものはありますか?」

当然のことながら、あなたが絶対に確かである何かが特定の方法で動作する場合、それが実際に行われたと主張することは控えますそのように振る舞う、そしてそれは大部分が合理的です。何も信用できなければ、ソフトウェアを書くことは実際には不可能です。

ただし、このルールにも例外があります。奇妙なことに、Ixrecの例をとると、iが確かに特定の範囲内にあるというアサーションで_int i = 3;_を追跡することが完全に望ましいタイプの状況が存在します。これは、さまざまなwhat-ifシナリオを試行している誰かによってiが変更される可能性があり、次のコードはiが特定の範囲内の値を持っていることに依存しています。許容範囲が何であるかを次のコードですばやく見ても、すぐにはわかりません。したがって、_int i = 3; assert i >= 0 && i < 5;_は一見無意味に思えるかもしれませんが、考えてみると、iで遊べる可能性があることと、滞在する必要がある範囲もわかります。 それはマシンによって強制されるため、アサーションは最良のタイプのドキュメントであり、完全な保証であり、 コードとともにリファクタリングされます、常に適切なままです。

あなたのgetUser(int id)の例は、_assign-constant-and-assert-in-range_の例のそれほど遠くない概念的な関係ではありません。当然のことながら、予期した動作とは異なる動作を示すときにバグが発生します。確かに、すべてをアサートすることはできないため、デバッグが必要になる場合があります。しかし、エラーをすばやく検出すればするほど、コストが低くなることは、業界でよく知られている事実です。質問する必要があるのは、getUser()がnullを返さない(およびnull名のユーザーを返さない)かどうかだけではなく、残りでどのような問題が発生するかです。実際にある日コードが予期しない結果を返す場合、コードの残りの部分をすばやく見て、getUser()に何が期待されているかを正確に知ることはどれほど簡単です。

予期しない動作によりデータベースが破損する場合は、101%であっても発生しないと断言する必要があります。 nullユーザー名が、何千行も下に何十億ものクロックサイクル後に奇妙な不可解で追跡不可能なエラーを引き起こす場合は、おそらくできるだけ早く失敗するのが最善です。

ですから、Ixrecの答えには同意しませんが、アサーションには何の費用もかからないという事実を真剣に検討することをお勧めします。したがって、それらを自由に使用することによって失われるものはなく、獲得するだけです。

22
Mike Nakis

明確な番号

呼び出し元は、呼び出している関数がその契約を尊重しているかどうかを確認してはなりません。その理由は単純です。呼び出し元が非常に多くなる可能性があり、他の関数の結果をチェックするロジックを複製することは、すべての呼び出し元の概念的な観点からは非現実的で間違いなく間違いです。

チェックが行われる場合、各関数は独自の事後条件をチェックする必要があります。事後条件により、関数が正しく実装され、ユーザーは関数の契約に依存できるようになります。

特定の関数がnullを返すことを意図していない場合は、これを文書化し、その関数のコントラクトの一部にする必要があります。これがこれらのコントラクトのポイントです。ユーザーが信頼できる不変条件をユーザーに提供し、独自の関数を作成するときにハードルが少なくなるようにします。

8
Paul

私はあなたの質問の前提が外れていると主張します:なぜ最初にnull参照を使用するのですか?

nullオブジェクトパターン は、基本的に何もしないオブジェクトを返す方法です。ユーザーに対してnullを返すのではなく、「ユーザーなし」を表すオブジェクトを返します。

このユーザーは非アクティブで、名前は空白で、その他のnull以外の値は「ここには何もない」ことを示します。主な利点は、プログラムが散らかす必要がなく、チェック、ログ記録などがnullになることです。オブジェクトを使用するだけです。

アルゴリズムがnull値を考慮する必要があるのはなぜですか?手順は同じでなければなりません。ゼロや空の文字列などを返すnullオブジェクトを用意するのも同じくらい簡単で、はるかに明確です。

Nullのコードチェックの例を次に示します。

User u = getUser();
String displayName = "";
if (u != null) {
  displayName = u.getName();
}
return displayName;

Nullオブジェクトを使用したコードの例を次に示します。

return getUser().getName();

どちらがより明確ですか? 2つ目はそうだと思います。


質問のタグは Java ですが、nullオブジェクトパターンは本当に参照を使用する場合にC++で役立つことに注意してください。 Java参照はC++ポインタに似ており、nullを許可します。C++参照は nullptr を保持できません。nullに類似したものを使用したい場合C++での参照では、nullオブジェクトパターンは、それを実現するために私が知っている唯一の方法です。

5
user22815

NullがgetUserメソッドの有効な戻り値である場合、コードはそれを例外ではなくIfで処理する必要があります。これは例外的なケースではありません。

NullがgetUserメソッドの有効な戻り値でない場合は、getUserにバグがあります。getUserメソッドは例外をスローするか、修正する必要があります。

GetUserメソッドが外部ライブラリの一部であるか、変更できない場合にnullが返された場合に例外をスローするには、クラスとメソッドをラップしてチェックを実行し、例外をスローして、 getUserの呼び出しは、コード内で一貫しています。

5
MatthewC

一般に、それは主に次の3つの側面に依存すると思います。

  1. 堅牢性:呼び出しメソッドはnull値に対処できますか? null値がRuntimeExceptionになる可能性がある場合は、チェックをお勧めします-複雑さが低く、呼び出し/呼び出しメソッドが同じ作成者によって作成され、nullが予期されていません(たとえば、同じパッケージに両方のprivate)。

  2. コードの責任:開発者AがgetUser()メソッドを担当し、開発者Bがそれを(たとえば、ライブラリの一部として)使用する場合、その値を検証することを強くお勧めします。開発者Bが、潜在的なnull戻り値をもたらす変更について知らない場合があるからです。

  3. 複雑さ:プログラムまたは環境の全体的な複雑さが高いほど、戻り値を検証することをお勧めします。呼び出しメソッドのコンテキストでは、今日はnullにできないと確信している場合でも、別のユースケースではgetUser()を変更する必要があるかもしれません。数か月または数年が経過し、数千行のコードが追加されると、これはかなりの罠になる可能性があります。

さらに、潜在的なnullの戻り値をJavaDocコメントに文書化することをお勧めします。ほとんどのIDEでJavaDocの説明が強調表示されているため、これはgetUser()を使用する人にとって役立つ警告になる可能性があります。

/** Returns ....
  * @param: id id of the user
  * @return: a User instance related to the id;
  *          null, if no user with identifier id exists
 **/
User getUser(Int id)
{
    ...
}
1
André