Search code examples
javaclassgenericsradixcompletable-future

Run a list of classes in parallel which implement the same interface in java


I have created the below mentioned base and derived class

public abstract class ContextBase {
    private String customerID;
    private String marketplaceID;
}

public class ReturnContext extends ContextBase {
    private String returnID;
}

Then I created an interface which has a method called perform and some classes which implements this interface

public interface ValidatorInterface<T extends ContextBase> {

    CompletableFuture<List<String>> perform(T context);
}

public class AlphaValidator implements ValidatorInterface<ContextBase> {
    @Override
    public CompletableFuture<List<String>> perform(ContextBase contextBase) {
        ....
    }
}

public class BetaValidator implements ValidatorInterface<ReturnContext> {
    @Override
    public CompletableFuture<List<String>> perform(ReturnContext context) {
        ....
    }
}

I want to run a list of classes which implements the ValidatorInterface in parallel, So I created a ValidatorRunner class

public class ValidatorRunner<T extends ContextBase> {

    public List<String> runValidators(final T context,
                                       final List<ValidatorInterface<T>> validatorsList) {

        final Map<String, CompletableFuture<List<String>>> futureAggregatedProblems = new LinkedHashMap<>(validatorsList.size());
        List<String> problems = new ArrayList<>();
        validatorsList.forEach(validator -> runValidator(
                validator, futureAggregatedProblems, context));

        futureAggregatedProblems.forEach((validatorName, futureProblems) -> {
            try {
                problems.addAll(futureProblems.get(FUTURE_TIMEOUT_MS, TimeUnit.MILLISECONDS));
            } catch (InterruptedException | ExecutionException | CompletionException | TimeoutException ex) {
                // TODO Do not ignore InterruptedException
                throw new InternalServiceException("Error executing validators : " + ex.getMessage(), ex);
            }
        });

        return problems;
    }

    private void runValidator(final ValidatorInterface<T> validator,
                              final Map<String, CompletableFuture<List<String>>> futureAggregatedProblems,
                              final T context) {

        futureAggregatedProblems.put(validator.getClass().getCanonicalName(), validator.perform(context));
    }

This implementation does seem to work when I do this

ValidatorRunner<ReturnContext> validatorRunner = new ValidatorRunner<ReturnContext>();
ReturnContext context = new ReturnContext();
BetaValidator beta = new BetaValidator();
List<ValidatorInterface<ReturnContext>> validatorList = new ArrayList<>();
validatorList.add(beta);
List<String> problems = validatorRunner.runValidators(context, validatorList);

The problem is that AlphaValidator is implemented on base type (ContextBase) while BetaValidator is implemented on derived type (ReturnContext). I want to run AlphaValidator and BetaValidator in parallel while passing an instance of ReturnContext as context. How it can be achieved ?

EDIT 1

The reason I created ValidatorInterface as T extends ContextBase because I want each validator to use either a ContextBase or a derived class of ContextBase.

I have created AlphaValidator on base type ContextBase because I want the AlphaValidator to be created and executed using any of the derived class of ContextBase. While BetaValidator is created on ReturnContext because I want the BetaValidator to be created and executed using ReturnContext only. Lets suppose I create a new derived class called ReplacementContext which extends ContextBase and also a new validator called as GammaValidator on derived type ReplacementContext. I want to be able to run AlphaValidator and GammaValidator using ReplacementContext. AlphaValidator and BetaValidator should run on ReturnContext. But I dont want to run BetaValidator and GammaValidator in parallel because they serve different purpose and separate contexts (thats why they are created on separate contexts, ReturnContext and ReplacementContext respectively).


Solution

  • Problem with the above code

    When providing a single concrete context for multiple validators each validator MUST support the given context class. This is a design issue.

    My solution

    An alternative solution is to provide multiple contexts to the runValidate method where each context can placed next to the correct validators.

    Given my example the ContextProcessor.of() will take care that the compiler only allows correct assignments for validators and contexts.

    I suggest to change your ValidatorRunner to a similar version to this code:

    public class ValidatorRunner {
    
        static class ContextProcessor<T extends ContextBase> {
            private final List<ValidatorInterface<T>> validators;
            private final T context;
    
            private ContextProcessor(List<ValidatorInterface<T>> validators, T context) {
                this.validators = validators;
                this.context = context;
            }
    
            public static <V extends ValidatorInterface<C>, C extends ContextBase> ContextProcessor<C> of(List<V> validators, C context) {
                //noinspection unchecked
                return new ContextProcessor<>((List<ValidatorInterface<C>>) validators, context);
            }
    
            public List<ValidatorInterface<T>> getValidators() {
                return validators;
            }
    
            public T getContext() {
                return context;
            }
        }
    
        public List<String> runValidators(final List<ContextProcessor<? extends ContextBase>> processors) {
    
            var validators = processors.stream().map(p -> p.validators.size()).reduce(Integer::sum).orElse(0);
            var latch = new CountDownLatch(validators);
    
            var problemStack = new ArrayList<String>();
    
            //noinspection unchecked
            processors.forEach(p -> p.getValidators().forEach(validator ->
                    runValidator((ValidatorInterface<ContextBase>) validator, p.getContext())
                            .orTimeout(10_000L, TimeUnit.MILLISECONDS)
                            .thenAccept(problems -> {
                                problemStack.addAll(problems);
                                latch.countDown();
                            })
            ));
    
            try {
                latch.await();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            return problemStack;
        }
    
        private CompletableFuture<List<String>> runValidator(final ValidatorInterface<ContextBase> validator, final ContextBase context) {
            return validator.perform(context);
        }
    
        public static void main(String[] args) {
            var problems = new ValidatorRunner().runValidators(
                    List.of(
                            ContextProcessor.of(List.of(new AlphaValidator(), new AlphaValidator()), new ReplacementContext()),
                            ContextProcessor.of(List.of(new AlphaValidator()), new ContextBase() {}),
                            ContextProcessor.of(List.of(new BetaValidator(), new BetaValidator()), new ReturnContext())
                    )
            );
    
            System.out.println(problems);
        }
    }
    

    When running the provided main method, the following result is expected:

    [error within context ReplacementContext, error within context ReplacementContext, error within context , error within context ReturnContext, error within context ReturnContext]
    

    Where every validator has returned the Future String "error within context" + context.getClass().getSimpleName()

    Maybe this will help you find a way to solve your problem.