Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Populating a zoo object with animal objects using enum in Java

Being used to old-school procedural C programming I am now learning Java and has come to the perhaps obvious insight that the hard bit is design and not syntax.

I have spent hours scrapping idea after idea of how to populate my zoo:

I have a class Zoo and and an abstract class Animal. There are several non-abstract subclasses of Animal called Lion, Giraffe, Zebra, Penguin, and so on. A Zoo object will contain exactly one instance of each subclass of Animal, and each such instance contains a reference to the unique Zoo instance it belongs to.

I would like to iterate over the animals at the zoo in a specific order (as you walk along a footpath say) as well as look up animals at the zoo in a dictionary. More precisely, at some point I am going to parse a text file containing animal names (e.g. for the string "LION" I want to get the unique Lion instance). There is a one-to-one mapping of strings to animals. I have settled on using LinkedHashMap<String, Animal>.

I want to write manageable code that enables me to easily add more animals in future. My best approach so far is as follows.

In the Zoo class I define an enum that reflects the ordering of the animals that I want, and its elements correspond exactly to the strings I will parse in the text file.

private enum Species { LION, GIRAFFE, ZEBRA, PENGUIN };

In Zoo I also have a method that creates an animal object:

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

As part of the constructor of Zoo I iterate over the enum and insert elements into the LinkedHashMap called animals:

for (Species species : Species.values())
    animals.put(species.name(), makeAnimal(species));

To add a new animal I have to

  1. add a subclass to Animal,
  2. stick a new element into the enum,
  3. add a case to the switch statement in the makeAnimal(Species species) method.

Is this a sound and sane approach? Now after taking the time to write this question down I am actually rather happy with my approach ;), but perhaps I am missing an obvious design pattern and my solution will back-fire at some point. I have the feeling that there is an undesirable separation between the name "LION" and its class Lion that is not ideal.

like image 733
DustByte Avatar asked Jan 14 '16 15:01

DustByte


1 Answers

Requirements

Reading your question I found the following requirements of Zoo:

  1. Lookup animals by their species name
  2. Define order of species
  3. Easily add animals in the future

1. Lookup animals by their species name

As you mentioned there is an undesirable separation between the string "LION" and the class Lion. In Effective Java, Item 50, the following is stated about strings:

Strings are poor substitutes for other value types. When a piece of data comes into a program from a file, from the network, or from keyboard input, it is often in string form. There is a natural tendency to leave it that way, but this tendency is justified only if the data really is textual in nature. If it’s numeric, it should be translated into the appropriate numeric type, such as int, float, or BigInteger. If it’s the answer to a yes-or-no question, it should be translated into a boolean. More generally, if there’s an appropriate value type, whether primitive or object reference, you should use it; if there isn’t, you should write one. While this advice may seem obvious, it is often violated.

Therefore, instead of looking up animals by their species name you should lookup animals by their species instance.

2. Define order of species

To define the ordering of species you need to use a collection with a predictable iteration order, such as the mentioned LinkedHashMap.

3. Easily add animals in the future

Adding animals currently consists of the three steps you mentioned. A side effect of the usage of an enum is that only a person with access to the source code has the capability to add new species (as the Species enum has to be extended).

Now consider, Species.LION, this is the species of the Lion class. Note that this relationship is semantically the same as the relation between a class and its instantiation. Therefore, a much more elegant solution would be to use Lion.class as the species of Lion. This also reduces the number of steps for adding an animal as you get the species for free.

Analysis of other parts of the code

Zoo

In your proposal, the zoo has the responsibility to create animals. The consequence is that every zoo ever created needs to use all defined animals (because of the Species enum) and use the specified ordering, there would be no variation among zoo's. It is better to decouple the creation of animals from the zoo in order to allow more flexibility of both the animals and the zoo.

Animal

Because of their flexibility, interfaces should be preferred over abstract classes. As explained in Effective Java, Item 18:

Item 18. Prefer interfaces to abstract classes

The Java programming language provides two mechanisms for defining a type that permits multiple implementations: interfaces and abstract classes. The most obvious difference between the two mechanisms is that abstract classes are permitted to contain implementations for some methods while interfaces are not. A more important difference is that to implement the type defined by an abstract class, a class must be a subclass of the abstract class. Any class that defines all of the required methods and obeys the general contract is permitted to implement an interface, regardless of where the class resides in the class hierarchy. Because Java permits only single inheritance, this restriction on abstract classes severely constrains their use as type definitions.

Circular references

In your question you mention that an animal should also have a reference to the zoo in which it resides. This introduces a circular reference which should be avoided as much as possible.

One of many disadvantages of circular references is:

Circular class references create high coupling; both classes must be recompiled every time either of them is changed.

Example implementation

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);
  }
}

Usage

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]

Discussion

The implementation above has support for multiple animal instances per species, as that seems to make more sense for a zoo. If only one animal is needed consider using Guava's ClassToInstanceMap instead of the SetMultimap.

The creation of the animals was not considered as part of the design problem. If more complex animals need to be constructed, consider using the builder pattern.

Adding an animal is now as simple as creating a new class that implements the Animal interface and adding it to the zoo.

like image 137
rinde Avatar answered Oct 24 '22 19:10

rinde