[Перевод] Валидация: внутри сущностей или снаружи?
Обратите внимание, что хотя пост написан от первого лица, это перевод статьи из блога Jimmy Bogard, автора AutoMapper.
Меня часто спрашивают, особенно в контексте архитектуры вертикальных слоев (vertical slice architecture), где должна происходить валидация? Если вы применяете DDD, вы можете поместить валидацию внутри сущностей. Но лично я считаю, что валидация не очень вписывается в ответственность сущности.
Часто валидация внутри сущностей делается с помощью аннотаций. Допустим, у нас есть Customer и его поля FirstName/LastName обязательны:
public class Customer
{
[Required]
public string FirstName { get; set; }
[Required]
public string LastName { get; set; }
}
Проблем с таким подходом две:
- Вы изменяете состояние сущности до валидации, то есть ваша сущность может находиться в невалидном состоянии
- Неясен контекст операции (что именно пытается сделать пользователь)
И хотя вы можете показать ошибки валидации (обычно генерируемые ORM) пользователю, не так-то просто сопоставить исходные намерения и детали реализации состояния. Как правило, я стараюсь избегать такого подхода.
Однако, если вы придерживаетесь DDD, вы можете обойти проблему изменяющегося состояния, добавив метод:
public class Customer
{
public string FirstName { get; private set; }
public string LastName { get; private set; }
public void ChangeName(string firstName, string lastName) {
if (firstName == null)
throw new ArgumentNullException(nameof(firstName));
if (lastName == null)
throw new ArgumentNullException(nameof(lastName));
FirstName = firstName;
LastName = lastName;
}
}
Немного лучше, но лишь немного, потому что исключения — единственный способ показать ошибки валидации. Исключения вы не любите, поэтому берете какой-нибудь вариант результата [выполнения] команды (command result):
public class Customer
{
public string FirstName { get; private set; }
public string LastName { get; private set; }
public CommandResult ChangeName(ChangeNameCommand command) {
if (command.FirstName == null)
return CommandResult.Fail("First name cannot be empty.");
if (lastName == null)
return CommandResult.Fail("Last name cannot be empty.");
FirstName = command.FirstName;
LastName = command.LastName;
return CommandResult.Success;
}
}
И опять отображение ошибки пользователю вызывает раздражение, так как возвращается только одна ошибка за раз. Я мог бы вернуть их всем скопом, но как тогда мне сопоставить их с именами полей на экране? Никак. Очевидно, сущности хреново подходят для валидации команды. Однако, для этого прекрасно подходят фреймворки валидации (validation frameworks).
Валидация команды (command validation)
Вместо перекладывания валидации команды на сущность/агрегат, я полностью полагаюсь на инварианты. Вся суть инвариантов в уверенности, что я могу перейти из одного состояния в другое целиком и полностью, а не частично. То есть, фактически, это не про валидацию запроса, а про выполнение перехода между состояниями.
При таком подходе моя валидация строится вокруг команд и действий, а не сущностей. Я мог бы сделать что-то типа такого:
public class ChangeNameCommand {
[Required]
public string FirstName { get; set; }
[Required]
public string LastName { get; set; }
}
public class Customer
{
public string FirstName { get; private set; }
public string LastName { get; private set; }
public void ChangeName(ChangeNameCommand command) {
FirstName = command.FirstName;
LastName = command.LastName;
}
}
Мои атрибуты валидации находятся в самой команде, и только при условии валидности команды я смогу применить ее к моим сущностям для перевода их в новое состояние. Внутри сущности я должен просто обработать команду ChangeNameCommand и выполнить переход в новое состояние, будучи уверенным, что выполняются мои инварианты. Во многих проектах я использую FluentValidation:
public class ChangeNameCommand {
public string FirstName { get; set; }
public string LastName { get; set; }
}
public class ChangeNameValidator : AbstractValidator {
public ChangeNameValidator() {
RuleFor(m => m.FirstName).NotNull().Length(3, 50);
RuleFor(m => m.LastName).NotNull().Length(3, 50);
}
}
public class Customer
{
public string FirstName { get; private set; }
public string LastName { get; private set; }
public void ChangeName(ChangeNameCommand command) {
FirstName = command.FirstName;
LastName = command.LastName;
}
}
Ключевое отличие здесь в том, что я валидирую команду, а не сущность. Сущности сами по себе — не библиотеки для валидации, так что гораздо более правильно (much cleaner) делать валидацию на уровне команд. При этом ошибки валидации прекрасно коррелируют с интерфейсом, так как именно вокруг команды в первую очередь и строился этот интерфейс.
Валидируйте команды, а не сущности, и выполняйте валидацию на границах (perform the validation at the edges).