危険...危険博士スミス...先の哲学的投稿
この投稿の目的は、検証ロジックをドメインエンティティの外部(実際には集約ルート)に配置することで、実際に柔軟性が向上するのか、それともカミカゼコードであるかを判断することです。 )
基本的に、ドメインエンティティを検証するためのより良い方法があるかどうかを知りたいです。これが私がやろうとしている方法ですが、あなたの意見をお願いします
私が考えた最初のアプローチは次のとおりです。
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();
}
}
検証ロジックを正しいエンティティにカプセル化している場合でも、これはOpen/Closeの原則(拡張の場合はOpen、変更の場合はClose)に違反しているため、実際にはこの検証が好きではありません。この原則に違反すると、コードのメンテナンスがアプリケーションが複雑になると、本当に苦痛になります。どうして?ドメインルールは私たちが認めたいよりも頻繁に変更されるため、ルールがこのようなエンティティに非表示および埋め込みである場合、テスト、読み取り、保守は困難ですが、実際のこのアプローチが気に入らない理由は、検証ルールが変更された場合、ドメインエンティティを編集する必要があるためです。これは本当に単純な例ですが、RLでは検証はもっと複雑になる可能性があります
したがって、Udi Dahanの哲学、役割を明示的にする、およびブルーブックのEric Evansからの推奨に従って、次の試みは、次のような仕様パターンを実装することでした。
class EmailDomainIsAllowedSpecification : IDomainSpecification<Customer>
{
private INotAllowedEmailDomainsResolver invalidEmailDomainsResolver;
public bool IsSatisfiedBy(Customer customer)
{
return !this.invalidEmailDomainsResolver.GetInvalidEmailDomains().Contains(customer.Email);
}
}
しかし、このアプローチに従うためには、検証中の値、この場合は電子メールを渡すために、最初にエンティティを変更する必要があることに気付きました。 、ただし、それらを変更すると、ドメインイベントが発生し、新しい電子メールが有効になるまで発生したくありません。
CQRSアーキテクチャを実装するので、これらのアプローチを検討した後、これを思いつきました:
class EmailDomainIsAllowedValidator : IDomainInvariantValidator<Customer, ChangeEmailCommand>
{
public void IsValid(Customer entity, ChangeEmailCommand command)
{
if(!command.Email.HasValidDomain()) throw new DomainException(“...”);
}
}
それが主なアイデアです。検証を実行するためにエンティティからの値が必要な場合、エンティティはバリデータに渡されます。コマンドにはユーザーからのデータが含まれ、バリデータは注入可能と見なされるためです。検証で必要な場合は、外部依存関係を挿入できるオブジェクト。
現在のジレンマ、検証が個々のオブジェクトにカプセル化されているため、このような設計に満足しています。これにより、ユニットテストが簡単で、保守が簡単で、ドメインの不変条件がユビキタス言語を使用して明示的に表現されます。拡張が容易で、検証ロジックは一元化されており、バリデーターを一緒に使用して複雑なドメインルールを適用できます。そして、私が自分のエンティティの検証をそれらの外に置いていることを知っているときでさえ(あなたはコードの臭いを主張することができます-貧血ドメイン)、しかし私はトレードオフは許容できると思います
しかし、それをクリーンな方法で実装する方法を私が理解していないことが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アーキテクチャでは、コマンドが到着すると、コマンドハンドラーが集約ルートにアクセスし、集計ルートに対して操作を実行すると、検証を配置するための完全なウィンドウが作成されます。
エンティティ内に検証を配置することと、個々のオブジェクトに検証を配置することの利点を少し比較したいと思います。
個々のオブジェクトでの検証
エンティティ内にカプセル化された検証
これについてのあなたの考えを読みたいです
私は他の回答で提示された多くの概念に同意しますが、それらをコードにまとめました。
まず、動作を含む値に値オブジェクトを使用することは、一般的なビジネスルールをカプセル化するための優れた方法であり、電子メールアドレスが最適な候補であることに同意します。ただし、これは一定で頻繁に変更されないルールに限定する傾向があります。より一般的なアプローチを探していると思います。電子メールは単なる例なので、その1つのユースケースには焦点を当てません。
私のアプローチの鍵は、検証がアプリケーションのさまざまな場所でさまざまな目的に役立つことを認識することです。簡単に言えば、現在の操作が予期しない/意図しない結果なしに実行できることを確認するために必要なものだけを検証します。それは、どの検証がどこで行われるべきかという質問につながりますか?
あなたの例では、ドメインエンティティが電子メールアドレスが何らかのパターンや他のルールに準拠していることを本当に気にかけているのか、それともChangeEmailが呼び出されたときに「email」がnullまたは空白にならないように気にかけているのかを自問します。後者の場合、値が存在することを確認するための単純なチェックよりも、ChangeEmailメソッドで必要なのはすべてです。
CQRSでは、アプリケーションの状態を変更するすべての変更は、コマンドハンドラーに実装されたコマンドとして発生します(これまでに示したように)。私は通常、操作がコマンドハンドラーで実行される可能性があることを検証するビジネスルールなどに「フック」を配置します。私は実際に、バリデーターをコマンドハンドラーに挿入するというあなたのアプローチに従います。これにより、ハンドラーを変更せずにルールセットを拡張/置換できます。これらの「動的」ルールを使用すると、エンティティの状態を変更する前に、有効な電子メールアドレスを構成するものなどのビジネスルールを定義できます。さらに、エンティティが無効な状態にならないようにします。しかし、この場合の「無効」はビジネスロジックによって定義され、ご指摘のとおり、非常に不安定です。
CSLAランクを通過した後、カプセル化が壊れているように見えるため、この変更を採用するのは難しいと感じました。しかし、一歩下がって、モデルで検証が実際にどのような役割を果たしているかを尋ねても、カプセル化は破られないことに同意します。
これらのニュアンスは、このテーマについて頭をはっきりさせる上で非常に重要であることがわかりました。メソッド自体に属する不正なデータ(引数の欠落、null値、空の文字列など)を防ぐための検証があり、ビジネスルールが適用されていることを確認するための検証があります。前者の場合、顧客が電子メールアドレスを持っている必要がある場合、ドメインオブジェクトが無効になるのを防ぐために心配する必要がある唯一のルールは、電子メールアドレスがに提供されていることを確認することです。 ChangeEmailメソッド。他のルールは、値自体の有効性に関するより高いレベルの懸念事項であり、ドメインエンティティ自体の有効性には実際には影響しません。
これは、他の開発者との多くの「議論」の源となっていますが、ほとんどの人がより広い視野を持って検証が実際に役立つ役割を調査すると、彼らは光を見る傾向があります。
最後に、UI検証の場所もあります(UIとは、画面、サービスエンドポイントなど、アプリケーションへのインターフェイスとして機能するものを意味します)。 UIのロジックの一部を複製して、ユーザーの対話性を向上させることは、完全に合理的だと思います。しかし、この検証がその単一の目的に役立つため、私はそのような複製を許可します。ただし、注入されたバリデーター/仕様オブジェクトを使用すると、これらのルールを複数の場所で定義することによる悪影響なしに、この方法で再利用が促進されます。
それが役立つかどうかわからない...
検証のためにドメインに大きなコードを挿入することはお勧めしません。厄介な検証のほとんどを、ドメインに欠けている概念の匂いとして見ることで排除しました。あなたが書いたサンプルコードでは、電子メールアドレスの検証が見られます。顧客は電子メールの検証とは何の関係もありません。
構築時にこの検証を行うValueObject
と呼ばれるEmail
を作成してみませんか?
私の経験では、厄介な場所にある検証は、ドメインで見逃されている概念のヒントです。それらはValidatorオブジェクトでキャッチできますが、関連する概念をドメインの一部にするため、値オブジェクトを好みます。
検証を間違った場所に置きました。
そのようなことにはValueObjectsを使用する必要があります。このプレゼンテーションを見る http://www.infoq.com/presentations/Value-Objects-Dan-Bergh-Johnsson 重心としてのデータについても説明します。
たとえば、Email.IsValid(string)の静的検証メソッドを使用するなど、データ検証を再利用する方法のサンプルもあります。
私はプロジェクトを開始しており、ドメインエンティティの外部で検証を実装する予定です。私のドメインエンティティには、不変条件(引数の欠落、null値、空の文字列、コレクションなど)を保護するロジックが含まれます。ただし、実際のビジネスルールはバリデータークラスに存在します。私は@SonOfPirateの考え方です...
私は FluentValidation を使用しています。これにより、基本的に、ドメインエンティティに作用する一連のバリデーターが得られます。別名、仕様パターンです。また、Ericのブルーブックに記載されているパターンに従って、検証を実行するために必要なデータ(データベース、別のリポジトリ、サービスなど)を使用してバリデーターを構築できます。ここにも依存関係を挿入するオプションがあります。これらのバリデーターを作成して再利用することもできます(たとえば、住所バリデーターは従業員バリデーターと会社バリデーターの両方で再利用できます)。 「サービスロケーター」として機能するバリデーターファクトリがあります。
public class ParticipantService : IParticipantService
{
public void Save(Participant participant)
{
IValidator<Participant> validator = _validatorFactory.GetValidator<Participant>();
var results = validator.Validate(participant);
//if the participant is valid, register the participant with the unit of work
if (results.IsValid)
{
if (participant.IsNew)
{
_unitOfWork.RegisterNew<Participant>(participant);
}
else if (participant.HasChanged)
{
_unitOfWork.RegisterDirty<Participant>(participant);
}
}
else
{
_unitOfWork.RollBack();
//do some thing here to indicate the errors:generate an exception (or fault) that contains the validation errors. Or return the results
}
}
}
そして、バリデーターには次のようなコードが含まれます。
public class ParticipantValidator : AbstractValidator<Participant>
{
public ParticipantValidator(DateTime today, int ageLimit, List<string> validCompanyCodes, /*any other stuff you need*/)
{...}
public void BuildRules()
{
RuleFor(participant => participant.DateOfBirth)
.NotNull()
.LessThan(m_today.AddYears(m_ageLimit*-1))
.WithMessage(string.Format("Participant must be older than {0} years of age.", m_ageLimit));
RuleFor(participant => participant.Address)
.NotNull()
.SetValidator(new AddressValidator());
RuleFor(participant => participant.Email)
.NotEmpty()
.EmailAddress();
...
}
}
Webサイト、WinForm、サービスを介したデータの一括読み込みなど、複数の種類のプレゼンテーションをサポートする必要があります。これらすべてを固定することで、システムの機能を単一の一貫した方法で公開する一連のサービスが提供されます。退屈させない理由で、EntityFrameworkまたはORMは使用しません。
これが私がこのアプローチが好きな理由です:
私がやったことが完璧なことだとは言えません。私はまだこの問題に苦しんでいて、一度に1つの戦いを戦っています。しかし、私はこれまで次のことを行ってきました:
検証をカプセル化するための基本的なクラスがあります。
public interface ISpecification<TEntity> where TEntity : class, IAggregate
{
bool IsSatisfiedBy(TEntity entity);
}
internal class AndSpecification<TEntity> : ISpecification<TEntity> where TEntity: class, IAggregate
{
private ISpecification<TEntity> Spec1;
private ISpecification<TEntity> Spec2;
internal AndSpecification(ISpecification<TEntity> s1, ISpecification<TEntity> s2)
{
Spec1 = s1;
Spec2 = s2;
}
public bool IsSatisfiedBy(TEntity candidate)
{
return Spec1.IsSatisfiedBy(candidate) && Spec2.IsSatisfiedBy(candidate);
}
}
internal class OrSpecification<TEntity> : ISpecification<TEntity> where TEntity : class, IAggregate
{
private ISpecification<TEntity> Spec1;
private ISpecification<TEntity> Spec2;
internal OrSpecification(ISpecification<TEntity> s1, ISpecification<TEntity> s2)
{
Spec1 = s1;
Spec2 = s2;
}
public bool IsSatisfiedBy(TEntity candidate)
{
return Spec1.IsSatisfiedBy(candidate) || Spec2.IsSatisfiedBy(candidate);
}
}
internal class NotSpecification<TEntity> : ISpecification<TEntity> where TEntity : class, IAggregate
{
private ISpecification<TEntity> Wrapped;
internal NotSpecification(ISpecification<TEntity> x)
{
Wrapped = x;
}
public bool IsSatisfiedBy(TEntity candidate)
{
return !Wrapped.IsSatisfiedBy(candidate);
}
}
public static class SpecsExtensionMethods
{
public static ISpecification<TEntity> And<TEntity>(this ISpecification<TEntity> s1, ISpecification<TEntity> s2) where TEntity : class, IAggregate
{
return new AndSpecification<TEntity>(s1, s2);
}
public static ISpecification<TEntity> Or<TEntity>(this ISpecification<TEntity> s1, ISpecification<TEntity> s2) where TEntity : class, IAggregate
{
return new OrSpecification<TEntity>(s1, s2);
}
public static ISpecification<TEntity> Not<TEntity>(this ISpecification<TEntity> s) where TEntity : class, IAggregate
{
return new NotSpecification<TEntity>(s);
}
}
そしてそれを使用するために、私は次のことをします:
コマンドハンドラー:
public class MyCommandHandler : CommandHandler<MyCommand>
{
public override CommandValidation Execute(MyCommand cmd)
{
Contract.Requires<ArgumentNullException>(cmd != null);
var existingAR= Repository.GetById<MyAggregate>(cmd.Id);
if (existingIntervento.IsNull())
throw new HandlerForDomainEventNotFoundException();
existingIntervento.DoStuff(cmd.Id
, cmd.Date
...
);
Repository.Save(existingIntervento, cmd.GetCommitId());
return existingIntervento.CommandValidationMessages;
}
集計:
public void DoStuff(Guid id, DateTime dateX,DateTime start, DateTime end, ...)
{
var is_date_valid = new Is_dateX_valid(dateX);
var has_start_date_greater_than_end_date = new Has_start_date_greater_than_end_date(start, end);
ISpecification<MyAggregate> specs = is_date_valid .And(has_start_date_greater_than_end_date );
if (specs.IsSatisfiedBy(this))
{
var evt = new AgregateStuffed()
{
Id = id
, DateX = dateX
, End = end
, Start = start
, ...
};
RaiseEvent(evt);
}
}
仕様は次の2つのクラスに組み込まれています。
public class Is_dateX_valid : ISpecification<MyAggregate>
{
private readonly DateTime _dateX;
public Is_data_consuntivazione_valid(DateTime dateX)
{
Contract.Requires<ArgumentNullException>(dateX== DateTime.MinValue);
_dateX= dateX;
}
public bool IsSatisfiedBy(MyAggregate i)
{
if (_dateX> DateTime.Now)
{
i.CommandValidationMessages.Add(new ValidationMessage("datex greater than now"));
return false;
}
return true;
}
}
public class Has_start_date_greater_than_end_date : ISpecification<MyAggregate>
{
private readonly DateTime _start;
private readonly DateTime _end;
public Has_start_date_greater_than_end_date(DateTime start, DateTime end)
{
Contract.Requires<ArgumentNullException>(start == DateTime.MinValue);
Contract.Requires<ArgumentNullException>(start == DateTime.MinValue);
_start = start;
_end = end;
}
public bool IsSatisfiedBy(MyAggregate i)
{
if (_start > _end)
{
i.CommandValidationMessages.Add(new ValidationMessage(start date greater then end date"));
return false;
}
return true;
}
}
これにより、さまざまな集計に対していくつかの検証を再利用でき、テストが簡単になります。フローが表示されている場合。私はそれについて話し合うことができて本当にうれしいです。
あなたの、
EntityBase
から継承するクラスは、永続層に結合されるため、ドメインモデルを呼び出さないことにします。しかし、それは私の意見です。
オープン/クローズの原則に従うために、電子メール検証ロジックをCustomer
から他のものに移動することはありません。私にとって、オープン/クローズに従うということは、次の階層があることを意味します。
public class User
{
// some basic validation
public virtual void ChangeEmail(string email);
}
public class Employee : User
{
// validates internal email
public override void ChangeEmail(string email);
}
public class Customer : User
{
// validate external email addresses.
public override void ChangeEmail(string email);
}
あなたの提案は、コントロールをドメインモデルから任意のクラスに移動し、カプセル化を破ります。クラス(Customer
)をリファクタリングして、新しいビジネスルールに準拠させたいと思います。
ドメインイベントを使用してシステムの他の部分をトリガーし、より疎結合のアーキテクチャを取得しますが、カプセル化に違反するためにコマンド/イベントを使用しないでください。
例外
DomainException
を投げていることに気づきました。これは、一般的な例外への方法です。引数の例外またはFormatException
を使用してみませんか?それらはエラーをはるかによく説明します。また、将来の例外を防ぐのに役立つコンテキスト情報を含めることを忘れないでください。
更新
クラスの外にロジックを配置することは、問題を抱えています。使用する検証ルールをどのように制御しますか?コードの一部は検証時にSomeVeryOldRule
を使用し、別の部分はNewAndVeryStrictRule
を使用する場合があります。意図的なものではないかもしれませんが、コードベースが大きくなると発生する可能性があります。
OOPファンダメンタルズ(カプセル化)の1つを無視することをすでに決定しているようです。先に進んで、汎用/外部検証フレームワークを使用してください。ただし、警告しなかったとは言わないでください。 ;)
pdate2
あなたの忍耐とあなたの答えに感謝します、そしてそれが私がこの質問を投稿した理由です、私は同じエンティティがそれが有効な状態にあることを保証する責任があるべきだと感じます(そして私は以前のプロジェクトでそれをしました)がそれを置くことの利点個々のオブジェクトは巨大で、私が投稿したように、個々のオブジェクトを使用してカプセル化を維持する方法もありますが、個人的にはデザインにあまり満足していませんが、一方でそれはテーブルの外ではありません、このChangeEmail(IEnumerable>バリデーターを検討してください、文字列メール)私は実装について詳しく考えていません。でも
これにより、プログラマーは任意のルールを指定できます。これは、現在正しいビジネスルールである場合とそうでない場合があります。開発者はただ書くことができます
customer.ChangeEmail(new IValidator<Customer>[] { new NonValidatingRule<Customer>() }, "notAnEmail")
すべてを受け入れます。また、ルールは、ChangeEmail
が呼び出されるすべての場所で指定する必要があります。
ルールエンジンを使用する場合は、シングルトンプロキシを作成します。
public class Validator
{
IValidatorEngine _engine;
public static void Assign(IValidatorEngine engine)
{
_engine = engine;
}
public static IValidatorEngine Current { get { return _engine; } }
}
..そして次のようなドメインモデルメソッド内からそれを使用します
public class Customer
{
public void ChangeEmail(string email)
{
var rules = Validator.GetRulesFor<Customer>("ChangeEmail");
rules.Validate(email);
// valid
}
}
このソリューションの問題は、ルールの依存関係が隠されているため、メンテナンスの悪夢になることです。すべてのドメインモデルメソッドとすべてのメソッドの各ルールシナリオをテストしない限り、すべてのルールが指定されて機能しているかどうかはわかりません。
このソリューションはより柔軟ですが、ビジネスルールが変更されたメソッドをリファクタリングするよりも実装に時間がかかります。
私のOO経験(私はDDDの専門家ではありません)から)コードをエンティティからより高い抽象化レベル(コマンドハンドラーに)に移動すると、コードが重複します。これは、コマンドが毎回実行されるためです。ハンドラーはメールアドレスを取得し、メール検証ルールをインスタンス化する必要があります。この種のコードはしばらくすると腐敗し、非常に悪臭を放ちます。現在の例では、を変更する別のコマンドがない場合は、そうではない可能性があります。メールアドレスですが、他の状況では確かに...
エンティティや電子メール値オブジェクトなど、ルールをより低い抽象化レベルに戻したくない場合は、ルールをグループ化して問題を軽減することを強くお勧めします。したがって、電子メールの例では、次の3つのルールがあります。
if(string.IsNullOrWhitespace(email)) throw new DomainException(“...”);
if(!email.IsEmail()) throw new DomainException();
if(email.Contains(“@mailinator.com”)) throw new DomainException();
EmailValidationRule
グループの一部にすることができ、再利用が容易になります。
私の見解では、検証ロジックをどこに置くかという質問に対する明確な答えはありません。抽象化レベルに応じて、すべてのオブジェクトの一部にすることができます。現在の場合、電子メールアドレスの正式なチェックはEmailValueObject
の一部であり、mailinator
ルールは、ユーザーがそのドメインを指すメールアドレス。したがって、たとえば、誰かが登録なしでユーザーと連絡を取りたい場合は、正式な検証に対してそのユーザーの電子メールをチェックできますが、mailinator
ルールに対してその電子メールをチェックする必要はありません。等々...
したがって、この種の厄介な配置の検証は悪い設計の兆候であると主張した@pjvdsに完全に同意します。カプセル化を破っても利益は得られないと思いますが、それはあなたの選択であり、あなたの苦痛になります。
しばらく前に、このトピックに関するブログ投稿を書きました。投稿の前提は、さまざまなタイプの検証があるということでした。私はそれらを表面的検証とドメインベースのコマンド検証と呼びました。
このシンプルなバージョンはこれです。 「それは数字ですか」や「メールアドレス」のようなものを検証することは、表面的なだけではないことがよくあります。これらは、コマンドがドメインエンティティに到達する前に実行できます。
ただし、検証がドメインにより密接に関連している場合、適切な場所はドメイン内です。たとえば、特定の大型トラックが運ぶことができる貨物の重量と種類についていくつかの規則があるかもしれません。これは、ドメインロジックのように聞こえます。
次に、ハイブリッドタイプがあります。セットベースの検証のようなもの。これらは、コマンドが発行されるかドメインに挿入される前に発生する必要があります(可能な限り回避するようにしてください。依存関係を制限することは良いことです)。
とにかく、ここで完全な投稿を読むことができます: CQRSアプリケーションでコマンドを検証する方法
この例の検証は、エンティティ(または集約ルート)ではなく、値オブジェクトの検証です。
検証を個別の領域に分けます。
Email
値オブジェクトの内部特性を内部的に検証します。アグリゲートが無効な状態になることは決してないというルールを順守します。私はこのプリンシパルを、実用的な値オブジェクトに拡張します。
createNew()
を使用して、ユーザー入力から電子メールをインスタンス化します。これにより、現在のルール(たとえば、「[email protected]」形式)に従って有効になります。
createExisting()
を使用して、永続ストレージから電子メールをインスタンス化します。これは検証を実行しません。これは重要です。昨日は有効でしたが今日は無効である保存された電子メールに対して例外がスローされないようにする必要があります。
_class Email
{
private String value_;
// Error codes
const Error E_LENGTH = "An email address must be at least 3 characters long.";
const Error E_FORMAT = "An email address must be in the '[email protected]' format.";
// Private constructor, forcing the use of factory functions
private Email(String value)
{
this.value_ = value;
}
// Factory functions
static public Email createNew(String value)
{
validateLength(value, E_LENGTH);
validateFormat(value, E_FORMAT);
}
static public Email createExisting(String value)
{
return new Email(value);
}
// Static validation methods
static public void validateLength(String value, Error error = E_LENGTH)
{
if (value.length() < 3)
{
throw new DomainException(error);
}
}
static public void validateFormat(String value, Error error = E_FORMAT)
{
if (/* regular expression fails */)
{
throw new DomainException(error);
}
}
}
_
Email
値オブジェクトの「外部」特性を外部(サービスなど)で検証します。
_class EmailDnsValidator implements IEmailValidator
{
const E_MX_MISSING = "The domain of your email address does not have an MX record.";
private DnsProvider dnsProvider_;
EmailDnsValidator(DnsProvider dnsProvider)
{
dnsProvider_ = dnsProvider;
}
public void validate(String value, Error error = E_MX_MISSING)
{
if (!dnsProvider_.hasMxRecord(/* domain part of email address */))
{
throw new DomainException(error);
}
}
}
class EmailDomainBlacklistValidator implements IEmailValidator
{
const Error E_DOMAIN_FORBIDDEN = "The domain of your email address is blacklisted.";
public void validate(String value, Error error = E_DOMAIN_FORBIDDEN)
{
if (/* domain of value is on the blacklist */))
{
throw new DomainException(error);
}
}
}
_
利点:
createNew()
およびcreateExisting()
ファクトリ関数を使用すると、内部検証を制御できます。
検証方法を直接使用して、特定の検証ルーチンを「オプトアウト」することができます。たとえば、長さのチェックをスキップできます。
外部検証(DNS MXレコードとドメインブラックリスト)を「オプトアウト」することもできます。たとえば、私が取り組んだプロジェクトでは、最初にドメインのMXレコードの存在を検証しましたが、「動的IP」タイプのソリューションを使用している顧客の数が多かったため、最終的にこれを削除しました。
現在の検証ルールに適合しないメールアドレスを永続ストアにクエリするのは簡単ですが、単純なクエリを実行し、各メールを「既存」ではなく「新規」として扱います。例外がスローされると、問題が発生します。そこから、たとえば、FlagCustomerAsHavingABadEmail
コマンドを発行して、例外エラーメッセージをユーザーがメッセージを見たときのガイダンスとして使用できます。
プログラマーがエラーコードを提供できるようにすることで、柔軟性が得られます。たとえば、UpdateEmailAddress
コマンドを送信する場合、「メールアドレスは3文字以上である必要があります」というエラーは自明です。ただし、複数の電子メールアドレス(自宅と職場)を更新する場合、上記のエラーメッセージは、どの電子メールが間違っていたかを示していません。エラーコード/メッセージを提供することで、エンドユーザーにより豊富なフィードバックを提供できます。
私はまだこの概念を試していますが、デコレータを試すことができます。 SimpleInjectorを使用すると、コマンドハンドラーの前に実行される独自の検証クラスを簡単に挿入できます。次に、コマンドは、それがそこまで到達した場合に有効であると見なすことができます。ただし、これは、すべての検証がエンティティではなくコマンドで実行される必要があることを意味します。エンティティが無効な状態になることはありません。ただし、各コマンドは独自の検証を完全に実装する必要があるため、同様のコマンドでルールが重複する可能性がありますが、共通のルールを抽象化して共有するか、異なるコマンドを真に別個のものとして扱うことができます。