Доброго времени, хаброчеловеки!
Мне хотелось бы поднять тему серверной валидации пользовательских данных. Поискав на хабре топики данной тематики и погуглив, пришёл к выводу, что люди часто изобретают свои собственные велосипеды для реализации механизма валидации. В данной статье хочу рассказать о простом и красивом решении, которое успешно применяется в нескольких проектах.
Проблема
Часто замечаю, что разработчики активно используют исключения (exceptions) для уведомления об ошибках валидации данных. Продемонстрирую примером (поскольку C# мне ближе, буду использовать его):
public void Validate(string userName, string userPassword)
{
if (/*проверяем имя пользователя*/)
throw new InvalidUsernameException();
if (/*проверяем пароль*/)
throw new InvalidPasswordException();
}
Далее это используется примерно так:
public void RegisterUser (string username, string password) {
try {
ValidateUser(username, password);
}
catch(InvalidUsernameException ex) {
//добавляем в коллекцию ошибок
}
catch(InvalidPasswordException ex) {
//добавляем в коллекцию ошибок
}
//что-то дальше делаем
}
Что плохого в этом примере?
— используются исключения (exceptions) на этапе бизнес валидации. Важно помнить, что ошибки валидации данных != ошибкам работы приложения;
— использование исключений на этапе бизнес валидации может привести к падению приложения. Такое может произойти, например, если человек забудет написать ещё один блок catch для новых правил валидации;
— код выглядит некрасиво;
— подобное решение сложно тестировать и поддерживать.
Решение
Механизм валидации данных можно реализовать при помощи паттерна Composite (компоновщик).
Нам потребуется непосредственно сам объект валидатор, который непосредственно проверяет данные на соответствие определённым правилам, композитный валидатор, который инкаплусирует в себе коллекцию валидаторов, а также дополнительный класс, используемый в
качестве хранилища результата валидации и коллекции ошибок — ValidationResult. Рассмотрим вначале последний:
public class ValidationResult{
private bool isSucceedResult = true;
private readonly List<ResultCode> resultCodes = new List();
protected ValidationResult() {
}
public ValidationResult(ResultCode code) {
isSucceedResult = false;
resultCodes.Add(code);
}
public static ValidationResult SuccessResult {
get { return new ValidationResult(); }
}
public List<ResultCode> GetResultCodes {
get { return resultCodes; }
}
public bool IsSucceed {
get { return isSucceedResult; }
}
public void AddError(ResultCode code) {
isSucceedResult = false;
resultCodes.Add(code);
}
public void Merge(ValidationResult result) {
resultCodes.AddRange(result.resultCodes);
isSucceedResult &= result.isSucceedResult;
}
}
Теперь перейдём непосредственно к механизму валидации. Нам необходимо создать базовый класс Validator, от которого будут наследоваться все валидаторы:
public abstract class Validator {
public abstract ValidationResult Validate();
}
У объектов-валидаторов существует 1 метод, который запускает процедуру проверки и возвращает результат.
CompositeValidator — класс, содержащий в себе коллекцию валидаторов и запускающий механизм проверки у всех дочерних объектов:
public class CompositeValidator : Validator {
private readonly List<Validator> validators = new List();
public void Add(Validator validator) {
validators.Add(validator);
}
public void Remove(Validator validator) {
validators.Remove(validator);
}
public override ValidationResult Validate() {
ValidationResult result = ValidationResult.SuccessResult;
foreach (Validator validator in validators) {
result.Merge(validator.Validate());
}
return result;
}
}
Используя эти классы, мы получили возможность создавать валидаторы с определёнными правилами, комбинировать их и получать результаты проверки.
Использование
Перепишем приведённый в начале статьи пример с использованием этого механизма. В нашем случае необходимо создать 2 валидатора, которые проверяют имя пользователя и пароль на соответствие определённым правилам.
Создаём объект для проверки имени пользователя UserNameValidator:
public class UserNameValidator: Validator {
private readonly string userName;
public UserNameValidator(string userName) {
this.userName= userName;
}
public override ValidationResult Validate() {
if (/*параметр не прошёл проверку на условие, например userName = null*/) {
return new ValidationResult(ResultCode.UserNameIncorrect);
}
return ValidationResult.SuccessResult;
}
}
Аналогично получаем UserPasswordValidator.
Теперь у нас есть всё, чтобы использовать новый механизм валидации данных:
public ValidationResult ValidateUser(string userName, string userPassword)
{
CompositeValidator validator = new CompositeValidator();
validator.add(new UserNameValidator(userName));
validator.add(new UserPasswordValidator(userPassword));
return validator.Validate();
}
public void RegisterUser (string username, string password) {
ValidationResult result = ValidateUser(username, password);
if (result.IsSucceed) {
//успешная валидация
}
else {
//получаем ошибки валидации result.GetResultCodes() и обрабатываем соответствующим образом
}
}
Выводы
Какие преимущества мы получили, используя данный подход:
— расширяемость. Добавление новых валидаторов стоит дешево;
— тестируемость. Все валидаторы могут быть модульно протестированы, что исключает наличие ошибок в общем процессе валидации;
— сопровождаемость. Валидаторы можно выделить в отдельную сборку и использовать во многих проектах с незначительными изменениями;
— красота и правильность. Данный код выглядит красивее, изящнее и правильнее, первоначального варианта, исключения не используются для бизнес валидации.
Заключение
В дальнейшем можно применить несколько улучшений к механизму валидации, а именно:
— инкапсулировать все валидаторы в одну сборку и создать фабрику, которая будет возвращать готовый механизм валидации для различных условий (проверка данных для регистрации пользователя, проверка данных при авторизации и т.д.);
— базовый класс Validator можно заменить на интерфейс, кому как нравится;
— правила валидации можно хранить в одном месте, для более удобного управления;
— можно написать обработчик ошибок валидации, который будет сопоставлять коды ошибок и сообщения, выводимые пользователям на UI, в таком случае ещё сильнее упрощается процесс добавления новых валидаторов. Также отпадает проблема локализации сообщений.
P.S.
Просьба сильно не пинать за возможные ошибки в написании и изложении. Я очень старался)
С удовольствием выслушаю критику и пожелания по поводу реализации и архитектуры.
* All source code was highlighted with Source Code Highlighter.