2013-05-28 32 views
25

Tôi hiện đang có một lớp dịch vụ dựa trên bài viết Validating with a service layer từ trang ASP.NET.Tách lớp dịch vụ khỏi lớp xác thực

Theo câu trả lời this, đây là phương pháp không tốt vì logic dịch vụ được trộn lẫn với logic xác thực vi phạm nguyên tắc trách nhiệm duy nhất.

Tôi thực sự thích lựa chọn thay thế được cung cấp nhưng trong quá trình tái bao thanh toán mã của tôi, tôi đã gặp một vấn đề mà tôi không thể giải quyết được.

Hãy xem xét các giao diện dịch vụ sau:

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

với việc thực hiện cụ thể như sau dựa trên các câu trả lời liên quan:

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(); 
    } 
} 

Đối tượng PurchaseOrder được truyền cho các validator cũng yêu cầu hai đơn vị khác, PartSupplier (giả sử ví dụ này là PO chỉ có một phần).

Cả hai đối tượng PartSupplier đều có thể rỗng nếu chi tiết do người dùng cung cấp không tương ứng với các thực thể trong cơ sở dữ liệu yêu cầu trình xác nhận để loại trừ ngoại lệ.

Vấn đề tôi có là ở giai đoạn này trình xác thực đã mất thông tin theo ngữ cảnh (số bộ phận và tên nhà cung cấp) để không thể báo cáo lỗi chính xác cho người dùng. Lỗi tốt nhất mà tôi có thể cung cấp nằm dọc theo dòng "Đơn đặt hàng phải có phần liên quan" sẽ không có ý nghĩa đối với người dùng vì họ đã cung cấp số bộ phận (nó không tồn tại trong cơ sở dữ liệu).

Sử dụng lớp dịch vụ từ các bài viết ASP.NET Tôi đang làm một cái gì đó như thế này:

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(); 
} 

này cho phép tôi để cung cấp thông tin xác nhận tốt hơn nhiều cho người sử dụng nhưng có nghĩa là logic xác nhận được chứa trực tiếp trong lớp dịch vụ, vi phạm nguyên tắc trách nhiệm duy nhất (mã cũng được nhân đôi giữa các lớp dịch vụ).

Có cách nào để tận dụng tối đa cả hai thế giới? Tôi có thể tách lớp dịch vụ khỏi lớp xác thực trong khi vẫn cung cấp cùng một mức thông tin lỗi không?

Trả lời

42

Câu trả lời ngắn:

Bạn đang chứng thực điều sai.

câu trả lời rất dài:

Bạn đang cố gắng để xác nhận một PurchaseOrder nhưng đó là một chi tiết thực hiện. Thay vào đó những gì bạn nên xác nhận là bản thân hoạt động, trong trường hợp này là các tham số partNumbersupplierName.

Xác thực hai tham số đó một mình sẽ là khó xử, nhưng điều này là do thiết kế của bạn — Bạn đang thiếu một trừu tượng.

Câu chuyện dài ngắn, sự cố nằm trong giao diện IPurchaseOrderService của bạn.Nó không nên lấy hai đối số chuỗi, nhưng một đối số duy nhất (một Parameter Object). Hãy gọi đối tượng tham số này: CreatePurchaseOrder. Trong trường hợp đó, giao diện sẽ trông như sau:

public class CreatePurchaseOrder 
{ 
    public string PartNumber; 
    public string SupplierName; 
} 

interface IPurchaseOrderService 
{ 
    void CreatePurchaseOrder(CreatePurchaseOrder command); 
} 

Đối tượng tham số CreatePurchaseOrder bao gồm các đối số ban đầu. Đối tượng tham số này là một thông điệp mô tả ý định tạo ra một đơn đặt hàng. Nói cách khác: đó là lệnh.

Sử dụng lệnh này, bạn có thể tạo triển khai IValidator<CreatePurchaseOrder> có thể thực hiện tất cả các xác thực hợp lệ bao gồm kiểm tra sự tồn tại của nhà cung cấp bộ phận thích hợp và báo cáo thông báo lỗi thân thiện với người dùng.

Nhưng tại sao IPurchaseOrderService chịu trách nhiệm xác nhận? Xác thực là một mối quan tâm chéo và bạn nên cố gắng tránh trộn nó với logic nghiệp vụ. Thay vào đó bạn có thể định nghĩa một trang trí cho việc này:

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); 
    } 
} 

Bằng cách này chúng ta có thể thêm xác nhận bằng cách đơn giản gói một thực PurchaseOrderService:

var service = 
    new ValidationPurchaseOrderServiceDecorator(
     new PurchaseOrderService(), 
     new CreatePurchaseOrderValidator()); 

Vấn đề tất nhiên với cách tiếp cận này là nó sẽ thực sự khó khăn khi phải xác định lớp trang trí như vậy cho mỗi dịch vụ trong hệ thống. Đó sẽ là một vi phạm nghiêm trọng nguyên tắc DRY.

Nhưng vấn đề là do lỗ hổng. Việc xác định giao diện cho mỗi dịch vụ cụ thể (chẳng hạn như IPurchaseOrderService) thường có vấn đề. Vì chúng tôi đã xác định CreatePurchaseOrder, chúng tôi đã có định nghĩa như vậy. Bây giờ chúng ta có thể định nghĩa một khái niệm trừu tượng duy nhất cho tất cả các hoạt động kinh doanh trong hệ thống:

public interface ICommandHandler<TCommand> 
{ 
    void Handle(TCommand command); 
} 

Với trừu tượng này ngay bây giờ chúng ta có thể cấu trúc lại PurchaseOrderService như sau:

