I have a nice and clean interface like so:
public interface Mapping<I extends Input, O extends Output> {
O calculate(I input);
Class<I> getInputClass();
Class<O> getOutputClass();
}
Let’s assume for simplicity that Input
and Output
are empty interfaces.
I want to define Mappings at runtime and instantiate them via reflection. One example for such a Mapping
is the following:
public class DoublerMapping implements Mapping<DoublerInput, DoublerOutput> {
@Override
DoublerOutput calculate(DoublerInput input){
DoublerOutput output = new DoublerOutput();
output.setValue(input.getValue() * 2);
return output;
}
@Override
Class<DoublerInput> getInputClass(){
return DoublerInput.class;
}
@Override
Class<O> getOutputClass(){
return DoublerInput.class;
}
}
When instantiating Mapping
s by reflection, however, I can only use getInputClass()
and getOutputClass()
to instantiate Inputs
and Outputs
respectively (utilizing mapping.getInputClass().getDeclaredConstructor().newInstance()
), instead of their actual extension. I don’t want the Mapping
to take a generic Input
as I want to make sure it’s getting the proper class I
it’s expecting. I also can’t pull the getValue()
and setValue()
methods to the interface, as they could take any arguments and have different signatures.
How do I go to create an I extends Input
at runtime and feeding it back to the parameterized Mapping
class? Not even mapping.execute(mapping.getInputClass().getDeclaredConstructor().newInstance())
compiles.
Or is there a better way to achieve this?
4
Class<I>
is your problem.
You want to abstract over the notion of the type itself – you don’t want the concept ‘some instance of DoublerInput’, no, you want to abstract the concept of ‘DoublerInput’ – the entire class.
It makes sense to think: “Ah, well, DoublerInput.class
, which is of type Class<DoublerInput>
, will serve as that thing!”
A sensible thought, but, that is wrong.
The answer you are looking for is Factories.
Yes, cliché, but move on from the jokes. It really is what it is for. Class
doesn’t work for quite a few reasons:
-
You can’t define anything in it. For example, when you write your reflective code, you’re just assuming that there’s a no-args public constructor. If it doesn’t exist, you get no compile time warnings, either in your framework, or in whomever is writing
class DoublerInput
. There is simply no way to dictate that any ‘class that implementsInput
‘ must necessarily have a no-args constructor. You also can’t define any class-level methods; there’s no such thing as an interface for static members, after all. -
j.l.Class
is incapable of representing type-argsed types. You can’t doList<String>.class
, and while you can writeClass<List<String>> x;
, that is meaningless; there is no class reference that uniquely represents specifically the notion of ‘lists of strings’. There’s justList.class
. NotList<String>.class
. -
Sideshow point perhaps, but,
int.class
exists, but, generics can’t do primitives. In that senseClass<T>
is doubly bad; IfT
contains generics thenj.l.Class
cannot represent them, andj.l.Class
can represent things that generics cannot.
Factories let you fix all of it. You define the notion of a factory and then add whatever you need. For example:
abstract class InputFactory<I extends Input> {
abstract I create();
}
and now you can make:
final class DoublerInputFactory extends InputFactory<DoublerInput> {
@Override DoublerInput create() {
return new DoublerInput();
}
}
and now all is well: instead of accepting a Class<I>
, you accept a InputFactory<I>
, and you can invoke .create()
on that instance which replaces .getConstructor().newInstance()
, which avoids a whole boatload of weird exceptions that are hard to explain in the context of what you’re trying to do, and you are guaranteed that the code that powers the object you are provided implements this; if it did not, it wouldn’t have compiled in the first place.
You can now also add on whatever you need. Want a concept (such as DoublerInput
) to be capable of naming itself? As in, not one particular instance of DoublerInput
, but the notion of DoublerInput as a whole?
Just shove a String getName()
method in the factory, easy!
Want the concept to be able to, say, clone an instance? Add it to the factory.
Want to convey the concept that any implementing classes must have a constructor that takes, say, an int
? Don’t do that, instead, stick abstract I create(int arg);
into your factory class.
This solves many problems, and ‘generics go ape when I attempt to use reflection as a crappy way to re-invent factories’ is one of the many problems that this will solve.
When JVM based languages invent the concept of type-level interfaces, they do so by enshrining the factory pattern into the language as a whole. Scala did it; kotlin did it. The amount of boilerplate involved is minimal.
Make factories.