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

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

Время на прочтение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.
Теги:
Хабы:
Всего голосов 72: ↑62 и ↓10+52
Комментарии66

Публикации