【问题标题】:What is the correct pattern for processing a subclass in a type-specific way?以特定于类型的方式处理子类的正确模式是什么?
【发布时间】:2012-05-10 13:13:38
【问题描述】:

我有一个 Animal 对象的集合。

我的核心代码希望将所有这些都视为动物,都一样。每个动物都需要以某种方式进行处理。处理的性质取决于动物的子类型(鸟类、哺乳动物等)。

我的代码目前如下所示。

public interface Animal {
    public String getTaxonomyClass(); 
}

public abstract class Bird implements Animal {

    @Override
    public String getTaxonomyClass() {
        return "aves";
    }

    // Specific to birds
    public abstract float getWingspan();

}

public abstract class Mammal implements Animal {

    @Override
    public String getTaxonomyClass() {
        return "mammalia";
    }

    // Specific to mammals
    public abstract int getToothCount();

}

public interface AnimalProcessor {
    public String getSupportedTaxonomyClass();
    public void process(Animal a);
}

public class MammalProcessor implements AnimalProcessor {

    @Override
    public String getSupportedTaxonomyClass() {
        return "mammalia";
    }

    @Override
    public void process(Animal a) {
        System.out.println("Tooth count is " + ((Mammal)a).getToothCount());
    }

}

public class BirdProcessor implements AnimalProcessor {

    @Override
    public String getSupportedTaxonomyClass() {
        return "aves";
    }

    @Override
    public void process(Animal a) {
        System.out.print("Wingspan is " + ((Bird)a).getWingspan());
    }

}

import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class ZooKeeper {

    Map<String, AnimalProcessor> registry = new HashMap<String, AnimalProcessor>();

    public void registerProcessor(AnimalProcessor ap)
    {
        registry.put(ap.getSupportedTaxonomyClass(), ap);
    }

    public void processNewAnimals(List<Animal> newcomers)
    {
        for(Animal critter : newcomers)
        {
            String taxonomy = critter.getTaxonomyClass();
            if(registry.containsKey(taxonomy))
            {
                // if I can process the animal, I will
                AnimalProcessor ap = registry.get(taxonomy);
                ap.process(critter);
            }

        }
    }
}

import java.util.LinkedList;
import java.util.List;

public class MainClass {

    public static void main(String[] args) {

        ZooKeeper keeper = new ZooKeeper();
        keeper.registerProcessor(new MammalProcessor());
        keeper.registerProcessor(new BirdProcessor());

        List<Animal> animals = new LinkedList<Animal>();

        animals.add(new Mammal() {  // badger

            @Override
            public int getToothCount() {
                return 40;
            } } 
        );

        animals.add(new Bird() {  // condor

            @Override
            public float getWingspan() {
                return 2.9f;
            } }
        );

        keeper.processNewAnimals(animals);

    }
}

通常这很容易理解并且效果很好!我可以在闲暇时添加插件新处理器和动物类型,而无需更改 ZooKeeper 类或任何接口。你可以想象一个更高级的主类,从数据库中加载 Animals,然后依次处理它们。

但是,我担心 AnimalProcessor 子类中的向下转换!这让我觉得不应该存在,并且可能违反了 OO 原则。毕竟,目前我可以将 Bird 传递给 MammalProcessor 的 process() 方法,并且会出现 ClassCastException。

谁能提出一个设计模式来解决这个问题?我查看了访问者模式,但不太清楚如何在这种情况下应用它!关键是让核心代码 (ZooKeeper) 对所有动物一视同仁,并使其能够轻松添加对新动物的支持。谢谢!

【问题讨论】:

  • 为了记录,现实生活中的系统很遗憾不是动物园管理系统。它是处理一般数值模型的系统的核心(想想,需要评估的不同类型的模板)!我将其简化为这个示例的基础。
  • 遗憾的是我不能每天都和企鹅一起玩。
  • 顺便说一句,从 Liskov 替换原则的角度来看,我相信这是可以的......代码当前使用 Animal 的任何地方,您都可以替换 Mammal 或 Bird 而不会破坏它。
  • 如果您的问题已得到解答,或者不再有效,请“打勾”以选择最合适的答案,以便所有人都知道问题已得到解决。谢谢。
  • 它从未真正解决,但我将功劳归功于最全面的帮助。

标签: java design-patterns subclass


【解决方案1】:

我建议如下:

public interface Animal {
    public AnimalProcessor<? extends Animal> getProcessor();
}

所以每只动物都会返回它的匹配处理器。

public interface AnimalProcessor<T extends Animal> {
     public void process(T a);
}

因此处理器将使用其应处理的匹配类型进行键入。 所以植入会是这样的:

public abstract class Bird implements Animal {
    private BirdProcessor processor = new BirdProcessor();
    public abstract float getWingspan();
    @Override
    public AnimalProcessor<Bird> getProcessor() {
        return processor; 
    }
}

public class BirdProcessor implements AnimalProcessor<Bird> {
    @Override
    public void process(Bird b) {
        System.out.print("Wingspan is " + b.getWingspan());
    }
}