public class CreatePurchaseOrderHandler : ICommandHandler<CreatePurchaseOrder> 
{ 
    public void Handle(CreatePurchaseOrder command) 
    { 
     var po = new PurchaseOrder 
     { 
      Part = ..., 
      Supplier = ..., 
     }; 

     unitOfWork.Savechanges(); 
    } 
} 

Với thiết kế này, bây giờ chúng ta có thể xác định một trang trí chung chung duy nhất để xử lý các xác thực cho mọi hoạt động kinh doanh trong hệ thống:

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); 
    } 
} 

Lưu ý cách trình trang trí này gần giống như trước đây được xác định ValidationPurchaseOrderServiceDecorator, nhưng bây giờ là một lớp chung chung. Người trang trí này có thể được bao quanh lớp dịch vụ mới của chúng tôi:

var service = 
    new ValidationCommandHandlerDecorator<PurchaseOrderCommand>(
     new CreatePurchaseOrderHandler(), 
     new CreatePurchaseOrderValidator()); 

Nhưng vì trang trí này là chung chung, chúng tôi có thể quấn quanh mọi trình xử lý lệnh trong hệ thống của chúng tôi. Wow! Làm thế nào để được DRY?

Thiết kế này cũng giúp bạn dễ dàng thêm các mối quan tâm xuyên suốt sau này. Ví dụ: dịch vụ của bạn hiện có vẻ như chịu trách nhiệm gọi số SaveChanges trên đơn vị công việc. Điều này có thể được coi là một mối quan tâm xuyên suốt là tốt và có thể dễ dàng được trích xuất một trang trí. Bằng cách này, các lớp dịch vụ của bạn trở nên đơn giản hơn nhiều với ít mã để kiểm tra.

Các CreatePurchaseOrder validator có thể nhìn như sau:

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."); 
     } 
    } 
} 

Và xử lý lệnh của bạn như thế này:

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); 
    } 
} 

Lưu ý rằng lệnh tin nhắn sẽ trở thành phần của tên miền của bạn. Có một ánh xạ một-một giữa các ca sử dụng và các lệnh và thay vì các thực thể xác nhận hợp lệ, các thực thể đó sẽ là một chi tiết thực hiện. Các lệnh trở thành hợp đồng và sẽ nhận được xác nhận hợp lệ.

Lưu ý rằng nó có thể làm cho cuộc sống của bạn dễ dàng hơn nhiều nếu lệnh của bạn chứa nhiều ID nhất có thể. Vì vậy, hệ thống của bạn sẽ có thể hưởng lợi từ việc xác định một lệnh như sau:

public class CreatePurchaseOrder 
{ 
    public int PartId; 
    public int SupplierId; 
} 

Khi bạn làm điều này, bạn sẽ không phải kiểm tra xem một phần của tên đã tồn tại chưa. Lớp trình bày (hoặc một hệ thống bên ngoài) đã truyền cho bạn một Id, vì vậy bạn không phải xác nhận sự tồn tại của phần đó nữa. Tất nhiên, trình xử lý lệnh sẽ không thành công khi không có phần nào của ID đó, nhưng trong trường hợp đó, có lỗi lập trình hoặc xung đột đồng thời. Trong cả hai trường hợp, không cần phải giao tiếp lỗi xác thực thân thiện với người dùng biểu cảm cho khách hàng.

Tuy nhiên, việc này sẽ chuyển vấn đề nhận ID phù hợp sang lớp trình bày. Trong lớp trình bày, người dùng sẽ phải chọn một phần từ danh sách để chúng tôi lấy ID của phần đó. Nhưng tôi vẫn trải qua điều này để làm cho hệ thống dễ dàng hơn và có thể mở rộng.

Nó cũng giải quyết hầu hết các vấn đề được nêu trong phần bình luận của bài báo bạn đang đề cập đến, chẳng hạn như:

  • Kể từ khi lệnh có thể dễ dàng đăng và mô hình ràng buộc, vấn đề với đơn vị serialization Đi đi.
  • Thuộc tính DataAnnotation có thể được áp dụng dễ dàng cho các lệnh và điều này cho phép xác thực phía máy khách (Javascript).
  • Trình trang trí có thể được áp dụng cho tất cả các trình xử lý lệnh kết thúc hoạt động hoàn chỉnh trong giao dịch cơ sở dữ liệu.
  • Nó loại bỏ tham chiếu vòng tròn giữa bộ điều khiển và lớp dịch vụ (thông qua ModelState của bộ điều khiển), loại bỏ sự cần thiết của bộ điều khiển đến lớp dịch vụ mới.

Nếu bạn muốn tìm hiểu thêm về loại thiết kế này, bạn hoàn toàn nên kiểm tra this article.

+1

+1 cảm ơn bạn, điều này được đánh giá rất nhiều. Tôi sẽ phải đi và đánh giá thông tin vì có rất nhiều thông tin. Bằng cách này, tôi hiện đang xem xét việc chuyển từ Ninject sang Simple Injector. Tôi đã đọc những điều tốt đẹp về hiệu suất nhưng thứ đã bán cho tôi là tài liệu cho Injector đơn giản là tốt hơn nhiều. –

+0

bạn có thể giải thích về sự khác biệt giữa 'PurchaseOrderCommandHandler' và' PurchaseOrderCommandValidator' được truyền cho trang trí khi chúng dường như làm điều tương tự không? Mục đích của trình xác thực có lấy một cá thể của thực thể làm tham số thay vì một đối tượng lệnh không? –

+0

'PurchaseOrderCommandValidator' kiểm tra các điều kiện tiên quyết cho' PurchaseOrderCommandHandler' để thực hiện. Nếu cần, nó sẽ truy vấn cơ sở dữ liệu để tìm hiểu xem trình xử lý có thể thực thi chính xác hay không bằng cách kiểm tra xem phần và nhà cung cấp có tồn tại hay không. – Steven

Các vấn đề liên quan