Search code examples
javaconstructorinterfacejava-8default

Creating a "default constructor" for inner sub-interfaces


Okay, the title is maybe hard to understand. I didn't find something correct. So, basically I'm using Java 8 functions to create a Retryable API. I wanted an easy implementation of these interfaces, so I created an of(...) method in each implementation of the Retryable interface where we can use lambda expressions, instead of creating manually an anonymous class.

import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Supplier;

public interface Retryable<T, R> extends Function<T, R>{

    void retrying(Exception e);

    void skipping(Exception e);

    int trials();

    @Override
    default R apply(T t) {
        int trial = 0;
        while (true) {
            trial++;
            try {
                return action(t);
            } catch (Exception e) {
                if (trial < trials()) {
                    retrying(e);
                } else {
                    skipping(e);
                    return null;
                }
            }
        }
    }

    R action(T input) throws Exception;

    interface RunnableRetryable extends Retryable<Void, Void> {

        static RunnableRetryable of(Consumer<Exception> retrying, Consumer<Exception> skipping, int trials, CheckedRunnable runnable) {
            return new RunnableRetryable() {
                @Override
                public void retrying(Exception e) {
                    retrying.accept(e);
                }

                @Override
                public void skipping(Exception e) {
                    skipping.accept(e);
                }

                @Override
                public int trials() {
                    return trials;
                }

                @Override
                public Void action(Void v) throws Exception {
                    runnable.tryRun();
                    return null;
                }
            };
        }

        @FunctionalInterface
        interface CheckedRunnable extends Runnable {

            void tryRun() throws Exception;

            @Override
            default void run() {
                try {
                    tryRun();
                } catch (Exception e) {
                    throw new RuntimeException(e);
                }
            }
        }
    }

    interface ConsumerRetryable<T> extends Retryable<T, Void> {

        static <T> ConsumerRetryable of(Consumer<Exception> retrying, Consumer<Exception> skipping, int trials, CheckedConsumer<T> consumer) {
            return new ConsumerRetryable<T>() {
                @Override
                public void retrying(Exception e) {
                    retrying.accept(e);
                }

                @Override
                public void skipping(Exception e) {
                    skipping.accept(e);
                }

                @Override
                public int trials() {
                    return trials;
                }

                @Override
                public Void action(T t) throws Exception {
                    consumer.tryAccept(t);
                    return null;
                }
            };
        }

        @FunctionalInterface
        interface CheckedConsumer<T> extends Consumer<T> {

            void tryAccept(T t) throws Exception;

            @Override
            default void accept(T t) {
                try {
                    tryAccept(t);
                } catch (Exception e) {
                    throw new RuntimeException(e);
                }
            }
        }
    }

    interface SupplierRetryable<T> extends Retryable<Void, T> {

        static <T> SupplierRetryable of(Consumer<Exception> retrying, Consumer<Exception> skipping, int trials, CheckedSupplier<T> supplier) {
            return new SupplierRetryable<T>() {
                @Override
                public void retrying(Exception e) {
                    retrying.accept(e);
                }

                @Override
                public void skipping(Exception e) {
                    skipping.accept(e);
                }

                @Override
                public int trials() {
                    return trials;
                }

                @Override
                public T action(Void v) throws Exception {
                    return supplier.tryGet();
                }
            };
        }

        @FunctionalInterface
        interface CheckedSupplier<T> extends Supplier<T> {

            T tryGet() throws Exception;

            @Override
            default T get() {
                try {
                    return tryGet();
                } catch (Exception e) {
                    throw new RuntimeException(e);
                }
            }
        }
    }

    interface FunctionRetryable<T, R> extends Retryable<T, R> {

        static <T, R> FunctionRetryable of(Consumer<Exception> retrying, Consumer<Exception> skipping, int trials, CheckedFunction<T, R> function) {
            return new FunctionRetryable<T, R>() {
                @Override
                public void retrying(Exception e) {
                    retrying.accept(e);
                }

                @Override
                public void skipping(Exception e) {
                    skipping.accept(e);
                }

                @Override
                public int trials() {
                    return trials;
                }

                @Override
                public R action(T t) throws Exception {
                    return function.tryApply(t);
                }
            };
        }

        @FunctionalInterface
        interface CheckedFunction<T, R> extends Function<T, R> {

            R tryApply(T t) throws Exception;

