web-dev-qa-db-ja.com

DDDでグローバルルール検証を配置する場所

私はDDDを初めて使い、実際にそれを適用しようとしています。 nullチェック、空の文字列チェックなど、エンティティコンストラクター/プロパティに直接行く検証ロジックについての質問はありません。しかし、「一意のユーザー名」などのいくつかのグローバルルールの検証をどこに置くのでしょうか。

したがって、エンティティUserがあります。

public class User : IAggregateRoot
{
   private string _name;

   public string Name
   {
      get { return _name; }
      set { _name = value; }
   }

   // other data and behavior
}

そしてユーザーのためのリポジトリ

public interface IUserRepository : IRepository<User>
{
   User FindByName(string name);
}

オプションは次のとおりです。

  1. エンティティにリポジトリを挿入する
  2. ファクトリにリポジトリを注入する
  3. ドメインサービスで操作を作成します
  4. ???

そして、各オプションの詳細:

1。エンティティにリポジトリを挿入

エンティティのコンストラクタ/プロパティでリポジトリを照会できます。しかし、エンティティのリポジトリへの参照を維持することは悪臭だと思います。

public User(IUserRepository repository)
{
    _repository = repository;
}

public string Name
{
    get { return _name; }
    set 
    {
       if (_repository.FindByName(value) != null)
          throw new UserAlreadyExistsException();

       _name = value; 
    }
}

更新:DIを使用して、Specificationオブジェクトを介してUserとIUserRepository間の依存関係を非表示にすることができます。

2。リポジトリをファクトリに挿入する

この検証ロジックをUserFactoryに配置できます。しかし、既存のユーザーの名前を変更したい場合はどうでしょうか?

。ドメインサービスでの操作の作成

ユーザーを作成および編集するためのドメインサービスを作成できます。しかし、誰かはそのサービスを呼び出さずにユーザーの名前を直接編集できます...

public class AdministrationService
{
    private IUserRepository _userRepository;

    public AdministrationService(IUserRepository userRepository)
    {
        _userRepository = userRepository;
    }

    public void RenameUser(string oldName, string newName)
    {
        if (_userRepository.FindByName(newName) != null)
            throw new UserAlreadyExistException();

        User user = _userRepository.FindByName(oldName);
        user.Name = newName;
        _userRepository.Save(user);
    }
}

4。???

エンティティのグローバル検証ロジックはどこに配置しますか?

ありがとう!

63

ほとんどの場合、この種のルールはSpecificationオブジェクトに配置するのが最適です。これらのSpecificationsをドメインパッケージに配置できるため、ドメインパッケージを使用するすべてのユーザーがそれらにアクセスできます。仕様を使用すると、ビジネスルールをエンティティにバンドルできます。サービスやリポジトリへの望ましくない依存関係を持つ読みにくいエンティティを作成する必要はありません。必要に応じて、サービスまたはリポジトリへの依存関係を仕様に挿入できます。

コンテキストに応じて、仕様オブジェクトを使用してさまざまなバリデーターを作成できます。

エンティティの主な関心事は、ビジネスの状態を追跡することです-それは十分な責任であり、検証に関係するべきではありません。

public class User
{
    public string Id { get; set; }
    public string Name { get; set; }
}

2つの仕様:

public class IdNotEmptySpecification : ISpecification<User>
{
    public bool IsSatisfiedBy(User subject)
    {
        return !string.IsNullOrEmpty(subject.Id);
    }
}


public class NameNotTakenSpecification : ISpecification<User>
{
    // omitted code to set service; better use DI
    private Service.IUserNameService UserNameService { get; set; } 

    public bool IsSatisfiedBy(User subject)
    {
        return UserNameService.NameIsAvailable(subject.Name);
    }
}

そしてバリデータ:

public class UserPersistenceValidator : IValidator<User>
{
    private readonly IList<ISpecification<User>> Rules =
        new List<ISpecification<User>>
            {
                new IdNotEmptySpecification(),
                new NameNotEmptySpecification(),
                new NameNotTakenSpecification()
                // and more ... better use DI to fill this list
            };

    public bool IsValid(User entity)
    {
        return BrokenRules(entity).Count() > 0;
    }

