I have an interface Fruit
with two implementations Apple
and Banana
. I want to create a Fruit
instance. The choice whether the concrete implementation should be an Apple
or a Banana
should be made by the user. I did not yet design the user interface, so there is no restriction how this choice is made by the user.
I know there are the following options:
What are the pros and cons of these options?
Please note that while there are several similar questions that discuss the one or the other approach, I did not find a single comparison.
Here is a list of related questions:
tl;dr I suggest to use the abstract factory pattern.
Long answer:
To compare the approaches, I attached four possible solutions below. Here is a summary:
Class::forName
First of all, the reflection solutions 2 and 3 identify the class object with a String that provides the class name. Doing this is bad, because it breaks automatic refactoring tools: When you rename the class, the String will not be changed. Also, there will be no compiler error. The error will only become visible at run-time.
Please note that this does not depend on the quality of the refactoring tool: In solution 2, the String which provides the class name might be constructed in the most obscure way that you can think of. It might even be entered by the user or read from a file. There is no way a refactoring tool can entirely solve this problem.
Solution 1 and 4 do not have these problems, since they directly link to the classes.
Since solution 2 directly uses the String given by the user for reflection to identify a class by name, the GUI is coupled to the class names that you use in your code. This is bad, since this requires you to change the GUI when you rename your classes. Renaming classes should always be as easy as possible to enable easy refactoring.
Solution 1, 3 and 4 does not have this problem, since they translate the String which is used by the GUI to something else.
Solution 2, 3 and 4 have to deal with exceptions when using the reflection methods forName
and newInstance
. Solution 2 even has to use the exceptions for flow control, since it does not have any other way to check whether the input is valid. Using exceptions for flow control is generally considered bad practice.
Solution 1 does not have this problem, since it does not use reflection.
Solution 2 directly uses the String provided by the user for reflection. This can be a security issue.
Solution 1, 3 and 4 does not have this problem, since they translate the String which is provided by the user to something else.
You cannot easily use this type of reflection in all environments. For example you will probably run into problem when using OSGi.
Solution 1 does not have this problem, since it does not use reflection.
The given example is still simple, because it does not use constructor parameters. It is quite common to use a similar pattern with constructor parameters. Solution 2, 3 and 4 become ugly in this case, see Can I use Class.newInstance() with constructor arguments?
Solution 1 only has to change the Supplier
to a functional interface which matches the constructor signatures.
Solution 2, 3 and 4 require that you instantiate the fruit via the constructor. However, this might be undesirable, since you generally don't want to put complex initialization logic into constructors, but into a factory (method).
Solution 1 does not have this problem, since it allows you to put any function which creates a fruit into the map.
Here are the elements which introduce code complexity, together with the solutions where they appear:
The exception handling was already discussed above.
The map is the part of the code which translates the String provided by the user to something else. Thus, the map is what solved many of the problems described above which means it serves a purpose.
Note that the map can also be replaced by a List
or an array. However this does not change any of the conclusions stated above.
public interface Fruit {
public static void printOptional(Optional<Fruit> optionalFruit) {
if (optionalFruit.isPresent()) {
String color = optionalFruit.get().getColor();
System.out.println("The fruit is " + color + ".");
} else {
System.out.println("unknown fruit");
}
}
String getColor();
}
public class Apple implements Fruit {
@Override
public String getColor() {
return "red";
}
}
public class Banana implements Fruit {
@Override
public String getColor() {
return "yellow";
}
}
public class AbstractFactory {
public static void main(String[] args) {
// this needs to be executed only once
Map<String, Supplier<Fruit>> map = createMap();
// prints "The fruit is red."
Fruit.printOptional(create(map, "apple"));
// prints "The fruit is yellow."
Fruit.printOptional(create(map, "banana"));
}
private static Map<String, Supplier<Fruit>> createMap() {
Map<String, Supplier<Fruit>> result = new HashMap<>();
result.put("apple", Apple::new);
result.put("banana", Banana::new);
return result;
}
private static Optional<Fruit> create(
Map<String, Supplier<Fruit>> map, String userChoice) {
return Optional.ofNullable(map.get(userChoice))
.map(Supplier::get);
}
}
public class Reflection {
public static void main(String[] args) {
// prints "The fruit is red."
Fruit.printOptional(create("stackoverflow.fruit.Apple"));
// prints "The fruit is yellow."
Fruit.printOptional(create("stackoverflow.fruit.Banana"));
}
private static Optional<Fruit> create(String userChoice) {
try {
return Optional.of((Fruit) Class.forName(userChoice).newInstance());
} catch (InstantiationException
| IllegalAccessException
| ClassNotFoundException e) {
return Optional.empty();
}
}
}
public class ReflectionWithMap {
public static void main(String[] args) {
// this needs to be executed only once
Map<String, String> map = createMap();
// prints "The fruit is red."
Fruit.printOptional(create(map, "apple"));
// prints "The fruit is yellow."
Fruit.printOptional(create(map, "banana"));
}
private static Map<String, String> createMap() {
Map<String, String> result = new HashMap<>();
result.put("apple", "stackoverflow.fruit.Apple");
result.put("banana", "stackoverflow.fruit.Banana");
return result;
}
private static Optional<Fruit> create(
Map<String, String> map, String userChoice) {
return Optional.ofNullable(map.get(userChoice))
.flatMap(ReflectionWithMap::instantiate);
}
private static Optional<Fruit> instantiate(String userChoice) {
try {
return Optional.of((Fruit) Class.forName(userChoice).newInstance());
} catch (InstantiationException
| IllegalAccessException
| ClassNotFoundException e) {
return Optional.empty();
}
}
}
public class ReflectionWithClassMap {
public static void main(String[] args) {
// this needs to be executed only once
Map<String, Class<? extends Fruit>> map = createMap();
// prints "The fruit is red."
Fruit.printOptional(create(map, "apple"));
// prints "The fruit is yellow."
Fruit.printOptional(create(map, "banana"));
}
private static Map<String, Class<? extends Fruit>> createMap() {
Map<String, Class<? extends Fruit>> result = new HashMap<>();
result.put("apple", Apple.class);
result.put("banana", Banana.class);
return result;
}
private static Optional<Fruit> create(
Map<String, Class<? extends Fruit>> map, String userChoice) {
return Optional.ofNullable(map.get(userChoice))
.flatMap(ReflectionWithClassMap::instantiate);
}
private static Optional<Fruit> instantiate(Class<? extends Fruit> c) {
try {
return Optional.of(c.newInstance());
} catch (InstantiationException
| IllegalAccessException e) {
return Optional.empty();
}
}
}