ドメイン駆動設計の複雑な集計の検証をどのように処理しますか?ビジネスルール/検証ロジックを統合していますか?
引数の検証について理解しています。また、モデル自体に添付できるプロパティ検証を理解し、電子メールアドレスまたは郵便番号が有効であること、または名の最小長と最大長を確認します。
しかし、複数のモデルを含む複雑な検証についてはどうでしょうか?通常、これらのルールとメソッドをアーキテクチャ内のどこに配置しますか?そして、それらを実装するためにどのようなパターンを使用しますか?
私はこの問題に対するジミー・ボガードの解決策が好きです。彼はブログに「 "訪問者と拡張メソッドによるエンティティの検証" というタイトルの投稿をしており、検証コードを格納する別のクラスの実装を提案するエンティティ検証への非常にエレガントなアプローチを提示しています。
public interface IValidator<T>
{
bool IsValid(T entity);
IEnumerable<string> BrokenRules(T entity);
}
public class OrderPersistenceValidator : IValidator<Order>
{
public bool IsValid(Order entity)
{
return BrokenRules(entity).Count() == 0;
}
public IEnumerable<string> BrokenRules(Order entity)
{
if (entity.Id < 0)
yield return "Id cannot be less than 0.";
if (string.IsNullOrEmpty(entity.Customer))
yield return "Must include a customer.";
yield break;
}
}
アプリケーション全体でIsValid(xx)
呼び出しに依存する代わりに、Greg Youngからいくつかのアドバイスを受けることを検討してください。
エンティティを無効な状態にしないでください。
これが基本的に意味することは、エンティティを純粋なデータコンテナーと考えることから、ビヘイビアーを持つオブジェクトについて考えることから移行することです。
人の住所の例を考えてみましょう。
person.Address = "123 my street";
person.City = "Houston";
person.State = "TX";
person.Zip = 12345;
これらの呼び出しの間では、エンティティは無効です(互いに一致しないプロパティがあるためです。次に、これを検討してください:
person.ChangeAddress(.......);
アドレスを変更する動作に関連するすべての呼び出しが、アトミックユニットになりました。あなたのエンティティはここでは無効になりません。
状態ではなく動作をモデル化するというこの考え方を採用すると、無効なエンティティを許可しないモデルに到達できます。
これについての良い議論については、このinfoqインタビューをチェックしてください: http://www.infoq.com/interviews/greg-young-ddd
私は通常、仕様クラスを使用し、メソッドを提供します(これはC#ですが、任意の言語で翻訳できます)。
bool IsVerifiedBy(TEntity candidate)
このメソッドは、候補とその関係の完全なチェックを実行します。仕様クラスの引数を使用して、チェックレベルのようにパラメーター化することができます...
メソッドを追加して、候補者が仕様を検証しなかった理由を知ることもできます。
IEnumerable<string> BrokenRules(TEntity canditate)
次のように、最初のメソッドを実装することを決定できます。
bool IsVerifiedBy(TEntity candidate)
{
return BrokenRules(candidate).IsEmpty();
}
ルールが壊れている場合は、通常、イテレータを記述します。
IEnumerable<string> BrokenRules(TEntity candidate)
{
if (someComplexCondition)
yield return "Message describing cleary what is wrong...";
if (someOtherCondition)
yield return
string.Format("The amount should not be {0} when the state is {1}",
amount, state);
}
ローカライズにはリソースを使用する必要があります。文化をBrokenRulesメソッドに渡してください。このクラスを、モデルの名前空間に、その使用を示唆する名前で配置します。
複数のモデルの検証は、集約ルートを通過する必要があります。集約ルート全体を検証する必要がある場合、おそらく設計上の欠陥があります。
集約の検証を行う方法は、検証が成功したか失敗したかを通知する応答インターフェイスと、失敗した理由に関するメッセージを返すことです。
集計ルートのすべてのサブモデルを検証して、一貫性を保つことができます。
// Command Response class to return from public methods that change your model
public interface ICommandResponse
{
CommandResult Result { get; }
IEnumerable<string> Messages { get; }
}
// The result options
public enum CommandResult
{
Success = 0,
Fail = 1
}
// My default implementation
public class CommandResponse : ICommandResponse
{
public CommandResponse(CommandResult result)
{
Result = result;
}
public CommandResponse(CommandResult result, params string[] messages) : this(result)
{
Messages = messages;
}
public CommandResponse(CommandResult result, IEnumerable<string> messages) : this(result)
{
Messages = messages;
}
public CommandResult Result { get; private set; }
public IEnumerable<string> Messages { get; private set; }
}
// usage
public class SomeAggregateRoot
{
public string SomeProperty { get; private set; }
public ICommandResponse ChangeSomeProperty(string newProperty)
{
if(newProperty == null)
{
return new CommandResponse(CommandResult.Fail, "Some property cannot be changed to null");
}
SomeProperty = newProperty;
return new CommandResponse(CommandResult.Success);
}
}