Fábrica estática vs fábrica como singleton

24

En algunos de mis códigos, tengo una fábrica estática similar a esta:

public class SomeFactory {

    // Static class
    private SomeFactory() {...}

    public static Foo createFoo() {...}

    public static Foo createFooerFoo() {...}
}

Durante una revisión de código, se propuso que este debería ser un singleton e inyectado. Entonces, debería verse así:

public class SomeFactory {

    public SomeFactory() {}

    public Foo createFoo() {...}

    public Foo createFooerFoo() {...}
}

Algunas cosas a destacar:

  • Ambas fábricas son apátridas.
  • La única diferencia entre los métodos son sus ámbitos (instancia frente a estático). Las implementaciones son las mismas.
  • Foo es un bean que no tiene una interfaz.

Los argumentos que tuve para volverme estático fueron:

  • La clase no tiene estado, por lo tanto, no necesita ser instanciada
  • Parece más natural poder llamar a un método estático que tener que crear una instancia de fábrica

Los argumentos para la fábrica como singleton fueron:

  • Es bueno inyectar todo
  • A pesar de la apatridia de la fábrica, las pruebas son más fáciles con la inyección (fácil de burlar)
  • Se debe burlar al probar al consumidor

Tengo algunos problemas serios con el enfoque singleton ya que parece sugerir que ningún método debería ser estático. También parece sugerir que las utilidades como StringUtilsdeberían ser envueltas e inyectadas, lo que parece una tontería. Por último, implica que tendré que burlarme de la fábrica en algún momento, lo que no parece correcto. No puedo pensar en cuándo necesitaría burlarme de la fábrica.

¿Qué piensa la comunidad? Si bien no me gusta el enfoque singleton, no parece tener un argumento terriblemente fuerte contra él.

bstempi
fuente
2
La fábrica está siendo utilizada por algo . Para probar esa cosa de forma aislada, debe proporcionarle una burla de su fábrica. Si no se burla de su fábrica, entonces un error puede causar una prueba unitaria fallida cuando no debería.
Mike
Entonces, ¿estás abogando contra las utilidades estáticas en general, o solo las fábricas estáticas?
bstempi
1
Estoy abogando contra ellos en general. En C #, por ejemplo, las clases DateTimey Fileson notoriamente difíciles de probar exactamente por las mismas razones. Si tiene una clase, por ejemplo, que establece la Createdfecha DateTime.Nowen el constructor, ¿cómo hace para crear una prueba unitaria con dos de estos objetos que se crearon con 5 minutos de diferencia? ¿Qué pasa con años de diferencia? Realmente no puedes hacerlo (sin mucho trabajo).
Mike
2
¿No debería su fábrica de singleton tener un privateconstructor y un getInstance()método? Lo siento, ¡recolector de liendres incorregible!
TMN
1
@Mike - De acuerdo: a menudo he usado una clase DateTimeProvider que devuelve trivialmente DateTime.Now. Luego puede cambiarlo por un simulacro que devuelve un tiempo predeterminado, en sus pruebas. Es un trabajo extra pasarlo a todos los constructores: el mayor dolor de cabeza es cuando los compañeros de trabajo no lo compran al 100% y deslizan referencias explícitas a DateTime.Ahora por pereza ... :)
Julia Hayward el

Respuestas:

15

¿Por qué separarías tu fábrica del tipo de objeto que crea?

public class Foo {

    // Private constructor
    private Foo(...) {...}

    public static Foo of(...) { return new Foo(...); }
}

Esto es lo que Joshua Bloch describe como su Elemento 1 en la página 5 de su libro, "Java eficaz". Java es lo suficientemente detallado sin agregar clases adicionales y singletons y demás. Hay algunos casos en los que una clase de fábrica separada tiene sentido, pero son pocos y distantes entre sí.

Parafraseando el Artículo 1 de Joshua Bloch, a diferencia de los constructores, los métodos estáticos de fábrica pueden:

  • Tener nombres que puedan describir tipos específicos de creación de objetos (Bloch usa probablePrime (int, int, Random) como ejemplo)
  • Devuelve un objeto existente (piensa: peso mosca)
  • Devuelve un subtipo de la clase original o una interfaz que implementa
  • Reduzca la verbosidad (de especificar parámetros de tipo; consulte Bloch por ejemplo)

Desventajas

  • Las clases sin un constructor público o protegido no pueden subclasificarse (pero puede usar constructores protegidos y / o Elemento 16: favorecer la composición sobre la herencia)
  • Los métodos de fábrica estáticos no se destacan de otros métodos estáticos (use un nombre estándar como of () o valueOf ())

Realmente, deben llevar el libro de Bloch a su revisor de códigos y ver si los dos pueden estar de acuerdo con el estilo de Bloch.

