Валидация является важной частью разработки. От проверки аргументов до проверки рабочих процессов, конвейеров, границ ввода / вывода, мы запускаем проверки, чтобы убедиться, что приложение будет работать правильно. Лично мне, как инженеру-программисту, мне нравится видеть бизнес-процессы перед реализацией и пытаться выразить себя с помощью кода таким образом.
С точки зрения валидации это означает, что написание кода прямо затрудняет понимание того, что поэт хотел сказать в сложных сценариях.
Пример:
////// Create new user in the system /// public int CreateUser(User user){ // check for faulted data if(user.IsNull()) throw new ArgumentException(“user"); // check if the email is valid if(!Regex.IsMatch(user.Email, “^([0-9a-zA-Z]([+-.\w]*[0-9a-zA-Z])*@([0-9a-zA-Z][-\w]*[0-9a-zA-Z]\.)+[a-zA-Z]{2,9})$"))) throw new InvalidDataException("Email is not valid"); // check if the email have been used before if(UserRepository.HasRegisteredWithEmail(user.Email)) throw new InvalidDataException("A user has already registered with the provided email"); // check if the username is not empty and has more then 6 chars if(user.Username.IsNullOrEmpty() && user.Username.Length > 6) throw new InvalidDataException("Username must have at least 7 chars"); // check if the user with that username exists in database if(UserRepository.Existis(user.Username)) throw new InvalidDataException("Username have been taken"); // check user pwd if(user.Password.IsNullOrEmpty() && user.Password.Length > 6) throw new InvalidDataException("Password must have at least 7 chars"); // check is user pass matches with repeat version if(user.Password != user.RepeatPassword) throw new InvalidDataException("Please retype your password. Password does not match"); //... var id = UserRepository.CreateUser(user); return id; }
Мы смотрим на хорошо документированный трудно читаемый код. Что если бы мы могли использовать некоторые шаблоны, которые помогут нам сделать проверку более читабельным образом?
Дамы и господа, вот где «образец спецификации» приходит на помощь.
Шаблон спецификации — это конкретный шаблон разработки программного обеспечения, в котором бизнес-правила можно объединять, объединяя бизнес-правила в логическую логику.
Чтобы упростить его, это означает, что нам нужен механизм проверки, а также правила и контейнеры, которые будут знать, как выполнять их в отношении входных данных.
Валидация двигателя
Базовый контракт механизма проверки будет состоять из таких методов, как:
- Зарегистрировать правило проверки
- Зарегистрировать контейнер проверки
- Выполнить зарегистрированные правила против ввода
- Execute If — выполнить зарегистрированные правила, если условие выполнено
- OnFalse — действие, которое будет выполнено, если проверенный ввод недействителен
- Получить правила по статусу — для предоставления информации об успешных или неудачных правилах
- Восстановить — восстановить механизм проверки до первоначальной настройки
- Сброс — удалить зарегистрированные правила и очистить выполненные данные
////// Validation engine contract /// public interface IValidationEngine{ /// /// Represent the exeution result /// ValidationResultExecutionResult { get; set; } /// /// Register a validation container before execution /// IValidationEngineRegister(IValidationRuleContainer container); /// /// Register a validation rule /// IValidationEngineRegister(IValidationRule validationRule); /// /// Exeute the registered validation rules and validation rule /// containers on the provided entity /// IValidationEngine Execute(T entity); /// /// Exeute the registered validation rules and validation rule /// containers on the provided entity /// IValidationEngine ExecuteIf(T entity, Func condition); /// /// Action to be executed if the result is false /// void OnFalse(Action > action); /// /// Return validation rule by execution status /// List > GetRulesByStatus(ValidationRuleExecutionStatus executionStatus); /// /// Restore the validation engine to initial setup state /// IValidationEngineRestore(); /// /// Remove registered rules and clears executed data /// IValidationEngineReSetup(); }
Технические характеристики
Спецификация будет единственной единицей, которая может быть проверена по входным данным в механизме проверки. Контракт базовой спецификации должен иметь:
- Выражение проверки, которое будет использоваться для выполнения с входными данными
- Результат — сохранить значение выполнения в отношении входных данных
- Выполнить метод, который будет вызываться из механизма проверки
////// Represents validation rule /// public interface IValidationRule{ /// /// Validation expression that must be fullfilled /// Expression > ValidationExpression { get; } /// /// Execution result /// bool Result { get; } ////// Execute the defined expression /// bool Execute(T entity); }
Чтобы добиться лучшей группировки правил спецификации, мы можем использовать контейнеры спецификации.
- Добавить правило проверки — Добавить правило проверки как часть контейнера
- Получить правила проверки из контейнера — возвращает все зарегистрированные правила проверки, которые будут выполнены для входных данных.
////// Contract defining containers for the rules /// public interface IValidationRuleContainer { /// /// Register a rule /// void AddValidationRule(IValidationRulevalidationRule); /// /// Return all validation rules registered in the container /// List> GetValidationRules(); }
Завершение этого
Двигатель
public class RuleEvaluator: IValidationEngine { /// /// Private constructor /// private RuleEvaluator() { ValidationRulesForExecutution = new List>(); } /// /// Represent all registered validation rules /// private List> ValidationRulesForExecutution { get; set; } /// /// Represent all registered validation rules /// private List> FalseReturnValidationRules { get; set; } /// /// Represent all registered validation rules /// private List> TrueReturnValidationRules { get; set; } /// /// Represent the exeution result /// public ValidationResultExecutionResult { get; set; } /// /// Restore the validation engine to initial setup state /// public IValidationEngineRestore() { this.TrueReturnValidationRules = new List >(); this.FalseReturnValidationRules = new List >(); return this; } /// /// Create new instance of the rule engine /// public IValidationEngine ReSetup() { this.TrueReturnValidationRules = new List >(); this.FalseReturnValidationRules = new List >(); this.ValidationRulesForExecutution = new List >(); return this; } #region Implementation of IValidationEngine /// /// Register a validation container before execution /// public IValidationEngineRegister(IValidationRuleContainer container) { if (container != null) { var _validationRules = container.GetValidationRules(); if (_validationRules != null) { ValidationRulesForExecutution.AddRange(_validationRules); } } return this; } /// /// Register a validation rule /// public IValidationEngineRegister(IValidationRule validationRule) { if (validationRule != null) { ValidationRulesForExecutution.Add(validationRule); } return this; } /// /// Exeute the registered validation rules and validation rule /// containers on the provided entity public IValidationEngine Execute(T entity) { FalseReturnValidationRules = new List >(); TrueReturnValidationRules = new List >(); // if no rules are defined return false var _isValid = ValidationRulesForExecutution.Count > 0; try { foreach (var _validationRule in ValidationRulesForExecutution) { bool _result; try { _result = _validationRule.Execute(entity); } catch (Exception) { _result = false; } _isValid = _isValid && _result; if (!_isValid) { FalseReturnValidationRules.Add(_validationRule); } else { TrueReturnValidationRules.Add(_validationRule); } } } catch (Exception) { _isValid = false; } ExecutionResult = new ValidationResult (_isValid, FalseReturnValidationRules); return this; } public IValidationEngine ExecuteIf(T entity, Func condition) { var _result = condition.Invoke(entity); if (_result) { Execute(entity); } else { ExecutionResult = new ValidationResult (true, new List >()); } return this; } /// /// Action to be executed if the result is false /// public void OnFalse(Action > action) { if (!ExecutionResult.Result) { action(ExecutionResult); } } /// /// Return validation rule by execution status /// public List > GetRulesByStatus(ValidationRuleExecutionStatus executionStatus) { switch (executionStatus) { case ValidationRuleExecutionStatus.True: return TrueReturnValidationRules; case ValidationRuleExecutionStatus.False: return FalseReturnValidationRules; default: return new List >(); } } #endregion /// /// Create new instance of the rule engine /// public static IValidationEngine New() { return new RuleEvaluator (); } }
Пример правила проверит адрес электронной почты пользователя
////// Validate user's email /// public class EmailMatchingSpecification : BaseSpecification{ #region Implementation of IValidationRule /// /// Validation expression that must be fulfilled /// public override Expression > ValidationExpression { get { return x => x.Email.IsMatch(RegexHelper.IsValidEmail, RegexOptions.Singleline | RegexOptions.IgnoreCase | RegexOptions.ExplicitCapture | RegexOptions.CultureInvariant); } } #endregion }
использование
Реорганизованный метод, который мы использовали в качестве примера, приведет к:
////// Create new user in the system ///public int CreateUser(User user){ var engine = RuleEvaluator .New(); engine.Register(new IsValidEmailSpecification()) .Register(new HasAlreadyRegisteredWithEmailSpecification()) .Register(new IsValidUsernameSpecification()) .Register(new IsUsernameInUseSpecification()) .Register(new IsValidPassword()) .Register(new ArePasswordsMatchingSpecification()) .Execute(user); if (!engine.ExecutionResult.Result) { // check the results and create proper exception } //... var id = UserRepository.CreateUser(user); return id; }
Или, если мы используем контейнер спецификации:
////// Create new user in the system /// public int CreateUser(User user){ var engine = RuleEvaluator.New(); engine.Register(new NewUserValidationContainer()) .Execute(user); if (!engine.ExecutionResult.Result) { // check the results and create proper exception } //... var id = UserRepository.CreateUser(user); return id; }
Вывод
Использование шаблона спецификации может помочь нам создать повторно используемую проверку и более читаемый код. Это не золотой молоток и может быть накладным расходом для приложений на основе CRUD. Если вы начинаете свой следующий тяжелый корпоративный проект, было бы неплохо подумать о поддержке, удобочитаемости кода и возможности его повторного использования.
* GitHub
Если вы хотите попробовать и поиграть с механизмом валидации, не стесняйтесь раскошелиться на GitHub .