Creating a factory method in Java that doesn't rely on if-else
Asked Answered
H

10

45

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?

Herb answered 8/8, 2010 at 13:28 Comment(6)
Use a HashMap :)Hindorff
Okay, but what should the value be? It could be the equivalent object. But say the method is called multiple times, requiring a new object on each call. Would I need to reinstantiate the object after returning it?Herb
You can have a HashMap<String, Callable> where the Callable returns an object of the type indicated by String. Then populate the map in the factory constructor.Verjuice
Map<String,Class> is good enough. Don't go architecture-crazy guys.Unflinching
Use enum instead of your action strings.Swound
it would be awesome if we could create enums dynamically e.g in the factory constructor you get your types form the database and create the enums like this so if you need new type of something added just add new row to db and no need to make changes to the code, but unfortunately it's not possibleTegantegmen
P
63

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.

Polyhistor answered 8/8, 2010 at 13:39 Comment(10)
Whats wrong just using a Callable instead of a new AnimalFactory interface?Verjuice
Callable throws. It's also rather non-nominative.Polyhistor
Argh, double-brace initialization... (you also have 2x a typo: ` facotryMap`)Schoen
@Schoen Was going to write the map initialisation in the old way, but I liked the double braces better. (Some typos fixed.)Polyhistor
"In JDK7 this may look something like..." You should update this for Java 8. ; )Slapstick
@Slapstick 8? Nobody seems to have gone near this question since 2010. Concise anonymous inner classes were in 7. Who knows when map literals will be in (presumably same time as selfless/value types).Polyhistor
Well it has 7,000 views. I came here from https://mcmap.net/q/49377/-the-best-way-to-implement-factory-without-if-switch/2891664 (Java 8 version would be lambdas.)Slapstick
The second example doesn't compile in Java 8 either so I suggest you delete it. Thought I was missing some fancy new feature! Just because nobody has commented doesn't mean people aren't seeing it - it came top in the google search I did for mapping strings to classes.Esdras
Does this work if your Animal subclass constructors (eg: Cat) take parameters?Allveta
@Allveta Sure, though createAnimal will need all the parameters even if you're creating a Dog. Add the necessary parameters to createAnimal and create (including all implementations).Polyhistor
H
14

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.

Hewes answered 8/8, 2010 at 15:12 Comment(2)
Generally, Class.newInstance() should be avoided (due to its poor exception handling).Inwardness
I got asked this in an interview and they refused to let me use reflection. I settled on HashMap as above, but wanted a more dynamic solution that didn't require a source change. Couldn't come up with a solution.Tsar
A
12

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.

Analgesia answered 8/8, 2010 at 20:37 Comment(1)
Cool solution, I'll have to give this a try.Thomsen
S
10

Use Scannotations!

Step 1. Create an annotation like below:

package animal;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

@Retention(RetentionPolicy.RUNTIME)
public @interface AniMake {
    String action();
}

Note that the RetentionPolicy is runtime, we'll be accessing this via reflection.

Step 2. (Optional) Create a common super class:

package animal;

public abstract class Animal {

    public abstract String greet();

}

Step 3. create the subclasses with your new annotation:

package animal;

@AniMake(action="Meow")
public class Cat extends Animal {

    @Override
    public String greet() {
        return "=^meow^=";
    }

}
////////////////////////////////////////////
package animal;

@AniMake(action="Woof")
public class Dog extends Animal {

    @Override
    public String greet() {
        return "*WOOF!*";
    }

}

Step 4. Create the factory:

package animal;

import java.util.Set;

import org.reflections.Reflections;

public class AnimalFactory {

    public Animal createAnimal(String action) throws InstantiationException, IllegalAccessException {
        Animal animal = null;
        Reflections reflections = new Reflections("animal");
        Set<Class<?>> annotated = reflections.getTypesAnnotatedWith(AniMake.class);

        for (Class<?> clazz : annotated) {
            AniMake annoMake = clazz.getAnnotation(AniMake.class);
            if (action.equals(annoMake.action())) {
                animal = (Animal) clazz.newInstance();
            }
        }

        return animal;
    }

    /**
     * @param args
     * @throws IllegalAccessException 
     * @throws InstantiationException 
     */
    public static void main(String[] args) throws InstantiationException, IllegalAccessException {
        AnimalFactory factory = new AnimalFactory();
        Animal dog = factory.createAnimal("Woof");
        System.out.println(dog.greet());
        Animal cat = factory.createAnimal("Meow");
        System.out.println(cat.greet());
    }

}

This factory, can be cleaned up a bit e.g. deal with the nasty checked exceptions etc.
In this factory, I've used the Reflections library.
I did this the hard way, i.e. I didn't make a maven project and I had to add the dependencies manually.
The dependencies are:

If you skipped Step 2, then you'll need to change the factory method to return Object.
From this point on you can keep adding subclasses, and as long as you annotating them with AniMake (or whatever better name you come up with), and place them in the package defined in the Reflections constructor (in this case "animal"), and leave the default no-args constructor visible, then the factory will instantiate your classes for you without having to be changed itself.

Here's the output:

