Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to generalize a static clousure?

I have the following scenario: two validation Helpers

the StringValidationHelper ...

public class StringValidationHelper {

    public static Validation<String> notNull = 
        SimpleValidation.from(s -> s != null, "must not be null.");

    public static Validation<String> moreThan(int size) {
        return SimpleValidation.from(
            s -> s.length() >= size, 
            String.format ("must have more than %s chars.", size));
    }
        ... // More methods (lessThan, etc)}

... and NumberValidationHelper.

public class NumberValidationHelper {

    public static Validation<Number> notNull = 
        SimpleValidation.from(n -> n != null, "must not be null");

    public static <N extends Number & Comparable<N>> Validation<N> lowerThan(N max){
        return SimpleValidation.from(
            n -> n.compareTo(max) == -1,
            String.format("must be lower than %s.", max));
    }
    ... // More methods like (greaterThan, etc)}

The method from is a static factory method that receives a Predicate and a message to eventual validation fails.

public class SimpleValidation<K> implements Validation<K>{
    private Predicate<K> predicate;
    private String onErrorMessage;

    private SimpleValidation(Predicate<K> predicate, String onErrorMessage) {
        this.predicate = predicate;
        this.onErrorMessage = onErrorMessage;
    }

    public static <K> SimpleValidation<K> from(Predicate<K> predicate, String onErrorMessage){
        return new SimpleValidation<>(predicate, onErrorMessage);
    }
    ... // Omitted for simplicity
}

Thanks to the Validation interface, you can enjoy a wonderfully smooth interface

    @FunctionalInterface
    public interface Validation<K> {

        ... // Omitted for simplicity

        default Validation<K> and(Validation<K> other) {
            return param -> {
                ValidationResult firstResult = this.test (param);
                return ! firstResult.isValid()? firstResult: other.test(param);
            };
        }
        ... // Omitted for simplicity
    }

So I can start, for example, a validation using the closure notNull.

Example: with NumberValidationHelper

public class MyValidate {
    void validate(int toValidate) {
        notNull.and(lowerThan(100)).test(toValidate).isValid();
    }
}

This validation framework I developed based on this article.

Well, notNull enclaves a type-independent behavior, so I'd like to remove the duplication of these two helpers. I'm not finding an obvious shape without losing the fluid interface.

Because the variable is static, you can not use generics and extend the behavior, for instance.

public abstract class GenericHelper<K> {
    public static Validation<K> notNull = SimpleValidation.from(o -> o != null, "must not be null.");
}

Also it does not bother me to type Validation with Object as below:

public abstract class GenericHelper {

    public static Validation<Object> notNull = SimpleValidation.from(o -> o != null, "must not be null.");
}

... because in the call chaining, it will give compilation error since the result of notNull will be a Validation< Object > and and will be expecting a Validation< Integer >

notNull.and(lowerThan(100)).test(toValidate).isValid(); //Does not compile

Is there any way to use the Java 8 function features that keep this interface flowing generically, running away from the solutions I've tried above?

thankful

like image 516
ProfessorX Avatar asked Apr 28 '18 02:04

ProfessorX


1 Answers

You should relax the generic signature of and, allowing a Validation<T> with a more specific T as parameter, to produce a Validation<T> as result:

default <T extends K> Validation<T> and(Validation<T> other) {
    return param -> {
        ValidationResult firstResult = this.test(param);
        return ! firstResult.isValid()? firstResult: other.test(param);
    };
}

Staying with your examples, you still cannot write

void validate(int toValidate) {
    notNull.and(moreThan(100)).test(toValidate).isValid();
}

as moreThan returns a Validation<String> which can not test an int, but spotting such errors is what Generics is all about (I suppose, you have another moreThan method in your actual code base which you didn’t include in your question). But the following will now work with your example:

void validate(int toValidate) {
    notNull.and(lowerThan(100)).test(toValidate).isValid();
}

Sometimes, you need to test a validation of a more specific type before a more generic validation which still doesn’t work with the method shown above. One solution would be to go the same route as the JDK developers and augment Function.andThen(after) with a Function.compose(before), allowing to swap the roles

default <T extends K> Validation<T> compose(Validation<T> other) {
    return param -> {
        ValidationResult firstResult = other.test(param);
        return ! firstResult.isValid()? firstResult: this.test(param);
    };
}

Or you create a static method, which allows both arguments to have a broader type than the resulting Validation:

static <T> Validation<T> and(Validation<? super T> first, Validation<? super T> second) {
    return param -> {
        ValidationResult firstResult = first.test(param);
        return ! firstResult.isValid()? firstResult: second.test(param);
    };
}

Note that the static method can be combined with the convenient instance method, so that the caller only needs to resort to the static method when hitting the limitations of the generic signature:

@FunctionalInterface
public interface Validation<K> {
    ValidationResult test(K item);

    default <T extends K> Validation<T> and(Validation<T> other) {
        return and(this, other);
    }
    static <T> Validation<T> and(Validation<? super T> first,Validation<? super T> second){
        return param -> {
            ValidationResult firstResult = first.test(param);
            return ! firstResult.isValid()? firstResult: second.test(param);
        };
    }
}

So you can still write

notNull.and(lowerThan(100)).test(toValidate).isValid();

but when hitting the limitation, e.g.

Validation<Object> anotherCriteria;
…
lowerThan(100).and(anotherCriteria).test(toValidate).isValid();

does not work, you can resort to

Validation.and(lowerThan(100), anotherCriteria).test(toValidate).isValid();
like image 77
Holger Avatar answered Oct 13 '22 18:10

Holger