Как выбрать, какую конкретную реализацию следует создать на основе выбора пользователя?

У меня есть интерфейс Fruit с двумя реализациями Apple и Banana. Я хочу создать экземпляр Fruit. Пользователь должен сделать выбор, должна ли конкретная реализация быть Apple или Banana. Я еще не разработал пользовательский интерфейс, поэтому нет никаких ограничений в отношении того, как этот выбор сделан пользователем.

Я знаю, что есть следующие варианты:

  • использование абстрактного шаблона factory
  • использование отражения для создания экземпляра из имени данного класса
  • использование отражения для создания экземпляра из данного объекта класса

Каковы плюсы и минусы этих опций?


Обратите внимание, что, хотя есть несколько аналогичных вопросов, которые обсуждают тот или иной подход, я не нашел ни одного сравнения.

Вот список связанных вопросов:

Ответ 1

tl; dr Я предлагаю использовать абстрактный шаблон factory.

Длинный ответ:

Чтобы сравнить подходы, я привел четыре возможных решения ниже. Вот резюме:

  • использует абстрактный шаблон factory
  • использует строку, которая непосредственно выбирается пользователем для создания экземпляра класса по имени
  • принимает строку, которая непосредственно выбирается пользователем и переводит ее в другую строку, чтобы создать экземпляр класса по имени
  • принимает строку, которая непосредственно выбирается пользователем и переводит ее в объект класса для создания экземпляра класса

Сравнение

Использование Class::forName

Прежде всего, решения отражения 2 и 3 идентифицируют объект класса с String, который предоставляет имя класса. Это плохо, потому что он разбивает инструменты автоматического рефакторинга: при переименовании класса строка не будет изменена. Кроме того, ошибка компилятора не будет. Ошибка будет видна только во время выполнения.

Обратите внимание, что это не зависит от качества инструмента рефакторинга: в решении 2 строка, которая предоставляет имя класса, может быть сконструирована самым неясным способом, о котором вы можете думать. Он может даже вводиться пользователем или считываться из файла. Инструмент рефакторинга не может полностью решить эту проблему.

Решение 1 и 4 не имеет этих проблем, поскольку они напрямую связаны с классами.

Связывание GUI с именами классов

Так как решение 2 напрямую использует String, заданное пользователем для отражения, чтобы идентифицировать класс по имени, GUI связан с именами классов, которые вы используете в своем коде. Это плохо, так как это требует от вас изменения GUI при переименовании ваших классов. Переименование классов всегда должно быть максимально простым, чтобы упростить рефакторинг.

Решение 1, 3 и 4 не имеет этой проблемы, поскольку они переводят строку, которая используется графическим интерфейсом для чего-то еще.

Исключения для управления потоком

Решение 2, 3 и 4 должно иметь дело с исключениями при использовании методов отражения forName и newInstance. Решение 2 даже должно использовать исключения для управления потоком, поскольку у него нет другого способа проверить, является ли вход действительным. Использование исключений для управления потоком обычно считается плохой практикой.

Решение 1 не имеет этой проблемы, так как оно не использует отражение.

Проблемы безопасности с отражением

Решение 2 напрямую использует строку, предоставленную пользователем для отражения. Это может быть проблема безопасности.

Решение 1, 3 и 4 не имеет этой проблемы, так как они переводят строку, которая предоставляется пользователем другому.

Отражение со специальными загрузчиками классов

Вы не можете легко использовать этот тип отражения во всех средах. Например, вы, вероятно, столкнетесь с проблемой при использовании OSGi.

Решение 1 не имеет этой проблемы, так как оно не использует отражение.

Конструктор с параметрами

Данный пример все еще прост, поскольку он не использует параметры конструктора. Обычно используется аналогичная модель с параметрами конструктора. Решение 2, 3 и 4 становится уродливым в этом случае, см. Могу ли я использовать Class.newInstance() с аргументами конструктора?

Решение 1 только должно изменить Supplier на функциональный интерфейс, который соответствует сигнатурам конструктора.

Используя factory (метод) для создания сложного фрукта

В решениях 2, 3 и 4 требуется, чтобы вы создавали плод через конструктор. Однако это может быть нежелательным, поскольку вы вообще не хотите вводить сложную логику инициализации в конструкторы, а в factory (метод).

Решение 1 не имеет этой проблемы, поскольку оно позволяет вам помещать любую функцию, которая создает плод в карте.

Сложность кода

Вот элементы, которые вводят сложность кода вместе с решениями, в которых они появляются:

  • создание карты в 1, 3 и 4
  • обработка исключений в 2, 3 и 4

Обработка исключений уже обсуждалась выше.

Карта - это часть кода, которая переводит строку, предоставленную пользователем, на что-то другое. Таким образом, карта - это то, что разрешило многие из описанных выше проблем, что означает, что она служит цели.

Обратите внимание, что карта также может быть заменена на List или массив. Однако это не меняет никаких выводов, изложенных выше.

Код

Общий код

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";
    }
}

Аннотация 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);
    }
}

Отражение (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();
        }
    }
}

Отражение с помощью карты (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();
        }
    }
}

Отражение с помощью карты классов (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();
        }
    }
}