特に垂直スライスアーキテクチャのコンテキストでは、どこで検証を行う必要がありますか? DDDを使用する場合、エンティティ内に検証を配置できます。 しかし、個人的には、検証はエンティティの責任とはあまり合わないと思います。
多くの場合、エンティティ内の検証は注釈を使用して行われます。 Customerがあり、FirstName / LastNameフィールドが必須であるとします:
public class Customer { [Required] public string FirstName { get; set; } [Required] public string LastName { get; set; } }
このアプローチには2つの問題があります。
- 検証前にエンティティの状態を変更します。つまり、エンティティが無効な状態になっている可能性があります
- 操作のコンテキストが不明確です(ユーザーが何をしようとしているか)
また、検証エラー(通常はORMによって生成される)をユーザーに表示できますが、元の意図と状態実装の詳細を比較するのはそれほど簡単ではありません。 原則として、私はこのアプローチを避けようとします。
ただし、DDDを使用する場合は、メソッドを追加することで状態の変化の問題を回避できます。
public class Customer { public string FirstName { get; private set; } public string LastName { get; private set; } public void ChangeName(string firstName, string lastName) { if (firstName == null) throw new ArgumentNullException(nameof(firstName)); if (lastName == null) throw new ArgumentNullException(nameof(lastName)); FirstName = firstName; LastName = lastName; } }
例外は検証エラーを表示する唯一の方法であるため、少しだけ良いですが、ほんの少しです。 例外は好きではないので、コマンド結果のいくつかのバージョンを使用します。
public class Customer { public string FirstName { get; private set; } public string LastName { get; private set; } public CommandResult ChangeName(ChangeNameCommand command) { if (command.FirstName == null) return CommandResult.Fail("First name cannot be empty."); if (lastName == null) return CommandResult.Fail("Last name cannot be empty."); FirstName = command.FirstName; LastName = command.LastName; return CommandResult.Success; } }
繰り返しになりますが、一度にエラーが1つしか返されないため、ユーザーにエラーを表示するのは面倒です。 それらをすべて一緒に返すこともできますが、どのようにしてそれらを画面上のフィールド名と一致させることができますか? まさか。 明らかに、エンティティはチームの検証にはあまり適していません。 ただし、検証フレームワークはこれに最適です。
コマンド検証
コマンド検証をエンティティ/集計にシフトする代わりに、私は完全に不変式に依存しています。 不変条件の全体のポイントは、ある状態から別の状態に全体では完全に移動でき、部分的には移動できないという信念です。 つまり、これはクエリの検証ではなく、状態間の遷移に関するものです。
このアプローチでは、エンティティではなくチームとアクションを中心に検証が構築されます。 私はこのようなことをすることができます:
public class ChangeNameCommand { [Required] public string FirstName { get; set; } [Required] public string LastName { get; set; } } public class Customer { public string FirstName { get; private set; } public string LastName { get; private set; } public void ChangeName(ChangeNameCommand command) { FirstName = command.FirstName; LastName = command.LastName; } }
私の検証属性はチーム自体にあり、チームが有効な場合にのみ、それをエンティティに適用して新しい状態に変換できます。 エンティティ内では、ChangeNameCommandコマンドを処理し、新しい状態に遷移するだけで、不変式が実行されることを確認できます。 多くのプロジェクトで、FluentValidationを使用しています。
public class ChangeNameCommand { public string FirstName { get; set; } public string LastName { get; set; } } public class ChangeNameValidator : AbstractValidator<ChangeNameCommand> { public ChangeNameValidator() { RuleFor(m => m.FirstName).NotNull().Length(3, 50); RuleFor(m => m.LastName).NotNull().Length(3, 50); } } public class Customer { public string FirstName { get; private set; } public string LastName { get; private set; } public void ChangeName(ChangeNameCommand command) { FirstName = command.FirstName; LastName = command.LastName; } }
ここでの主な違いは、エンティティではなくチームを検証することです。 エンティティ自体は検証用のライブラリではないため、コマンドレベルで検証を行う方がはるかに正確(はるかにクリーン)です。 同時に、このインターフェイスが最初に構築されたのはチームの周りであったため、検証エラーはインターフェイスと完全に相関しています。
エンティティではなくコマンドを検証し、エッジで検証を実行します。