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.