Решение, данное в этой статье, смешивает логику проверки с логикой обслуживания. Это две проблемы, и их следует разделять. Когда ваше приложение разрастется, вы быстро обнаружите, что логика проверки усложняется и дублируется на уровне сервиса. Поэтому я хотел бы предложить другой подход.
Прежде всего, IMO было бы намного лучше, чтобы сервисный уровень выдавал исключение при возникновении ошибки проверки. Это делает более явным и трудным забыть проверить на наличие ошибок. Таким образом, способ обработки ошибок остается на уровне представления. В следующем листинге показан ProductController
, использующий этот подход:
public class ProductController : Controller
{
private readonly IProductService service;
public ProductController(IProductService service) => this.service = service;
public ActionResult Create(
[Bind(Exclude = "Id")] Product productToCreate)
{
try
{
this.service.CreateProduct(productToCreate);
}
catch (ValidationException ex)
{
this.ModelState.AddModelErrors(ex);
return View();
}
return RedirectToAction("Index");
}
}
public static class MvcValidationExtension
{
public static void AddModelErrors(
this ModelStateDictionary state, ValidationException exception)
{
foreach (var error in exception.Errors)
{
state.AddModelError(error.Key, error.Message);
}
}
}
Класс ProductService
не должен сам иметь никакой проверки, но должен делегировать это классу, специализирующемуся на проверке, т.е. IValidationProvider
:
public interface IValidationProvider
{
void Validate(object entity);
void ValidateAll(IEnumerable entities);
}
public class ProductService : IProductService
{
private readonly IValidationProvider validationProvider;
private readonly IProductRespository repository;
public ProductService(
IProductRespository repository,
IValidationProvider validationProvider)
{
this.repository = repository;
this.validationProvider = validationProvider;
}
// Does not return an error code anymore. Just throws an exception
public void CreateProduct(Product productToCreate)
{
// Do validation here or perhaps even in the repository...
this.validationProvider.Validate(productToCreate);
// This call should also throw on failure.
this.repository.CreateProduct(productToCreate);
}
}
Этот IValidationProvider
, однако, не должен проверять сам себя, а скорее должен делегировать проверку классам проверки, которые специализируются на проверке одного конкретного типа. Когда объект (или набор объектов) недействителен, поставщик проверки должен выдать ValidationException
, который может быть обнаружен выше в стеке вызовов. Реализация провайдера может выглядеть так:
sealed class ValidationProvider : IValidationProvider
{
private readonly Func<Type, IValidator> validatorFactory;
public ValidationProvider(Func<Type, IValidator> validatorFactory)
{
this.validatorFactory = validatorFactory;
}
public void Validate(object entity)
{
IValidator validator = this.validatorFactory(entity.GetType());
var results = validator.Validate(entity).ToArray();
if (results.Length > 0)
throw new ValidationException(results);
}
public void ValidateAll(IEnumerable entities)
{
var results = (
from entity in entities.Cast<object>()
let validator = this.validatorFactory(entity.GetType())
from result in validator.Validate(entity)
select result)
.ToArray();
if (results.Length > 0)
throw new ValidationException(results);
}
}
ValidationProvider
зависит от IValidator
экземпляров, которые выполняют фактическую проверку. Сам провайдер не знает, как создавать эти экземпляры, но использует для этого внедренный делегат Func<Type, IValidator>
. Этот метод будет иметь специфичный для контейнера код, например, для Ninject:
var provider = new ValidationProvider(type =>
{
var valType = typeof(Validator<>).MakeGenericType(type);
return (IValidator)kernel.Get(valType);
});
Этот фрагмент показывает класс Validator<T>
— я покажу этот класс через секунду. Во-первых, ValidationProvider
зависит от следующих классов:
public interface IValidator
{
IEnumerable<ValidationResult> Validate(object entity);
}
public class ValidationResult
{
public ValidationResult(string key, string message)
{
this.Key = key;
this.Message = message;
}
public string Key { get; }
public string Message { get; }
}
public class ValidationException : Exception
{
public ValidationException(ValidationResult[] r) : base(r[0].Message)
{
this.Errors = new ReadOnlyCollection<ValidationResult>(r);
}
public ReadOnlyCollection<ValidationResult> Errors { get; }
}
Весь вышеприведенный код — это сантехника, необходимая для проведения проверки. Теперь вы можете определить класс проверки для каждой сущности, которую вы хотите проверить. Однако, чтобы немного помочь вашему DI-контейнеру, вы должны определить общий базовый класс для валидаторов. Это позволит вам зарегистрировать типы проверки:
public abstract class Validator<T> : IValidator
{
IEnumerable<ValidationResult> IValidator.Validate(object entity)
{
if (entity == null) throw new ArgumentNullException("entity");
return this.Validate((T)entity);
}
protected abstract IEnumerable<ValidationResult> Validate(T entity);
}
Как видите, этот абстрактный класс наследуется от IValidator
. Теперь вы можете определить класс ProductValidator
, производный от Validator<Product>
:
public sealed class ProductValidator : Validator<Product>
{
protected override IEnumerable<ValidationResult> Validate(
Product entity)
{
if (entity.Name.Trim().Length == 0)
yield return new ValidationResult(
nameof(Product.Name), "Name is required.");
if (entity.Description.Trim().Length == 0)
yield return new ValidationResult(
nameof(Product.Description), "Description is required.");
if (entity.UnitsInStock < 0)
yield return new ValidationResult(
nameof(Product.UnitsInStock),
"Units in stock cnnot be less than zero.");
}
}
Как видите, класс ProductValidator
использует оператор C# yield return
, который делает возврат ошибок проверки более быстрым.
Последнее, что вам нужно сделать, чтобы все это заработало, — настроить конфигурацию Ninject:
kernel.Bind<IProductService>().To<ProductService>();
kernel.Bind<IProductRepository>().To<L2SProductRepository>();
Func<Type, IValidator> validatorFactory = type =>
{
var valType = typeof(Validator<>).MakeGenericType(type);
return (IValidator)kernel.Get(valType);
};
kernel.Bind<IValidationProvider>()
.ToConstant(new ValidationProvider(validatorFactory));
kernel.Bind<Validator<Product>>().To<ProductValidator>();
Мы действительно закончили? По-разному. Недостатком приведенной выше конфигурации является то, что для каждого объекта в нашем домене вам потребуется реализация Validator<T>
. Даже если, возможно, большинство реализаций будут пустыми.
Вы можете решить эту проблему, выполнив две вещи:
- Вы можете использовать авторегистрацию для автоматической динамической загрузки всех реализаций из данной сборки.
- Вы можете вернуться к реализации по умолчанию, когда не существует регистрации.
Такая реализация по умолчанию может выглядеть так:
sealed class NullValidator<T> : Validator<T>
{
protected override IEnumerable<ValidationResult> Validate(T entity)
{
return Enumerable.Empty<ValidationResult>();
}
}
Вы можете настроить этот NullValidator<T>
следующим образом:
kernel.Bind(typeof(Validator<>)).To(typeof(NullValidator<>));
После этого Ninject будет возвращать NullValidator<Customer>
, когда запрашивается Validator<Customer>
, и для него не зарегистрирована конкретная реализация.
Последнее, чего сейчас не хватает, так это авторегистрации. Это избавит вас от необходимости добавлять регистрацию для каждой реализации Validator<T>
и позволит Ninject динамически искать ваши сборки для вас. Я не смог найти никаких примеров этого, но я предполагаю, что Ninject может это сделать.
ОБНОВЛЕНИЕ: см. ответ Кайесса, чтобы узнать, как автоматически регистрировать эти типы.
И последнее замечание: чтобы это сделать, вам нужно довольно много сантехники, поэтому, если ваш проект (и остается) довольно маленьким, этот подход может дать вам слишком много накладных расходов. Однако когда ваш проект разрастется, вы будете очень рады такому гибкому дизайну. Подумайте, что вам нужно сделать, если вы хотите изменить валидацию (скажем, блок приложения валидации или аннотации данных). Единственное, что вам нужно сделать, это написать реализацию для NullValidator<T>
(в этом случае я бы переименовал ее в DefaultValidator<T>
. Кроме того, все еще возможно иметь свои собственные классы проверки для дополнительных проверок, которые трудно реализовать с другими проверками). технологии.
Обратите внимание, что использование абстракций, таких как IProductService
и ICustomerService
, нарушает принципы SOLID, и вам может быть полезно перейти от этого шаблона к шаблон, который абстрагирует варианты использования.
Обновление: также взгляните на этот q/a; в нем обсуждается дополнительный вопрос по той же статье.
person
Steven
schedule
31.01.2011