2013-05-28 130 views
25

我目前有一個基於ASP.NET網站上文章Validating with a service layer的服務層。將服務層與驗證層分開

根據this回答,這是一種糟糕的方法,因爲服務邏輯與驗證邏輯混合在一起,違反了單一責任原則。

我真的很喜歡這個提供的替代方案,但是在我的代碼的重新分解過程中,我遇到了一個我無法解決的問題。

考慮以下服務接口:

interface IPurchaseOrderService 
{ 
    void CreatePurchaseOrder(string partNumber, string supplierName); 
} 

與基於鏈接回答以下具體落實:

public class PurchaseOrderService : IPurchaseOrderService 
{ 
    public void CreatePurchaseOrder(string partNumber, string supplierName) 
    { 
     var po = new PurchaseOrder 
     { 
      Part = PartsRepository.FirstOrDefault(p => p.Number == partNumber), 
      Supplier = SupplierRepository.FirstOrDefault(p => p.Name == supplierName), 
      // Other properties omitted for brevity... 
     }; 

     validationProvider.Validate(po); 
     purchaseOrderRepository.Add(po); 
     unitOfWork.Savechanges(); 
    } 
} 

傳遞給驗證還需要其他兩個實體的PurchaseOrder對象, PartSupplier(讓我們假設這個例子PO只有一個部分)。

如果用戶提供的詳細信息與數據庫中不需要驗證器引發異常的實體不匹配,則PartSupplier對象可能爲空。

我的問題是,在這個階段驗證器已經丟失了上下文信息(零件號和供應商名稱),因此無法向用戶報告準確的錯誤。我可以提供的最好的錯誤是沿着「購買訂單必須有一個關聯部分」這些行,這對用戶來說是沒有意義的,因爲他們確實提供了一個零件號(它只是不存在於數據庫中)。

從ASP.NET文章中,我做這樣的事情使用服務類:

public void CreatePurchaseOrder(string partNumber, string supplierName) 
{ 
    var part = PartsRepository.FirstOrDefault(p => p.Number == partNumber); 
    if (part == null) 
    { 
     validationDictionary.AddError("", 
      string.Format("Part number {0} does not exist.", partNumber); 
    } 

    var supplier = SupplierRepository.FirstOrDefault(p => p.Name == supplierName); 
    if (supplier == null) 
    { 
     validationDictionary.AddError("", 
      string.Format("Supplier named {0} does not exist.", supplierName); 
    } 

    var po = new PurchaseOrder 
    { 
     Part = part, 
     Supplier = supplier, 
    }; 

    purchaseOrderRepository.Add(po); 
    unitOfWork.Savechanges(); 
} 

這讓我提供更好的驗證信息給用戶,但意味着驗證邏輯直接包含服務類違反了單一職責原則(代碼也在服務類之間重複)。

有沒有一種獲得兩全其美的方法?我是否可以將服務層與驗證層分開,同時仍提供相同級別的錯誤信息?

回答

42

簡短的回答:

您要驗證錯誤的事情。

很長的回答:

您試圖驗證PurchaseOrder但是這是一個實現細節。相反,你應該驗證的是操作本身,在這種情況下,參數爲partNumbersupplierName

驗證這兩個參數本身會很尷尬,但這是由您的設計引起的 - 您錯過了一個抽象。

長話短說,問題在於你的​​界面。它不應該接受兩個字符串參數,而是一個參數(一個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

+1謝謝,非常感謝。由於需要消化很多東西,我將不得不離開並評估信息。順便說一下,我目前正在從Ninject轉移到Simple Injector。我已經讀過關於性能的好消息,但把它賣給我的是,簡單噴油器的文檔要好得多。 –

+0

能否詳細說明傳遞給裝飾器的'PurchaseOrderCommandHandler'和'PurchaseOrderCommandValidator'之間的差異,因爲它們似乎做同樣的事情?驗證者的意圖是將實體的實例作爲參數而不是命令對象嗎? –

+0

'PurchaseOrderCommandValidator'檢查執行'PurchaseOrderCommandHandler'的先決條件。如果需要,它將通過檢查零件和供應商是否存在來查詢數據庫,以確定處理程序是否可以正確執行。 – Steven