Currently I have a method that acts as a factory based on a given String. For example:
public Animal createAnimal(String action)
{
if (action.equals("Meow"))
{
return new Cat();
}
else if (action.equals("Woof"))
{
return new Dog();
}
...
etc.
}
What I want to do is avoid the entire if-else issue when the list of classes grows. I figure I need to have two methods, one that registers Strings to classes and another that returns the class based on the String of the action.
What's a nice way to do this in Java?
the abstract factory pattern,the static factory method, the simple factory (also called factory).
Factory method is a creational design pattern which solves the problem of creating product objects without specifying their concrete classes. The Factory Method defines a method, which should be used for creating objects instead of using a direct constructor call ( new operator).
A potential disadvantage of Factory methods is that clients might have to sub-class the creator class just to create a particular concrete product object. Subclassing is fine when the client has to subclass the creator class anyway, but otherwise, the client now must deal with another point of evolution.
The advantage of a Factory Method is that it can return the same instance multiple times, or can return a subclass rather than an object of that exact type.
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 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