    public IEnumerable<string> BrokenRules(User entity)
    {
        return Rules.Where(rule => !rule.IsSatisfiedBy(entity))
                    .Select(rule => GetMessageForBrokenRule(rule));
    }

    // ...
}

完全を期すために、インターフェースは次のとおりです。

public interface IValidator<T>
{
    bool IsValid(T entity);
    IEnumerable<string> BrokenRules(T entity);
}

public interface ISpecification<T>
{
    bool IsSatisfiedBy(T subject);
}

注意事項

Vijay Patelの以前の答えは正しい方向にあると思いますが、少しずれているように感じます。彼は、ユーザーエンティティは仕様に依存することを示唆しています。仕様はこれとは逆であると私は信じています。このように、仕様の依存関係を通じてエンティティをサービス、リポジトリ、およびコンテキストに依存させることなく、仕様をサービス、リポジトリ、およびコンテキストに依存させることができます。

参照

ドメイン主導型設計での検証 の良い答えのある関連する質問。

Eric Evansは、検証、選択、およびオブジェクト構築のための仕様パターンの使用について chapter 9、pp 145 で説明しています。

この 仕様パターンに関する記事 .Netのアプリケーションを使用すると、興味があるかもしれません。

56
Marijn

ユーザー入力の場合、エンティティのプロパティを変更できないようにすることはお勧めしません。たとえば、検証に合格しなかった場合でも、インスタンスを使用して検証結果を含むユーザーインターフェイスにインスタンスを表示し、ユーザーがエラーを修正できるようにすることができます。

Jimmy Nilssonは、「ドメイン駆動の設計とパターンの適用」で、永続化だけでなく、特定の操作を検証することを推奨しています。エンティティは正常に永続化できますが、実際の検証は、エンティティが状態を変更しようとしているときに発生します。たとえば、「注文済み」状態が「購入済み」に変更されます。

作成中、インスタンスは保存のために有効である必要があります。これには一意性のチェックが含まれます。これは、一意性をチェックする必要があるだけでなく、たとえば、クライアントの信用度や店舗での在庫状況も確認する必要がある注文に対して有効な場合とは異なります。

したがって、検証ロジックは、プロパティの割り当てでは呼び出さないでください。永続的かどうかにかかわらず、集約レベルの操作で呼び出す必要があります。

11
George Polevoy

編集:他の回答から判断すると、そのような「ドメインサービス」の正しい名前はspecificationです。より詳細なコードサンプルを含め、これを反映するように回答を更新しました。

オプション3を選択します。作成する ドメインサービス 検証を実行する実際のロジックをカプセル化する仕様。たとえば、仕様では最初にリポジトリを呼び出しますが、後の段階でそれをWebサービス呼び出しに置き換えることができます。抽象仕様の背後にあるすべてのロジックがあると、全体的な設計がより柔軟になります。

誰かが名前を検証せずに編集できないようにするには、名前を編集するために、仕様を必須の要素にします。これを実現するには、エンティティのAPIを次のように変更します。

public class User
{
    public string Name { get; private set; }

    public void SetName(string name, ISpecification<User, string> specification)
    {
        // Insert basic null validation here.

        if (!specification.IsSatisfiedBy(this, name))
        {
            // Throw some validation exception.
        }

        this.Name = name;
    }
}

public interface ISpecification<TType, TValue>
{
    bool IsSatisfiedBy(TType obj, TValue value);
}

public class UniqueUserNameSpecification : ISpecification<User, string>
{
    private IUserRepository repository;

    public UniqueUserNameSpecification(IUserRepository repository)
    {
        this.repository = repository;
    }

    public bool IsSatisfiedBy(User obj, string value)
    {
        if (value == obj.Name)
        {
            return true;
        }

        // Use this.repository for further validation of the name.
    }
}

呼び出しコードは次のようになります。

var userRepository = IoC.Resolve<IUserRepository>();
var specification = new UniqueUserNameSpecification(userRepository);

user.SetName("John", specification);

そしてもちろん、単体テストでISpecificationをモックしてテストを簡単にすることができます。

7

Specificationを使用してルールをカプセル化します。次に、UserNameプロパティが更新されたときに(またはそれを必要とする可能性のある他の場所から)呼び出すことができます。

public class UniqueUserNameSpecification : ISpecification
{
  public bool IsSatisifiedBy(User user)
  {
     // Check if the username is unique here
  }
}

