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 ?
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); });