【问题标题】:Processing an object depending on its implementation of an interface in Java根据对象在 Java 中的接口实现来处理对象
【发布时间】:2018-06-05 00:38:34
【问题描述】:

我一直在寻找用 Java 解决这个问题的设计思路。

我正在使用一个库(我无法更改它),对于这个示例,我将只调用“Animals”。它包含一个 Animal 接口和一堆实现;我需要根据我得到的 Animal 的实现来调用不同的方法:

List<Animal> animals = Service.getAnimals();

for(Animal a : animals) {
    process(a);
}

private void process(Animal animal) {

    if (animal instanceOf Cat) {
        processCat(animal);
    } else if (animal instanceOf Dog) {
        processDog(animal);
    } else {
        System.out.println("unsopported animal");
    }
}

我目前正在通过反射解决这个问题,使用一个包含所有“处理器”并使用它们调用它们的类

String methodName = "process" + Animal getClass().getSimpleName(); //ugh

我正在使用 Java 8,我很确定必须有更好的设计来解决这个问题。

感谢任何帮助!

【问题讨论】:

  • 查找“访问者”设计模式,因为它可以帮助您解决这个问题
  • 使用不包含您要调用的方法的接口有什么意义?在任何情况下,接口都应该有一个通用的方法,并且每个类都以不同的方式实现它。
  • 其他语言将调用此模式匹配,在 Java 访问者模式将是要走的路。
  • @HovercraftFullOfEels 同意当且仅当您自己创建实例时。如果外部服务为您提供了一组不透明的接口实现,那么您又回到了原点...
  • @EJP 如果您不控制源代码,并且源还没有接受Visitor 对象的方法,并且您也不自己实例化基类型(而是接收超类型对象的集合),那么我看不出如何避免提问者描述的检查和转换模式(或反射)。

标签: java


【解决方案1】:

如果 Animal 是一个密封类,也就是说,它不是动态可扩展的,并且具有有限的已知子类数量,那么您在示例中偶然发现的 if-instanceof 模式就是经典的“模式匹配” ”。

如果 Animal 是一个你可以控制的类,那么你可以使用 Visitor Pattern 直接在 Animal 上创建一个访问方法。

但是您声明 Animal 来自外部库,这限制了您可以采取的方法。

您仍然可以使用访问者模式,将负责与动物交互的所有代码保留在单个类中,使用方法重载在运行时解析类型(假设您对泛型没有任何问题)。

但实际上这和 if-instanceof 方法一样不灵活,只是让 OO 的人感觉更好。

因此,要采取的方法归结为代码组织,以及对您的代码库有意义的方法。

老实说,我会使用 if-instanceof,除非方法/行为的数量开始变得过于复杂。

在这种情况下,我会创建一种注册表,为每种动物类型注册一个处理器。

然后,您可以创建一个简单的类,从注册表中获取动物类型所需的处理器。

我在 Minecraft 中经常使用这种注册表模式,但我不确定它是否记录在其他地方。

基本上它的使用看起来像这样。

void onApplicationStart(){
    Registry registry = new Registry();
    registry.register(Cat.class, cat -> catProcessor.process(cat));
    registry.register(Dog.class, dog -> dogProcessor.process(dog));
    registry.registerFallback(Animal.class, ani -> animalProcessor.process(ani));
}

然后,您可以获取注册表以及执行处理的方法。

void whenNeeded(Animal animal){
    Registry registry = fetchRegistrySomehow();
    registry.for(animal.getClass()).apply(animal);
}

编辑: 注册表的实现是故意缺失的,因为具体的行为会有所不同,具体取决于您希望如何进行查找、处理类层次结构、何时以及是否应该在某些应用程序启动事件后密封注册表。

