在 Java 中创建不依赖 if-else 的工厂方法
Posted
技术标签:
【中文标题】在 Java 中创建不依赖 if-else 的工厂方法【英文标题】:Creating a factory method in Java that doesn't rely on if-else 【发布时间】:2011-03-26 22:56:24 【问题描述】:目前我有一个方法可以作为基于给定字符串的工厂。 例如:
public Animal createAnimal(String action)
if (action.equals("Meow"))
return new Cat();
else if (action.equals("Woof"))
return new Dog();
...
etc.
我想要做的是在类列表增长时避免整个 if-else 问题。 我想我需要有两种方法,一种将字符串注册到类,另一种根据操作的字符串返回类。
在 Java 中有什么好的方法可以做到这一点?
【问题讨论】:
使用 HashMap :) 好的,但是值应该是多少?它可能是等效的对象。但是假设该方法被多次调用,每次调用都需要一个新对象。我需要在返回对象后重新实例化它吗? 您可以拥有一个 HashMap你所做的可能是最好的方法,直到可以打开字符串。 (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 的功能可能会使代码如下所示。事实证明,lambda 出现在 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
。超过两个参数,您又要重新尝试使其可读。
【讨论】:
只使用 Callable 而不是新的 AnimalFactory 接口有什么问题?Callable
抛出。它也相当非主格。
啊,双括号初始化...(您还有 2 个错字:`facotryMap`)
"在 JDK7 中,这可能看起来像..." 你应该为 Java 8 更新这个。; )
第二个例子在 Java 8 中也不能编译,所以我建议你删除它。以为我错过了一些花哨的新功能!仅仅因为没有人发表评论并不意味着人们没有看到它 - 它在我为将字符串映射到类所做的谷歌搜索中名列前茅。【参考方案2】:
使用此解决方案不需要地图。无论如何,地图基本上只是执行 if/else 语句的不同方式。利用一点点反射,只需几行代码就可以解决所有问题。
public static Animal createAnimal(String action)
Animal a = (Animal)Class.forName(action).newInstance();
return a;
您需要将参数从“Woof”和“Meow”更改为“Cat”和“Dog”,但这应该很容易做到。这避免了在某些映射中使用类名对字符串进行任何“注册”,并使您的代码可重复用于您将来可能添加的任何 Animal。
【讨论】:
一般来说,应该避免使用 Class.newInstance()(因为它的异常处理能力很差)。 我在一次采访中被问到这个问题,他们拒绝让我使用反射。我如上所述选择了 HashMap,但想要一个不需要更改源的更动态的解决方案。无法提出解决方案。【参考方案3】:如果你没有使用字符串,你可以为动作使用枚举类型,并定义一个抽象工厂方法。
...
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】:使用扫描注释!
第 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 项目,我必须手动添加依赖项。 依赖项是:
reflections-0.9.5-RC2.jar google-collections-1.0.jar slf4j-api-1.5.6.jar nlog4j-1.2.25.jar javassist-3.8.0.GA.jar dom4j-1.6.jar如果您跳过了第 2 步,则需要更改工厂方法以返回 Object。 从这一点开始,您可以继续添加子类,只要您使用 AniMake(或您想出的任何更好的名称)注释它们,并将它们放在 Reflections 构造函数中定义的包中(在本例中为“动物”),并保持默认的无参数构造函数可见,然后工厂将为您实例化您的类,而无需自行更改。
这是输出:
log4j:WARN No appenders could be found for logger (org.reflections.Reflections).
log4j:WARN Please initialize the log4j system properly.
*WOOF!*
=^meow^=
【讨论】:
过度设计、缓慢且品味相当差(当然,至少最后一个是主观的)。即使最初的问题更大,需要更复杂的解决方案,应用像 Guice 这样更通用的解决方案也会更可取。 @Dimitris,你的概要完全没有根据! 1) 单个可重用的 Annotation 类比每次添加类时都必须编辑映射更可取,如果无论如何要更改它,最好将 if else 结构留在工厂中。您可以消除库并自己进行反射,但我不喜欢重新发明***。 2)你需要量化慢......像“反射很慢”这样的陈述与“Java很慢”生活在同一个时代。 3) 即使你使用 Guice,你仍然需要以某种方式将类映射到任意关键字并提供工厂。 虽然反射 比虚拟方法调用慢(好吧,试试吧),我指的是类路径扫描,这可能是异常慢(你确实意识到这必须搜索类路径的所有 jar,并解析其中的所有类的部分字节码 - 额外如果 jar 甚至不在本地文件系统中,则点...) 具有讽刺意味的是,这个技巧会产生,只是您使用的依赖项,解压缩和解析约 1200 个类的成本。更不用说引入新的、无声的依赖类路径的错误了。现在将所有这些与此处其他一些答案的简单性、可靠性和效率进行比较。嗯,确实有味道! :P 这个答案是开玩笑吗?我知道如果你有 100 种动物,这可能会有用,但除此之外,它似乎代表了人们抱怨 java 和过度工程的一切。【参考方案5】:我还没有尝试过,但可以创建一个 Map
并使用“喵”等作为键
和(比如)Cat.class
作为值。
通过接口提供静态实例生成并调用为
Animal classes.get("Meow").getInstance()
【讨论】:
【参考方案6】:我希望检索字符串的 Enum 表示并打开它。
【讨论】:
您仍然需要使用相同的 if-else somewhere 从输入字符串中获取枚举,不是吗?那么,这样会更好吗? 神圣的 11 年前。我似乎记得我在想 Enum.valueOf() 是否适合,或者查找值的映射等。自从发布此消息以来,就已经开始启用字符串了。 打开字符串是否比 if-else 类型比较更有效?【参考方案7】:您已经选择了该问题的答案,但这仍然会有所帮助。
虽然我是 .NET/C# 开发人员,但这确实是一个普遍的 OOP 问题。我遇到过同样的问题,并且我找到了一个很好的解决方案(我认为)使用 IoC 容器。
如果您还没有使用,那可能是一个很好的开始。我不知道 Java 中的 IoC 容器,但我认为一定有一个具有类似功能的容器。
我拥有的是一个包含对 IoC 容器的引用的工厂,该引用由容器本身解析(在 BootStrapper 中)
...
public AnimalFactory(IContainer container)
_container = container;
然后,您可以设置您的 IoC 容器以根据键(示例中的声音)解析正确的类型。它将完全抽象您的工厂需要返回的具体类。
最后,你的工厂方法被缩小到这个:
...
public Createable CreateAnimal(string action)
return _container.Resolve<Createable>(action);
This *** question 说明了与现实世界元素相同的问题,并且经过验证的答案显示了我的解决方案草稿(伪代码)。 后来我写了a blog post with the real pieces of code,那里更清楚了。
希望这会有所帮助。但在简单的情况下,这可能是矫枉过正。我使用它是因为我需要解决 3 个级别的依赖关系,并且 IoC 容器已经组装了我的所有组件。
【讨论】:
【参考方案8】:我的想法是以某种方式将字符串映射到函数。这样您就可以将Meow
传递给映射并返回已经映射出来的构造函数。我不确定如何在 Java 中执行此操作,但快速搜索返回了 this SO thread。不过,其他人可能有更好的主意。
【讨论】:
【参考方案9】:人们对在 Tom Hawtin 的回答中使用 Class.newInstance() 有何看法? 这将避免我们在内存中存储不必要的匿名类? Plus 代码会更干净。
它看起来像这样:
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";
【讨论】:
以上是关于在 Java 中创建不依赖 if-else 的工厂方法的主要内容,如果未能解决你的问题,请参考以下文章
如何在 laravel 路由中创建不包括某些 slug 的 slug 路由?
如何在Controller中创建不可变的BusinessLayer而不更改其属性?