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ố partNumber
và supplierName
.
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 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. –
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? –
'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