モジュール全体の使用状況を検証する必要があるのか​​、それともパブリックメソッドの引数だけを検証する必要があるのか​​?


9

パブリックメソッドの引数を検証することをお勧めします。

動機は理解できます。モジュールが誤った方法で使用される場合は、予測できない動作ではなく、すぐに例外をスローする必要があります。

気になるのは、モジュールの使用中に発生する可能性のあるエラーは間違った引数だけではないということです。推奨事項に従ってエラーのエスカレーションを望まない場合に、チェックロジックを追加する必要があるいくつかのエラーシナリオを以下に示します。

  • 着信-予期しない引数
  • 着信-モジュールの状態が間違っています
  • 外部呼び出し-予期しない結果が返されました
  • 外部呼び出し-予期しない副作用(呼び出しモジュールへの二重入力、他の依存関係の状態を壊す)

私はこれらすべての条件を考慮して、1つのメソッド(申し訳ありませんが、C#ではありません)で単純なモジュールを作成しようとしました:

public sealed class Room
{
    private readonly IDoorFactory _doorFactory;
    private bool _entered;
    private IDoor _door;
    public Room(IDoorFactory doorFactory)
    {
        if (doorFactory == null)
            throw new ArgumentNullException("doorFactory");
        _doorFactory = doorFactory;
    }
    public void Open()
    {
        if (_door != null)
            throw new InvalidOperationException("Room is already opened");
        if (_entered)
            throw new InvalidOperationException("Double entry is not allowed");
        _entered = true;
        _door = _doorFactory.Create();
        if (_door == null)
            throw new IncompatibleDependencyException("doorFactory");
        _door.Open();
        _entered = false;
    }
}

今では安全です=)

