49

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. オブジェクトを外部で検証する (コマンド ハンドラーから)

オプション 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) の法則を破っていませんか?
    • プロ。私は中央集権的です
    • プロ。再利用できる可能性があります
    • プロ。必要に応じて、外部依存関係を簡単に注入できます
    • プロ。プラグイン モデルを使用している場合、アプリケーション全体を再コンパイルする必要なく、新しいアセンブリをドロップするだけで新しいバリデータを追加できます。
    • プロ。ルールエンジンを実装する方が簡単だろう
    • 短所 カプセル化の解除
    • 短所 カプセル化が必須の場合、個々のバリデーターをエンティティ (集約) メソッドに渡す必要があります。
  • エンティティ内にカプセル化された検証

    • プロ。カプセル化?
    • プロ。再利用可能?

これについてのあなたの考えを読んでみたいです

4

11 に答える 11

13

他の回答で提示された多くの概念に同意しますが、コードにまとめました。

まず、動作を含む値に値オブジェクトを使用することは、一般的なビジネス ルールをカプセル化する優れた方法であり、電子メール アドレスが最適な候補であることに同意します。ただし、私はこれを一定で頻繁に変更されないルールに限定する傾向があります。より一般的なアプローチを探していると思いますが、電子メールは一例にすぎないため、その 1 つのユース ケースには焦点を当てません。

私のアプローチの鍵は、検証がアプリケーション内のさまざまな場所でさまざまな目的を果たすことを認識することです。簡単に言えば、現在の操作が予期しない/意図しない結果なしに実行できることを確認するために必要なものだけを検証します。これにより、どの検証をどこで行うべきかという疑問が生じます。

あなたの例では、ドメインエンティティが電子メールアドレスが何らかのパターンやその他の規則に準拠していることを本当に気にかけているのか、それともChangeEmailが呼び出されたときに「メール」がnullまたは空白にならないことを単に気にしているのかを自問します. 後者の場合、ChangeEmail メソッドで必要なのは、値が存在することを確認するための単純なチェックだけです。

CQRS では、アプリケーションの状態を変更するすべての変更は、コマンド ハンドラーに実装されたコマンドとして発生します (前述のとおり)。私は通常、操作がコマンド ハンドラーで実行される可能性があることを検証するビジネス ルールなどに「フック」を配置します。私は実際にコマンドハンドラーにバリデーターを挿入するというあなたのアプローチに従います。これにより、ハンドラーを変更せずにルールセットを拡張/置換できます。これらの「動的な」ルールにより、エンティティの状態を変更する前に、有効な電子メール アドレスを構成するものなどのビジネス ルールを定義できます。さらに、エンティティが無効な状態にならないようにします。しかし、この場合の「無効」はビジネスロジックによって定義されており、ご指摘のとおり非常に揮発性です。

CSLA のランクを上げてきたので、カプセル化を破るように見えるため、この変更を採用するのは難しいと感じました。しかし、一歩下がって、検証がモデルで実際にどのような役割を果たしているのかを尋ねると、カプセル化は壊れていないことに同意します。

これらのニュアンスは、この主題について頭をすっきりさせる上で非常に重要であることがわかりました. メソッド自体に属する不正なデータ (引数の欠落、null 値、空の文字列など) を防止するための検証と、ビジネス ルールが適用されていることを確認するための検証があります。前者の場合、顧客が電子メール アドレスを持っている必要がある場合、私のドメイン オブジェクトが無効にならないように注意する必要がある唯一のルールは、電子メール アドレスが提供されていることを確認することです。 ChangeEmail メソッド。他のルールは、値自体の有効性に関するより高いレベルの問題であり、実際にはドメイン エンティティ自体の有効性には影響しません。

これは、仲間の開発者との多くの「議論」の源でしたが、ほとんどの人がより広い視野を持ち、検証が実際に果たす役割を調査すると、彼らは光を見る傾向があります.

最後に、UI 検証の場所もあります (UI とは、画面、サービス エンドポイントなど、アプリケーションへのインターフェイスとして機能するものを意味します)。UI のロジックの一部を複製して、ユーザーにより良い対話性を提供することは完全に合理的だと思います。しかし、私がそのような重複を許可するのは、この検証がその単一の目的に役立つためです。ただし、注入されたバリデータ/仕様オブジェクトを使用すると、これらのルールを複数の場所で定義するというマイナスの影響なしに、この方法で再利用が促進されます。

それが役立つかどうかはわかりません...

于 2012-06-11T13:31:01.233 に答える
8

検証のためにドメインに大きなコードを追加することはお勧めしません。私たちは、私たちのドメインに欠けている概念の匂いとしてそれらを見ることによって、私たちの厄介な配置された検証のほとんどを排除しました。あなたが書いたサンプルコードには、電子メールアドレスの検証が表示されています。顧客は電子メールの検証とは何の関係もありません。

構築時にこの検証を行うValueObject呼び出しを行ってみませんか?Email

私の経験では、厄介な配置の検証は、ドメインで見逃されている概念のヒントです。それらはValidatorオブジェクトでキャッチできますが、関連する概念をドメインの一部にするため、値オブジェクトを使用することをお勧めします。

于 2012-06-06T06:08:37.573 に答える
6

私はプロジェクトの開始時に、ドメイン エンティティの外部で検証を実装しようとしています。ドメイン エンティティには、不変条件 (引数の欠落、null 値、空の文字列、コレクションなど) を保護するためのロジックが含まれます。しかし、実際のビジネス ルールはバリデータ クラスに存在します。私は@SonOfPirateの考え方です...

