Java method accepting different functional interface types - possible?
Asked Answered
N

3

26

First of all, sorry for the bad title, but I find it somewhat difficult to summarize my issue in one short sentence...

There's some code in our software which I'm quite unhappy with. It goes like this:

    @FunctionalInterface
    public interface OneArgCall<T, U, A> {
        T execute(U u, A arg);
    }

    @FunctionalInterface
    public interface TwoArgCall<T, U, A, B> {
        T execute(U u, A arg, B arg2);
    }

    public <T, U, A, B> T execCall(String x, Class<U> c, OneArgCall<T, U, A> call, A arg) {
        U u = doSomething(x, c);
        try {
            return call.execute(u, arg);
        } catch (SomeException se) {
           handleSe(se);
        } catch (SomeOtherException soe) {
           handleSoe(soe);
    }
    
    public <T, U, A, B> T execCall(String x, Class<P> c, TwoArgCall<T, U, A, B> call, A arg, B arg2) {
        U u = doSomething(x, c);
        try {
            return call.execute(u, arg, arg2);
        } catch (SomeException se) {
           handleSe(se);
        } catch (SomeOtherException soe) {
           handleSoe(soe);
    }

I.e. the execCall methods are identical except for the functional interface passed as the third argument (and the argument list for that interface, of course). Now, I could still live with that, but there are more of these methods (imagine a ThreeArgCall, FourArgCall...) - and this is where it becomes kind of unbearable.

So, in the name of all that's DRY: How would you go about cleaning up this code? I imagine something along the lines of T execCall(String x, Class<U> c, SOMETHING, SOMETHING_ELSE) where SOMETHING can be any of the OneArgCall, TwoArgCall... interfaces and SOMETHING_ELSE represents the list(?) of arguments.

Can this be done at all? Or is there some other way I could refactor this code to be less repititive?

Nereid answered 21/9, 2021 at 20:4 Comment(2)
You could remove the duplication of execCall by having it accept just one interface and sending into execCall adapters that wrap the original interfaces and arguments and delegate to them. This would mean less code in execCalls, but more code elsewhere. The adapters would have a uniform interface which means the arguments would have to be wrapped in a uniform base class, and the specific adapters would have to downcast to the types of arguments they know their actual target interfaces need.Hadst
@gonen then please delete the comment.Myrica
T
23

You don't actually need all of those interfaces. You don't need to take in any extra method parameters. all of that can be handled by the caller using lambda syntax.

This is the only method you need:

public <T, U> T execCall(String x, Class<U> c, Function<U, T> call) {
    U u = doSomething(x, c);
    try {
        return call.apply(u);
    } catch (SomeException se) {
       handleSe(se);
    } catch (SomeOtherException soe) {
       handleSoe(soe);
    }
}

Now, say you have a method which takes multiple arguments with which you would like to use execCall(), how does that work?

public Foo someMethodCall(Bar bar, Arg1 a1, Arg2 a2) { .... }

Arg1 a1 = ...;
Arg2 a2 = ...;
String x = ...;
Bar b = execCall(x, Bar.class, (u) -> someMethodCall(u, a1, a2));

Using the lambda syntax, you "adapt" your 3 param method to the one arg Function interface. This is using the functional programming concept of "partial application".

Toxic answered 21/9, 2021 at 20:55 Comment(4)
I love it. Traditional Class Oriented OO versus functional programming. The plot thickens.Hadst
@GonenI it's using the new lambda features, but you could just have easily implemented this pre-java8 by using an anonymous inner class or even a fully defined interface. the point is, you let the caller encapsulate the additional args, you don't need to care about them.Toxic
lambdas are hardly new, believe it or not, they have been around for 7 yearsKissel
@Kissel wow, time flies!Toxic
H
6

I could think of some GOF Design Patterns inspired ways to reduce duplication in execCall, at the price of added complexity, and also some added burden to the callers of execCall.

1. Using the Adapter or Command pattern:

You could remove the duplication of execCall by having it accept just one interface and sending into execCall adapters that wrap the original interfaces and arguments and delegate to them. This would mean less code in execCalls, but more code elsewhere. The adapters would have a uniform interface which means the arguments would have to be wrapped in a uniform base class, and the specific adapters would have to downcast to the types of arguments they know their actual target interfaces need.

Instead of thinking of this solution as Adapters, you can also think of it as implementing the Command pattern. Instead of having each client sending execCall just their 2 or 3 arguments, you have them send a complete command object. This would be some class implementing an interface that included an abstract execute function. The 2 argument client would send execCall an object implementing the command interface with arg,arg2 and the line call.execute(u, arg). The original execCall would then do commandObj.setU(u) and then call commandObj.execute(). The price of this is burdening callers with the knowledge of call.execute

interface CallCommand<T,U> 
{
    T execute() ;
    void setU(U u);
}
class  OneArgCallCommand<T,U,A> implements CallCommand<T,U> {
    A arg;
    U u;
    OneArgCall<T, U, A> call;
    public void setU( U u ) { this.u = u; }
    @Override
    public T execute() {
        return call.execute(u, arg);
    }
}
class  TwoArgCallCommand<T,U,A,B> implements CallCommand<T,U> {
    A arg;
    B arg2;
    U u;
    TwoArgCall<T, U, A,B> call;
    public void setU( U u ) { this.u = u; }
    @Override
    public T execute() {
        return call.execute(u, arg,arg2);
    }
}


class Logic
{
public <T, U, A, B> T execCall(String x, Class<U> c, CallCommand<T,U> callCommand) {
    U u = doSomething(x, c);
    callCommand.setU(u);
    try {
        return callCommand.execute();
    } catch (SomeException se) {
       handleSe(se);
    } catch (SomeOtherException soe) {
       handleSoe(soe);
    }
    return null;
}

2. Using the Template Method pattern

Put execCall in an abstract base class. Have most of execCall in the abstract class including doSomething, and the tryCatch. Turn the line of call.Execute into an abstract method which is overridden by each derived class . Created a derived version of execCall for the 2 args call, for the 3 arg call etc. Again the different arguments would need to be packed in a concrete class with a common abstract argument-holder parent, and each specific execCall would need to downcast into the arg type it knows it needs. Is it worth it?

3. Get rid of execCall completely?

Finally it might be worth it to rethink the entire design. What is the benefit of the execCall function? Is it worth the added complexity it brings?. It may or may not be possible to completely avoid it.

        try {
    
        // place that originally called execCallWithOneArgument
        call.execute(doSomething(x, c), arg);
    
        // place that originally called execCallWithTwoArguments
        call.execute(doSomething(x, c), arg, arg2);
    
    
        } catch (SomeException se) {
           handleSe(se);
        } catch (SomeOtherException soe) {
           handleSoe(soe);
    }

Not sure if this one is possible, it depends on how the code is structured.

Hadst answered 21/9, 2021 at 20:31 Comment(0)
M
4

Not without employing code generation trickery, or refactoring the API itself. Both of which are certainly options, though I'd prefer the second one I think.

Code gen

It would most likely take this form:

  • You write an annotation processor. That's a bit of a misnomer; APs run as part of the compilation process. They are usually 'triggered' by annotations and get most of their input parameters from annotations, but that's not a requirement. "Compiler plugin" would be how you should think about them.
  • This annotation processor will process a 'template' class. This template class is like whatever your 20 execCall methods exist in right now, except it is named ExecCallContainer0 or ExecCallContainerTemplate, and is package private. And annotated, of course. It contains only one execCall method, not all 20. The annotation serves to 'trigger' the processor (so that it runs; you can also design it to trigger on anything and detect classes ending in Template or what not).
  • The annotation processor creates the actual ExecCallContainer class, generating all 20 variants for you. These methods presumably just process the arguments (e.g. gather them up in a list or create a closure that wraps the invoke, e.g.:
/** Generated by AP. Do not edit. */
public <T, U, A, B> T execCall(String x, Class<P> c, TwoArgCall<T, U, A, B> call, A arg, B arg2) {

    Supplier<T> s = () -> call.execute(u, arg, arg2);
    return ExecCallContainerTemplate.exec(x, c, s);
}
  • That generated source file is the source for your public class that all your other code in this project shall use.

You do run into the usual downsides of APs: They slow the build process down a little bit, and if you're working on the AP code itself, your code tends to be a mess (as all your code is now calling methods that do not exist until they are generated, which hasn't happened yet and cannot currently happen because your AP is being worked on) - at least, until you run an actual build. Eclipse tends to do a good job on making this easy and fast (Just save the file, eclipse runs the AP on just what's needed), most other IDEs farm this work out to the build which tends not to be nearly that fast about it, but, you don't usually work on that AP all that much once you finish the work on it, so it's not that big a deal.

The biggest complication here is that the annotation processor API is not exactly trivial, so somebody on the team should probably be quite familiar with that API and this code generator, or if problems occur in it, the entire team will go into hair-on-fire mode which is not so nice. A problem that any use of a complex library suffers from.

Refactor the API itself

There are a few smells in this code. Could be that they were simply the lesser evil, but it suggests that the API itself could simply be refactored so that it's easier to use and less maintenance - win win.

For example, passing java.lang.Class instances around is bad, and having the generics on the j.l.Class type matter is worse (usually that should be some sort of factory interface instead; what are you using the Class instance for? If it is to construct instances of it, you should have a factory instead. If you are using it as a key, a dedicated key class is probably a better bet. If you are using it for reflection purposes, such as 'programatically fetch all fields from this for some reason', again the idea of a factory is generally the better bet, except perhaps you don't want to call it that (a 'factory' is just taking the constructors and meta-aspects of the class itself and making it abstractable).

In other words, bad:

public <T> T make(Class<T> type, String param) {
    Constructor<T> c = type.getConstructor(String.class);
    return c.newInstance(param);
}

Good:

public <T> make(Function<String, T> factory, String param) {
    return factory.apply(param);
}

Where you can invoke it with:

Function<String, QuitMessage> quitMessageFactory = param -> new QuitMessage(param);

make(quitMessageFactory, "Going to sleep for the night");

instead of:

make(QuitMessage.class, "Going to sleep for the night");

possibly call.execute can be abstracted by externalizing the job of passing the xArgsCall parameter and all the arguments. So, instead of having:

public class Calculator {
    public TwoArgCall<Double, Double, Double> addButton = (a, b) -> a + b;

  ....

    public void foo() {
        double lhs = 5.5;
        double rhs = 3.3;
        calculatorTape.execCall(addButton, lhs, rhs);
    }
}

try:

public class Calculator {
    public TwoArgCall<Double, Double, Double> addButton = (a, b) -> a + b;

  ....

    public void foo() {
        double lhs = 5.5;
        double rhs = 3.3;
        calculatorTape.execCall(() -> addButton.exec(lhs, rhs));
    }
}

This second snippet is not much more code than the first, and removes the need to have 20 execCall methods, 20 XArgsCall functional interfaces, etc. I'd call that worth any day.

Mischievous answered 21/9, 2021 at 20:30 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.