【讨论】:

    【解决方案2】:

    我认为您可以将自己的方法隐藏在漂亮的糖果包装纸中,例如使用 EnumClass hierarchy 以防许多方法被覆盖。

    例如这是两个 Animal 实现,使用不同的方法名称来获取动物的名称:

    class Dog implements Animal {
    
        public String getDogName() {
            return "dog";
        }
    }
    
    class Cat implements Animal {
    
        public String getCatName() {
            return "cat";
        }
    }
    

    然后你可以为每个实现定义一个EnumFunctions

    enum AnimalRegistry {
        DOG(Dog.class, animal -> ((Dog)animal).getDogName()),
        CAT(Cat.class, animal -> ((Cat)animal).getCatName());
    
        private final Class<? extends Animal> cls;
        private final Function<Animal, String> getName;
    
        AnimalRegistry(Class<? extends Animal> cls, Function<Animal, String> getName) {
            this.cls = cls;
            this.getName = getName;
        }
    
        public final String getName(Animal animal) {
            return getName.apply(animal);
        }
    
        public static AnimalRegistry parseClass(Animal animal) {
            for (AnimalRegistry registry : values())
                if (registry.cls == animal.getClass())
                    return registry;
            throw new RuntimeException("Unknown Animal implementation: " + animal.getClass().getSimpleName());    
        }
    }
    

    最后,您的客户端代码可能如下所示:

    Animal animal = ...;
    String animalName = AnimalRegistry.parseClass(animal).getName(animal);
    

    重复,如果你有例如超过 2 种实现方法,Enum 使用起来不太舒服;然后您可以切换到类层次结构并完全像在枚举中一样(不要忘记,在JVM中Enum中的每个常量都是Enum接口的不同实现)。

    P.S. 你的方法还不错,在很多情况下都非常有用:

    private void process(Animal animal) {
        if (animal instanceof Cat)
            process((Cat)animal);
        else if (animal instanceof Dog)
            process((Dog)animal);
        else
            System.out.println("unsopported animal");
    }
    
    private void process(Cat cat) {
        cat.getCatName();
    }
    
    private void process(Dog dog) {
        dog.getDogName();
    }
    

    【讨论】:

    • 那是……字面上他已经拥有的东西,除了使用方法重载而不是唯一命名的方法。
    • @user69513 不完全是。据我了解,他不想使用reflection。我举例说明如何避免它。
    • 在他的第一个代码块中,他没有使用反射。然后他描述了使用反射作为一种避免硬编码查找表方法的方法。
    • @RyanTheLeach 方法重载在 Java 中在运行时不起作用,仅在编译时起作用(即它使用静态确定的类型)。这与继承不同,后者使用虚函数并在运行时动态调度它们(我并不是说听起来居高临下,它有时确实让我感到不安)。
    • @LuaiGhunim 你不对。 processCat() 不属于 Animal 实例,可以更改。
    【解决方案3】:

    我认为the answer provided by @RyanTheLeach 通过基于注册表的方法很好地解决了您的问题。但是,在您的问题的 cmets 中,提出了 visitor pattern。我提到了“如果你自己创建实例”,并且......目前任何现有答案都没有详细说明。我发现您在这里遇到的问题的细微变化往往非常频繁。由于在 Java 中实现基于包装器的访问者模式可能有点令人困惑,我想我会添加一些有关该特定方法的细节。

    首先,这似乎应该非常简单。只需创建一个访问者接口、一个基本类型的包装器和一个接受访问者实现的包装器方法。

    public interface AnimalVisitor {
        public void visit(Animal instance);
        public void visit(Cat instance);
        public void visit(Dog instance);
        // ...
    }
    
    public class AnimalWrapper {
        public final Animal instance;
        // ...
        public void acceptVisitor(AnimalVisitor visitor) {
            visitor.visit(instance);
        }
    }
    

    方法重载会处理剩下的事情,对吗?事实证明这不起作用,因为Java determines the method overload to call based on the statically determined type,而不是运行时的实际类型。请注意,这与继承不同,其中dynamic dispatch 用于在运行时查找virtual method 的具体实现。

    也许我们可以用泛型来解决这个问题?

    public class AnimalWrapper <T extends Animal> {
        public final T instance;
        // ...
        public void acceptVisitor(AnimalVisitor visitor) {
            visitor.visit(instance);
        }
    }
    

    不。它可能看起来像对visitor.visit(instance) 的调用应该静态确定子类型,但实际上泛型的静态类型是其捕获的上限(在本例中为Animal)。

    也许我们可以通过利用通常的 Java 模式来获取runtime type information for generics 来解决这个问题?

    public class AnimalWrapper <T extends Animal> {
        public final T instance;
        private final Class<T> type;
        // ...
        public void acceptVisitor(AnimalVisitor visitor) {
            visitor.visit(type.cast(instance));
        }
    }
    

    很遗憾,由于与以前相同的原因,这也不起作用; Class&lt;T&gt; type 也是通用的,因此它静态解析为基本类型 Animal

    那么我们怎样才能使这个工作呢? This answer 非常清楚地说明了如何为要包装的每个子类型使用单独的包装类来解决此问题。如果我们可以在不需要这么多不同的独特包装类的情况下完成它,那不是很好吗?你说你使用的是 Java 8,所以让我们结合 lambdas 和静态工厂方法来或多或少透明地实现所有这些(并且只使用一个包装类)。

    第三方代码

    public abstract class Animal {
    
        private final String instanceName;
    
        private Animal(String instanceName) {
            this.instanceName = instanceName;
        }
    
        public abstract String typeName();
    
        public String instanceName() {
            return instanceName;
        }
    
        public static class Cat extends Animal {
    
            public Cat(String instanceName) {
                super(instanceName);
            }
    
            @Override
            public String typeName() {
                return "Cat";
            }
        }
    
        public static class Dog extends Animal {
    
            public Dog(String instanceName) {
                super(instanceName);
            }
    
            @Override
            public String typeName() {
                return "Dog";
            }
        }
    
        public static class Fox extends Animal {
    
            public Fox(String instanceName) {
                super(instanceName);
            }
    
            @Override
            public String typeName() {
                return "Fox";
            }
        }
    }
    

    我们的准则

    /*
     * The Wrapper Class
     */
    public class AnimalWrapper {
    
        private final Animal instance;
        private final Consumer<AnimalVisitor> visitResolver;
    
        private AnimalWrapper(Animal instance, Consumer<AnimalVisitor> visitResolver) {
            this.instance = instance;
            this.visitResolver = visitResolver;
        }
    
        public Animal getInstance() {
            return instance;
        }
    
        public void acceptVisitor(AnimalVisitor visitor) {
            visitResolver.accept(visitor);
        }
    
        public static AnimalWrapper create(Animal instance) {
            final Consumer<AnimalVisitor> visitResolver = visitor -> AnimalVisitor.visitResolver(visitor, instance);
            return new AnimalWrapper(instance, visitResolver);
        }
    
        public static AnimalWrapper create(Animal.Cat instance) {
            final Consumer<AnimalVisitor> visitResolver = visitor -> AnimalVisitor.visitResolver(visitor, instance);
            return new AnimalWrapper(instance, visitResolver);
        }
    
        public static AnimalWrapper create(Animal.Dog instance) {
            final Consumer<AnimalVisitor> visitResolver = visitor -> AnimalVisitor.visitResolver(visitor, instance);
            return new AnimalWrapper(instance, visitResolver);
        }
    
        public static AnimalWrapper create(Animal.Fox instance) {
            final Consumer<AnimalVisitor> visitResolver = visitor -> AnimalVisitor.visitResolver(visitor, instance);
            return new AnimalWrapper(instance, visitResolver);
        }
    }
    
    /*
     * The Visitor Interface
     */
    public interface AnimalVisitor {
    
        public default void visit(Animal instance) {
            printMessage("Default implementation", "Animal (base type)", instance);
        }
    
        public static void visitResolver(AnimalVisitor visitor, Animal instance) {
            visitor.visit(instance);
        }
    
        public default void visit(Animal.Cat instance) {
            printMessage("Default implementation", "Cat", instance);
        }
    
        public static void visitResolver(AnimalVisitor visitor, Animal.Cat instance) {
            visitor.visit(instance);
        }
    
        public default void visit(Animal.Dog instance) {
            printMessage("Default implementation", "Dog", instance);
        }
    
        public static void visitResolver(AnimalVisitor visitor, Animal.Dog instance) {
            visitor.visit(instance);
        }
    
        public static void printMessage(String implementation, String signature, Animal instance) {
            System.out.println();
            System.out.println(implementation);
            System.out.println("\tSignature: " + signature);
            System.out.println("\tInstance type: " + instance.typeName());
            System.out.println("\tInstance name: " + instance.instanceName());
        }
    }
    
    /*
     * The Visitor Implementation
     */
    public class AnimalVisitorImpl implements AnimalVisitor {
    
        @Override
        public void visit(Animal instance) {
            AnimalVisitor.printMessage("Specialized implementation", "Animal (base type)", instance);
        }
    
        @Override
        public void visit(Animal.Cat instance) {
            AnimalVisitor.printMessage("Specialized implementation", "Cat", instance);
        }
    }
    
    /*
     * Actual Usage
     */
    public static void main(String[] args) {
        final List<AnimalWrapper> wrappedAnimals = new ArrayList<>();
        wrappedAnimals.add(AnimalWrapper.create(new Animal.Cat("A normal cat.")));
        wrappedAnimals.add(AnimalWrapper.create((Animal) new Animal.Cat("A stealthy cat.")));
        wrappedAnimals.add(AnimalWrapper.create(new Animal.Dog("A dog (only default support).")));
        wrappedAnimals.add(AnimalWrapper.create(new Animal.Fox("A fox (no support).")));
    
        final AnimalVisitor visitor = new AnimalVisitorImpl();
        for (AnimalWrapper w : wrappedAnimals)
            w.acceptVisitor(visitor);
    }
    

    输出

    专业实施 签名:猫 实例类型:猫 实例名称:一只普通的猫。 专业实施 签名:动物(基础类型) 实例类型:猫 实例名称:一只隐形猫。 默认实现 签名:狗 实例类型:狗 实例名称:一只狗(仅默认支持)。 专业实施 签名:动物(基础类型) 实例类型:狐狸 实例名称:一只狐狸(不支持)。

    其他想法

    • 图书馆应该首先提供一种接受访问者的方法!
    • 所有这些只有在我们实例化单个实例时才有效。一旦我们不得不处理从第三方代码接收不透明的东西(例如Collection&lt;Animal&gt;),这种方法就会失效,我们发现自己又回到了基于检查和转换(或反射)的方法。这就是registry based approach outlined by @RyanTheLeach 真正闪耀的原因; animalInstance.getClass() 返回由该特定子类型的所有实例共享的唯一静态类对象,从而使我们即使在呈现基类型实例时也能检索到正确的 lambda。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2015-07-03
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多