Danger ... Danger Dr. Smith... 哲学的なポストの先
この投稿の目的は、ドメイン エンティティ (実際には集約ルート) の外に検証ロジックを配置することで、実際により多くの柔軟性が得られるのか、それともカミカゼ コードなのかを判断することです。
基本的に、ドメイン エンティティを検証するためのより良い方法があるかどうかを知りたいです。このように考えていますが、ご意見をお聞かせください。
私が最初に検討したアプローチは次のとおりです。
class Customer : EntityBase<Customer>
{
public void ChangeEmail(string email)
{
if(string.IsNullOrWhitespace(email)) throw new DomainException(“...”);
if(!email.IsEmail()) throw new DomainException();
if(email.Contains(“@mailinator.com”)) throw new DomainException();
}
}
検証ロジックを正しいエンティティにカプセル化していても、これはオープン/クローズの原則 (拡張はオープン、変更はクローズ) に違反しているため、実際にはこの検証が好きではありません。この原則に違反すると、コードのメンテナンスがアプリケーションが複雑になると、本当に苦痛です。なんで?ドメイン ルールは、私たちが認めているよりも頻繁に変更されます。また、ルールが隠され、このようなエンティティに埋め込まれていると、テストも読み取りも維持も困難になりますが、私がこれを好まない本当の理由は次のとおりです。アプローチは次のとおりです。検証ルールが変更された場合、ドメインエンティティを編集する必要があります。これは非常に単純な例ですが、強化学習では検証がより複雑になる可能性があります
Udi Dahan の哲学、ロールの明示化、および青本での Eric Evans の推奨に従って、次の試みは仕様パターンを実装することでした。
class EmailDomainIsAllowedSpecification : IDomainSpecification<Customer>
{
private INotAllowedEmailDomainsResolver invalidEmailDomainsResolver;
public bool IsSatisfiedBy(Customer customer)
{
return !this.invalidEmailDomainsResolver.GetInvalidEmailDomains().Contains(customer.Email);
}
}
しかし、このアプローチに従うには、最初にエンティティを変更して値を渡す必要があることに気付きました valdiated、この場合は電子メールですが、エンティティを変更すると、ドメインイベントが発生し、望ましくない新しい電子メールが有効になるまで発生する
これらのアプローチを検討した後、CQRS アーキテクチャを実装するため、次のアプローチにたどり着きました。
class EmailDomainIsAllowedValidator : IDomainInvariantValidator<Customer, ChangeEmailCommand>
{
public void IsValid(Customer entity, ChangeEmailCommand command)
{
if(!command.Email.HasValidDomain()) throw new DomainException(“...”);
}
}
それが主なアイデアです。検証を実行するためにエンティティからの値が必要な場合に備えて、エンティティはバリデーターに渡されます。コマンドにはユーザーからのデータが含まれます。バリデーターは注入可能なオブジェクトと見なされるため、外部依存関係が注入される可能性があります。検証で必要な場合。
ジレンマ、私はこのような設計に満足しています。私の検証は個々のオブジェクトにカプセル化されているため、多くの利点があります: 簡単な単体テスト、保守が容易、ドメインの不変条件はユビキタス言語を使用して明示的に表現され、拡張が容易、検証ロジックは集中化されたバリデーターを一緒に使用して、複雑なドメイン ルールを適用できます。そして、エンティティの検証をエンティティの外に置いていることを知っていても(コードの匂いを主張することができます-Anemic Domain)、トレードオフは許容できると思います
しかし、それをきれいな方法で実装する方法がわからなかったことが1つあります。このコンポーネントをどのように使用すればよいですか...
それらは注入されるため、ドメイン エンティティ内に自然に収まらないため、基本的に次の 2 つのオプションがあります。
エンティティの各メソッドにバリデータを渡す
オブジェクトを外部で検証する (コマンド ハンドラーから)
オプション 1 には満足できないので、オプション 2 でそれを行う方法を説明します。
class ChangeEmailCommandHandler : ICommandHandler<ChangeEmailCommand>
{
// here I would get the validators required for this command injected
private IEnumerable<IDomainInvariantValidator> validators;
public void Execute(ChangeEmailCommand command)
{
using (var t = this.unitOfWork.BeginTransaction())
{
var customer = this.unitOfWork.Get<Customer>(command.CustomerId);
// here I would validate them, something like this
this.validators.ForEach(x =. x.IsValid(customer, command));
// here I know the command is valid
// the call to ChangeEmail will fire domain events as needed
customer.ChangeEmail(command.Email);
t.Commit();
}
}
}
さて、これです。これについての考えを教えてください。または、ドメイン エンティティの検証に関する経験を共有してください。
編集
私の質問からは明確ではないと思いますが、本当の問題は次のとおりです。ドメイン ルールを非表示にすることは、アプリケーションの将来の保守性に深刻な影響を及ぼします。また、ドメイン ルールは、アプリのライフサイクル中に頻繁に変更されます。したがって、これを念頭に置いて実装すると、簡単に拡張できます。将来、ルール エンジンが実装されることを想像してみてください。ルールがドメイン エンティティの外部でカプセル化されている場合、この変更はより簡単に実装できます。
@jgauffinが彼の回答で述べたように、検証をエンティティの外に配置するとカプセル化が壊れることは承知していますが、検証を個々のオブジェクトに配置することの利点は、エンティティのカプセル化を維持するよりもはるかに重要であると思います。エンティティはドメイン層のいくつかの場所で使用されていたため、従来の n 層アーキテクチャではカプセル化がより理にかなっていると思いますが、CQRS アーキテクチャでは、コマンドが到着すると、集約ルートにアクセスするコマンド ハンドラーが存在し、集計ルートに対して操作を実行するだけで、検証を配置するための完璧なウィンドウが作成されます。
検証をエンティティ内に配置する利点と、個々のオブジェクトに配置する利点を少し比較したいと思います
個々のオブジェクトの検証
- プロ。書きやすい
- プロ。テストが簡単
- プロ。はっきりと表れている
- プロ。ドメイン設計の一部となり、現在のユビキタス言語で表現される
- プロ。これは設計の一部になっているため、UML ダイアグラムを使用してモデル化できます。
- プロ。メンテナンスが非常に簡単
- プロ。エンティティと検証ロジックを疎結合にする
- プロ。拡張が容易
- プロ。SRPに従う
- プロ。オープン/クローズの原則に従う
- プロ。Demeter (mmm) の法則を破っていませんか?
- プロ。私は中央集権的です
- プロ。再利用できる可能性があります
- プロ。必要に応じて、外部依存関係を簡単に注入できます
- プロ。プラグイン モデルを使用している場合、アプリケーション全体を再コンパイルする必要なく、新しいアセンブリをドロップするだけで新しいバリデータを追加できます。
- プロ。ルールエンジンを実装する方が簡単だろう
- 短所 カプセル化の解除
- 短所 カプセル化が必須の場合、個々のバリデーターをエンティティ (集約) メソッドに渡す必要があります。
エンティティ内にカプセル化された検証
- プロ。カプセル化?
- プロ。再利用可能?
これについてのあなたの考えを読んでみたいです