GlenPeterson
fuente
También he pensado en esto, y creo que me gusta. No recuerdo por qué los separé en primer lugar.
bstempi
En términos generales, estamos de acuerdo con el estilo de Bloch. En última instancia, vamos a mover nuestros métodos de fábrica a la clase que están creando instancias. La única diferencia entre nuestra solución y la suya es que el constructor seguirá siendo público para que las personas aún puedan instanciar directamente la clase. ¡Gracias por tu respuesta!
bstempi
Pero ... eso es horrible. Lo más probable es que desee una clase que implemente IFoo y que se la pase. Usar este patrón ya no es posible; tienes que codificar IFoo para que signifique Foo para obtener instancias. Si tiene IFooFactory que contiene un IFoo create (), el código del cliente no necesita conocer los detalles de implementación de qué Foo concreto se eligió como IFoo.
Wilbert
@Wilbert public static IFoo of(...) { return new Foo(...); } ¿Cuál es el problema? Bloch enumera satisfacer su preocupación como una de las ventajas de este diseño (segundo punto anterior). No debo entender tu comentario.
GlenPeterson
Para crear una instancia, este diseño espera: Foo.of (...). Sin embargo, la existencia de Foo nunca debe exponerse, solo debe conocerse IFoo (ya que Foo no es más que una implicación concreta de IFoo). Tener un método de fábrica estático en el impl concreto rompe esto y conduce a un acoplamiento entre el código del cliente y la implementación concreta.
Wilbert
5

Justo fuera de mi cabeza, estos son algunos de los problemas con la estática en Java:

  • los métodos estáticos no juegan según las "reglas" de OOP. No puede forzar a una clase a implementar métodos estáticos específicos (que yo sepa). Por lo tanto, no puede tener varias clases implementando el mismo conjunto de métodos estáticos con las mismas firmas. Entonces estás atrapado con uno de lo que sea que estés haciendo. Realmente tampoco puedes subclasificar una clase estática. Entonces no hay polimorfismo, etc.

  • Como los métodos no son objetos, los métodos estáticos no se pueden pasar y no se pueden usar (sin hacer un montón de codificación repetitiva), excepto por el código que está vinculado a ellos, lo que hace que el código del cliente sea altamente acoplado. (Para ser justos, esto no es solo culpa de la estática).

  • los campos estáticos son bastante similares a los globales


Mencionaste:

Tengo algunos problemas serios con el enfoque singleton ya que parece sugerir que ningún método debería ser estático. También parece sugerir que las utilidades como StringUtils deberían envolverse e inyectarse, lo que parece una tontería. Por último, implica que tendré que burlarme de la fábrica en algún momento, lo que no parece correcto. No puedo pensar en cuándo necesitaría burlarme de la fábrica.

Lo que me da curiosidad por preguntarte:

  • ¿Cuáles son, en su opinión, las ventajas de los métodos estáticos?
  • ¿Crees que StringUtilsestá correctamente diseñado e implementado?
  • ¿Por qué crees que nunca necesitarás burlarte de la fábrica?

fuente
Hola, gracias por la respuesta! En el orden que solicitó: (1) Las ventajas de los métodos estáticos son el azúcar sintáctico y la falta de una instancia innecesaria. Es bueno leer el código que tiene una importación estática y decir algo así como Foo f = createFoo();no tener una instancia con nombre. La instancia en sí no proporciona ninguna utilidad. (2) Creo que sí. Fue diseñado para superar el hecho de que muchos de esos métodos no existen dentro de la Stringclase. Reemplazar algo tan fundamental como Stringno es una opción, por lo que las utilidades son el camino a seguir. (continuación)
bstempi
Son fáciles de usar y no requieren que te preocupes por ninguna instancia. Se sienten naturales (3) Porque mi fábrica es muy similar a los métodos de fábrica dentro Integer. Son muy simples, tienen muy poca lógica y solo están ahí para proporcionar conveniencia (por ejemplo, no hay otra razón OO para tenerlos). La parte principal de mi argumento es que no se basan en ningún estado, no hay razón para aislarlos durante las pruebas. Las personas no se burlan StringUtilscuando hacen pruebas, ¿por qué necesitarían burlarse de esta simple fábrica de conveniencia?
bstempi
3
No se burlan StringUtilsporque hay una garantía razonable de que los métodos allí no tienen ningún efecto secundario.
Robert Harvey
@RobertHarvey Supongo que estoy haciendo el mismo reclamo sobre mi fábrica: no modifica nada. Al igual que StringUtilsdevuelve algún resultado sin alterar las entradas, mi fábrica tampoco modifica nada.
bstempi
@bstempi Iba por un poco de método socrático allí. Supongo que apesta.
0

Creo que la aplicación que está creando debe ser bastante sencilla, o de lo contrario, está relativamente temprano en su ciclo de vida. El único otro escenario en el que no podría haber tenido problemas con sus estadísticas estáticas es si ha logrado limitar las otras partes de su código que conocen su Fábrica a uno o dos lugares.