public class User
{
   string _Name;
   UniqueUserNameSpecification _UniqueUserNameSpecification;  // You decide how this is injected 

   public string Name
   {
      get { return _Name; }
      set
      {
        if (_UniqueUserNameSpecification.IsSatisifiedBy(this))
        {
           _Name = value;
        }
        else
        {
           // Execute your custom warning here
        }
      }
   }
}

ルールは常に実行されるため、別の開発者がUser.Nameを直接変更しようとしても問題ありません。

詳細はこちら

3
Vijay Patel

私はDDDの専門家ではありませんが、同じ質問をしてきましたが、これが私が思いついたものです。通常、検証ロジックはコンストラクター/ファクトリーとセッターに入る必要があります。このようにして、常に有効なドメインオブジェクトがあることを保証します。ただし、検証にパフォーマンスに影響を与えるデータベースクエリが含まれる場合、効率的な実装には別の設計が必要です。

(1)エンティティの注入:エンティティの注入は技術的に困難な場合があり、データベースロジックの断片化のためにアプリケーションパフォーマンスの管理が非常に困難になります。一見単純な操作が、パフォーマンスに予期しない影響を与える可能性があります。また、同じ種類のエンティティのグループでの操作用にドメインオブジェクトを最適化することもできなくなり、単一のグループクエリを作成できなくなり、代わりに各エンティティに対して常に個別のクエリができます。

(2)Injecting repository:リポジトリにビジネスロジックを配置しないでください。リポジトリをシンプルかつ集中してください。それらはコレクションであるかのように動作し、オブジェクトを追加、削除、検索するためのロジックのみを含む必要があります(他のオブジェクトへのfindメソッドをスピンオフする場合もあります)。

(3)ドメインサービスこれは、データベースクエリを必要とする検証を処理する最も論理的な場所のようです。優れた実装は、コンストラクター/ファクトリーおよびセッターに関連するパッケージをプライベートにするため、エンティティーはドメインサービスでのみ作成/変更できます。

2
Kdeveloper

私のCQRSフレームワークでは、すべてのコマンドハンドラークラスにValidateCommandメソッドも含まれており、ドメイン内の適切なビジネス/検証ロジックを呼び出します(ほとんどがエンティティメソッドまたはエンティティ静的メソッドとして実装されています)。

したがって、呼び出し元は次のようにします。

if (cmdService.ValidateCommand(myCommand) == ValidationResult.OK)
{
    // Now we can assume there will be no business reason to reject
    // the command
    cmdService.ExecuteCommand(myCommand); // Async
}

専用のコマンドハンドラーにはすべて、次のようなラッパーロジックが含まれています。

public ValidationResult ValidateCommand(MakeCustomerGold command)
{
    var result = new ValidationResult();
    if (Customer.CanMakeGold(command.CustomerId))
    {
        // "OK" logic here
    } else {
        // "Not OK" logic here
    }
}

次に、コマンドハンドラのExecuteCommandメソッドがValidateCommand()を再度呼び出します。そのため、クライアントが気にならなかったとしても、想定されていないドメインでは何も起こりません。

1
Roy Dictus

私はオプション3が好きです。最も単純な実装は次のようになります。

public interface IUser
{
    string Name { get; }
    bool IsNew { get; }
}

public class User : IUser
{
    public string Name { get; private set; }
    public bool IsNew { get; private set; }
}

public class UserService : IUserService
{
    public void ValidateUser(IUser user)
    {
        var repository = RepositoryFactory.GetUserRepository(); // use IoC if needed

        if (user.IsNew && repository.UserExists(user.Name))
            throw new ValidationException("Username already exists");
    }
}
0
SlavaGu

IsUserNameValid()などのメソッドを作成し、どこからでもアクセスできるようにします。自分でユーザーサービスに追加します。これを行っても、将来の変更が発生したときに制限されません。検証コードを1つの場所(実装)に保持し、検証に変更があれば、それに依存する他のコードを変更する必要はありません。視覚的に表示するためのuiなど、後で複数の場所から呼び出す必要がある場合があります。例外処理に頼る必要はありません。正しい操作のためのサービスレイヤー、および保存されたアイテムが有効であることを確認するためのリポジトリー(キャッシュ、データベースなど)レイヤー。

0
Charles Lambert