私はFluentValidationを使用しています。これは基本的に、ドメイン エンティティに作用するバリデーターの束を提供します。別名、仕様パターンです。また、Eric のブルーブックで説明されているパターンに従って、検証を実行するために必要なデータ (データベース、別のリポジトリ、またはサービスからのもの) を使用してバリデーターを構築できます。ここにも依存関係を注入するオプションがあります。これらのバリデーターを作成して再利用することもできます (たとえば、住所バリデーターは従業員バリデーターと会社バリデーターの両方で再利用できます)。「サービスロケータ」として機能する Validator ファクトリがあります。

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、サービスを介したデータの一括読み込みなど、複数のタイプのプレゼンテーションをサポートする必要があります。これらすべてをピン留めすると、システムの機能を単一の一貫した方法で公開する一連のサービスになります。退屈させないために、Entity Framework や ORM は使用しません。

私がこのアプローチを好む理由は次のとおりです。

  • バリデータに含まれるビジネス ルールは、完全に単体テスト可能です。
  • より単純なルールからより複雑なルールを構成できる
  • システム内の複数の場所でバリデーターを使用できます (Web サイトと Winforms、および機能を公開するサービスをサポートしています)。私はそれを扱うことができます。
  • すべての検証は 1 つの場所で表現され、これを挿入して構成する方法/場所を選択できます。
于 2012-06-12T15:05:28.943 に答える
5

検証を間違った場所に置きます。

そのようなものには ValueObjects を使用する必要があります。このプレゼンテーションを見るhttp://www.infoq.com/presentations/Value-Objects-Dan-Bergh-Johnsson また、重心としてのデータについても説明します。

静的検証メソッド ala Email.IsValid(string) を使用するなど、データ検証を再利用する方法のサンプルもあります。

于 2012-06-06T19:44:10.303 に答える
2

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。これは、一般的な例外への道です。引数 exceptions または ? を使用しないのはなぜFormatExceptionですか? 彼らはエラーをよりよく説明しています。また、将来の例外を防ぐのに役立つコンテキスト情報を含めることを忘れないでください。

アップデート

クラスの外にロジックを配置すると、問題が発生します。使用する検証ルールをどのように制御しますか? コードの一部はSomeVeryOldRule検証時に使用し、別の部分はNewAndVeryStrictRule. 意図的ではないかもしれませんが、コード ベースが大きくなると発生する可能性があります。

OOP の基本 (カプセル化) の 1 つを無視することを既に決定しているようです。先に進んで、汎用/外部検証フレームワークを使用してください。ただし、私が警告しなかったとは言わないでください ;)

Update2

あなたの忍耐とあなたの答えに感謝します。それが私がこの質問を投稿した理由です。私はエンティティが有効な状態であることを保証する責任があると感じています(そして私は以前のプロジェクトでそれを行いました)が、それを配置する利点個々のオブジェクトは巨大で、私が投稿したように、個々のオブジェクトを使用してカプセル化を維持する方法さえありますが、個人的にはデザインにあまり満足していませんが、一方でそれはテーブルの外ではありません。 、文字列メール) 実装については詳しく考えていません。けれど

これにより、プログラマーは任意のルールを指定できますが、それは現在正しいビジネス ルールである場合とそうでない場合があります。開発者はただ書くことができました

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
    }

}

このソリューションの問題は、ルールの依存関係が隠されているため、メンテナンスの悪夢になることです。すべてのドメイン モデル メソッドとすべてのメソッドの各ルール シナリオをテストしない限り、すべてのルールが指定され、機能しているかどうかはわかりません。

ソリューションはより柔軟ですが、ビジネスルールが変更されたメソッドをリファクタリングするよりも実装に時間がかかります。

于 2012-06-04T10:16:58.233 に答える
2

私がやったことは完璧なことだとは言えません. しかし、私はこれまで次のことを行ってきました:

検証をカプセル化するための基本的なクラスがあります。

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;
        }
    }

これにより、さまざまな集計に対していくつかの検証を再利用でき、テストが簡単になります。その中に流れが見られる場合。喜んで議論させていただきます。

あなたの、

于 2012-06-04T13:42:13.970 に答える
1

私の 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に完全に同意します。カプセル化を破ることで何の利益も得られないと思いますが、それはあなたの選択であり、あなたの苦痛になるでしょう.

于 2014-09-25T03:02:07.970 に答える
0

あなたの例の検証は、エンティティ(または集約ルート)ではなく、値オブジェクトの検証です。

検証を個別の領域に分けます。

  1. Email値オブジェクトの内部特性を内部的に検証します。

私は、集計が無効な状態であってはならないという規則に従います。このプリンシパルを実用的な値オブジェクトに拡張します。

createNew()ユーザー入力から電​​子メールをインスタンス化するために使用します。これにより、現在のルール (「user@email.com」形式など) に従って有効になります。

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 'user@email.com' 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);
        }
    }

}
  1. 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 文字の長さである必要があります」というエラーは自明です。ただし、複数の電子メール アドレス (自宅と職場) を更新する場合、上記のエラー メッセージはどの電子メールが間違っていたかを示しません。エラー コード/メッセージを提供することで、より豊富なフィードバックをエンド ユーザーに提供できます。

于 2016-05-20T04:46:22.827 に答える
-4

こちら で説明されているように、ドメイン イベントでメッセージ ベースのソリューションを使用できます。

例外は、すべての検証エラーに対する適切な方法ではありません。無効なエンティティが例外的なケースであるとは言えません。

検証が簡単でない場合は、集計を検証するロジックをサーバー上で直接実行できます。新しい入力を設定しようとしている間に、ユーザー(またはドメインを使用しているアプリケーション)に伝えるドメイン イベントを発生させることができます。入力が正しくない理由。

于 2012-06-04T10:06:25.657 に答える