【讨论】:

  • 嗨 Shem - 这种方法让我非常接近让整个事情正常工作,但后来我在keeper.processNewAnimals(..) 遇到了问题。由于原始类型,我收到代码的警告。但是,如果我尝试参数化,我总是会在某处遇到类型冲突。
  • 是的,这仍然不完美,因为一些程序员可以在 Mammal.getProcessor() 上返回 AnimalProcessor。你可能不得不忍受这种原始的输入警告(尽管你知道你没有得到类型转换异常),直到 java 更好地实现它的泛型。
【解决方案2】:

这就是 generics 工作得很好的地方。

首先,您需要将 AnimalProcessor 设为泛型:

public interface AnimalProcessor <T extends Animal> {
    public String getSupportedTaxonomyClass();
    public void process(T a);
}

接下来,在您的特定处理器中,您指定通用类型 - 例如对于哺乳动物:

public class MammalProcessor implements AnimalProcessor<Mammal> {

    public String getSupportedTaxonomyClass() {
        return "mammalia";
    }

    public void process(Mammal a) {
        System.out.println("Tooth count is " + a.getToothCount());
    }

}

现在,process 方法只接受 Mammal 对象,这里不接受鸟类。

【讨论】:

  • 仍有一些问题,当我到达此代码时:AnimalProcessor ap = registry.get(taxonomy); ap.process(critter); 然后我收到警告,因为 AnimalProcessor 是原始类型。但是,我无法对其进行参数化,因为此时我不知道小动物的子类型。这样做的正确方法是什么?
  • 你应该把AnimalProcessor ap = registry.get(taxonomy);改成AnimalProcessor&lt;? extends Animal&gt; ap = registry.get(taxonomy);
  • 不幸的是,这不起作用。在 Eclipse 中我现在得到一个错误...The method process(? extends Animal) in the type AnimalProcessor&lt;? extends Animal&gt; is not applicable for the arguments (Animal).
  • 我想知道 - 也许泛型无​​法工作,因为解决方案不安全。例如,您可能不小心创建了一个 ReptileProcessor,它返回了 Taxonomy “mammalia”(复制和粘贴错误)。然后,您最终可能会将 Mammal 传递给 ReptileProcessor... 这将是一个运行时错误。泛型的使用可能会引发这种情况......但是,这仍然让我没有一个很好的模式来修复它。
  • 确实,你是对的。我认为我的解决方案不能正常工作。
【解决方案3】:

我建议如下:

public interface Animal {
    public String getTaxonomyClass(); 
    public void process();
}

现在每个实现 Animal 的动物类都应该实现自己的处理逻辑。 例如:

public class Bird implements Animal {

    public Bird(float wingSpan) {
        this.wingSpan = wingSpan;
    }

    @Override
    public String getTaxonomyClass() {
        return "aves";
    }

    @Override
    public void process() {
         System.out.print("Wingspan is " + wingSpan);
    }

    // Specific to birds
    private float wingspan;
}

现在你只能有一个 AnimalProcessor 处理如下:

 public void process(Animal a) {
      a.process();
 }

【讨论】:

  • 使用像 process() 这样的通用名称是好的,前提是没有 Animal 可以有两个不同的进程。例如,“鲈鱼”可以“有鳞”,“飞鱼”可以“有鳞”和“有翼展”。如果您只想了解翼展,那您就被困住了。
  • 但您想将处理与数据对象分开,所以它应该在其他类中。
  • 也许 process 不是最合适的名称,而是每个动物类型都知道如何处理自己并访问自己的属性的想法,否则无法避免强制转换
  • @giorashc,+1 我同意你的观点,我喜欢这种方法。我会改为将其称为 getDetails() ,但这只是一个挑剔的细节。这种方法可以更进一步,使用模板设计模式,但这需要将 Animal 接口更改为抽象类。
  • 过程令人困惑。 @Brady 建议的 getDetails 可能更适合这个原因。关键是一个特定的动物应该在它自己的类中封装它自己的特定属性,否则你会发现自己做了很多转换和 ifs。
【解决方案4】:

让你AnimalProcessor通用;

public interface AnimalProcessor<T extends Animal> {
    public String getSupportedTaxonomyClass();
    public void process(T a);
}

public class MammalProcessor implements AnimalProcessor<Mammal> {

    @Override
    public String getSupportedTaxonomyClass() {
        return "mammalia";
    }

    @Override
    public void process(Mammal a) {
        System.out.println("Tooth count is " + a.getToothCount());
    }

}

【讨论】:

  • 嗨,Qwerky - 有几个人建议了这种方法,而且它几乎可以工作!不幸的是,我无法弄清楚如何消除 ProcessNewAnimals 方法中的所有错误和警告。使用原始类型会产生警告(代码异味),然后,当我尝试参数化时,我总是会遇到类型冲突错误。从根本上说,这似乎是因为我们不能排除注册表在我们尝试传递哺乳动物时会返回 AnimalProcessor 的可能性。那么我们什么都没得到吗?
【解决方案5】:

所以你有这样的课程......

public abstract class Bird implements Animal {

    @Override
    public String getTaxonomyClass() {
        return "aves";
    }

