在 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,其中 Callable 返回一个由 String 指示的类型的对象。然后在工厂构造函数中填充地图。 Map 已经足够好了。不要去建筑疯狂的家伙。 使用枚举代替您的操作字符串。 【参考方案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 的功能可能会使代码如下所示。事实证明,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 路由?

如何在 Django 中创建不区分大小写的数据库索引?

在组中创建不透明数据集

如何在Controller中创建不可变的BusinessLayer而不更改其属性?

如何在 Spring Boot 客户端应用程序中创建不共享底层网络连接的套接字

使用策略模式+工厂模式干掉代码中过多的if-else