Creating a factory method in Java that doesn't

2019-01-08 08:33发布

问题:

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?

回答1:

What you've done is probably the best way to go about it, until a switch on string is available.

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.

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


回答2:

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.



回答3:

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.



回答4:

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:

  • reflections-0.9.5-RC2.jar
  • google-collections-1.0.jar
  • slf4j-api-1.5.6.jar
  • nlog4j-1.2.25.jar
  • javassist-3.8.0.GA.jar
  • dom4j-1.6.jar

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^=


回答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()


回答6:

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



回答7:

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.



回答8:

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.



回答9:

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


回答10:

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