Creating a "default constructor" for internal sub-interfaces

Okay, the title can be difficult to understand. I have not found something correct. So, basically I am using Java 8 features to create the Retryable API. I wanted to easily implement these interfaces, so I created a method of(...)

in each implementation of the Retryable interface where we can use lambda expressions instead of manually creating the 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, of(...)

there is a lot of duplicate code in each method . I could create a kind of "constructor" (that's not the right word, because interfaces can't have a constructor) in the Retryable interface, but I don't know how. Anyone have an idea?

+3


source to share


2 answers


The main problem is your API explosion. All of these nested interfaces extending Retryable

don't add any functionality, but require the user of this code to deal with them as soon as they become part of the API. They also cause code duplication, since each of these redundant interfaces requires its own implementation, whereas all implementations basically do the same.

After removing these deprecated types, you can simply perform 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); }
        }
    }
}

      

Only one implementation class is required to deal with the argument and the return value, others can simply delegate it using the adapter function, doing either by dropping the argument, or by returning null

, or both.



In most cases, the form of a lambda expression is appropriate for choosing the correct method, eg.

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 needed:

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

      

+5


source


It looks like you can influence some of these in a (perhaps in a package-private) abstract class:

abstract class AbstractRetryable<T, R> implements Retryable<T, R> {
    private final Consumer<Exception> retrying;
    private final Consumer<Exception> skipping;
    private final int                 trials;
    AbstractRetryable(Consumer<Exception> retrying,
                      Consumer<Exception> skipping,
                      int                 trials) {
        this.retrying = Objects.requireNonNull(retrying, "retrying");
        this.skipping = Objects.requireNonNull(skipping, "skipping");
        this.trials   = trials;
    }
    @Override
    public void retrying(Exception x) {
        retrying.accept(x);
    }
    @Override
    public void skipping(Exception x) {
        skipping.accept(x);
    }
    @Override
    public int trials() {
        return trials;
    }
}

      

The only problem is that you are using subinterfaces, so you cannot create an anonymous class that extends the abstract class and implements a helper interface.

Then you could write additional (again, possibly batch) subclasses:

final class RunnableRetryableImpl
extends    AbstractRetryable<Void, Void>
implements RunnableRetryable {
    private final CheckedRunnable runnable;
    RunnableRetryableImpl(Consumer<Exception> retrying,
                          Consumer<Exception> skipping,
                          int                 trials,
                          CheckedRunnable     runnable) {
        super(retrying, skipping, trials);
        this.runnable = Objects.requireNonNull(runnable, "runnable");
    }
    @Override
    public Void apply(Void ignored) {
        try {
            runnable.tryRun();
        } catch (Exception x) {
            // BTW I would consider doing this.
            if (x instanceof RuntimeException)
                throw (RuntimeException) x;
            // I would also probably write a class like:
            // class RethrownException extends RuntimeException {
            //     RethrownException(Exception cause) {
            //         super(cause);
            //     }
            // }
            // This way the caller can catch a specific type if
            // they want to.
            // (See e.g. java.io.UncheckedIOException)
            throw new RuntimeException(x);
        }
        return null;
    }
}

      



Or, you can reduce the number of lines with local classes:

static RunnableRetryable of(Consumer<Exception> retrying,
                            Consumer<Exception> skipping,
                            int                 trials,
                            CheckedRunnable     runnable) {
    Objects.requireNonNull(runnable, "runnable");
    final class RunnableRetryableImpl
    extends    AbstractRetryable<Void, Void>
    implements RunnableRetryable {
        RunnableRetryable() {
            // Avoid explicitly declaring parameters
            // and passing arguments.
            super(retrying, skipping, trials);
        }
        @Override
        public Void apply(Void ignored) {
            try {
               runnable.tryRun();
            } catch (Exception x) {
                if (x instanceof RuntimeException)
                    throw (RuntimeException) x;
                throw new RuntimeException(x);
            }
            return null;
        }
    }
    return new RunnableRetryableImpl();
}

      

Personally, I think I'll just write package-private implementations instead of local classes, but this of course requires a fair amount of boiler room code.

Also, as a side note, when you write factories that return anonymous classes, you must use requireNonNull

inside the method itself (as in my example of

). It is so that if null

passed to a method, the method throws the NPE instead of eg. some call on retrying

or skipping

drop NPE after a while.

0


source







All Articles