Создание фабричного метода в Java, который не зависит от if-else - PullRequest
37 голосов
/ 08 августа 2010

В настоящее время у меня есть метод, который действует как фабрика на основе заданной строки.Например:

public Animal createAnimal(String action)
{
    if (action.equals("Meow"))
    {
        return new Cat();
    }
    else if (action.equals("Woof"))
    {
        return new Dog();
    }

    ...
    etc.
}

Что я хочу сделать, так это избежать всей проблемы if-else при расширении списка классов.Я полагаю, что мне нужно иметь два метода: один, который регистрирует строки в классах, а другой, который возвращает класс на основе строки действия.

Какой хороший способ сделать это на Java?

Ответы [ 10 ]

50 голосов
/ 08 августа 2010

То, что вы сделали, это, вероятно, лучший способ сделать это, пока не станет доступна строка включения.( Редактировать 2019: Доступно переключение на строку - используйте это.)

Вы можете создавать фабричные объекты и карту из строк в них.Но в современной Java это немного многословно.

private interface AnimalFactory {
    Animal create();
}
private static final Map<String,AnimalFactory> factoryMap =
    Collections.unmodifiableMap(new HashMap<String,AnimalFactory>() {{
        put("Meow", new AnimalFactory() { public Animal create() { return new Cat(); }});
        put("Woof", new AnimalFactory() { public Animal create() { return new Dog(); }});
    }});

public Animal createAnimal(String action) {
    AnimalFactory factory = factoryMap.get(action);
    if (factory == null) {
        throw new EhException();
    }
    return factory.create();
}

В то время, когда этот ответ был изначально написан, функции, предназначенные для JDK7, могли сделать код таким, как показано ниже.Как оказалось, лямбды появились в Java SE 8, и, насколько мне известно, планов литералов карт нет.( Отредактировано 2016 )

private interface AnimalFactory {
    Animal create();
}
private static final Map<String,AnimalFactory> factoryMap = {
    "Meow" : { -> new Cat() },
    "Woof" : { -> new Dog() },
};

public Animal createAnimal(String action) {
    AnimalFactory factory = factoryMap.get(action);
    if (factory == null) {
        throw EhException();
    }
    return factory.create();
}

Редактировать 2019: В настоящее время это будет выглядеть примерно так:

import java.util.function.*;
import static java.util.Map.entry;

private static final Map<String,Supplier<Animal>> factoryMap = Map.of(
    "Meow", Cat::new, // Alternatively: () -> new Cat()
    "Woof", Dog::new // Note: No extra comma like arrays.
);

// For more than 10, use Map.ofEntries and Map.entry.
private static final Map<String,Supplier<Animal>> factoryMap2 = Map.ofEntries(
    entry("Meow", Cat::new),
    ...
    entry("Woof", Dog::new) // Note: No extra comma.
);

public Animal createAnimal(String action) {
    Supplier<Animal> factory = factoryMap.get(action);
    if (factory == null) {
        throw EhException();
    }
    return factory.get();
}

Если вы хотите добавитьпараметр, вам нужно переключить Supplier на Factoryget становится apply, что также не имеет смысла в контексте).Для двух параметров BiFunction.Более двух параметров, и вы снова пытаетесь сделать его читабельным.

13 голосов
/ 08 августа 2010

Нет необходимости в Картах с этим решением.В любом случае, карты - это просто другой способ сделать оператор if / else.Воспользуйтесь небольшим размышлением, и всего несколько строк кода будут работать для всего.

public static Animal createAnimal(String action)
{
     Animal a = (Animal)Class.forName(action).newInstance();
     return a;
}

Вам необходимо изменить аргументы с «Woof» и «Meow» на «Cat» и«Собака», но это должно быть достаточно легко сделать.Это исключает любую «регистрацию» строк с именем класса на карте и делает ваш код многократно используемым для любого будущего Animal, который вы можете добавить.

12 голосов
/ 09 августа 2010

Если у вас нет для использования строк, вы можете использовать тип enum для действий и определить метод абстрактной фабрики.

...
public enum Action {
    MEOW {
        @Override
        public Animal getAnimal() {
            return new Cat();
        }
    },

    WOOF {
        @Override
        public Animal getAnimal() {
            return new Dog();
        }
    };

    public abstract Animal getAnimal();
}

Тогда вы можете делать вещикак:

...
Action action = Action.MEOW;
Animal animal = action.getAnimal();
...

Это немного забавно, но работает.Таким образом, компилятор будет скулить, если вы не определили getAnimal () для каждого действия и не можете передать действие, которое не существует.

8 голосов
/ 08 августа 2010

Используйте Scannotations!

Шаг 1. Создайте аннотацию, как показано ниже:

package animal;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

@Retention(RetentionPolicy.RUNTIME)
public @interface AniMake {
    String action();
}

Обратите внимание, чтоRetentionPolicy - это среда выполнения, к которой мы будем обращаться через рефлексию.

Шаг 2. (Необязательно) Создайте общий суперкласс:

package animal;

public abstract class Animal {

    public abstract String greet();

}

Шаг 3. Создайте подклассы с новой аннотацией:

package animal;

@AniMake(action="Meow")
public class Cat extends Animal {

    @Override
    public String greet() {
        return "=^meow^=";
    }

}
////////////////////////////////////////////
package animal;

@AniMake(action="Woof")
public class Dog extends Animal {

    @Override
    public String greet() {
        return "*WOOF!*";
    }

}

Шаг 4. Создайте фабрику:

package animal;

import java.util.Set;

import org.reflections.Reflections;

public class AnimalFactory {

