Validator Design Pattern

Validator Design Pattern

A simple structural Design Pattern

The Validator Pattern is a simple structural Design Pattern that aids in maintaining code organization, separation, simplicity, and maintainability. First, I will show you how you may write code without using the validator pattern. Afterwards, I will illustrate how I prefer to leverage this structural design pattern to have cleaner code.

Without Validator Pattern

It is crucial in backend development to verify the user input before processing it. Without using the validator pattern, you may implement the verification logic in your controller. This is ok for small verifications, but this could get messy pretty quickly. If you have a lot of verifications, your code could look like this.

    @Autowired
    private CatService catService;

    @PostMapping("/add")
    public Boolean addCat(AddCatRequest request){

        if(request.getId() != null){
            throw new IllegalArgumentException("Id must be null");
        }

        if(StringUtils.isBlank(request.getName())){
            throw new IllegalArgumentException("Name cannot be blank");
        }

        if(request.getAge() == null || request.getAge() < 0){
            throw new IllegalArgumentException("Age cannot be null");
        }

        if(request.getBirthday() == null){
            throw new IllegalArgumentException("Birthday cannot be null");
        }

        if(request.getRegisterDay() == null){
            throw new IllegalArgumentException("RegisterDay cannot be null");
        }

        if(request.getBirthday().after(request.getRegisterDay())){
            throw new IllegalArgumentException("Birthday cannot be after registerDay");
        }

        return catService.addCat(request);
    }

This single method, 'add', already consists of 28 lines of code. Considering that we will likely incorporate additional methods like 'update' and 'delete', our controller is rapidly accumulating a significant amount of code and becomes messy.

Validator Pattern

With the validator pattern, we separate the validation logic from the controller logic into a separate file. Therefore our test is cleaner and better testable. The add method in our main controller now uses just two lines of code. We use addValidator.validate(request) to do the validation for us.

    @Autowired
    private CatService catService;

    @Autowired
    private AddValidator addValidator;

    @PostMapping("/add")
    public Boolean addCat(AddCatRequest request){
        addValidator.validate(request);
        return catService.addCat(request);
    }

The validation logic is outsourced into a separate AddValidator.java file.

@Service
public class AddValidator {

    public void validate(AddCatRequest request){

        if(request.getId() != null){
            throw new IllegalArgumentException("Id must be null");
        }

        if(StringUtils.isBlank(request.getName())){
            throw new IllegalArgumentException("Name cannot be blank");
        }

        if(request.getAge() == null || request.getAge() < 0){
            throw new IllegalArgumentException("Age cannot be null");
        }

        if(request.getBirthday() == null){
            throw new IllegalArgumentException("Birthday cannot be null");
        }

        if(request.getRegisterDay() == null){
            throw new IllegalArgumentException("RegisterDay cannot be null");
        }

        if(request.getBirthday().after(request.getRegisterDay())){
            throw new IllegalArgumentException("Birthday cannot be after registerDay");
        }

    }
}

Also supports Polymorphism

Another great advantage of this design pattern is, it perfectly supports Polymorphism. Let's say we have different kind of cats and our AddCatRequest will be changed to an abstract class. The concrete classes are AddSiameseRequest and AddRagDollRequest. With our validation pattern, we can easily use the same controller logic and add different validators to handle each and every cat specifically.

First we are adding our folder structure as follows

├── controller/
│   └── CatController.java
└── validators/
    ├── handler/
    │   ├── ragdoll/
    │   │   └── RagDollAddValidator.java
    │   └── siamese/
    │       └── SiameseAddValidator.java
    ├── router/
    │   └── ValidationRouter.java
    └── AbstractAddValidator.java

We are creating an abstract class AbstractAddValidator.java, which contains a validate() method

public abstract class AbstractAddValidator{
    public abstract void validate(AddCatRequest request);
}

Now we can implement concrete classes RagDollAddValidator.java and SiameseAddValidator.java for handling the specific CatType.

@Service
public class RagDollAddValidator extends AbstractAddValidator {

    @Override
    public void validate(AddCatRequest request){

        if(request.getType().equals(CatType.RAGDOLL)){
            throw new IllegalArgumentException("CatType is not supported");
        }

        if(request.getId() != null){
            throw new IllegalArgumentException("Id must be null");
        }

        if(StringUtils.isBlank(request.getName())){
            throw new IllegalArgumentException("Name cannot be blank");
        }

        if(request.getAge() == null || request.getAge() < 0){
            throw new IllegalArgumentException("Age cannot be null");
        }

        if(request.getBirthday() == null){
            throw new IllegalArgumentException("Birthday cannot be null");
        }

        if(request.getRegisterDay() == null){
            throw new IllegalArgumentException("RegisterDay cannot be null");
        }

        if(request.getBirthday().after(request.getRegisterDay())){
            throw new IllegalArgumentException("Birthday cannot be after registerDay");
        }
    }
}
@Service
public class SiameseAddValidator extends AbstractAddValidator {

    @Override
    public void validate(AddCatRequest request) {

        if(request.getType().equals(CatType.SIAMESE)){
            throw new IllegalArgumentException("CatType is not supported");
        }

        if(request.getId() != null){
            throw new IllegalArgumentException("Id must be null");
        }

        if(StringUtils.isBlank(request.getName())){
            throw new IllegalArgumentException("Name cannot be blank");
        }

        if(request.getAge() == null || request.getAge() < 0){
            throw new IllegalArgumentException("Age cannot be null");
        }

        if(request.getBirthday() == null){
            throw new IllegalArgumentException("Birthday cannot be null");
        }

        if(request.getRegisterDay() == null){
            throw new IllegalArgumentException("RegisterDay cannot be null");
        }

        if(request.getBirthday().after(request.getRegisterDay())){
            throw new IllegalArgumentException("Birthday cannot be after registerDay");
        }
    }
}

Now we need a ValidationRouter.java, which is responsible for returning the correct handler, based on the requests CatType.

@Service
public class ValidationRouter {

    public AbstractAddValidator getValidator(AddCatRequest request){
        if(request.getType().equals(CatType.RAGDOLL)){
            return new RagDollAddValidator();
        }
        if(request.getType().equals(CatType.SIAMESE)){
            return new SiameseAddValidator();
        }

        throw new IllegalArgumentException("CatType not supported");
    }
}

Our CatController.java is still very tidy. The only thing we have to do now, is injecting the ValidationRouter to get the AbstractAddValidator and then call validate().

    @Autowired
    private CatService catService;

    @Autowired
    private ValidationRouter router;

    @PostMapping("/add")
    public Boolean addCat(AddCatRequest request){
        AbstractAddValidator addValidator = router.getValidator(request);
        addValidator.validate(request);
        return catService.addCat(request);
    }

Conclusion

In this article, I have written down one of my favorite simple yet highly useful structural design patterns. We all appreciate clean and well-organized code that is easy to maintain and testable. The validation pattern offers an ideal approach of validating your requests and allowing for clear separation from the business logic. Additionally, we have seen how the validator pattern embraces Polymorphism, thus adhering to the Open-Closed Principle in Software Engineering.