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:
- usage of the abstract factory pattern
- usage of reflection to create an instance from a given class name
- usage of reflection to create an instance from a given class object
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:
- Why is Class.newInstance() "evil"?
- Can I use Class.newInstance() with constructor arguments?
- How to make an ArrayList of Classes?
- Does Class.newInstance() follow the "Abstract factory" design pattern?
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:
- uses the abstract factory pattern
- uses a String which is directly chosen by the user to instantiate a class by name
- takes a String which is directly chosen by the user and translates it to another String to instantiate a class by name
- takes a String which is directly chosen by the user and translates it to a Class object to instantiate the class
Comparison
Using 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.
Coupling of the GUI to the class names
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.
Exceptions for flow control
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.
Security issues with 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.
Reflection with special class loaders
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.
Constructor with parameters
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.
Using a factory (method) to create a complex fruit
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.
Code complexity
Here are the elements which introduce code complexity, together with the solutions where they appear:
- creation of the map in 1, 3 and 4
- exception handling in 2, 3 and 4
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.
Code
Common Code
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";
}
}
Abstract Factory (1)
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);
}
}
Reflection (2)
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();
}
}
}
Reflection with Map (3)
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();
}
}
}
Reflection with Class Map (4)
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();
}
}
}