Imagine que su aplicación evoluciona y ahora, en algunos casos, necesita producir una FooBar(subclase de Foo). Ahora necesita revisar todos los lugares en su código que conocen su SomeFactoryy escribir algún tipo de lógica que mire someConditiony llame SomeFactoryo SomeOtherFactory. En realidad, mirando tu código de ejemplo, es peor que eso. Planea simplemente agregar método tras método para crear diferentes Foos y todo el código de su cliente debe verificar una condición antes de determinar qué Foo debe hacer. Lo que significa que todos los clientes necesitan tener un conocimiento íntimo de cómo funciona su Fábrica y todos deben cambiar cada vez que se necesita un nuevo Foo (¡o se lo quita!).

Ahora imagine si acaba de crear un IFooFactoryque hace un IFoo. Ahora, producir una subclase diferente o incluso una implementación completamente diferente es un juego de niños: simplemente ingresas la IFooFactory correcta más arriba en la cadena y luego, cuando el cliente la obtiene, llama gimmeAFoo()y obtendrá la información correcta porque obtuvo la fábrica correcta .

Tal vez se pregunte cómo se desarrolla esto en el código de producción. Para darle un ejemplo de la base de código en la que estoy trabajando que está comenzando a nivelarse después de poco más de un año de elaborar proyectos, tengo un montón de Fábricas que se utilizan para crear objetos de datos de Preguntas (son algo así como Modelos de Presentación ) Tengo QuestionFactoryMapun hash básicamente para buscar qué fábrica de preguntas usar cuando. Entonces, para crear una aplicación que haga diferentes preguntas, pongo diferentes fábricas en el mapa.

Las preguntas pueden ser presentadas en cualquiera de las instrucciones o ejercicios (que tanto implemento IContentBlock), por lo que cada uno InstructionsFactoryo ExerciseFactoryrecibirá una copia de la QuestionFactoryMap. Luego, las diferentes fábricas de Instrucciones y Ejercicios se entregan a la ContentBlockBuilderque nuevamente mantiene un hash para usar cuando. Y luego, cuando se carga el XML, ContentBlockBuilder llama a la Fábrica de ejercicios o instrucciones del hash y lo solicita createContent(), y luego la Fábrica delega la construcción de las preguntas a lo que sea que extraiga de su QuestionFactoryMap interno en función de qué está en cada nodo XML frente al hash. Desafortunadamente , esa cosa es un BaseQuestionlugar en lugar de unIQuestion, pero eso demuestra que incluso cuando se tiene cuidado con el diseño, puede cometer errores que pueden perseguirlo en el futuro.

Tu instinto te dice que estas estadísticas te harán daño, y ciertamente hay mucho en la literatura para apoyar tu intestino. Nunca perderá al recibir la Inyección de dependencia que termina usando solo para sus Pruebas de unidad (no es que crea que si construye un buen código no lo encontrará usando más que eso), pero las estadísticas pueden destruir completamente su habilidad para modificar rápida y fácilmente su código. Entonces, ¿por qué incluso ir allí?

Amy Blankenship
fuente
Piense por un momento en los métodos de fábrica en la Integerclase, cosas simples como la conversión de una cadena. Eso es lo que Foohace. Mi Foono está destinado a extenderse (es mi culpa por no decir esto), por lo que no tengo sus problemas polimórficos. Entiendo el valor de tener fábricas polimórficas y las he usado en el pasado ... Simplemente no creo que sean apropiadas aquí. ¿Te imaginas si tienes que crear una instancia de una fábrica para realizar las operaciones simples Integerque actualmente están integradas a través de fábricas estáticas?
bstempi
Además, sus suposiciones sobre la aplicación son bastante erróneas. La aplicación está bastante involucrada. Esto Foo, sin embargo, es mucho más simple que muchas de las clases. Es solo un simple POJO.
bstempi
Si Foo está destinado a extenderse, entonces hay una razón para que Factory sea una instancia: usted proporciona la instancia correcta de la fábrica para darle la Subclase correcta en lugar de llamar a if (this) then {makeThisFoo()} else {makeThatFoo()}través de su código. Una cosa que he notado sobre las personas con una arquitectura crónicamente mala es que son como personas con dolor crónico. En realidad, no saben lo que se siente no sentir dolor, por lo que el dolor que sienten no parece tan malo.
Amy Blankenship
Además, es posible que desee consultar este enlace sobre los problemas con los métodos estáticos (¡incluso los matemáticos!)
Amy Blankenship
He actualizado la pregunta. Usted y otra persona mencionaron extender Foo. Con razón, nunca lo declaró definitivo. Fooes un bean que no debe extenderse.
bstempi