简短的回答:
您正在验证错误的事情。
很长的答案:
您正在尝试验证 aPurchaseOrder
但这是一个实现细节。相反,您应该验证的是操作本身,在这种情况下是partNumber
andsupplierName
参数。
自己验证这两个参数会很尴尬,但这是由您的设计引起的——您缺少抽象。
长话短说,问题出在你的IPurchaseOrderService
界面上。它不应该采用两个字符串参数,而是一个单一的参数(一个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);
}
}
这样,您可以通过简单地包装一个 real 来添加验证PurchaseOrderService
:
var service =
new ValidationPurchaseOrderServiceDecorator(
new CreatePurchaseOrderValidator(),
new PurchaseOrderService());
当然,使用这种方法的问题是,为系统中的每个服务定义这样的装饰器类真的很尴尬。这将导致严重的代码发布。
但问题是由一个缺陷引起的。为每个特定服务(例如IPurchaseOrderService
)定义一个接口通常是有问题的。您定义了CreatePurchaseOrder
,因此,已经有了这样的定义。您现在可以为系统中的所有业务操作定义一个抽象:
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();
}
}
通过这种设计,您现在可以定义一个通用装饰器来处理系统中每个业务操作的所有验证:
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);
}
}
请注意,命令消息将成为您域的一部分。用例和命令之间存在一对一的映射,而不是验证实体,这些实体将是一个实现细节。这些命令成为合同并将得到验证。
请注意,如果您的命令包含尽可能多的 ID,它可能会让您的生活更轻松。因此,您的系统可以从如下定义命令中受益:
public class CreatePurchaseOrder
{
public int PartId;
public int SupplierId;
}
当您这样做时,您不必检查给定名称的部件是否存在。表示层(或外部系统)向您传递了一个 ID,因此您不必再验证该部分的存在。当该 ID 没有任何部分时,命令处理程序当然应该失败,但在这种情况下,要么存在编程错误,要么存在并发冲突。在任何一种情况下,都无需将富有表现力的用户友好验证错误传达回客户端。
然而,这确实将获取正确 ID 的问题转移到了表示层。在表示层中,用户必须从列表中选择一个零件,以便我们获取该零件的 ID。但我仍然经历了这一点,使系统更容易和可扩展。
它还解决了您所指文章的评论部分中所述的大多数问题,例如:
- 实体序列化的问题消失了,因为命令可以很容易地序列化和模型绑定。
- DataAnnotation 属性可以很容易地应用于命令,这使得客户端(Javascript)验证成为可能。
- 装饰器可以应用于将完整操作包装在数据库事务中的所有命令处理程序。
- 它消除了控制器和服务层之间的循环引用(通过控制器的 ModelState),从而消除了控制器对新服务类的需要。
如果你想了解更多关于这种类型的设计,你绝对应该看看这篇文章。