    // Specific to birds
    public abstract float getWingspan();

}

所有Birds 都会有一个翼展,即使翼展是0。所以,你为什么不把这个类改成这样……

public class Bird implements Animal {

    float wingspan = 0.0f;

    public Bird(float wingspan){
        this.wingspan = wingspan;
    }

    @Override
    public String getTaxonomyClass() {
        return "aves";
    }

    // Specific to birds
    public float getWingspan(){
        return wingspan;
    }

}

所以,要创建一个新的Bird,而不是这样做...

    animals.add(new Bird() {  // condor

        @Override
        public float getWingspan() {
            return 2.9f;
        } }
    );

你会这样做......

animals.add(new Bird(2.9f)); // condor

对于您的目的,这似乎会使整个事情变得更简单和更好。你也可以为你的 Mammal 类做类似的改变。

现在,对于动物的处理...如果要处理所有Animals,您可以只在Bird 中实现process(),而不需要单独的BirdProcessor 类。为此,在Animal 中,声明一个方法public void process();。你的Bird 会这样实现它...

public void process() {
     System.out.print("Wingspan is " + getWingspan());
}

您可以更改您的AnimalProcessor 来简单地执行此操作(注意:不再是界面)...

public class AnimalProcessor {
    public void process(Animal a) {
        a.process();
    }
}

您的AnimalProcessor 类将能够处理所有Animals

或者,如果您想保留 AnimalProcessor 原样,最好更改以下内容,以避免 ClassCastException(此代码用于 BirdProcessor)...

public void process(Animal a) {
    if (a instanceof Bird){
        System.out.print("Wingspan is " + ((Bird)a).getWingspan());
    }
}

这就是你想要的吗?

【讨论】:

  • @WATTO 构造函数的方式是 IMO 的最佳方式。
【解决方案6】:

您的问题是诸如

之类的方法
   public abstract int getToothCount();

...未在 Animal 中定义。相反,它们是在 Animal 的特定子类中定义的。这意味着您不能笼统地对待动物,因为它们根本不同。

要克服这个问题,一种方法是在 Animal 类中为所有这些创建抽象方法。

Bird 可能会以“0”响应 getToothCount()。

由于所有动物都可以响应 getWingspan()、getTootCount() 等,因此您不必执行任何特定于类型的检查。如果这还不够好,请在 Animal 中创建“boolean hasWings()”、“boolean hasTeeth()”等的抽象实现。

现在你可以说,对于某些动物来说:

if (a.hasWings()) System.out.println("My wingspan is "+a.getWingSpan());

适用于任何动物。当然,Animal 的每个子类都必须实现所有不同的方法。

另一种选择是向 Animal 添加非抽象方法。这些方法将提供默认答案。例如,getWingSpan() 将返回 0,getToothCount() 将返回 0,等等。Shark 将覆盖 getToothCount()。 Eagle 会覆盖 getWingSpan()...

那么您的子类只需覆盖(甚至知道)与它们直接相关的方法。

【讨论】:

  • 对我来说,让狗对象拥有 getWingspan() 方法,或者让乌龟拥有 getToothCount(),即使它们返回 0,似乎也不是很直观也没有凝聚力。我正在考虑回答我自己,但这会有点重复@giorashc。我会将 getDetails() 放在 Animal 中,并在 Mammal、Bird、Amphibian 等中实现模板模式,从而实现 getDetails() 并调用具体类中实现的抽象方法 doGetDetails()。
  • 那么“将默认方法放入动物”替代方案对您来说会更好。诸如 getDetails() 之类的通用方法不能满足实现 getToothCount() 的要求。或者能够找到所有有牙齿的动物的列表等。这种事情可以通过 OP 的解决方案实现,用我提出的解决方案很容易完成,而 getDetails() 解决方案非常丑陋(充其量)。 getDetails() 解决方案对我来说是一种逃避。它使用文字游戏来模糊一些动物与其他动物非常不同的事实。
  • 这是一个好点,但我没有看到能够获得有牙齿的动物的任何要求。听起来有点像功能蠕变。我只是为这样的类层次结构成像 API 文档,而 getWingspan() 在 Animal 基类中真的很奇怪。我可以想象看到 hasTeeth()、hasWings() 等方法,但不是 getTeeth()。顺便说一句,OP 是谁?
  • OP = 原始海报。我支持更明确的方法名称,因为 OP 使用了这些方法。不过,没有功能蠕变——我选择的实现支持 OP 正在做的事情。由于它不会弄脏事实,因此更容易支持附加功能。
  • 有些鱼有翅膀,有些哺乳动物有翅膀,有些虫子有翅膀,有些(?所有?)鸟类有翅膀。有所有非常不同类型的动物。如果你不把 getWingSpan() 放在 Animal 中,你打算把它放在哪里?如果你不把它放在基类中,你将无法创建一个可以被一般处理的动物列表。这就是 OP 的问题/问题。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2012-04-07
  • 2021-04-21
  • 1970-01-01
  • 2010-11-04
  • 1970-01-01
  • 2015-02-18
  • 2022-01-02
相关资源
最近更新 更多