在 Java 中创建一个不依赖 if-else 的工厂方法
声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow
原文地址: http://stackoverflow.com/questions/3434466/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me):
StackOverFlow
Creating a factory method in Java that doesn't rely on if-else
提问by Brad
Currently I have a method that acts as a factory based on a given String. For example:
目前我有一个方法可以作为基于给定字符串的工厂。例如:
public Animal createAnimal(String action)
{
if (action.equals("Meow"))
{
return new Cat();
}
else if (action.equals("Woof"))
{
return new Dog();
}
...
etc.
}
What I want to do is avoid the entire if-else issue when the list of classes grows. I figure I need to have two methods, one that registers Strings to classes and another that returns the class based on the String of the action.
我想要做的是在类列表增长时避免整个 if-else 问题。我想我需要有两种方法,一种将字符串注册到类,另一种根据操作的字符串返回类。
What's a nice way to do this in Java?
在 Java 中这样做的好方法是什么?
采纳答案by Tom Hawtin - tackline
What you've done is probably the best way to go about it, until a switch on string is available. (Edit 2019:A switch on string is available - use that.)
您所做的可能是最好的方法,直到可以使用打开字符串为止。(编辑 2019:可以使用打开字符串的开关 - 使用它。)
You could create factory objects and a map from strings to these. But this does get a tad verbose in current Java.
您可以创建工厂对象和从字符串到这些的映射。但这在当前的 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();
}
At the time this answer was originally written, the features intended for JDK7 could make the code look as below. As it turned out, lambdas appeared in Java SE 8 and, as far as I am aware, there are no plans for map literals. (Edited 2016)
在最初编写此答案时,针对 JDK7 的功能可以使代码如下所示。事实证明,lambdas 出现在 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();
}
Edit 2019:Currently this would look something like this.
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();
}
If you want to add a parameter, you'll need to switch Supplier
to Factory
(and get
becomes apply
which also makes no sense in the context). For two parameters BiFunction
. More than two parameters, and you're back to trying to make it readable again.
如果你想添加一个参数,你需要切换Supplier
到Factory
(并且get
变成apply
这在上下文中也没有意义)。对于两个参数BiFunction
。超过两个参数,您又要尝试使其再次可读。
回答by Dave McClelland
My thought would be to somehow map a String to a function. That way you can pass Meow
to the map and return the constructor function that was already mapped out. I'm not sure how to do this in Java, but a quick search returned this SO thread. Someone else may have a better idea, though.
我的想法是以某种方式将字符串映射到函数。这样你就可以传递Meow
给映射并返回已经映射出来的构造函数。我不确定如何在 Java 中执行此操作,但是快速搜索返回了这个 SO thread。不过,其他人可能有更好的主意。
回答by Rui Vieira
I haven't tried this, but could with create a Map
with "Meow", etc as keys
and (say) Cat.class
as value.
我还没有尝试过这个,但是可以创建一个Map
以“喵”等为键和(比如)Cat.class
作为值的。
Provide a static instance generation via an interface and call as
通过接口提供静态实例生成并调用
Animal classes.get("Meow").getInstance()
回答by crowne
Use Scannotations!
使用扫描注释!
Step 1.Create an annotation like below:
步骤 1.创建一个注释,如下所示:
package animal;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@Retention(RetentionPolicy.RUNTIME)
public @interface AniMake {
String action();
}
Note that the RetentionPolicy is runtime, we'll be accessing this via reflection.
请注意,RetentionPolicy 是运行时,我们将通过反射访问它。
Step 2.(Optional) Create a common super class:
第 2 步。(可选)创建一个公共超类:
package animal;
public abstract class Animal {
public abstract String greet();
}
Step 3.create the subclasses with your new annotation:
步骤 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!*";
}
}
Step 4.Create the factory:
步骤 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());
}
}
This factory, can be cleaned up a bit e.g. deal with the nasty checked exceptions etc.
In this factory, I've used the Reflectionslibrary.
I did this the hard way, i.e. I didn't make a maven project and I had to add the dependencies manually.
The dependencies are:
这个工厂可以稍微清理一下,例如处理讨厌的检查异常等。
在这个工厂中,我使用了反射库。
我这样做很困难,即我没有制作 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
- 反射-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
If you skipped Step 2, then you'll need to change the factory method to return Object.
From this point on you can keep adding subclasses, and as long as you annotating them with AniMake (or whatever better name you come up with), and place them in the package defined in the Reflections constructor (in this case "animal"), and leave the default no-args constructor visible, then the factory will instantiate your classes for you without having to be changed itself.
如果您跳过了第 2 步,那么您需要更改工厂方法以返回 Object。
从这一点开始,您可以继续添加子类,只要您使用 AniMake(或您想出的任何更好的名称)注释它们,并将它们放置在 Reflections 构造函数中定义的包中(在本例中为“animal”),并保留默认的无参数构造函数可见,然后工厂将为您实例化您的类,而无需自行更改。
Here's the output:
这是输出:
log4j:WARN No appenders could be found for logger (org.reflections.Reflections).
log4j:WARN Please initialize the log4j system properly.
*WOOF!*
=^meow^=
回答by bluedevil2k
There's no need for Maps with this solution. Maps are basically just a different way of doing an if/else statement anyway. Take advantage of a little reflection and it's only a few lines of code that will work for everything.
使用此解决方案不需要地图。无论如何,映射基本上只是执行 if/else 语句的一种不同方式。利用一点点反射,只需几行代码就可以解决所有问题。
public static Animal createAnimal(String action)
{
Animal a = (Animal)Class.forName(action).newInstance();
return a;
}
You'll need to change your arguments from "Woof" and "Meow" to "Cat" and "Dog", but that should be easy enough to do. This avoids any "registration" of Strings with a class name in some map, and makes your code reusable for any future Animal you might add.
您需要将参数从“Woof”和“Meow”更改为“Cat”和“Dog”,但这应该很容易做到。这避免了在某些映射中使用类名对字符串进行任何“注册”,并使您的代码可重用于您将来可能添加的任何动物。
回答by rich
I'd look to retrieve an Enum representation of the String and switch on that.
我希望检索字符串的 Enum 表示并打开它。
回答by Stéphane
You already selected the answer to that question, but that could still help.
您已经选择了该问题的答案,但这仍然有帮助。
Although I am a .NET/C# developer, this is really a general OOP problem. I have run in the same kind of problem and I have found a nice solution (I think) using an IoC Container.
虽然我是 .NET/C# 开发人员,但这确实是一个普遍的 OOP 问题。我遇到了同样的问题,我找到了一个很好的解决方案(我认为)使用IoC Container。
If you don't use one yet, that is probably a good reason to start. I don't know IoC containers in Java, but I assume there must be one with similar features.
如果您还没有使用,这可能是一个很好的开始理由。我不知道 Java 中的 IoC 容器,但我认为一定有一个具有类似功能的容器。
What I had was a Factory that contains a reference to the IoC container, which is resolved by the container itself (in the BootStrapper)
我拥有的是一个包含对 IoC 容器的引用的工厂,它由容器本身解析(在 BootStrapper 中)
...
public AnimalFactory(IContainer container)
{
_container = container;
}
You can then setup your IoC container to resolve the correct types based on a key (the sound in your example). It would abstracts completely the concrete classes that your factory needs to return.
然后,您可以设置 IoC 容器以根据键(示例中的声音)解析正确的类型。它将完全抽象您的工厂需要返回的具体类。
in the end, your factory method is shrunk down to this :
最后,您的工厂方法缩减为:
...
public Createable CreateAnimal(string action)
{
return _container.Resolve<Createable>(action);
}
This stackoverflow questionillustrates the same kind of problem with real world elements and the validated answer shows a draft of my solution (pseudo code). I later wrote a blog post with the real pieces of codewhere it is much clearer.
这个 stackoverflow 问题说明了与现实世界元素相同的问题,经过验证的答案显示了我的解决方案的草稿(伪代码)。后来我写了一篇博客文章,其中包含更清晰的真实代码片段。
Hope this can help. But it might be overkill in simple cases. I used that because I had 3 levels of dependencies to resolve, and an IoC container already assembling all my components.
希望这能有所帮助。但在简单的情况下,这可能是矫枉过正。我使用它是因为我需要解决 3 个级别的依赖关系,并且一个 IoC 容器已经组装了我的所有组件。
回答by Jeff
If you don't haveto use Strings, you could use an enum type for the actions, and define an abstract factory method.
如果你不具备使用字符串,你可以使用一个枚举类型的行为,并定义一个抽象的工厂方法。
...
public enum Action {
MEOW {
@Override
public Animal getAnimal() {
return new Cat();
}
},
WOOF {
@Override
public Animal getAnimal() {
return new Dog();
}
};
public abstract Animal getAnimal();
}
Then you can do things like:
然后你可以做这样的事情:
...
Action action = Action.MEOW;
Animal animal = action.getAnimal();
...
It's kind of funky, but it works. This way the compiler will whine if you don't define getAnimal() for every action, and you can't pass in an action that doesn't exist.
这有点时髦,但它有效。这样,如果您没有为每个动作定义 getAnimal() ,编译器就会抱怨,并且您无法传入不存在的动作。
回答by goRGon
And what do people think about using Class.newInstance()inside Tom Hawtin's answer? This will avoid us from storing unnecessary anonymous classes in memory? Plus code will be more clean.
人们对在 Tom Hawtin 的回答中使用Class.newInstance() 有何看法?这将避免我们在内存中存储不必要的匿名类?加上代码会更干净。
It will look something like this:
它看起来像这样:
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();
}
回答by Ron McLeod
Now you could use Java 8 constructor references and a functional interface.
现在您可以使用 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";
}
}