簡短的回答:
您要驗證錯誤的事情。
很長的回答:
您試圖驗證PurchaseOrder
但是這是一個實現細節。相反,你應該驗證的是操作本身,在這種情況下,參數爲partNumber
和supplierName
。
驗證這兩個參數本身會很尷尬,但這是由您的設計引起的 - 您錯過了一個抽象。
長話短說,問題在於你的界面。它不應該接受兩個字符串參數,而是一個參數(一個Parameter Object)。我們稱之爲參數對象:CreatePurchaseOrder
。在這種情況下,該接口是這樣的:
public class CreatePurchaseOrder
{
public string PartNumber;
public string SupplierName;
}
interface IPurchaseOrderService
{
void CreatePurchaseOrder(CreatePurchaseOrder command);
}
參數對象CreatePurchaseOrder
包裝原有參數。該參數對象是描述創建採購訂單意向的消息。換句話說:這是一個命令。
使用此命令,可以創建一個IValidator<CreatePurchaseOrder>
實現,該實現可以執行所有適當的驗證,包括檢查是否存在正確的零件供應商並報告用戶友好的錯誤消息。
但是爲什麼負責驗證? 驗證是一個交叉問題,您應該嘗試阻止將其與業務邏輯混合。相反,你可以定義一個裝飾了這一點:
public class ValidationPurchaseOrderServiceDecorator : IPurchaseOrderService
{
private readonly IPurchaseOrderService decoratee;
private readonly IValidator<CreatePurchaseOrder> validator;
ValidationPurchaseOrderServiceDecorator(IPurchaseOrderService decoratee,
IValidator<CreatePurchaseOrder> validator)
{
this.decoratee = decoratee;
this.validator = validator;
}
public void CreatePurchaseOrder(CreatePurchaseOrder command)
{
this.validator.Validate(command);
this.decoratee.CreatePurchaseOrder(command);
}
}
這樣我們就可以通過簡單地包裹一個真正PurchaseOrderService
添加驗證:當然
var service =
new ValidationPurchaseOrderServiceDecorator(
new PurchaseOrderService(),
new CreatePurchaseOrderValidator());
問題這種方法是,它會很尷尬爲系統中的每個服務定義這樣的裝飾器類。這將嚴重違反DRY原則。
但問題是由缺陷造成的。定義每個特定服務的接口(如)通常是有問題的。由於我們定義了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 ICommandHandler<T> decoratee;
private readonly IValidator<T> validator;
ValidationCommandHandlerDecorator(
ICommandHandler<T> decoratee, IValidator<T> validator)
{
this.decoratee = decoratee;
this.validator = validator;
}
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 CreatePurchaseOrderHandler(),
new CreatePurchaseOrderValidator());
但由於這個裝飾是通用的,我們可以在我們的系統中它環繞的每一個命令處理程序。哇!那是幹什麼的?
此設計也使得以後很容易增加橫切關注點。例如,您的服務目前似乎負責在工作單元上調用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.Get(p => p.Number == command.PartNumber);
if (part == null)
{
yield return new ValidationResult("Part Number",
$"Part number {partNumber} does not exist.");
}
var supplier = this.supplierRepository.Get(p => p.Name == command.SupplierName);
if (supplier == null)
{
yield return new ValidationResult("Supplier Name",
$"Supplier named {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),不再需要控制器來創建新的服務類。
如果你想了解更多關於這種類型的設計,你應該絕對檢查出this article。
+1謝謝,非常感謝。由於需要消化很多東西,我將不得不離開並評估信息。順便說一下,我目前正在從Ninject轉移到Simple Injector。我已經讀過關於性能的好消息,但把它賣給我的是,簡單噴油器的文檔要好得多。 –
能否詳細說明傳遞給裝飾器的'PurchaseOrderCommandHandler'和'PurchaseOrderCommandValidator'之間的差異,因爲它們似乎做同樣的事情?驗證者的意圖是將實體的實例作爲參數而不是命令對象嗎? –
'PurchaseOrderCommandValidator'檢查執行'PurchaseOrderCommandHandler'的先決條件。如果需要,它將通過檢查零件和供應商是否存在來查詢數據庫,以確定處理程序是否可以正確執行。 – Steven