    public Animal createAnimal(String action) throws InstantiationException, IllegalAccessException {
        Animal animal = null;
        Reflections reflections = new Reflections("animal");
        Set<Class<?>> annotated = reflections.getTypesAnnotatedWith(AniMake.class);

        for (Class<?> clazz : annotated) {
            AniMake annoMake = clazz.getAnnotation(AniMake.class);
            if (action.equals(annoMake.action())) {
                animal = (Animal) clazz.newInstance();
            }
        }

        return animal;
    }

    /**
     * @param args
     * @throws IllegalAccessException 
     * @throws InstantiationException 
     */
    public static void main(String[] args) throws InstantiationException, IllegalAccessException {
        AnimalFactory factory = new AnimalFactory();
        Animal dog = factory.createAnimal("Woof");
        System.out.println(dog.greet());
        Animal cat = factory.createAnimal("Meow");
        System.out.println(cat.greet());
    }

}

Эта фабрика может бытьнемного убрал, например, разберись с неприятными проверенными исключениями и т. д.
На этой фабрике я использовал библиотеку Reflections .
Я сделал это трудным путем, то есть не сделалпроект maven, и мне пришлось добавить зависимости вручную.
Зависимости:

Если вы пропустили шаг 2, вам нужно будет изменить заводской метод для возврата Object.
С этого момента вы можетепродолжайте добавлять подклассы, и пока вы аннотируете их с помощью AniMake (или любого другого лучшего имени, которое вы придумали), и помещаете их в пакет, определенный в конструкторе Reflections (в данном случае «animal»), и оставьте значение по умолчанию no-конструктор args виден, тогда фабрика создаст для вас экземпляры ваших классов без необходимости их изменения.

Вот вывод:

log4j:WARN No appenders could be found for logger (org.reflections.Reflections).
log4j:WARN Please initialize the log4j system properly.
*WOOF!*
=^meow^=
5 голосов
/ 08 августа 2010

Я не пробовал это, но мог создать Map с "Мяу" и т. Д. В качестве ключей и (скажем) Cat.class как значение.

Обеспечение генерации статического экземпляра через интерфейс и вызов

Animal classes.get("Meow").getInstance()
4 голосов
/ 08 августа 2010

Я бы посмотрел, чтобы получить Enum-представление String и включить его.

2 голосов
/ 08 августа 2010

Вы уже выбрали ответ на этот вопрос, но это все равно может помочь.

Хотя я являюсь разработчиком .NET / C #, это действительно общая проблема ООП.Я столкнулся с такой же проблемой, и я нашел хорошее решение (я думаю), используя IoC Container .

Если вы еще не используете его, это, вероятно,хорошая причина для начала.Я не знаю IoC-контейнеры в Java, но я предполагаю, что должен быть один с подобными функциями.

То, что у меня было, было Factory, которое содержит ссылку на контейнер IoC, который разрешается самим контейнером (в BootStrapper)

...
public AnimalFactory(IContainer container) 
{ 
    _container = container; 
}

Затем вы можете настроить свой контейнер IoC для разрешения правильных типов на основе ключа (звук в вашем примере).Это полностью абстрагирует конкретные классы, которые должна возвращать ваша фабрика.

, в конце концов, ваш фабричный метод сокращен до этого:

...
public Createable CreateAnimal(string action) 
{ 
    return _container.Resolve<Createable>(action); 
} 

Этот вопрос о стековом потоке иллюстрирует ту же проблему с элементами реального мира и с провереннымиответ показывает черновик моего решения (псевдокод).Позже я написал пост в блоге с реальными фрагментами кода , где он намного понятнее.

Надеюсь, это поможет.Но это может быть излишним в простых случаях.Я использовал это, потому что у меня было 3 уровня зависимостей для разрешения, и контейнер IoC уже собирал все мои компоненты.

1 голос
/ 03 апреля 2015

И что люди думают об использовании Class.newInstance () в ответе Тома Хотина? Это позволит нам не хранить ненужные анонимные классы в памяти? Плюс код будет более чистым.

Это будет выглядеть примерно так:

private static final Map<String,Class> factoryMap =
    Collections.unmodifiableMap(new HashMap<String,Class>() {{
        put("Meow", Cat.class);
        put("Woof", Dog.class);
}});

public Animal createAnimal(String action) {
    return (Animal) factoryMap.get(action).newInstance();
}
1 голос
/ 08 августа 2010

Я думал бы как-то сопоставить строку с функцией.Таким образом, вы можете передать Meow на карту и вернуть функцию конструктора, которая уже была отображена.Я не уверен, как это сделать в Java, но быстрый поиск вернул эту тему .У кого-то еще может быть идея получше.

0 голосов
/ 05 сентября 2018

Теперь вы можете использовать ссылки на конструктор Java 8 и функциональный интерфейс.

import java.util.HashMap;
import java.util.Map;
import java.util.function.Supplier;

public class AnimalFactory {
    static final Map<String, Supplier<Animal>> constructorRefMap = new HashMap<>();

    public static void main(String[] args) {
        register("Meow", Cat::new);
        register("Woof", Dog::new);

        Animal a = createAnimal("Meow");
        System.out.println(a.whatAmI());
    }

    public static void register(String action, Supplier<Animal> constructorRef) {
        constructorRefMap.put(action, constructorRef);
    }

    public static Animal createAnimal(String action) {
        return constructorRefMap.get(action).get();
    }
}

interface Animal {
    public String whatAmI();
}

class Dog implements Animal {
    @Override
    public String whatAmI() {
        return "I'm a dog";
    }
}

class Cat implements Animal {
    @Override
    public String whatAmI() {
        return "I'm a cat";
    }
}
...