パブリックメソッドの引数を検証することが推奨されると聞きました:
動機は理解できます。モジュールが誤った方法で使用される場合、予測できない動作ではなく、すぐに例外をスローする必要があります。
気になるのは、モジュールの使用中に発生する可能性のあるエラーが間違った引数だけではないことです。推奨事項に従い、エラーのエスカレーションを望まない場合に、チェックロジックを追加する必要があるいくつかのエラーシナリオを以下に示します。
私はこれらすべての条件を考慮して、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
を取得します。では、なぜ引数のチェックがそれほど特別なのでしょうか。
モジュールの使用状況を確認しないでください。非常に人気のない意見です。理由を説明できますか?
TL; DR:状態変更を検証し、現在の状態の[有効性]に依存します。
以下では、リリース対応の検証のみを検討します。デバッグのみのアクティブなアサーションはドキュメントの一種であり、それ自体は有用であり、この質問の範囲外です。
次の原則を検討してください。
命令型言語では、エラーの症状とその原因は、何時間もの重労働によって分離される場合があります。現在の状態の検査では破損の完全なプロセス、つまりエラーの原因を明らかにできないため、状態の破損はそれ自体を隠し、変異して不可解な障害を引き起こす可能性があります。
状態の変化はすべて、慎重に作成して検証する必要があります。変更可能な状態に対処する1つの方法は、状態を最小限に抑えることです。これは、以下によって実現されます。
コンポーネントの状態を拡張するときは、コンパイラーに新しいデータの不変性を強制させることで拡張することを検討してください。また、すべての適切なランタイム制約を適用し、潜在的な結果の状態を可能な限り最小の明確なセットに制限します。
// 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();
}
}
クライアントの状態は、独自のフィールド値と、独自の不変条件でカバーされないコンポーネントの状態の一部で構成されます。検証は、クライアントの実際の状態変更の前にのみ行う必要があります。
クラスは自身の状態を担当します。だから、それが物事を許容可能な状態に保つまたは置く程度まで検証します。
モジュールが誤った方法で使用される場合、予測できない動作ではなく、すぐに例外をスローする必要があります。
いいえ、例外をスローせず、代わりに予測可能な動作を提供します。州の責任の当然の結果は、クラス/アプリケーションをできるだけ実用的なものにすることです。たとえば、null
をaCollection.Add()
に渡すと、追加しないでください。オブジェクトを作成するためのnull
入力を取得しますか? nullオブジェクトまたはデフォルトオブジェクトを作成します。上記のdoor
はすでにopen
ですか?それでは、続けてください。 DoorFactory
引数はnullですか?新しいものを作成します。 enum
を作成すると、常にUndefined
メンバーができます。私はDictionary
sとenums
を自由に使用して、物事を明示的に定義しています。これは、予測可能な動作を実現するのに大いに役立ちます。
(こんにちは、依存性注入愛好家!)
ええ、私はパラメーターの谷の影を通り抜けますが、私は議論を恐れません。上記の例では、デフォルトパラメータとオプションパラメータも可能な限り使用しています。
上記すべてにより、内部処理を続行できます。特定のアプリケーションでは、例外がスローされる場所が1つしかない複数のクラスにまたがる数十のメソッドがあります。それでも、それはnull引数が原因ではなく、処理を続行できなかったのも、コードが「非機能」/「null」オブジェクトを作成してしまったためです。
編集
コメント全体を引用します。デザインは「ヌル」に遭遇したときに単に「あきらめる」べきではないと思います。特に複合オブジェクトを使用します。
ここでは重要な概念/前提を忘れています-
encapsulation
&single responsibility
。最初のクライアントと相互作用するレイヤーの後には、実質的にnullチェックはありません。コードは寛容壮健。クラスはデフォルトの状態で設計されているため、相互作用するコードにバグが多く、悪質なジャンクであるかのように記述されていなくても機能します。複合親は、有効性を評価するために子レイヤーに到達する必要はありません(暗黙的に、すべての隅と隅でnullをチェックします)。親は子供のデフォルトの状態の意味を知っています
編集を終了