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
Animal
,enum
,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.
Reading your question I found the following requirements of Zoo:
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
, orBigInteger
. If it’s the answer to a yes-or-no question, it should be translated into aboolean
. 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.
To define the ordering of species you need to use a collection with a predictable iteration order, such as the mentioned LinkedHashMap
.
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.
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.
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.
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.
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);
}
}
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]
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With