Good time, habrocheloveki!
I would like to raise the topic of server validation of user data. Searching on Habré topics of this subject and googling, I came to the conclusion that people often invent their own bikes to implement the validation mechanism. In this article I want to talk about a simple and beautiful solution that has been successfully used in several projects.
Problem
I often notice that developers are actively using exceptions to report data validation errors. I will demonstrate with an example (since C # is closer to me, I will use it):
public void Validate( string userName, string userPassword)
{
if ( /* */ )
throw new InvalidUsernameException();
if ( /* */ )
throw new InvalidPasswordException();
}
Further it is used like this:
public void RegisterUser ( string username, string password) {
try {
ValidateUser(username, password);
}
catch (InvalidUsernameException ex) {
//
}
catch (InvalidPasswordException ex) {
//
}
//-
}
What is wrong with this example?- exceptions are used at the business validation stage. It is important to remember that data validation errors! = Application errors;
- the use of exceptions at the stage of business validation may lead to the fall of the application. This can happen, for example, if a person forgets to write another
catch block for new validation rules;
- the code looks ugly;
- this solution is difficult to test and maintain.
Decision
The mechanism of data validation can be implemented using the
Composite pattern
(linker) .
We will need the actual object itself a validator, which directly checks the data for compliance with certain rules, a composite validator that encapsulates a collection of validators, as well as an additional class used in
as the repository of the validation result and error collection -
ValidationResult . Consider the last one first:
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;
}
}
We now go directly to the validation mechanism. We need to create a base class
Validator , from which all validators will be inherited:
public abstract class Validator {
public abstract ValidationResult Validate();
}
For validator objects there is 1 method that starts the verification procedure and returns the result.
CompositeValidator is a class containing a collection of validators and a triggering mechanism for checking all child objects:
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;
}
}
Using these classes, we were able to create validators with specific rules, combine them and get the test results.
')
Using
We will rewrite the example given at the beginning of the article using this mechanism. In our case, we need to create 2 validators that check the username and password for compliance with certain rules.
Create an object to check the username
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;
}
}
Similarly, we get the
UserPasswordValidator .
Now we have everything to use the new data validation mechanism:
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()
}
}
findings
What advantages have we gained using this approach:- extensibility. Adding new validators is cheap;
- testability. All validators can be modularly tested, which eliminates errors in the overall validation process;
- maintainability. Validators can be separated into a separate assembly and used in many projects with minor modifications;
- beauty and correctness. This code looks prettier, sleeker and more correct, of the original version, exceptions are not used for business validation.
Conclusion
In the future, you can apply several improvements to the validation mechanism, namely:- encapsulate all validators in one assembly and create a factory that will return a ready-made validation mechanism for various conditions (validation of data for user registration, verification of data upon authorization, etc.);
- The base class Validator can be replaced by the interface, as you like;
- Validation rules can be stored in one place, for more convenient management;
- You can write a validation error handler that will match the error codes and messages displayed to users on the UI, in this case, the process of adding new validators is further simplified. Also, there is no problem localizing messages.
PS
Please do not kick for possible mistakes in spelling and presentation. I tried very hard)
I am pleased to hear criticism and suggestions about the implementation and architecture.
* All source code was highlighted with Source Code Highlighter .