Создание метода factory в Java, который не полагается на if-else

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

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

    ...
    etc.
}

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

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

Ответ 1

То, что вы сделали, это, вероятно, лучший способ сделать это, пока не станет доступен переключатель на строку. (Изменить 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. Более двух параметров, и вы снова пытаетесь сделать его снова читабельным.

Ответ 2

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

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

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

Ответ 3

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

...
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() для каждого действия, и вы не можете передать действие, которое не существует.

Ответ 4

Использовать 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. Создайте factory:

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());
    }

}

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

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

Здесь вывод:

log4j:WARN No appenders could be found for logger (org.reflections.Reflections).
log4j:WARN Please initialize the log4j system properly.
*WOOF!*
=^meow^=

Ответ 5

Я не пробовал это, но мог бы создать Map с "Meow" и т.д. как ключи и (скажем) Cat.class как значение.

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

Animal classes.get("Meow").getInstance()

Ответ 6

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

Ответ 7

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

Ответ 8

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

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

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

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

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

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

в конце, ваш метод Factory сжимается до этого:

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

fooobar.com/questions/9151/... иллюстрирует ту же проблему с элементами реального мира, и проверенный ответ показывает проект моего решения (псевдокод). Я позже написал сообщение в блоге с реальными фрагментами кода, где он намного яснее.

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

Ответ 9

И что люди думают об использовании 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();
}

Ответ 10

Теперь вы можете использовать ссылки на конструктор 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";
    }
}