How should I select which concrete implementation should be instantiated based on the user choice?

旧城冷巷雨未停 提交于 2019-11-28 06:27:55

问题


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:

  1. usage of the abstract factory pattern
  2. usage of reflection to create an instance from a given class name
  3. 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?

回答1:


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:

  1. uses the abstract factory pattern
  2. uses a String which is directly chosen by the user to instantiate a class by name
  3. takes a String which is directly chosen by the user and translates it to another String to instantiate a class by name
  4. 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();
        }
    }
}


来源:https://stackoverflow.com/questions/36272566/how-should-i-select-which-concrete-implementation-should-be-instantiated-based-o

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!