            @Override
            default R apply(T t) {
                try {
                    return tryApply(t);
                } catch (Exception e) {
                    throw new RuntimeException(e);
                }
            }
        }
    }
}

But as you can see, there's a lot of duplicate code in every of(...) methods. I could create a kind of "constructor" (that's not the correct word, because interfaces can't have a constructor) in the Retryable interface, but I don't know how. Does someone have an idea ?


Solution

  • The main problem is your API explosion. All these nested interfaces extending Retryable do not add any functionality, but require the user of this code to deal with them, once they are part of the API. Further, they are the cause of this code duplication, as each of these redundant interfaces requires its own implementation, whereas all implementations are basically doing the same.

    After removing these obsolete types, you can simply implement the operations as delegation:

    public interface Retryable<T, R> extends Function<T, R>{
        void retrying(Exception e);
        void skipping(Exception e);
        int trials();
        @Override default R apply(T t) {
            try { return action(t); }
            catch(Exception e) {
                for(int trial = 1; trial < trials(); trial++) {
                    retrying(e);
                    try { return action(t); } catch (Exception next) { e=next; }
                }
                skipping(e);
                return null;
            }
        }
    
        R action(T input) throws Exception;
    
        public static Retryable<Void, Void> of(Consumer<Exception> retrying,
                Consumer<Exception> skipping, int trials, CheckedRunnable runnable) {
            return of(retrying, skipping, trials, x -> { runnable.tryRun(); return null; });
        }
    
        @FunctionalInterface interface CheckedRunnable extends Runnable {
            void tryRun() throws Exception;
            @Override default void run() {
                try { tryRun(); } catch (Exception e) { throw new RuntimeException(e); }
            }
        }
    
        public static <T> Retryable<T, Void> of(Consumer<Exception> retrying,
                Consumer<Exception> skipping, int trials, CheckedConsumer<T> consumer) {
            return of(retrying, skipping, trials,
                      value -> { consumer.tryAccept(value); return null; });
        }
    
        @FunctionalInterface interface CheckedConsumer<T> extends Consumer<T> {
            void tryAccept(T t) throws Exception;
            @Override default void accept(T t) {
                try { tryAccept(t); } catch (Exception e) { throw new RuntimeException(e); }
            }
        }
    
        public static <T> Retryable<Void, T> of(Consumer<Exception> retrying,
                Consumer<Exception> skipping, int trials, CheckedSupplier<T> supplier) {
            return of(retrying, skipping, trials, voidArg -> { return supplier.tryGet(); });
        }
    
        @FunctionalInterface interface CheckedSupplier<T> extends Supplier<T> {
            T tryGet() throws Exception;
            @Override default T get() {
                try { return tryGet(); }
                catch (Exception e) { throw new RuntimeException(e); }
            }
        }
    
        public static <T, R> Retryable<T, R> of(Consumer<Exception> retrying,
                Consumer<Exception> skipping, int trials, CheckedFunction<T, R> function) {
            return new Retryable<T, R>() {
                @Override public void retrying(Exception e) { retrying.accept(e); }
                @Override public void skipping(Exception e) { skipping.accept(e); }
                @Override public int trials() { return trials; }
                @Override public R action(T t) throws Exception {
                    return function.tryApply(t);
                }
            };
        }
    
        @FunctionalInterface interface CheckedFunction<T, R> extends Function<T, R> {
            R tryApply(T t) throws Exception;
            @Override default R apply(T t) {
                try { return tryApply(t); }
                catch (Exception e) { throw new RuntimeException(e); }
            }
        }
    }
    

    There is only one implementation class needed, which has to be able to deal with an argument and a return value, the others can simply delegate to it using an adapter function, doing either, dropping the argument or returning null, or both.

    For most use cases, the shape of the lambda expression is appropriate to select the right method, e.g.

    Retryable<Void,Void> r = Retryable.of(e -> {}, e -> {}, 3, () -> {});
    Retryable<Void,String> s = Retryable.of(e -> {}, e -> {}, 3, () -> "foo");
    Retryable<Integer,Integer> f = Retryable.of(e -> {}, e -> {}, 3, i -> i/0);
    

    but sometimes, a little hint is required:

    // braces required to disambiguate between Function and Consumer
    Retryable<String,Void> c = Retryable.of(e->{}, e ->{}, 3,
                                            str -> { System.out.println(str); });