使用Java中的枚举用动物对象填充动物园对象

Dus*_*yte 8 java design-patterns

习惯于老式的程序化C编程,我现在正在学习Java,并且可能已经明显地认识到硬件是设计而不是语法.

在想到如何填充我的动物园后,我花了几个小时的时间去了解想法:

我有一个class Zoo和一个抽象的class Animal.有几个非抽象的子类AnimalLion,Giraffe,Zebra,Penguin,等.甲Zoo对象将包含的每个子类中的恰好一个实例Animal,并且每个这样的实例包含于唯一的参考Zoo实例它属于.

我想以特定的顺序在动物园迭代动物(当你沿着一条小径行走时),以及在动物园里用字典查找动物.更确切地说,在某些时候,我将解析包含动物名称的文本文件(例如,对于字符串"LION",我想获得唯一的Lion实例).字符串与动物的一对一映射.我已经决定使用了LinkedHashMap<String, Animal>.

我想编写可管理的代码,以便将来轻松添加更多动物.到目前为止,我最好的方法如下.

Zoo类中,我定义了一个enum反映我想要的动物的顺序,它的元素完全对应于我将在文本文件中解析的字符串.

private enum Species { LION, GIRAFFE, ZEBRA, PENGUIN };
Run Code Online (Sandbox Code Playgroud)

Zoo我也有创建的动物对象的方法:

private Animal makeAnimal(Species species)
{
    switch (species)
    {
        case LION:
            // create a Lion object;
            break;
        case GIRAFFE:
            // ...
    }
    // return the Animal object created above;
}
Run Code Online (Sandbox Code Playgroud)

作为Zoo我的构造函数的一部分迭代enum并将元素插入到被LinkedHashMap调用的animals:

for (Species species : Species.values())
    animals.put(species.name(), makeAnimal(species));
Run Code Online (Sandbox Code Playgroud)

要添加一种新的动物,我必须这样做

  1. 添加一个子类Animal,
  2. 坚持一个新的元素enum,
  3. 在案例中的switch语句中添加一个案例makeAnimal(Species species).

这是一个健全和理智的方法吗?现在花了很多时间把这个问题写下来之后,我实际上对我的方法很满意;),但也许我错过了一个明显的设计模式,我的解决方案将在某些时候回击.我觉得名称"LION"和它之间存在不合理的分离class Lion并不理想.

rin*_*nde 8

要求

阅读你的问题我发现了Zoo的以下要求:

  1. 按物种名称查找动物
  2. 定义物种的顺序
  3. 将来轻松添加动物

1.按物种名称查找动物

正如您所提到的,字符串"LION"和类之间存在不合需要的分离Lion.在Effective Java,Item 50中,以下是关于字符串的说明:

字符串是其他值类型的不良替代品.当一段数据从文件,网络或键盘输入进入程序时,它通常是字符串形式.有一种自然倾向,就是这样,但只有当数据本质上是文本性的时候,这种趋势才是合理的.如果它的数字,应该翻译成相应的数字类型,如int,floatBigInteger.如果这是一个是或否的问题的答案,它应该被翻译成一个boolean.更一般地说,如果存在适当的值类型,无论是原始值还是对象引用,都应该使用它; 如果没有,你应该写一个.虽然这个建议似乎很明显,但它经常被违反.

因此,不应按物种名称查找动物,而应根据物种实例查找动物.

2.定义物种的顺序

要定义物种的排序,您需要使用具有可预测迭代顺序的集合,例如上面提到的LinkedHashMap.

3.将来容易添加动物

目前添加动物包括您提到的三个步骤.使用枚举的一个副作用是只有访问源代码的人才有能力添加新物种(因为Species枚举必须扩展).

现在考虑一下,Species.LION这就是Lion班级的种类.请注意,此关系在语义上与类及其实例化之间的关系相同.因此,更优雅的解决方案是Lion.class用作物种Lion.当您免费获得物种时,这也减少了添加动物的步骤数.

分析代码的其他部分

动物园

在你的建议中,动物园有责任创造动物.结果是每个动物园都需要使用所有定义的动物(因为Species枚举)使用指定的顺序,动物园之间不会有变化.最好将动物的动物与动物园分开,以使动物和动物园更具灵活性.

动物

由于它们的灵活性,接口应该优于抽象类.正如Effective Java,第18项中所述:

项目18.首选接口到抽象类

Java编程语言提供了两种机制来定义允许多个实现的类型:接口和抽象类.两种机制之间最明显的区别是允许抽象类包含某些方法的实现,而接口则不允许.更重要的区别是,要实现抽象类定义的类型,类必须是抽象类的子类.任何定义所有必需方法并遵守常规合同的类都允许实现接口,无论类位于类层次结构中的何处.因为Java只允许单继承,所以对抽象类的这种限制严重限制了它们作为类型定义的使用.

循环参考

在你的问题中,你提到动物也应该参考它所在的动物园.这引入了一个循环引用,应该尽可能避免.

循环引用的许多缺点之一是:

循环类引用创建高耦合; 每次更改其中任何一个类时,都必须重新编译这两个类.

示例实现

public interface Animal {}

public class Zoo {
  private final SetMultimap<Class<? extends Animal>, Animal> animals;

  public Zoo() {
    animals = LinkedHashMultimap.create();
  }

  public void addAnimal(Animal animal) {
    animals.put(animal.getClass(), animal);
  }

  @SuppressWarnings("unchecked") // the cast is safe
  public <T extends Animal> Set<T> getAnimals(Class<T> species) {
    return (Set<T>) animals.get(species);
  }
}
Run Code Online (Sandbox Code Playgroud)

用法

static class Lion implements Animal {}

static class Zebra implements Animal {}

final Zoo zoo = new Zoo();
zoo.addAnimal(new Lion());
zoo.addAnimal(new Zebra());
zoo.addAnimal(new Lion());

zoo.getAnimals(Lion.class); // returns two lion instances

zoo.getSpeciesOrdering(); // returns [Lion.class, Zebra.class]
Run Code Online (Sandbox Code Playgroud)

讨论

上面的实现支持每个物种的多个动物实例,因为这似乎对动物园更有意义.如果只需要一只动物,可以考虑使用Guava的ClassToInstanceMap而不是SetMultimap.

动物的创造不被视为设计问题的一部分.如果需要构建更复杂的动物,请考虑使用构建器模式.

添加动物现在就像创建一个实现Animal界面并将其添加到动物园的新类一样简单.


小智 5

虽然可能是一件小事,但应该注意避免不必要的循环引用/依赖.我不想援引认为,所有形式的循环依赖是不好的教条式的点,但是这取决于你想如何使用你的程序,你可能没有动物更好地依靠动物园和动物园依赖多个动物同时出现.

在您的示例中,Zoo和Animal类可能比它们必须更紧密耦合.如果要将动物重新定位到另一个动物园,则必须同时更新动物园实例和动物实例.如果你想保持双向通航,可以使用例如,在动物园一个双向映射,作为动物园将有聚集动物的责任(这也是什么动物园确实在现实世界中,想起来吧),但动物园会再也是保持这种关系的唯一地方.在我看来,这更清洁.

关于循环引用的另一个讨论可以在这里找到:https: //softwareengineering.stackexchange.com/questions/11856/whats-wrong-with-circular-references