2013-05-28 4 views
25

В настоящее время у меня есть сервисный уровень на основе статьи Validating with a service layer с сайта ASP.NET.Отделив сервисный уровень от уровня проверки

В соответствии с 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 объект, который передается в валидатор также требует двух других объектов, Part и Supplier (предположим для этого примера, что PO имеет только одну часть).

Оба объекта Part и Supplier могут быть пустыми, если предоставленные пользователем детали не соответствуют объектам в базе данных, для которых требуется, чтобы валидатор выдавал исключение.

Проблема заключается в том, что на этом этапе валидатор потерял контекстуальную информацию (номер детали и имя поставщика), поэтому не может сообщить пользователю точную ошибку. Лучшая ошибка, которую я могу предоставить, соответствует строкам «Заказ на поставку должен иметь связанную с ним часть», которая не имела бы смысла для пользователя, поскольку они поставляли номер детали (его просто не существует в базе данных).

Использование класса обслуживания из статьи 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, но это деталь реализации. Вместо этого вы должны проверить, является ли сама операция, в этом случае параметры partNumber и supplierName.

Проверка этих двух параметров сама по себе будет неловкой, но это вызвано вашим дизайном. Вам не хватает абстракции.

Короче говоря, проблема в вашем интерфейсе IPurchaseOrderService.Он не должен принимать два строковых аргумента, но один единственный аргумент (a Parameter Object). Назовем этот объект параметра: CreatePurchaseOrder. В этом случае интерфейс будет выглядеть следующим образом:

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

interface IPurchaseOrderService 
{ 
    void CreatePurchaseOrder(CreatePurchaseOrder command); 
} 

Параметр Объект CreatePurchaseOrder оборачивает оригинальные аргументы. Этот объект параметра представляет собой сообщение, описывающее намерение создания заказа на поставку. Другими словами: это команда.

Используя эту команду, вы можете создать реализацию IValidator<CreatePurchaseOrder>, которая может выполнять все правильные проверки, включая проверку наличия правильного поставщика деталей и предоставление сообщений об ошибках, которые могут быть полезны для пользователя.

Но почему ответственность за подтверждение подтверждается IPurchaseOrderService? Валидация является сквозной проблемой, и вы должны попытаться предотвратить ее смешивание с бизнес-логикой. Вместо того, чтобы вы могли определить, декоратор для этого:

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

Проблемы, конечно, с этим подходом является то, что было бы очень неудобно определите класс декоратора для каждой службы в системе. Это было бы серьезным нарушением принципа СУХОЙ.

Но проблема связана с недостатком. Определение интерфейса для конкретной службы (например, 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 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); 
    } 
} 

Обратите внимание, что команда сообщения будет частью домена. Существует сопоставление «один к одному» между вариантами использования и командами, и вместо проверки сущностей эти объекты будут деталями реализации. Команды становятся контрактом и будут проходить проверку.

Обратите внимание, что это, вероятно, упростит вашу жизнь, если ваши команды содержат как можно больше идентификаторов. Таким образом, ваша система будет может извлечь выгоду из определения команды следующим образом:

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

Когда вы сделаете это, вы не должны проверить, если часть по данному имени существует. Уровень презентации (или внешняя система) передал вам идентификатор, поэтому вам больше не нужно проверять существование этой части. Разумеется, обработчик команд не работает, если нет этого элемента, но в этом случае возникает либо ошибка программирования, либо конфликт параллелизма. В любом случае не нужно сообщать пользователю об ошибках проверки достоверности пользователя.

Это, однако, переносит проблему получения правильных идентификаторов на уровень презентации. На уровне презентации пользователь должен будет выбрать часть из списка для получения идентификатора этой части. Но все же я испытал это, чтобы сделать систему намного проще и масштабируемо.

Он также решает большинство проблем, которые заявлены в разделе комментариев статьи вы ссылаетесь, такие как:

  • Поскольку команды может быть легко сериализованным и модель привязки, проблема сущности сериализации уходит.
  • Атрибуты DataAnnotation могут легко применяться к командам, и это позволяет проверять на стороне клиента (Javascript).
  • Декоратор можно применять ко всем обработчикам команд, которые завершают полную операцию в транзакции базы данных.
  • Удаляет круговую ссылку между контроллером и уровнем обслуживания (через модель ModelState контроллера), устраняя необходимость в новом классе обслуживания для контроллера.

Если вы хотите узнать больше об этом типе конструкции, вы должны обязательно проверить this article.

+1

+1 спасибо, это очень ценится. Мне придется уйти и оценить информацию, так как есть много возможностей для переваривания. Кстати, я сейчас смотрю на переход от Ninject к Simple Injector. Я хорошо читал о производительности, но то, что продало его мне, было то, что документация для простого инжектора намного лучше. –

+0

Вы могли бы подробнее рассказать о различиях между «PurchaseOrderCommandHandler» и «PurchaseOrderCommandValidator», переданным декоратору, поскольку они, похоже, делают то же самое? Является ли намерение для валидатора принимать экземпляр объекта как параметра, а не объекта команды? –

+0

«PurchaseOrderCommandValidator» проверяет предварительные условия для выполнения «PurchaseOrderCommandHandler». При необходимости он будет запрашивать базу данных, чтобы узнать, может ли обработчик правильно выполнить проверку, проверяя, существуют ли часть и поставщик. – Steven

Смежные вопросы