Skip to main content

How to implement complexed validation classes neatly in Spring

đź’» Tech

I’ve worked in an HR Tech company which provides a web application like LinkedIn.
At some point, I was implementing validation methods for “Event” data to create and update the ones.

Then I encounter a problem. There were so many items to validate.
It’s needed to check that:

  • a capacity number is not empty
  • the application end date is not after the event date
  • the event date is not empty
  • the event date is not after the end date
  • URL is not empty

… and 9 more!

If all validations are in one class, the number of lines will be more than 300. It’s too fat!

To be successful in the diet, I’ve done the steps below.

Step1: Categorizing validations

There were 14 validations to implement.
At first, I divide them into 5 groups following these rules:

  • The amount should be equal as much as possible
  • Each group should not mix the validations applicable only to internal events and the ones applicable only to external events* so that we can choose the necessary ones when validating

*… The detail about it will be explained later.

Then I did categorization and created 5 groups:

  • Capacity
  • Relations between contract and dates
  • Empty dates
  • Date reversal
  • URL

Step2: Implement validations neatly with leveraging the Spring feature

In this chapter, I’ll show you the example codes.
Please note every class below are in the same directory (that would be newly created and be like com.example.product.event.validate so that we can keep everything organized.)

Then, I created an interface for implementing the validation classes.
The reason why I choose Interface instead of Abstract Class is that I’d like to make it compulsory to implement unique validation methods in each class.

Here’s the actual code:

/**
 * The base class for validating EventForm data
 */
public interface BaseValidator {

    // Argument "errors" should be replaced with error handling class.
    // In actual case, when "errors" are not empty a program detect it and understand that there are errors.
    void validate(EventForm form, List<String> errors);

    ValidateType getValidateType();
}

validate is a method for implementing concrete validations and getValidateType is used for filtering validations.

Every validation group is categorized into one of the three types below:

  • ALL_EVENTS: Applicable to all events
  • EXCLUSIVE_EVENT_ONLY: Only applicable when the event is an exclusive one
  • PUBLIC_EVENT_ONLY: Only applicable when the event is not an exclusive one

For instance, if an event’s isExclusive boolean status is true, the validation groups which have the status ALL_EVENTS or EXCLUSIVE_EVENT_ONLY will be executed.

The type is stated as Enum and this is the actual code:

/**
 * Types for categorizing validation classes
 */
enum ValidateType {

    /**
     * Applicable to all events
     */
    ALL_EVENTS,

    /**
     * Only applicable when the event is an exclusive one
     */
    EXCLUSIVE_EVENT_ONLY,

    /**
     * Only applicable when the event is "not" an exclusive one
     */
    PUBLIC_EVENT_ONLY;

    /**
     * This returns what kind of validation class will be applicable to the target Event.
     * It's only used in ValidatorFactory class for filtering unnecessary validation class.
     *
     * @param isExclusive: the boolean value indicate whether the event is the exclusive one or not
     * @return Validate types to use this case
     */
    public static EnumSet<ValidateType> getEventTypes(boolean isExclusive) {
        if (isExclusive) {
            return EnumSet.of(ALL_EVENTS, EXCLUSIVE_EVENT_ONLY);
        }
        return EnumSet.of(ALL_EVENTS, PUBLIC_EVENT_ONLY);
    }
}

getEventTypes will be used for filtering validation classes and the actual implementation will later be described.

Then I made implementations of BaseValidator interface.
Every concrete class is corresponding to a validation group so I implemented five validation classes.

Here is an example of implementation:

@Component
public class DateEmptyValidator implements BaseValidator {
    @Override
    public void validate(EventForm form, List<String> errors) {
        validateStartDate(form, errors);
        validateEndDate(form, errors);
    }

    @Override
    public ValidateType getValidateType() {
        return ValidateType.ALL_EVENTS;
    }

    /**
     * Ensure eventStartDate is not empty
     */
    private void validateStartDate(EventForm form, List<String> errors) {
        String formStartTm = form.getEventStartDate();
        if (formStartTm.isEmpty()) {
            errors.add("eventStartDate is empty");
        }
    }

    /**
     * Ensure eventEndDate is not empty
     */
    private void validateEndDate(EventForm form, List<String> errors) {
        String formEndTm = form.getEventEndDate();
        if (formEndTm.isEmpty()) {
            errors.add("eventEndDate is empty");
        }
    }
}

Lastly, I implemented the Service Class and Factory Class for it.

@Service
@AllArgsConstructor(onConstructor = @__(@Autowired))
public class ValidationService {
    private ValidatorFactory factory;

    public void execute(boolean isExclusive, EventForm form, List<String> errors) {
        List<BaseValidator> validators = factory.get(isExclusive);
        validators.forEach(validator -> {
            validator.validate(form, errors);
        });
    }
}
@Component
@AllArgsConstructor(onConstructor = @__(@Autowired))
public class ValidatorFactory {
    // All concrete classes of an interface can be asserted like this in Spring.
    // If you want to know more, see: https://dzone.com/articles/load-all-implementors
    private List<BaseValidator> validators;

    /**
     * Filter out all unnecessary validation classes by checking event types
     * and returns only needed ones.
     * 
     * @param isExclusive: the boolean value indicate whether the event is the exclusive one or not
     * @return All necessary validation methods
     */
    List<BaseValidator> get(boolean isExclusive) {
        EnumSet<ValidateType> eventTypes = ValidateType.getEventTypes(isExclusive);
        return validators.stream().filter(v -> eventTypes.contains(v.getValidateType())).collect(Collectors.toList());
    }
}

When an Event is created or edited and posted as EventForm , this service is called passing the form as an argument.

Look at ValidateFactory. The Spring feature enables to declare all implementations of an interface like this.
For more detail, see: https://dzone.com/articles/load-all-implementors

Tell me what you think of this article! 👉️ @curryisdrink