Crear un método de fábrica en Java que no dependa de if-else

Actualmente tengo un método que actúa como una fábrica basada en una Cadena dada. Por ejemplo:

public Animal createAnimal(String action) { if (action.equals("Meow")) { return new Cat(); } else if (action.equals("Woof")) { return new Dog(); } ... etc. } 

Lo que quiero hacer es evitar todo el problema if-else cuando la lista de clases crezca. Me imagino que necesito tener dos métodos, uno que registre cadenas en las clases y otro que devuelva la clase en función de la cadena de la acción.

¿Cuál es una buena manera de hacer esto en Java?

Lo que has hecho es probablemente la mejor manera de hacerlo, hasta que esté disponible un interruptor de cadena.

Puede crear objetos de fábrica y un mapa de cadenas a estos. Pero esto tiene un poco de verbosa en Java actual.

 private interface AnimalFactory { Animal create(); } private static final Map factoryMap = Collections.unmodifiableMap(new HashMap() {{ 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(); } 

En el momento en que se escribió originalmente esta respuesta, las características destinadas para JDK7 podrían hacer que el código se vea como a continuación. Al final resultó que, lambdas apareció en Java SE 8 y, hasta donde yo sé, no hay planes para literales de mapas.

 private interface AnimalFactory { Animal create(); } private static final Map 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(); } 

No hay necesidad de Maps con esta solución. Los mapas son básicamente una forma diferente de hacer una statement if / else de todos modos. Aproveche una pequeña reflexión y son solo unas pocas líneas de código las que funcionarán para todo.

 public static Animal createAnimal(String action) { Animal a = (Animal)Class.forName(action).newInstance(); return a; } 

Tendrás que cambiar tus argumentos de “Woof” y “Miau” a “Gato” y “Perro”, pero eso debería ser lo suficientemente fácil de hacer. Esto evita cualquier “registro” de cadenas con un nombre de clase en algún mapa, y hace que su código sea reutilizable para cualquier Animal futuro que pueda agregar.

Si no tiene que usar cadenas, puede usar un tipo de enumeración para las acciones y definir un método abstracto de fábrica.

 ... public enum Action { MEOW { @Override public Animal getAnimal() { return new Cat(); } }, WOOF { @Override public Animal getAnimal() { return new Dog(); } }; public abstract Animal getAnimal(); } 

Entonces puedes hacer cosas como:

 ... Action action = Action.MEOW; Animal animal = action.getAnimal(); ... 

Es un poco raro, pero funciona. De esta forma, el comstackdor silbará si no define getAnimal () para cada acción, y no puede pasar una acción que no existe.

Use Scannotations!

Paso 1. Crea una anotación como la siguiente:

 package animal; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @Retention(RetentionPolicy.RUNTIME) public @interface AniMake { String action(); } 

Tenga en cuenta que RetentionPolicy es el tiempo de ejecución, vamos a acceder a esto a través de la reflexión.

Paso 2. (Opcional) Crea una superclase común:

 package animal; public abstract class Animal { public abstract String greet(); } 

Paso 3. crea las subclases con tu nueva anotación:

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

Paso 4. Crea la fábrica:

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

Esta fábrica, se puede limpiar un poco, por ejemplo, lidiar con las desagradables excepciones comprobadas, etc.
En esta fábrica, he usado la biblioteca Reflections .
Hice esto de la manera difícil, es decir, no hice un proyecto de maven y tuve que agregar las dependencias manualmente.
Las dependencias son:

  • 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

Si se salta el Paso 2, entonces deberá cambiar el método de fábrica para devolver el Objeto.
A partir de este momento, puedes seguir agregando subclases, y siempre que las anotes con AniMake (o cualquier nombre mejor que encuentres), y las coloques en el paquete definido en el constructor Reflections (en este caso, “animal”), y deje visible el constructor no args por defecto, luego la fábrica instanciará sus clases por usted sin tener que cambiarse.

Aquí está el resultado:

 log4j:WARN No appenders could be found for logger (org.reflections.Reflections). log4j:WARN Please initialize the log4j system properly. *WOOF!* =^meow^= 

No lo he intentado, pero podría crear un Map con “Meow”, etc. como claves y (por ejemplo) Cat.class como valor.

Proporcione una generación de instancias estáticas a través de una interfaz y llame al

 Animal classes.get("Meow").getInstance() 

Buscaría recuperar una representación Enum de la cadena y activar eso.

Mi pensamiento sería de alguna manera mapear una Cadena para una función. De esa forma, puede pasar Meow al mapa y devolver la función de constructor que ya estaba mapeada. No estoy seguro de cómo hacer esto en Java, pero una búsqueda rápida devolvió este hilo SO . Sin embargo, alguien más puede tener una mejor idea.

Ya seleccionó la respuesta a esa pregunta, pero eso aún podría ayudar.

Aunque soy desarrollador .NET / C #, este es realmente un problema general de progtwigción orientada a objetos. He corrido en el mismo tipo de problema y he encontrado una buena solución (creo) usando un contenedor IoC .

Si aún no usa uno, esa es probablemente una buena razón para comenzar. No conozco los contenedores de IoC en Java, pero supongo que debe haber uno con características similares.

Lo que tenía era una fábrica que contiene una referencia al contenedor IoC, que es resuelto por el propio contenedor (en el BootStrapper)

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

Luego puede configurar su contenedor IoC para resolver los tipos correctos basados ​​en una clave (el sonido en su ejemplo). Resumiría completamente las clases concretas que su fábrica necesita devolver.

al final, su método de fábrica se reduce a esto:

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

Esta pregunta de stackoverflow ilustra el mismo tipo de problema con elementos del mundo real y la respuesta validada muestra un borrador de mi solución (pseudo código). Más tarde escribí una publicación de blog con los códigos reales donde está mucho más claro.

Espero que esto pueda ayudar. Pero podría ser excesivo en casos simples. Lo usé porque tenía 3 niveles de dependencias para resolver y un contenedor IoC que ya ensamblaba todos mis componentes.

¿Y qué piensa la gente acerca de usar Class.newInstance () dentro de la respuesta de Tom Hawtin? Esto evitará que almacenemos clases anónimas innecesarias en la memoria? El código Plus será más limpio.

Se verá algo como esto:

 private static final Map factoryMap = Collections.unmodifiableMap(new HashMap() {{ put("Meow", Cat.class); put("Woof", Dog.class); }}); public Animal createAnimal(String action) { return (Animal) factoryMap.get(action).newInstance(); } 

Ahora podría usar referencias de constructor de Java 8 y una interfaz funcional.

 import java.util.HashMap; import java.util.Map; import java.util.function.Supplier; public class AnimalFactory { static final Map> 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 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"; } }