単純なドメインオブジェクト(POCO)からデータ検証を分離する方法は?
質問
この質問は言語に依存しませんが、私はC#の男なので、POCOという用語は、通常はゲッターフィールドとセッターフィールドを使用して、データストレージのみを実行するオブジェクトを意味します。
ドメインモデルをスーパーオーバーPOCOに作り直したところ、ドメイン内でプロパティ値が意味をなすことを保証する方法についての懸念がいくつか残っています。
たとえば、サービスのEndDateは、サービスが属する契約のEndDateを超えてはなりません。ただし、チェックをService.EndDateセッターに設定することはSOLID違反であるように思われます。言うまでもなく、実行する必要がある検証の数が増えると、POCOクラスが乱雑になります。
解決策はいくつかありますが(回答を掲載します)、欠点があり、このジレンマを解決するためのお気に入りのアプローチは何ですか?
解決
あなたは悪い仮定、つまりデータを保存するだけのオブジェクトを持ち、アクセサ以外のメソッドを持たないという前提で始めていると思います。オブジェクトを持つことのポイントは、データをと動作をカプセル化することです。基本的に構造体だけのものがある場合、どの動作をカプセル化しますか?
他のヒント
「検証」についての人々の議論をいつも聞いています。または「IsValid」」メソッド。
個人的にはこれでうまくいくと思いますが、ほとんどのDDDプロジェクトでは通常 オブジェクトの特定の状態に応じて複数の検証が許可されます。
だから、「IsValidForNewContract」、「IsValidForTermination」、ほとんどのプロジェクトはクラスごとに複数のそのようなバリデーター/状態を持つと信じているためです。また、インターフェイスがないことも意味しますが、私が主張しているビジネス条件を非常によく反映する読み取り集約されたバリデータを書くことができます。
この場合の一般的な解決策は、技術的な優雅さ(インターフェイス、デリゲートなど)の非常にわずかな向上のために、重要なこと-コードが何をしているのか-に焦点を当てることが非常に多いと本当に信じています)。ただ投票してください;)
私の同僚が思いついたアイデアを思いつきました。素晴らしい名前は思いつきませんでしたが、インスペクター/ジャッジと呼びました。
インスペクターはオブジェクトを見て、違反したすべてのルールを通知します。裁判官はそれについてどうするかを決定します。この分離により、いくつかのことができます。すべてのルールを1つの場所(インスペクター)に配置できますが、複数のジャッジを持ち、コンテキストによってジャッジを選択できます。
複数のジャッジの使用例の1つは、顧客が住所を持っている必要があるというルールに基づいています。これは標準の3層アプリでした。 UI層では、ジャッジは、入力する必要があるフィールドを示すためにUIが使用できるものを生成します。UIジャッジは例外をスローしませんでした。サービス層には別の裁判官がいました。保存中に住所のない顧客が見つかった場合、例外がスローされます。その時点で、あなたは本当に物事の進行を止める必要があります。
また、オブジェクトの状態が変化するにつれて、より厳格な審査員がいました。これは保険申請であり、見積プロセス中にポリシーを不完全な状態で保存することが許可されました。しかし、そのポリシーをアクティブにする準備ができたら、多くのことを設定する必要がありました。そのため、サービス側のクォーティングジャッジは、アクティベーションジャッジほど厳密ではありませんでした。ただし、インスペクターで使用されるルールは同じであるため、何もしないと決定した場合でも、完了しなかったものを伝えることができます。
1つの解決策は、各オブジェクトのDataAccessObjectにバリデーターのリストを取得させることです。 Saveが呼び出されると、各バリデータに対してチェックが実行されます。
public class ServiceEndDateValidator : IValidator<Service> {
public void Check(Service s) {
if(s.EndDate > s.Contract.EndDate)
throw new InvalidOperationException();
}
}
public class ServiceDao : IDao<Service> {
IValidator<Service> _validators;
public ServiceDao(IEnumerable<IValidator<Service>> validators) {_validators = validators;}
public void Save(Service s) {
foreach(var v in _validators)
v.Check(service);
// Go on to save
}
}
利点は、非常に明確なSoCです。欠点は、Save()が呼び出されるまでチェックが行われないことです。
過去、私は通常、ValidationServiceなどのサービスに検証を委任していました。原則として、これはまだDDDの哲学に耳を傾けています。
内部には、Validatorsのコレクションと、エラーオブジェクトのコレクションを返すValidate()などの非常に単純なパブリックメソッドのセットが含まれます。
非常に単純に、C#では次のようになります
public class ValidationService<T>
{
private IList<IValidator> _validators;
public IList<Error> Validate(T objectToValidate)
{
foreach(IValidator validator in _validators)
{
yield return validator.Validate(objectToValidate);
}
}
}
バリデーターは、デフォルトのコンストラクター内に追加するか、ValidationServiceFactoryなどの他のクラスを介して注入できます。
実際には、おそらく論理にとって最適な場所になると思いますが、それは私だけです。すべての条件もチェックしてtrue / falseを返すIsValidメソッドを使用できます。ErrorMessagesコレクションのようなものかもしれませんが、エラーメッセージは実際にはドメインモデルの一部ではないので、それは不確かなトピックです。 RoRでいくつかの作業を行ったので、私は少し偏見があり、それは基本的にそのモデルが行うことです。
別の可能性は、各クラスを実装することです
public interface Validatable<T> {
public event Action<T> RequiresValidation;
}
各クラスの各セッターに、設定する前にイベントを発生させます(属性を介してこれを達成できる可能性があります)。
利点は、リアルタイムの検証チェックです。しかし、より複雑なコードであり、誰がアタッチするべきかは不明です。
別の可能性があります。検証は、ドメインオブジェクトのプロキシまたはデコレータを介して行われます。
public class ServiceValidationProxy : Service {
public override DateTime EndDate {
get {return EndDate;}
set {
if(value > Contract.EndDate)
throw new InvalidOperationexception();
base.EndDate = value;
}
}
}
利点:インスタント検証。 IoC経由で簡単に設定できます。
短所:デコレーターがすべてのドメインモデルをインターフェイスベースにする必要がある場合、プロキシ、検証済みのプロパティは仮想でなければなりません。検証クラスは少し重いものになります-プロキシはクラスを継承する必要があり、デコレータはすべてのメソッドを実装する必要があります。命名と編成が混乱する可能性があります。