Filling a zoo object with animal objects using enum in Java

Being used to the programming process in the old school, I study Java and have come to the obvious understanding that hard bit is a design, not a syntax.

I spent hours abandoning the idea after the idea of ​​how to fill out my zoo:

I have a class Zoo , as well as an abstract class Animal . There are several non-abstract subclasses of Animal called Lion , Giraffe , Zebra , Penguin , etc. 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 to which it belongs.

I would like to sort out the animals in the zoo in a certain order (as you walk along the path), and also look for animals in the zoo in the dictionary. More precisely, at some point I will parse a text file containing the names of animals (for example, for the string "LION" I want to get a unique instance of Lion ). There is a one-to-one mapping of strings for animals. I decided to use LinkedHashMap<String, Animal> .

I want to write managed code that will allow me to easily add more animals in the future. My best approach so far is as follows.

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

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

In Zoo , I also have a method that creates an object for animals:

 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 Zoo constructor, I iterate over enum and insert elements into a LinkedHashMap called animals :

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

To add a new animal, I have to

  • add a subclass to Animal ,
  • insert a new element into enum ,
  • add a case to the switch in the makeAnimal(Species species) method.

Is this a sensible and robust approach? Now, having taken the time to write this question, I am really happy with my approach;), but maybe I am missing an obvious design template, and at some point my decision will be fine. I have the feeling that there is an undesirable separation between the name "LION" and its class Lion , which is not ideal.

+8
java design design-patterns
source share
5 answers

Requirements

After reading your question, I found the following requirements of the zoo:

  • Search for animals by their species name
  • Define Species Order
  • Easy to add animals in the future

1. Search for animals by their species name

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

Strings are bad replacements for other types of values . When part of the data enters the program from a file, from the network, or from the keyboard, 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 is really textual in nature. If its numeric value, it must be converted to the corresponding numeric type, for example int , float or BigInteger . If his answer to the question is yes or no, he must be translated into boolean . More generally, if you have an appropriate type of value, whether primitive or object, you should use it; if not, you should write one. Although this advice may seem obvious, it is often broken.

Therefore, instead of looking for animals by their species, you should search for animals by their species.

2. Determine the order of species

To determine the order of views, you need to use a collection with a predictable iteration order, for example, the mentioned LinkedHashMap .

3. Easy to add animals in the future

Adding animals currently consists of the three steps you described. A side effect of using an enumeration is that only a person who has access to the source code has the ability to add new species (since the Species extension needs to be expanded).

Now let's look at Species.LION , this is a view of the Lion class. Note that this relationship semantically matches the relationship between a class and its instance. Therefore, a much more elegant solution would be to use Lion.class as a kind of Lion . It also reduces the number of steps to add an animal when you get this species for free.

Analysis of other parts of the code

Zoo

In its proposal, the zoo is responsible for the creation of animals. The consequence of this is that every zoo ever created must use all the specific animals (due to the Species enumeration) and use the specified ordering, there would be no change among the zoos. It is better to separate the creation of animals from the zoo in order to provide greater flexibility for both animals and the zoo.

Animal

Because of their flexibility, interfaces should be preferable to abstract classes. As explained in Effective Java, paragraph 18:

Clause 18. Prefers interfaces for abstract classes

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

Circular links

In your question, you mentioned that the animal should also have a link to the zoo in which it lives. This introduces a circular link that should be avoided as much as possible.

One of the many drawbacks of circular links:

Circular class references create high linkage; both classes must be recompiled every time either of them changes.

Implementation example

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

Using

 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

In the above implementation, there is support for several animal specimens for each species, since this seems to make more sense for the zoo. If only one animal is required, consider using the Guava ClassToInstanceMap instead of SetMultimap.

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

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

+8
source share

Although there may be a minor thing, care must be taken to avoid unnecessary circular references / dependencies. I don’t want to refer to the dogmatic point of view that all forms of circular addiction are bad, but depending on how you want to use your program, you might be better off not having animals dependent on the zoo, and the zoo depends on several animals at the same time.

In your example, the Zoo and Animal classes are perhaps more closely related than they should be. If you want to move the animal to another zoo, you need to update the zoo instance and the animal instance. If you want to support bi-directional navigation, you can use, for example, a bi-directional map at the zoo, as the zoo will be responsible for aggregating animals (which also does the zoo in the real world, think about it), but the zoo will also be the only place where this relationship will be saved. In my opinion, this is cleaner.

Another discussion of the circular link point can be found here: https://softwareengineering.stackexchange.com/questions/11856/whats-wrong-with-circular-references

+5
source share

Yes, your approach seems right.

However, if you have only one animal by species, perhaps you can directly create it in a static field without any specific method.

You can also add a factory class as follows:

 interface AnimalFactory<T extends Animal> { T make(); } 

and implements a factory for each type of animal:

 class LionFactory implements AnimalFactory<Lion> { public Lion make() { return new Lion(); } } 

And you can also add this factory directly to your list of enumerations:

 enum Species { LION ( new LionFactory() ), // As AnimalFactory is a functional interface, you can use lambda expressions GIRAFFE ( () -> new Giraffe() ); private AnimalFactory _factory; Species( AnimalFactory factory ) { this._factory = factory; } AnimalFactory getFactory() { return _factory; } } 

and then if you want a giraffe you should just call:

 Animal giraffe = Species.GIRAFFE.getFactory().make(); 
+4
source share

In general, your design looks good, I will touch on a few questions that you raised:

  • I propose the Animal interface, which inherits another specific class of animals - so that you can access all animals equally.
  • Define the order - here is the option:

     List<Animal> animals = new ArrayList<Animal>(); // List animals.Add(new Lion()); // Order 0 animals.Add(new Giraffe()); // Order 1 animals.Add(new Zebra()); // Order 2 

    You can replace the new ConcreteAnimal () with the method that creates the instance. Something like:

     Animal CreateAnimal(AnimalType animalType); 

    List (and ArrayList ) is a sequential list. Thus, the insertion and extraction order is guaranteed equally. In addition, you can easily add / remove the type of animals.

+3
source share

The design template you used is very similar to the Factory Design Pattern. which falls into the category of development template. These design templates deal with the creation of objects.

The method that creates makeAnimal(Species species) objects for animals makeAnimal(Species species) can be placed in a separate class, which can essentially be called something in the order of "AnimalFactory" or something like that.

Also, I think your design sounds.

+2
source share

All Articles