簡潔な答え:
あなたは間違ったことを検証しています。
非常に長い答え:
を検証しようとしていますPurchaseOrder
が、それは実装の詳細です。代わりに、検証する必要があるのは操作自体です。この場合はパラメーターpartNumber
とsupplierName
パラメーターです。
これら 2 つのパラメーターを単独で検証するのは面倒ですが、これは設計が原因であり、抽象化が欠けています。
要するに、問題はIPurchaseOrderService
インターフェースにあります。2 つの文字列引数を取るのではなく、1 つの引数 ( Parameter Object ) を取るべきです。これを Parameter Object と呼びましょうCreatePurchaseOrder
:
public class CreatePurchaseOrder
{
public string PartNumber;
public string SupplierName;
}
変更されたIPurchaseOrderService
インターフェースでは:
interface IPurchaseOrderService
{
void CreatePurchaseOrder(CreatePurchaseOrder command);
}
パラメータ オブジェクトは元のCreatePurchaseOrder
引数をラップします。このパラメータ オブジェクトは、発注書の作成の意図を説明するメッセージです。つまり、それはコマンドです。
このコマンドを使用するIValidator<CreatePurchaseOrder>
と、適切な部品サプライヤの存在の確認やユーザー フレンドリなエラー メッセージの報告など、すべての適切な検証を実行できる実装を作成できます。
しかし、なぜIPurchaseOrderService
検証の責任者なのですか? 検証は分野横断的な問題であり、ビジネス ロジックと混合しないようにする必要があります。代わりに、このためのデコレータを定義できます:
public class ValidationPurchaseOrderServiceDecorator : IPurchaseOrderService
{
private readonly IValidator<CreatePurchaseOrder> validator;
private readonly IPurchaseOrderService decoratee;
ValidationPurchaseOrderServiceDecorator(
IValidator<CreatePurchaseOrder> validator,
IPurchaseOrderService decoratee)
{
this.validator = validator;
this.decoratee = decoratee;
}
public void CreatePurchaseOrder(CreatePurchaseOrder command)
{
this.validator.Validate(command);
this.decoratee.CreatePurchaseOrder(command);
}
}
このようにして、単純に実数をラップするだけで検証を追加できますPurchaseOrderService
:
var service =
new ValidationPurchaseOrderServiceDecorator(
new CreatePurchaseOrderValidator(),
new PurchaseOrderService());
もちろん、このアプローチの問題は、システム内のサービスごとにそのようなデコレータ クラスを定義するのが非常に面倒なことです。これにより、深刻なコード公開が発生します。
しかし、問題は欠陥によって引き起こされます。IPurchaseOrderService
通常、特定のサービス( など)ごとにインターフェイスを定義するのは問題があります。を定義したCreatePurchaseOrder
ので、既にそのような定義があります。システム内のすべてのビジネス オペレーションに対して 1 つの抽象化を定義できるようになりました。
public interface ICommandHandler<TCommand>
{
void Handle(TCommand command);
}
この抽象化によりPurchaseOrderService
、次のようにリファクタリングできるようになりました。
public class CreatePurchaseOrderHandler : ICommandHandler<CreatePurchaseOrder>
{
public void Handle(CreatePurchaseOrder command)
{
var po = new PurchaseOrder
{
Part = ...,
Supplier = ...,
};
unitOfWork.Savechanges();
}
}
この設計により、1 つの汎用デコレーターを定義して、システム内のすべてのビジネス オペレーションのすべての検証を処理できるようになりました。
public class ValidationCommandHandlerDecorator<T> : ICommandHandler<T>
{
private readonly IValidator<T> validator;
private readonly ICommandHandler<T> decoratee;
ValidationCommandHandlerDecorator(
IValidator<T> validator, ICommandHandler<T> decoratee)
{
this.validator = validator;
this.decoratee = decoratee;
}
void Handle(T command)
{
var errors = this.validator.Validate(command).ToArray();
if (errors.Any())
{
throw new ValidationException(errors);
}
this.decoratee.Handle(command);
}
}
このデコレーターが以前に定義された とほぼ同じであることに注意してくださいValidationPurchaseOrderServiceDecorator
。ただし、現在はジェネリック クラスになっています。このデコレーターは、新しいサービス クラスをラップできます。
var service =
new ValidationCommandHandlerDecorator<PurchaseOrderCommand>(
new CreatePurchaseOrderValidator(),
new CreatePurchaseOrderHandler());
ただし、このデコレータは汎用であるため、システム内のすべてのコマンド ハンドラをラップできます。わお!DRYであるということはどうですか?
この設計により、横断的な懸念事項を後で簡単に追加することもできます。たとえば、あなたのサービスは現在SaveChanges
、作業単位の呼び出しを担当しているようです。これは分野横断的な懸念事項と見なすこともでき、デコレーターに簡単に抽出できます。このようにして、サービス クラスは非常に単純になり、テストするコードが少なくなります。
バリデータは次のCreatePurchaseOrder
ようになります。
public sealed class CreatePurchaseOrderValidator : IValidator<CreatePurchaseOrder>
{
private readonly IRepository<Part> partsRepository;
private readonly IRepository<Supplier> supplierRepository;
public CreatePurchaseOrderValidator(
IRepository<Part> partsRepository,
IRepository<Supplier> supplierRepository)
{
this.partsRepository = partsRepository;
this.supplierRepository = supplierRepository;
}
protected override IEnumerable<ValidationResult> Validate(
CreatePurchaseOrder command)
{
var part = this.partsRepository.GetByNumber(command.PartNumber);
if (part == null)
{
yield return new ValidationResult("Part Number",
$"Part number {command.PartNumber} does not exist.");
}
var supplier = this.supplierRepository.GetByName(command.SupplierName);
if (supplier == null)
{
yield return new ValidationResult("Supplier Name",
$"Supplier named {command.SupplierName} does not exist.");
}
}
}
コマンド ハンドラは次のようになります。
public class CreatePurchaseOrderHandler : ICommandHandler<CreatePurchaseOrder>
{
private readonly IUnitOfWork uow;
public CreatePurchaseOrderHandler(IUnitOfWork uow)
{
this.uow = uow;
}
public void Handle(CreatePurchaseOrder command)
{
var order = new PurchaseOrder
{
Part = this.uow.Parts.Get(p => p.Number == partNumber),
Supplier = this.uow.Suppliers.Get(p => p.Name == supplierName),
// Other properties omitted for brevity...
};
this.uow.PurchaseOrders.Add(order);
}
}
コマンド メッセージはドメインの一部になることに注意してください。ユース ケースとコマンドの間には 1 対 1 のマッピングがあり、エンティティを検証する代わりに、それらのエンティティが実装の詳細になります。コマンドはコントラクトになり、検証されます。
コマンドにできるだけ多くの ID が含まれていると、作業がずっと楽になることに注意してください。したがって、システムは次のようにコマンドを定義することで利益を得ることができます:
public class CreatePurchaseOrder
{
public int PartId;
public int SupplierId;
}
これを行うと、指定された名前のパーツが存在するかどうかを確認する必要がなくなります。プレゼンテーション層 (または外部システム) が ID を渡したので、その部分の存在を検証する必要はもうありません。もちろん、その ID による部分がない場合、コマンド ハンドラーは失敗するはずですが、その場合、プログラミング エラーまたは同時実行の競合が発生します。いずれの場合も、表現力豊かなユーザーフレンドリーな検証エラーをクライアントに伝える必要はありません。
ただし、これにより、適切な ID を取得するという問題がプレゼンテーション層に移されます。プレゼンテーション層では、ユーザーはリストからパーツを選択して、そのパーツの ID を取得する必要があります。それでも、システムをより簡単かつスケーラブルにするためにこれを経験しました。
また、参照している記事のコメント セクションに記載されている問題のほとんどを解決します。
- コマンドを簡単にシリアル化してモデルをバインドできるため、エンティティのシリアル化の問題は解消されます。
- DataAnnotation 属性はコマンドに簡単に適用でき、これによりクライアント側 (Javascript) の検証が可能になります。
- デコレーターは、データベース トランザクションで完全な操作をラップするすべてのコマンド ハンドラーに適用できます。
- (コントローラーの ModelState を介して) コントローラーとサービス層の間の循環参照が削除され、コントローラーがサービス クラスを新規作成する必要がなくなります。
このタイプのデザインについて詳しく知りたい場合は、この記事を必ずチェックしてください。