1
\$\begingroup\$

I'm experimenting with some variations of validation pattern. I've created Validator class:

public class Validator<O> {

    private O toValidate;
    private ValidationResult error;

    private Validator(O object) {
        this(object, null);
    }

    private Validator(O object, ValidationResult existingError) {
        toValidate = object;
        error = existingError;
    }

    public static <O> Validator<O> toFirstError(O object) {
        return new Validator<>(object);
    }

    private static <O> Validator<O> toFirstError(O object, ValidationResult existingError) {
        return new Validator<>(object, existingError);
    }

    public Validator<O> must(Function<O, ValidationResult> predicate) {
        if (error != null) {
            return this;
        }
        var validationResult = predicate.apply(toValidate);
        validationResult.ifInvalid(e -> error = e);
        return this;
    }

    public Validator<O> ifValid(Consumer<O> consumer) {
        if (error == null) {
            consumer.accept(toValidate);
        }

        return this;
    }

    public <N> Validator<N> mapIfValid(Function<O, N> mapFun) {
        if (error == null) {
            var newObject = mapFun.apply(toValidate);
            return toFirstError(newObject, error);
        }
        return toFirstError(null, error);
    }

    public ValidationResult getFirstError() {
        if (error == null) {
            return ValidationResult.valid();
        }
        return error;
    }

    public CommandResult<O> toCommandResult() {
        if (error == null) {
            return CommandResult.success(toValidate);
        }

        return CommandResult.error(error.getErrorCode());
    }
}

ValidationResult is pretty simple

@AllArgsConstructor(access = AccessLevel.PRIVATE)
public class ValidationResult {

    @Getter
    private final ErrorCode errorCode;

    public static ValidationResult valid() {
        return new ValidationResult(null);
    }

    public static ValidationResult error(ErrorCode errorCode) {
        return new ValidationResult(errorCode);
    }

    public boolean isInvalid() {
        return errorCode != null;
    }

    public boolean isValid() {
        return errorCode == null;
    }

    public void ifInvalid(Consumer<ValidationResult> consumer) {
        if (isInvalid()) {
            consumer.accept(this);
        }
    }

    @Override
    public String toString() {
        if (isValid()) {
            return "VALID";
        } else {
            return "INVALID[errorCode=%s]".formatted(errorCode);
        }
    }

}

Next, I have some command handler. And this is the example of how to use the validator:

@Override
    public CommandResult<Profile> handle(CreateProfileCommand command) {
        return Validator.toFirstError(command)
                .must(this::userDoNotHaveProfile)
                .mapIfValid(this::buildProfile)
                .must(Profile::validate)
                .ifValid(repository::save)
                .toCommandResult();
    }

    private ValidationResult userDoNotHaveProfile(CreateProfileCommand command) {
        var existForUser = repository.existByOwnerId(command.owner());
        if (existForUser) {
            return ValidationResult.error(USER_ALREADY_HAVE_PROFILE);
        }
        return ValidationResult.valid();
    }

    private Profile buildProfile(CreateProfileCommand command) {
        var slug = createUniqueSlug(command.name());
        return new Profile(
                ProfileId.random(),
                command.owner(),
                command.name(),
                slug,
                Description.empty(),
                clock.instant()
        );
    }

And some domain object with different usage of the validator:

public record Profile(
        ProfileId id,
        UserAccountId owner,
        Name name,
        ProfileSlug slug,
        Description description,
        Instant created
) {

    public ValidationResult validate() {
        return Validator.toFirstError(this)
                .must(p -> p.name().validate())
                .getFirstError();
    }

}


public record Name(
        String asString
) {

    private static final int MIN_LEN = 2;
    private static final int MAX_LEN = 200;

    public static Name of(String name) {
        return new Name(name);
    }

    public ProfileSlug createSlug() {
        return ProfileSlug.of(this);
    }

    public ValidationResult validate() {
        if (asString == null
                || asString.length() < MIN_LEN
                || asString.length() > MAX_LEN) {
            return ValidationResult.error(PROFILE_NAME_INVALID_NAME_LENGTH);
        }
        return ValidationResult.valid();
    }

}

