Создание метода 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
на Factory
(и get
становится 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";
}
}