log4j:WARN No appenders could be found for logger (org.reflections.Reflections).
log4j:WARN Please initialize the log4j system properly.
*WOOF!*
=^meow^=
Scaliger answered 8/8, 2010 at 14:52 Comment(7)
Overengineered, slow, and in rather poor taste (at least the last is subjective, of course). Even in case the original problem was bigger to warrant something more sophisticated, applying a more general solution like Guice would be preferable.Inwardness
@Dimitris, your synopsis is completely unfounded! 1) A single reusable Annotation class is preferable to having to edit a map every time you add a class, you might as well leave the if else structure in the factory if you're going to change it anyway. You could eliminate the libraries and do the reflection yourself, but I don't like to reinvent the wheel. 2) You need to quantify slow ... statements like "Reflection is slow" live in the same era as "Java is slow". 3) Even if you use Guice, you still need to somehow map the classes to an arbitrary keyword and provide a factory.Scaliger
Though reflection is slow compared to a virtual method call (well, try it), I was referring to classpath scanning, which can be exceptionally slow (you do realize that this has to search all jars of the classpath, and parse part of the bytecode of all classes inside them - extra points if the jars are not even at the local filesystem...)Inwardness
Ironically, this trick would incur, just by the dependencies you used, the cost of decompressing and parsing ~1200 classes. Let alone introducing new, silent kind of classpath-dependent bugs. Now compare all of this with the simplicity, reliability and the efficiency of some of the other answers here. Oh well, tasteful indeed! :PInwardness
@Dimitris, thanks for quantifying - you raise some valuable points. Although, I should point out that these haven't been left unattended by the makers of the Reflections library. They do provide means of scanning at compile time and referencing the stored results at runtime.Scaliger
Was this answer a joke? I understand how this might be useful if you had 100's of types of animals, but otherwise it seems to represent everything that people complain about java and over-engineering.Raskind
What a really really bad way to do a simple thingSmetana
R
5

I haven't tried this, but could with create a Map with "Meow", etc as keys and (say) Cat.class as value.

Provide a static instance generation via an interface and call as

Animal classes.get("Meow").getInstance()
Rollback answered 8/8, 2010 at 13:37 Comment(0)
E
4

I'd look to retrieve an Enum representation of the String and switch on that.

Executory answered 8/8, 2010 at 15:26 Comment(3)
You'd still need to have the same if-else somewhere to get the enum from the input string, no? So then, how would that be any better?Swedenborgian
Holy 11 years ago. I seem to recall I was thinking Enum.valueOf() if that suits, or a map of lookup values, etc. Switching on strings has arrived since this was posted.Executory
Is switching on strings more efficient than an if-else type comparison?Swedenborgian
Y
2

You already selected the answer to that question, but that could still help.

Although I am a .NET/C# developer, this is really a general OOP problem. I have run in the same kind of problem and I have found a nice solution (I think) using an IoC Container.

If you don't use one yet, that is probably a good reason to start. I don't know IoC containers in Java, but I assume there must be one with similar features.

What I had was a Factory that contains a reference to the IoC container, which is resolved by the container itself (in the BootStrapper)

...
public AnimalFactory(IContainer container) 
{ 
    _container = container; 
}

You can then setup your IoC container to resolve the correct types based on a key (the sound in your example). It would abstracts completely the concrete classes that your factory needs to return.

in the end, your factory method is shrunk down to this :

...
public Createable CreateAnimal(string action) 
{ 
    return _container.Resolve<Createable>(action); 
} 

This stackoverflow question illustrates the same kind of problem with real world elements and the validated answer shows a draft of my solution (pseudo code). I later wrote a blog post with the real pieces of code where it is much clearer.

Hope this can help. But it might be overkill in simple cases. I used that because I had 3 levels of dependencies to resolve, and an IoC container already assembling all my components.

Yirinec answered 8/8, 2010 at 15:48 Comment(0)
M
1

My thought would be to somehow map a String to a function. That way you can pass Meow to the map and return the constructor function that was already mapped out. I'm not sure how to do this in Java, but a quick search returned this SO thread. Someone else may have a better idea, though.

Mcgee answered 8/8, 2010 at 13:36 Comment(0)
D
1

And what do people think about using Class.newInstance() inside Tom Hawtin's answer? This will avoid us from storing unnecessary anonymous classes in memory? Plus code will be more clean.

It will look something like this:

private static final Map<String,Class> factoryMap =
    Collections.unmodifiableMap(new HashMap<String,Class>() {{
        put("Meow", Cat.class);
        put("Woof", Dog.class);
}});

public Animal createAnimal(String action) {
    return (Animal) factoryMap.get(action).newInstance();
}
Damiandamiani answered 3/4, 2015 at 1:25 Comment(0)
D
0

Now you could use Java 8 constructor references and a functional interface.

import java.util.HashMap;
import java.util.Map;
import java.util.function.Supplier;

public class AnimalFactory {
    static final Map<String, Supplier<Animal>> constructorRefMap = new HashMap<>();

    public static void main(String[] args) {
        register("Meow", Cat::new);
        register("Woof", Dog::new);

        Animal a = createAnimal("Meow");
        System.out.println(a.whatAmI());
    }

    public static void register(String action, Supplier<Animal> constructorRef) {
        constructorRefMap.put(action, constructorRef);
    }

    public static Animal createAnimal(String action) {
        return constructorRefMap.get(action).get();
    }
}

interface Animal {
    public String whatAmI();
}

class Dog implements Animal {
    @Override
    public String whatAmI() {
        return "I'm a dog";
    }
}

class Cat implements Animal {
    @Override
    public String whatAmI() {
        return "I'm a cat";
    }
}
Durstin answered 5/9, 2018 at 0:29 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.