使用反射动态调用正确的 setter 方法

Dynamically call correct correct setter method with Reflection

我正在使用一个我无法修改的库,它有一个 class 以及以下 Enum 和 setters.

public class MyClass {
  public enum MyEnum {
    ClassA,
    ClassB,
    ClassC
    ...
  }

  private SomeEnum myEnum;
  private Interface ifc; // parent of ClassA, ClassB, ClassC, etc.

  public ClassA setClassA(ClassA classA) {
    ifc = classA;
    myEnum= SomeEnum.ClassA;
  }
  public ClassB setClassB(ClassB classB) {
    ifc = classB;
    myEnum= SomeEnum.ClassB;
  }
  public ClassC setClassC(ClassC classC) {
    ifc = classC;
    myEnum= SomeEnum.ClassC;
  }
  // ... more of these setters 
}

注意每个 Enum 名称是如何与实现 Interface 的相应 class 名称匹配的字符串文字,以及每个 class 名称如何具有自己特定的 setter.

正如您想象的那样,调用它的代码非常简单:

Interface ifc = someCallToGetAnImpl();
MyClass myClass = new MyClass(); 
myClass.set???(ifc);

很多 Interface 的实现,我不能保证库的未来版本不会添加更多。所以我希望创建一个可以动态派生和调用正确 setter.

的函数

当然,我可以构建一个大的旧 if... else if... 块,但是在构建 Interface 的新实现时,这将需要更改软件。我还考虑过使用 Class.getDeclaredMethod(String name, Class<?>... parameterTypes) 来构建 name 参数,例如

"set" + ifc.getClass.getSimpleName()...

应该 保持软件动态,但看起来很笨拙。

欢迎任何干净的、生产质量的建议或方法。

您正在考虑的方法,通过 Class.getDeclaredMethod(String name, Class... parameterTypes) 等方法使用反射是解决您所描述问题的正确方法。这是一个笨拙的问题,值得采用笨拙的方法。

不过,我要补充的一件事是,您编写单元测试来验证您对 class 中的模式的假设,这样如果他们添加了一个不遵循该模式的方法,您得到提醒。例如,如果添加了一个新的枚举值,但没有方法支持它怎么办?你会希望自动提醒类似的事情。

无法使用反射在 Enum class 中动态添加元素。 可能您可以使用动态代码来调用该方法。没有办法改变 enumclass.

还有另一种实现方式,您可以拥有一个 xml/properties 文件,您可以在其中动态配置枚举 class,如 https://dzone.com/articles/enum-tricks-dynamic-enums

中所述

如果您的调用者在 Interface 级别工作,而没有 caring/knowing 而 class 实际上正在实现该接口,请不要让这成为他们的问题。实施 setInterface(Interface) 方法。

不知道你为什么 want/need Enumifc.getClass() 还不够好吗?如果 subclassing 很深,它将 return Class<?> 对象,尽管是叶子 class。

为了确保您 select Enum ClassA 每当 ClassA 或其中一个子 class 被传递时,请执行以下操作。

package test;

interface Interface { /*content not included*/ }

class ClassA implements Interface { /*content not included*/ }
class ClassB implements Interface { /*content not included*/ }
class ClassC implements Interface { /*content not included*/ }

class MyClass {

    public enum MyEnum {
        ClassA(test.ClassA.class),
        ClassB(test.ClassB.class),
        ClassC(test.ClassC.class);

        private final Class<? extends Interface> impl;

        private MyEnum(Class<? extends Interface> impl) {
            this.impl = impl;
        }

        Class<? extends Interface> getImpl() {
            return this.impl;
        }
    }

    private MyEnum    myEnum;
    private Interface ifc;

    public void setInterface(Interface ifc) {
        for (MyEnum e : MyEnum.values()) {
            if (e.getImpl().isAssignableFrom(ifc.getClass())) {
                this.myEnum = e;
                this.ifc = ifc;
                return;
            }
        }
        throw new IllegalArgumentException("Interface class is unknown: " + ifc.getClass().getName());
    }

}

现在调用者很容易了:

Interface ifc = someCallToGetAnImpl();
MyClass myClass = new MyClass(); 
myClass.setInterface(ifc);

注意: 如果您有 class 的枚举,它们是彼此的子class,请确保子class被列在第一位。例如。如果 class ClassB extends ClassA,则 enum ClassB 必须列在 enum ClassA.

之前

写一个适配器

使用 adapter pattern,您可以包装您不喜欢的 MyClass 的不可修改的 API,并为消费者提供更清洁的 API。您无法修改库,因此适配器将存在于您的代码库中。

当我 doSomethingStraightForward 时,我并不关心如何调用 MyClass 的复杂细节,我只想在其上设置 Interface。可以隐藏与MyClass真正互动的顾虑

例如:

void doSomethingStraightForward() {
    Interface ifc = someCallToGetAnImpl();
    MyClassInterfaceAdapter myClassAdapter = new MyClassInterfaceAdapter();
    myClassAdapter.setInterface(ifc);
}    

示例实现

如前所述,您可以使用反射获取设置器。这是我尝试实现 MyClassInterfaceAdapter.

的一些代码
public MyClassInterfaceAdapter() {
    this.myClass = new MyClass();
    this.interfaceSetters = getSetterMap(myClass);
}

public void setInterface(Interface iface) throws Exception {
    if (iface == null) {
        throw new IllegalArgumentException("iface must not be null");
    }

    Class<? extends Interface> ifaceClass = iface.getClass();

    if (!interfaceSetters.containsKey(ifaceClass)) {
        throw new IllegalArgumentException(String.format("iface type %s is not supported", ifaceClass.getName()));
    }

    Method ifaceSetter = interfaceSetters.get(ifaceClass);
    ifaceSetter.invoke(myClass, iface);
}

private static Map<Class<? extends Interface>, Method> getSetterMap(MyClass myClass) {
    Map<Class<? extends Interface>, Method> setterMap = new HashMap<>();

    Class<MyClass> myClassType = (Class<MyClass>) myClass.getClass();
    Class<Interface> interfaceType = Interface.class;

    for (Method method : myClassType.getDeclaredMethods()) {
        final String methodName = method.getName();

        if (methodName.startsWith(SetterPrefix)
                && method.getParameterCount() == 1
                && interfaceType.isAssignableFrom(method.getParameterTypes()[0])) {
            try {
                getMatchingMyEnum(method);
                setterMap.put((Class<? extends Interface>) method.getParameterTypes()[0], method);
            } catch (Exception e) {
                logger.log(Level.WARNING, "Setter not compatible: " + methodName, e);
            }
        }
    }

    return setterMap;
}

private static MyEnum getMatchingMyEnum(Method m) {
    int MyEnumStartIndex = SetterPrefix.length();
    String enumName = m.getName().substring(MyEnumStartIndex);
    return Enum.valueOf(MyEnum.class, enumName);
}

生产就绪

就在生产中使用它而言,我会考虑以下几点:

  • 您需要非常高的单元测试、集成测试和端到端测试覆盖率。测试应涵盖快乐路径和失败场景。
  • 设置Interface失败时,您需要考虑您要做什么。 throws Exception 到处都不是最佳做法。
  • 您可能希望通过工厂或 IoC 容器管理 MyClass 及其适配器的创建,以使代码更易于单元测试(类似于我们上面所做的概念,移动创建对象的关注点和他们在其他地方的依赖关系)。