【问题标题】:Selecting Implementation of Interface based on the Parameter passed根据传递的参数选择接口的实现
【发布时间】:2020-07-06 08:37:25
【问题描述】:

我有一个父接口。

public interface Parent{...}

还有两个实现这个接口的类。

public class ChildA implements Parent {...}
public class ChildB implements Parent {...}

现在我想根据传递的参数创建一个孩子的对象。像这样的,

Parent instance1 = Parent.getChild("Key1");

目前,我正在通过使用 hashmaps 来实现这一点。

static Map<String,Class<?>> map = new HashMap<String,Class<?>>(); 
    
static void init() throws ClassNotFoundException {
    map.put("Key1",Class.forName("ChildA"));
    map.put("Key2",Class.forName("ChildB"));
}
    
static Parent getChild(String key) throws InstantiationException, IllegalAccessException {
    return (Parent) map.get(key).newInstance();
}

但是这里的问题是每次我实现一个新的孩子时,我都必须将它添加到 Parent init 方法中。那么有没有更清洁的方法来做到这一点?比如给孩子本身添加钥匙的信息,

public class ChildA implements Parent {
    
    private String key = "Key1";
    ...

因此,当我从父类调用 getChild 时,它指的是相应的 Child。

基本上,我要求一种基于传递的参数动态引用父级子级的方法。

【问题讨论】:

标签: java interface


【解决方案1】:

您可以使用service provider mechanism。这在积极使用 Java 的模块系统时效果最好,因为那时必要的部分甚至都集成到了 Java 语言中。

如果其他模块中的实现应该是可能的,包含Parent 接口的模块必须导出它的包。那么,一个想要查找接口实现的模块必须有一个

uses yourpackage.Parent;

在其模块信息中的指令。提供实现的模块必须具有类似

的指令
provides yourpackage.Parent with yourpackage.ChildA, yourpackage.ChildB;

在其模块信息中。可以在同一个模块中使用接口并提供实现。这也可能是包含接口声明的模块。在这种情况下,甚至可以只在单个模块中使用该机制,而根本不导出接口。

编译器将已经检查指定的实现类是public,有一个public无参数构造函数,并真正实现了服务接口。实现不需要驻留在导出的包中;接口可能是从另一个模块访问实现的唯一方法。

由于编译器会预先检查所需的不变量,因此您无需处理反射固有的问题。例如,您不需要声明或处理InstantiationExceptionIllegalAccessException

一个简单的设置可以是

模块信息
module ExampleApp {
    uses yourpackage.Parent;
    provides yourpackage.Parent with yourpackage.ChildA, yourpackage.ChildB;
}
你的包裹/父母
package yourpackage;

public interface Parent {
}

你的包裹/TheKey
package yourpackage;

import java.lang.annotation.*;

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface TheKey {
    String value();
}
你的包裹/ChildA
package yourpackage;

@TheKey("Key1")
public class ChildA implements Parent {
}
你的包裹/ChildB
package yourpackage;

@TheKey("Key2")
public class ChildB implements Parent {
}
独立包/UsingTheInterfaces
package independentpackage;

import java.util.ServiceLoader;
import yourpackage.Parent;
import yourpackage.TheKey;

public class UsingTheInterfaces {
    public static void main(String[] args) {
        Parent p = getChild("Key1");
        System.out.println(p);
    }
    static Parent getChild(String key) {
        return ServiceLoader.load(Parent.class).stream()
            .filter(p -> {
                TheKey keyAnno = p.type().getAnnotation(TheKey.class);
                return keyAnno != null && keyAnno.value().equals(key);
            })
            .findAny()
            .map(ServiceLoader.Provider::get)
            .orElse(null); // decide how to handle absent keys
    }
}

如前所述,当您有 exports yourpackage; 指令时,其他模块可以提供实现,ServiceLoader 将为运行时存在的所有模块动态发现这些实现。 using 模块在编译时不需要知道它们。

对于没有模块系统的旧 Java 版本,有一个前身机制,它也由 ServiceLoader 以向后兼容的方式处理。这些提供程序必须打包在一个 jar 文件中,该文件包含一个文件META-INF/services/yourpackage.Parent,其中包含实现该接口的 jar 文件中所有类的列表。在 Java 9 之前,ServiceLoader 还缺少允许在实例化实现之前查询注释的 Stream API 支持。您只能使用已经实例化类并返回实例的Iterator

避免旧 API 不必要开销的一种方法是将接口拆分为服务提供者接口和实际服务接口。提供者接口将提供元信息并充当实际服务实现的工厂。比较CharsetProviderCharsetFileSystemProviderFileSystem 之间的关系。这些例子也证明了服务提供者机制已经被广泛使用。更多示例请参见java.base module documentation

这只是一个概述; API documentation of ServiceLoader 包含有关该机制的更多详细信息。

【讨论】:

    【解决方案2】:

    选项 1:我建议你使用像这样的简单工厂类:

    public class ChildFactory {
        public static Parent getChild(String key) throws ClassNotFoundException {
             if (key.equals("Key1"))
                 return new ChildA();
             if (key.equals("key2"))
                 return new ChildB();
             throw new ClassNotFoundException();
        }
    }
    

    原因如下:

    1. 简单易扩展:类只做一件事,创建对象
    2. 不要使用已弃用的 newInstance()
    3. 从静态“工厂”方法中清除接口 Parent

    选项2:定义一个给定键的函数,它返回类名并使用它来创建实例:

    public class ChildFactory {
        public static Parent getChild(String key) {
            String className = childClassNameFor(key);
            return (Parent) Class.forName("my.package."+className).newInstance();
        }
        private static String childClassNameFor(String key) {
            return "Child" + key;
        }
    }
    

    然后你可以像这样使用它:

    Parent p = ChildFactory.getChild("A");
    

    【讨论】:

    • 是的,工厂模式很好,但他的问题仍然没有解决。添加新子类时需要更改子工厂。如何解决这个问题
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-07-29
    • 1970-01-01
    • 2017-09-16
    • 2011-04-23
    相关资源
    最近更新 更多