This variation in general looks good for me, but the whole logic is inside validation class, and this look a little stupid for me. Maybe I'm reinventing the wheel and this method have its name, or maybe exist lib that implements this pattern?

\$\endgroup\$
2
  • 1
    \$\begingroup\$ Are you aware of "Bean Validation API"? And if yes, why do you consider it not suitable? \$\endgroup\$ Nov 17, 2021 at 6:35
  • 1
    \$\begingroup\$ 1) I'm trying to learn the intuition - it't good or bad idea 2) Bean Validation means countless annotations, and finally (for me) it looks like 'annotation driven development' 3) It's hard to valid bussiness logic 4) Throws exception. I'm learning to code universally, simple code without a dozen of catch blocks \$\endgroup\$
    – Mateusz
    Nov 17, 2021 at 7:38

2 Answers 2

3
\$\begingroup\$

Frankly, I have a hard time trying figuring out how the validator is supposed to work. Either the code needs a lot of documentation or it requires comprehensive redesigning.

I don't understand the purpose of the toFirstError methods. The names suggest that they perform a validation action, but instead they are just static replacements for the constructors. The purpose of having static initialization methods instead of direct constructors, is to allow the initializer to return a different type depending on the parameters. You're not doing that so they are just confusing now.

I get a feeling that you are trying to mimic Java streams with the validator. But you end up loading responsibilities about converting and processing the input into the validator. This requires you to maintain state in the validator. All this makes the validator unnecessarily complex and may case loss of reusability. Is the ability to make your code look like stream operations worth it? Can you efficiently use the validator as part in a regular Java stream processing?

Defining each validation criteria as a predicate helps in testing but at the moment each component that requires validation has to know the correct combination of predicates in order to successfully validate an object. You're better of creating a single composite predicate for each class, in which case having the ability to define the validation as a list of predicates becomes a bit useless.

Regarding your comment about annotation driven development, you are placing validation code into the business objects with the methods that return ValidationResult. You could as well do that with the Bean Validation API I asked about. Your approach is no better, you are still binding your business objects to a single concrete validation library. If you want to use some other way to validate your objects, you then have a lot of useless validation code lying around in your classes. Functionally it's no different and you wouldn't be trying to reinvent the wheel. Whether it is annotation or your own code, the amount of checks is equal and with a well known library you don't force others to learn new undodumented libraries.

Personally I do not like having validation code in business objects, whether it is Bean Validation API or home grown stuff. It breaks the single responsibility principle. I prefer having validators be separate classes that implement validation rules for a specific class for a specific use case. I have been enough times in a place where I would have wanted to reuse a common data class but have not been able to do so because they have been riddled with validation rules invented by the person who created the first use case.

Can you report all validation errors in one pass? The code isn't very straightforward but I get the impression that you stop at the first one, right? Not being able to list all validation errors immediately would be a complete deal braker.

\$\endgroup\$
2
\$\begingroup\$

In opposition to (just posted) answer @TorbenPutkonen - I think that separating validation from data is exactly against OOP principles (cohesion and encapsulation) and leads to massive duplication of if (something.isValid) checks and ultimately (from my experience) harder to maintain and secure code.

If you indeed desire to have validation of separate fields summed up in one validation result abstraction (I don't recommend it as I prefer to fail faster, also it introduces no real benefit while heavily complicating the code) then mentioned bean validation api seems the way to go. Incidentally I happened to be in similar discussion on reddit: https://www.reddit.com/r/java/comments/ongysb/bean_validation_vs_calling_validation_methods/

However if you would like to go for more compiler-relaying way (functional languages inspired) I'd recommend reading the legendary parse don't validate article. Tldr: keep validation with types, if you validate something in constructor then no instance of class is invalidated - guaranteed by compiler.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.