Chain of validators with Kotlin

Chain of validators with Kotlin

If “Making mistakes is the privilege of philosophers,” as Socrates used to say, then I’m a philosopher. I’m also a programmer whose job is to make life easier for others and myself, which prompted me to write tailor-made validators.

As we well know, most of humanity’s problems have long been solved and written down in countless books, articles, and all kinds of scientific materials. It’s no different in programming. Despite the rapid development of this field of science, some elements rarely change — design patterns.

Without delaying the introduction, we will use two design patterns to write our validators — “Composite,” which will help us to standardize validator objects (leaf) and compositions of these objects (composite). Then we will use the “Chain of responsibility” pattern, through which we will pass tasks to the following validators.

Composite design pattern
Composite design pattern

The “Composite” design pattern treats both single objects (leaf) and compositions of these objects (composite) in the same way, thanks to the implementation of a common interface (component).

“Composite” design pattern, UML class diagram
“Composite” design pattern, UML class diagram

Following this, we will create a common TextValidator interface to prepare the objects of the individual validators and the object that will group these validators. The interface includes the validate method that returns a validation result as a sealed class ValidatorResult and field validatorResult.

interface TextValidator {
fun validate(stringToValidate: String): ValidatorResult
var validatorResult: ValidatorResult
}
sealed class ValidatorResult {
class Error(val message: String) : ValidatorResult()
class Hint(val message: String) : ValidatorResult()
object NoResult : ValidatorResult()
object Success : ValidatorResult()
}

Once the interface has been created, we develop individual validators and an object that will be a composition of many validators. As an example, let’s assume that we need to validate a phone number whose criteria are as follows:

  • value is required,
  • characters length from 9 to 12,
  • the value should match the specified phone pattern.
  1. NonEmptyValidator checks if the value has been filled in.
class NonEmptyTextValidator(private val message: String) : TextValidator() {override var validatorResult: ValidatorResult = ValidatorResult.NoResultoverride fun validate(stringToValidate: String): ValidatorResult {
validatorResult = if (stringToValidate.trim().isEmpty())
ValidatorResult.Error(message)
else
ValidatorResult.Success

return validatorResult
}
}

2. LengthValidator checks if the length of the string is within the specified limits. If it is out of range, it will return ValidatorResult.Error, if it is equal to the min or max value, it will return ValidatorResult.Hint.

class LengthTextValidator(
private val minLength: Int? = null,
private val maxLength: Int? = null,
private val message: String) : TextValidator() {
override var validatorResult: ValidatorResult = ValidatorResult.NoResultoverride fun validate(stringToValidate: String): ValidatorResult {

validatorResult = when {
minLength != null && stringToValidate.count() < minLength ->
ValidatorResult.Error(String.format(message, minLength))
maxLength != null && stringToValidate.count() > maxLength ->
ValidatorResult.Error(String.format(message, maxLength))
maxLength != null && stringToValidate.count() == maxLength ->
ValidatorResult.Hint(String.format(message, maxLength))
minLength != null && stringToValidate.count() == minLength ->
ValidatorResult.Hint(String.format(message, minLength))
else -> ValidatorResult.Success

}
return validatorResult
}
}

And the last PhoneValidator, whose job will be to check whether the filled string matches the pattern of a phone number.

class PhoneTextValidator(
private val message: String,
private val patternProvider: PatternProvider)
: TextValidator() {
override var validatorResult: ValidatorResult = ValidatorResult.NoResultoverride fun validate(stringToValidate: String): ValidatorResult {
validatorResult = when {
patternProvider.providePhonePatterns().any {
it.matcher(stringToValidate).matches()
} -> ValidatorResult.Success
else -> ValidatorResult.Error(message)
}
return validatorResult
}
}

After preparing individual validators, we can move on to create a composition class. In this class, we will use the “Chain of responsibility” design pattern, which will take care of passing tasks to subsequent validators. The implementation of the validation method in this class will rely on calling validation on all individual validator objects belonging to the composition.

Chain of responsibility design pattern.
Chain of responsibility design pattern.

Let’s create a ChainTextValidator class with a variable number of arguments/validators — vararg in the parameter.

class ChainTextValidator(
private vararg val validators: TextValidator
) : TextValidator() {
override var validatorResult: ValidatorResult = ValidatorResult.NoResultoverride fun validate(stringToValidate: String): ValidatorResult {
validators.forEach { validator ->
validatorResult = validator.validate(stringToValidate)

if (validatorResult is ValidatorResult.Error)
return validatorResult

if (validatorResult is ValidatorResult.Hint)
return validatorResult

}
return validatorResult
}
}

Calling the validate method in the composition class will call the validate method on each validator object.

In conclusion, to validate text fields, we used a Composition design pattern, which allowed us to group and unify the validator objects. Then, the Chain of responsibility design pattern was used to pass validation jobs to individual validators. This approach allows us to create independent validators that we can compose in any way we want, and it’s easy to test.

Many thanks to my Team Leader Mateusz Zając who gave me the idea and pushed me to write my first article 😀

To dive deep into the design pattern, I recommend Christopher Okhravi on YouTube: https://www.youtube.com/c/ChristopherOkhravi

--

--