Статьи

Come Undone — проверка аргументов для ракетчиков

Валидация является важной частью разработки. От проверки аргументов до проверки рабочих процессов, конвейеров, границ ввода / вывода, мы запускаем проверки, чтобы убедиться, что приложение будет работать правильно. Лично мне, как инженеру-программисту, мне нравится видеть бизнес-процессы перед реализацией и пытаться выразить себя с помощью кода таким образом.

С точки зрения валидации это означает, что написание кода прямо затрудняет понимание того, что поэт хотел сказать в сложных сценариях.

Пример:

/// 
/// 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
    /// 
    ValidationResult
  
    ExecutionResult { get; set; }

    /// 
   
    /// Register a validation container before execution
    /// 
    IValidationEngine
   
     Register(IValidationRuleContainer
    
      container);

    /// 
     
    /// Register a validation rule
    /// 
    IValidationEngine
     
       Register(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
    /// 
    IValidationEngine
            
              Restore();

    /// 
             
    /// Remove registered rules and clears executed data
    /// 
    IValidationEngine
             
               ReSetup();
}

             
            
           
          
         
        
       
      
     
    
   
  
 

Технические характеристики

Спецификация будет единственной единицей, которая может быть проверена по входным данным в механизме проверки. Контракт базовой спецификации должен иметь:

  • Выражение проверки, которое будет использоваться для выполнения с входными данными
  • Результат — сохранить значение выполнения в отношении входных данных
  • Выполнить метод, который будет вызываться из механизма проверки
/// 
/// 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(IValidationRule
  
    validationRule);

    /// 
   
    /// 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 ValidationResult
       
         ExecutionResult { get; set; }

    /// 
        
    /// Restore the validation engine to initial setup state
    /// 
    public IValidationEngine
        
          Restore()
    {
        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 IValidationEngine
                
                  Register(IValidationRuleContainer
                 
                   container)
    {
        if (container != null)
        {
            var _validationRules = container.GetValidationRules();
            if (_validationRules != null)
            {
                ValidationRulesForExecutution.AddRange(_validationRules);
            }
        }

        return this;
    }

    /// 
                  
    /// Register a validation rule
    ///
    public IValidationEngine
                  
                    Register(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 .