かなり気味悪いです。しかし、何十ものメソッド、複雑な状態、そして多くの外部呼び出し(依存関係注入の愛好家!)動作をオーバーライドできるモジュール(C#の非シールクラス)を呼び出す場合は、外部呼び出しを行っているため、呼び出し元のスコープでは結果を予測できません。

要約すると、正しい方法は何ですか?なぜですか?以下のオプションから選択できる場合は、追加の質問に答えてください。

モジュール全体の使用状況を確認してください。単体テストが必要ですか?そのようなコードの例はありますか?依存関係の注入は、使用が制限されている必要がありますか?これらのチェックをデバッグ時(リリースには含めない)に移動するのは現実的ではありませんか?

引数のみをチェックします。私の経験では、引数チェック、特にnullチェックは、引数エラーが複雑なミスやエラーのエスカレーションにつながることはめったにないため、最も効果的なチェックではありません。ほとんどの場合NullReferenceException、次の行でを取得します。では、なぜ引数チェックはそれほど特別なのでしょうか。

モジュールの使用状況をチェックしません。それは非常に不人気な意見ですが、その理由を説明できますか?


不変条件が維持されていることを確認するために、フィールド割り当て中にチェックを行う必要があります。
バシレフ2015

@Basilevs興味深い...コードコントラクトのイデオロギーか、それより古いものか (コメントに関連して)何か読みたいことはありますか?
astef

これは、懸念事項の基本的な分離です。すべてのケースがカバーされますが、コードの重複は最小限で、責任は明確に定義されています。
バシレフ2015

@Basilevsしたがって、他のモジュールの動作をまったくチェックせず、独自の状態不変条件をチェックします。良さそうですね。しかし、なぜ引数チェックに関する関連する質問にこの単純な領収書が表示されないのですか?
astef 2015

まあ、いくつかの動作チェックはまだ必要ですが、実際に使用された値に対してのみ実行する必要があります。他の場所に転送される値では実行しないでください。たとえば、クライアントコードのインデックスをチェックするのではなく、List実装に依存してOOBエラーをチェックします。通常、これらは低レベルのフレームワーク障害であり、手動で発行する必要はありません。
バシレフ2015

回答:


2

TL; DR:状態変更を検証し、現在の状態の[有効性]に依存します。

以下では、リリース対応の検証のみを検討します。デバッグのみのアクティブなアサーションはドキュメントの一種であり、それ自体は有用であり、この質問の範囲外です。

次の原則を検討してください。

  • 常識
  • 速く失敗する
  • ドライ
  • SRP

定義

  • コンポーネント-APIを提供するユニット
  • クライアント-コンポーネントのAPIのユーザー

可変状態

問題

命令型言語では、エラーの症状とその原因は、何時間もの重労働によって分離される場合があります。現在の状態の検査では破損の完全なプロセス、したがってエラーの原因を明らかにできないため、状態の破損はそれ自体を隠蔽し、変異して不可解な障害を引き起こす可能性があります。

解決

状態のすべての変化は注意深く作成され、検証されるべきです。変更可能な状態に対処する1つの方法は、状態を最小限に抑えることです。これは、以下によって実現されます。

  • 型システム(constおよびfinalメンバー宣言)
  • 不変条件の導入
  • パブリックAPIを介してコンポーネントの状態のすべての変更を確認する

コンポーネントの状態を拡張するときは、コンパイラーに新しいデータの不変性を強制させることで拡張することを検討してください。また、すべての適切な実行時制約を適用して、潜在的な結果の状態を可能な限り明確に定義された最小のセットに制限します。

// Wrong
class Natural {
    private int number;
    public Natural(int number) {
        this.number = number;
    }
    public int getInt() {
      if (number < 1)
          throw new InvalidOperationException();
      return number;
    }
}

// Right
class Natural {
    private readonly int number;
    /**
     * @param number - positive number
     */
    public Natural(int number) {
      // Going to modify state, verification is required
      if (number < 1)
        throw new ArgumentException("Natural number should be  positive: " + number);
      this.number = number;
    }
    public int getInt() {
      // State is guaranteed by construction and compiler
      return number;
    }
}

繰り返しと責任の結束

問題

操作の前提条件と事後条件を確認すると、クライアントとコンポーネントの両方で検証コードが重複することになります。コンポーネントの呼び出しを検証すると、多くの場合、クライアントはコンポーネントの責任の一部を負うことになります。

解決

可能な場合は、コンポーネントに依存して状態検証を実行してください。コンポーネントは、コンポーネントの状態を明確に保つために特別な使用検証(引数の検証や操作シーケンスの適用など)を必要としないAPIを提供します。彼らは、必要に応じてAPI呼び出し引数を検証し、必要な手段で失敗を報告し、状態の破損を防止するよう努める義務があります。

クライアントは、APIの使用を確認するためにコンポーネントに依存する必要があります。繰り返しが回避されるだけでなく、クライアントはコンポーネントの追加の実装詳細に依存しなくなります。フレームワークをコンポーネントと見なしてください。コンポーネントの不変条件が十分に厳格でない場合、またはコンポーネントの例外を実装の詳細としてカプセル化できない場合にのみ、カスタム検証コードを記述します。

操作が状態を変更せず、状態変更の検証の対象にならない場合は、可能な限り深いレベルですべての引数を検証します。

class Store {
  private readonly List<int> slots = new List<int>();
  public void putToSlot(int slot, int data) {
    if (slot < 0 || slot >= slots.Count) // Unnecessary, validated by List, only needed for custom error message
      throw new ArgumentException("data");
    slots[slot] = data;
  }
}

class Natural {
   int _number;
   public Natural(int number) {
       if (number < 1)
          number = 1;  //Wrong: client can't rely on argument verification, additional state uncertainity is introduced.  Right: throw new ArgumentException(number);
       _number = number;
   }
}

回答

説明されている原則が問題の例に適用されると、次のようになります。

public sealed class Room
{
    private bool _entered = false;
    // Do not use lazy instantiation if not absolutely necessary, this introduces additional mutable state
    private readonly IDoor _door;
    public Room(IDoorFactory doorFactory)
    {
        // Rely on system null check
        IDoor door = _doorFactory.Create();
        // Modifying own state, verification is required
        if (door == null)
           throw new ArgumentNullException("Door");
        _door = door;
    }
    public void Enter()
    {
        // Room invariants do not guarantee _entered value. Door state is indirectly a part of our state. Verification is required to prevent second door state change below.
        if (_entered)
           throw new InvalidOperationException("Double entry is not allowed");
        _entered = true;     
        // rely on immutability for _door field to be non-null
        // rely on door implementation to control resulting door state       
        _door.Open();            
    }
}

概要

クライアントの状態は、独自のフィールド値と、独自の不変条件でカバーされないコンポーネントの状態の一部で構成されます。検証は、クライアントの実際の状態変更の前にのみ行う必要があります。


1

クラスは自身の状態を担当します。だから、それが物事を許容可能な状態に保つか置くかどうかを検証します。

モジュールが誤った方法で使用される場合は、予測できない動作ではなく、すぐに例外をスローする必要があります。

いいえ、例外をスローせず、代わりに予測可能な動作を提供します。州の責任の当然の結果は、クラス/アプリケーションをできるだけ実用的なものにすることです。例えば、合格nullaCollection.Add()?追加しないでください。nullオブジェクトを作成するための入力を取得しますか?nullオブジェクトまたはデフォルトオブジェクトを作成します。上記doorはすでにopenですか?それでは、続けてください。DoorFactory引数はnullですか?新しいものを作成します。を作成するときは、enum常にUndefinedメンバーがいます。私はDictionarys を自由に使用し、enums物事を明示的に定義しています。これは、予測可能な動作を実現するのに大いに役立ちます。

(こんにちは、依存性注入愛好家!)

ええ、私はパラメーターの谷の影を通り抜けますが、私は議論を恐れません。上記の例では、可能な限りデフォルトパラメータとオプションパラメータも使用しています。

上記のすべてにより、内部処理を続行できます。特定のアプリケーションでは、例外がスローされる場所が1つしかない複数のクラスにまたがる数十のメソッドがあります。それでも、それはnull引数が原因ではなく、処理を続行できなかったのも、コードが「非機能」/「null」オブジェクトを作成してしまったためです。

編集する

コメント全体を引用します。デザインは、「ヌル」に遭遇したときに単に「あきらめる」べきではないと思います。特に複合オブジェクトを使用します。

ここでは、重要な概念/仮定を忘れています- encapsulationsingle responsibility。最初のクライアントと相互作用するレイヤーの後には、実質的にnullチェックはありません。コードは耐性があり堅牢です。クラスはデフォルトの状態で設計されているため、相互作用するコードにバグが多く、悪質なジャンクであるかのように記述されていなくても機能します。複合親は、有効性を評価するために子レイヤーに到達する必要はありません(暗黙的に、すべての隅と隅でnullをチェックします)。親は子供のデフォルトの状態の意味を知っています

編集を終了


1
無効なコレクション要素を追加しないことは、非常に予測できない動作です。
バシレフ2015

1
すべてのインターフェースがそのような寛容な方法で設計されると、ある日、平凡なエラーのために、プログラムが誤って目を覚まし、人類を破壊します。
astef

ここでは、重要な概念/仮定を忘れています- encapsulationsingle responsibilitynull最初の、クライアントと相互作用する層の後のチェックは事実上ありません。コードは<strike> tolerant </ strike>堅牢です。クラスはデフォルトの状態で設計されているため、相互作用するコードにバグが多く、悪質なジャンクであるかのように記述されていなくても機能します。複合親は、有効性を評価するために子レイヤーに到達する必要はありません(暗黙的に、nullすべての隅と隅でチェックします)。親は子供のデフォルトの状態が何を意味するかを知っています
レーダーボブ
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.