Ответ 1
Короткий ответ:
Вы проверяете не ту вещь.
Очень длинный ответ:
Вы пытаетесь проверить PurchaseOrder
но это деталь реализации. Вместо этого вам следует проверить саму операцию, в этом случае параметры partNumber
и supplierName
.
Проверка этих двух параметров сама по себе была бы неудобной, но это вызвано вашим дизайном - вы упускаете абстракцию.
Короче говоря, проблема в вашем интерфейсе IPurchaseOrderService
. Он не должен принимать два строковых аргумента, а скорее один единственный аргумент (объект параметра). Позвольте вызвать этот параметр объекта CreatePurchaseOrder
:
public class CreatePurchaseOrder
{
public string PartNumber;
public string SupplierName;
}
С измененным интерфейсом IPurchaseOrderService
:
interface IPurchaseOrderService
{
void CreatePurchaseOrder(CreatePurchaseOrder command);
}
CreatePurchaseOrder
параметров 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);
}
}
Таким образом, вы можете добавить проверку, просто обернув реальный объект 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());
Но так как этот декоратор является общим, вы можете обернуть его вокруг каждого обработчика команд в вашей системе. Вот Это Да! Как это для СУХОГО?
Этот дизайн также позволяет легко добавлять сквозные проблемы позже. Например, ваш сервис в настоящее время кажется ответственным за вызов 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);
}
}
Обратите внимание, что командные сообщения станут частью вашего домена. Существует взаимно-однозначное сопоставление между вариантами использования и командами, и вместо проверки сущностей эти сущности будут деталями реализации. Команды становятся контрактом и получат подтверждение.
Обратите внимание, что это, вероятно, сделает вашу жизнь намного проще, если ваши команды будут содержать как можно больше идентификаторов. Таким образом, ваша система может выиграть от определения команды следующим образом:
public class CreatePurchaseOrder
{
public int PartId;
public int SupplierId;
}
Когда вы сделаете это, вам не нужно будет проверять, существует ли деталь с указанным именем. Уровень представления (или внешняя система) передал вам идентификатор, поэтому вам больше не нужно проверять существование этой части. Обработчик команд, конечно, должен завершиться сбоем, если нет части с этим идентификатором, но в этом случае возникает либо ошибка программирования, либо конфликт параллелизма. В любом случае нет необходимости сообщать клиенту об очевидных ошибках проверки.
Это, однако, перемещает проблему получения правильных идентификаторов на уровне представления. На уровне представления пользователь должен будет выбрать деталь из списка, чтобы мы могли получить идентификатор этой детали. Но все же я испытал это, чтобы сделать систему намного проще и масштабируемой.
Это также решает большинство проблем, указанных в разделе комментариев статьи, на которую вы ссылаетесь, например:
- Проблема с сериализацией сущностей исчезает, потому что команды могут быть легко сериализованы и привязка модели.
- Атрибуты DataAnnotation можно легко применять к командам, что позволяет выполнять проверку на стороне клиента (Javascript).
- Декоратор может быть применен ко всем обработчикам команд, которые оборачивают завершенную операцию в транзакцию базы данных.
- Он удаляет циклическую ссылку между контроллером и уровнем обслуживания (через контроллер ModelState), устраняя необходимость в контроллере для нового класса обслуживания.
Если вы хотите узнать больше об этом типе дизайна, вы должны обязательно прочитать эту статью.