Как стать автором
Обновить

Серверная валидация пользовательских данных

Время на прочтение 5 мин
Количество просмотров 9K

Доброго времени, хаброчеловеки!


Мне хотелось бы поднять тему серверной валидации пользовательских данных. Поискав на хабре топики данной тематики и погуглив, пришёл к выводу, что люди часто изобретают свои собственные велосипеды для реализации механизма валидации. В данной статье хочу рассказать о простом и красивом решении, которое успешно применяется в нескольких проектах.

Проблема


Часто замечаю, что разработчики активно используют исключения (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.
Теги:
Хабы:
+52
Комментарии 66
Комментарии Комментарии 66

Публикации

Истории

Работа

.NET разработчик
65 вакансий

Ближайшие события

Московский туристический хакатон
Дата 23 марта – 7 апреля
Место
Москва Онлайн
Геймтон «DatsEdenSpace» от DatsTeam
Дата 5 – 6 апреля
Время 17:00 – 20:00
Место
Онлайн