Creating a factory method in Java that doesn't rely on if-else
What you've done is probably the best way to go about it, until a switch on string is available. (Edit 2019: A switch on string is available - use that.)
You could create factory objects and a map from strings to these. But this does get a tad verbose in current Java.
private interface AnimalFactory {
Animal create();
}
private static final Map<String,AnimalFactory> factoryMap =
Collections.unmodifiableMap(new HashMap<String,AnimalFactory>() {{
put("Meow", new AnimalFactory() { public Animal create() { return new Cat(); }});
put("Woof", new AnimalFactory() { public Animal create() { return new Dog(); }});
}});
public Animal createAnimal(String action) {
AnimalFactory factory = factoryMap.get(action);
if (factory == null) {
throw new EhException();
}
return factory.create();
}
At the time this answer was originally written, the features intended for JDK7 could make the code look as below. As it turned out, lambdas appeared in Java SE 8 and, as far as I am aware, there are no plans for map literals. (Edited 2016)
private interface AnimalFactory {
Animal create();
}
private static final Map<String,AnimalFactory> factoryMap = {
"Meow" : { -> new Cat() },
"Woof" : { -> new Dog() },
};
public Animal createAnimal(String action) {
AnimalFactory factory = factoryMap.get(action);
if (factory == null) {
throw EhException();
}
return factory.create();
}
Edit 2019: Currently this would look something like this.
import java.util.function.*;
import static java.util.Map.entry;
private static final Map<String,Supplier<Animal>> factoryMap = Map.of(
"Meow", Cat::new, // Alternatively: () -> new Cat()
"Woof", Dog::new // Note: No extra comma like arrays.
);
// For more than 10, use Map.ofEntries and Map.entry.
private static final Map<String,Supplier<Animal>> factoryMap2 = Map.ofEntries(
entry("Meow", Cat::new),
...
entry("Woof", Dog::new) // Note: No extra comma.
);
public Animal createAnimal(String action) {
Supplier<Animal> factory = factoryMap.get(action);
if (factory == null) {
throw EhException();
}
return factory.get();
}
If you want to add a parameter, you'll need to switch Supplier
to Factory
(and get
becomes apply
which also makes no sense in the context). For two parameters BiFunction
. More than two parameters, and you're back to trying to make it readable again.
There's no need for Maps with this solution. Maps are basically just a different way of doing an if/else statement anyway. Take advantage of a little reflection and it's only a few lines of code that will work for everything.
public static Animal createAnimal(String action)
{
Animal a = (Animal)Class.forName(action).newInstance();
return a;
}
You'll need to change your arguments from "Woof" and "Meow" to "Cat" and "Dog", but that should be easy enough to do. This avoids any "registration" of Strings with a class name in some map, and makes your code reusable for any future Animal you might add.
If you don't have to use Strings, you could use an enum type for the actions, and define an abstract factory method.
...
public enum Action {
MEOW {
@Override
public Animal getAnimal() {
return new Cat();
}
},
WOOF {
@Override
public Animal getAnimal() {
return new Dog();
}
};
public abstract Animal getAnimal();
}
Then you can do things like:
...
Action action = Action.MEOW;
Animal animal = action.getAnimal();
...
It's kind of funky, but it works. This way the compiler will whine if you don't define getAnimal() for every action, and you can't pass in an action that doesn't exist.