¿Por qué debería usar una clase de fábrica en lugar de la construcción directa de objetos?

162

He visto la historia de varios proyectos de biblioteca de clases С # y Java en GitHub y CodePlex, y veo una tendencia de cambiar a clases de fábrica en lugar de instanciación directa de objetos.

¿Por qué debería usar las clases de fábrica ampliamente? Tengo una biblioteca bastante buena, donde los objetos se crean a la antigua usanza, invocando constructores públicos de clases. En las últimas confirmaciones, los autores cambiaron rápidamente todos los constructores públicos de miles de clases a internos, y también crearon una gran clase de fábrica con miles de CreateXXXmétodos estáticos que simplemente devuelven nuevos objetos llamando a los constructores internos de las clases. La API externa del proyecto está rota, bien hecha.

¿Por qué sería útil tal cambio? ¿Cuál es el punto de refactorizar de esta manera? ¿Cuáles son los beneficios de reemplazar llamadas a constructores de clase pública con llamadas a métodos de fábrica estáticos?

¿Cuándo debo usar constructores públicos y cuándo debo usar fábricas?

rufanov
fuente
1
¿Qué es una clase / método de tela?
Doval

Respuestas:

56

Las clases de fábrica a menudo se implementan porque permiten que el proyecto siga los principios SÓLIDOS más de cerca. En particular, la segregación de interfaz y los principios de inversión de dependencia.

Las fábricas y las interfaces permiten mucha más flexibilidad a largo plazo. Permite un diseño más desacoplado, y por lo tanto más comprobable. Aquí hay una lista no exhaustiva de por qué podría seguir este camino:

  • Le permite introducir fácilmente un contenedor de Inversión de control (IoC)
  • Hace que su código sea más comprobable ya que puede simular interfaces
  • Le da mucha más flexibilidad a la hora de cambiar la aplicación (es decir, puede crear nuevas implementaciones sin cambiar el código dependiente)

Considera esta situación.

Asamblea A (-> medios depende de):

Class A -> Class B
Class A -> Class C
Class B -> Class D

Quiero mover la Clase B a la Asamblea B, que depende de la Asamblea A. Con estas dependencias concretas, tengo que mover la mayor parte de toda mi jerarquía de clases. Si uso interfaces, puedo evitar gran parte del dolor.

Asamblea A:

Class A -> Interface IB
Class A -> Interface IC
Class B -> Interface IB
Class C -> Interface IC
Class B -> Interface ID
Class D -> Interface ID

Ahora puedo mover la clase B al conjunto B sin ningún tipo de dolor. Todavía depende de las interfaces en el ensamblado A.

El uso de un contenedor IoC para resolver sus dependencias le permite aún más flexibilidad. No es necesario actualizar cada llamada al constructor cada vez que cambie las dependencias de la clase.

Seguir el principio de segregación de interfaz y el principio de inversión de dependencia nos permite crear aplicaciones desacopladas y altamente flexibles. Una vez que haya trabajado en uno de estos tipos de aplicaciones, nunca más querrá volver a usar la newpalabra clave.

Stephen
fuente
40
Las fábricas de la OMI son lo menos importante para SOLID. Puedes hacer SOLID muy bien sin fábricas.
Eufórico
26
Una cosa que nunca tuvo sentido para mí es que si usas fábricas para hacer nuevos objetos, tienes que hacer la fábrica en primer lugar. Entonces, ¿qué es exactamente lo que te atrapa? ¿Se supone que alguien más le dará la fábrica en lugar de que usted la instale por su cuenta o algo más? Esto debería mencionarse en la respuesta, de lo contrario no está claro cómo las fábricas realmente resuelven cualquier problema.
Mehrdad
8
@ BЈовић - Excepto que cada vez que agrega una nueva implementación, ahora puede abrir la fábrica y modificarla para tener en cuenta la nueva implementación, violando explícitamente OCP.
Telastyn
66
@Telastyn Sí, pero el código que usa los objetos creados no cambia. Eso es más importante que los cambios en la fábrica.
Bћовић
8
Las fábricas son útiles en ciertas áreas que aborda la respuesta. No son apropiados para usar en todas partes . Por ejemplo, usar fábricas para crear cadenas o listas lo llevaría demasiado lejos. Incluso para UDT no siempre son necesarios. La clave es usar una fábrica cuando la implementación exacta de una interfaz necesita ser desacoplada.
126

Como dijo whatsisname , creo que este es el caso del diseño de software de culto de carga . Las fábricas, especialmente las de tipo abstracto, solo se pueden usar cuando su módulo crea varias instancias de una clase y desea dar al usuario la capacidad de este módulo de especificar qué tipo crear. Este requisito es realmente bastante raro, porque la mayoría de las veces solo necesita una instancia y puede pasar esa instancia directamente en lugar de crear una fábrica explícita.

La cuestión es que las fábricas (y los singletons) son extremadamente fáciles de implementar, por lo que las personas los usan mucho, incluso en lugares donde no son necesarios. Entonces, cuando el programador piensa "¿Qué patrones de diseño debo usar en este código?" La fábrica es lo primero que le viene a la mente.

Muchas fábricas se crean porque "Quizás, algún día, tendré que crear esas clases de manera diferente" en mente. Lo cual es una clara violación de YAGNI .

Y las fábricas se vuelven obsoletas cuando introduces el marco de IoC, porque IoC es solo una especie de fábrica. Y muchos marcos de IoC pueden crear implementaciones de fábricas específicas.

Además, no hay un patrón de diseño que diga crear grandes clases estáticas con CreateXXXmétodos que solo llaman a constructores. Y especialmente no se llama Fábrica (ni Fábrica abstracta).

Eufórico
fuente
66
Estoy de acuerdo con la mayoría de sus puntos, excepto "IoC es una especie de fábrica" : los contenedores de IoC no son fábricas . Son un método conveniente para lograr la inyección de dependencia. Sí, algunos son capaces de construir fábricas automáticas, pero no son fábricas en sí mismas y no deben tratarse como tales. También afirmaría el punto YAGNI. Es útil poder sustituir un doble de prueba en su prueba unitaria. Refactorizar todo para proporcionar esto después del hecho es un dolor de cabeza . Planifique con anticipación y no caiga en la excusa de "YAGNI"
AlexFoxGill
11
@AlexG - eh ... en la práctica, casi todos los contenedores de IoC funcionan como fábricas.
Telastyn
8
@AlexG El enfoque principal de IoC es construir objetos concretos para pasar a otros objetos según la configuración / convención. Esto es lo mismo que usar la fábrica. Y no necesita la fábrica para poder crear un simulacro para la prueba. Simplemente crea una instancia y pasa el simulacro directamente. Como dije en el primer párrafo. Las fábricas solo son útiles cuando desea pasar la creación de una instancia al usuario del módulo, que llama a la fábrica.
Eufórico
55
Hay que hacer una distinción. El consumidor usa el patrón de fábrica para crear entidades durante el tiempo de ejecución de un programa. Se utiliza un contenedor de IoC para crear el gráfico de objetos del programa durante el inicio. Puede hacer esto a mano, el contenedor es solo una conveniencia. El consumidor de una fábrica no debe conocer el contenedor de IoC.
AlexFoxGill
55
Re: "Y no es necesario que la fábrica sea capaz de crear un simulacro para la prueba. Simplemente crea una instancia y pasa el simulacro directamente" , nuevamente, diferentes circunstancias. Utiliza una fábrica para solicitar una instancia : el consumidor tiene el control de esta interacción. Proporcionar una instancia a través del constructor o un método no funciona, es un escenario diferente.
AlexFoxGill
79

La moda del patrón Factory surge de una creencia casi dogmática entre los codificadores en lenguajes de "estilo C" (C / C ++, C #, Java) de que el uso de la palabra clave "nueva" es malo y debe evitarse a toda costa (o al menos centralizado). Esto, a su vez, proviene de una interpretación ultra estricta del Principio de Responsabilidad Única (la "S" de SÓLIDO) y también del Principio de Inversión de la Dependencia (la "D"). En pocas palabras, el SRP dice que, idealmente, un objeto de código debe tener una "razón para cambiar", y solo una; esa "razón para cambiar" es el propósito central de ese objeto, su "responsabilidad" en la base de código, y cualquier otra cosa que requiera un cambio en el código no debería requerir abrir ese archivo de clase. El DIP es aún más simple; un objeto de código nunca debería depender de otro objeto concreto,

Caso en cuestión, al usar "nuevo" y un constructor público, está acoplando el código de llamada a un método de construcción específico de una clase concreta específica. Su código ahora debe saber que existe una clase MyFooObject y tiene un constructor que toma una cadena y un int. Si ese constructor alguna vez necesita más información, todos los usos del constructor tienen que actualizarse para pasar esa información, incluida la que está escribiendo ahora, y por lo tanto, deben tener algo válido para transmitir, por lo que deben tener o cambiarlo para obtenerlo (agregando más responsabilidades a los objetos consumidores). Además, si MyFooObject alguna vez se reemplaza en la base de código por BetterFooObject, todos los usos de la clase anterior tienen que cambiar para construir el nuevo objeto en lugar del anterior.

Entonces, en cambio, todos los consumidores de MyFooObject deberían depender directamente de "IFooObject", que define el comportamiento de la implementación de clases, incluyendo MyFooObject. Ahora, los consumidores de IFooObjects no pueden simplemente construir un IFooObject (sin saber que una clase concreta en particular es un IFooObject, que no necesitan), por lo que deben recibir una instancia de una clase o método de implementación de IFooObject desde afuera, por otro objeto que tiene la responsabilidad de saber cómo crear el IFooObject correcto para la circunstancia, que en nuestro lenguaje generalmente se conoce como Fábrica.

Ahora, aquí es donde la teoría se encuentra con la realidad; un objeto nunca puede cerrarse a todo tipo de cambio todo el tiempo. Por ejemplo, IFooObject ahora es un objeto de código adicional en la base de código, que debe cambiar cada vez que cambie la interfaz requerida por los consumidores o las implementaciones de IFooObjects. Esto introduce un nuevo nivel de complejidad involucrado en cambiar la forma en que los objetos interactúan entre sí a través de esta abstracción. Además, los consumidores aún tendrán que cambiar, y más profundamente, si la interfaz en sí es reemplazada por una nueva.

Un buen programador sabe cómo equilibrar YAGNI ("No lo vas a necesitar") con SOLID, analizando el diseño y encontrando lugares que es muy probable que tengan que cambiar de una manera particular, y refactorizándolos para que sean más tolerantes a ese tipo de cambio, porque en ese caso "que está a necesitarlo".

KeithS
fuente
21
Me encanta esta respuesta, en especial después de leer todas las demás. Puedo agregar que casi todos los codificadores nuevos (buenos) son demasiado dogmáticos sobre los principios por el simple hecho de que realmente quieren ser buenos, pero aún no han aprendido el valor de mantener las cosas simples y no exagerar.
jean
1
Otro problema con los constructores públicos es que no hay una buena manera para que una clase Fooespecifique que un constructor público debería ser utilizable para la creación de Fooinstancias, o la creación de otros tipos dentro del mismo paquete / ensamblaje , pero no debería ser utilizable para la creación de tipos derivados en otros lugares. No conozco ninguna razón particularmente convincente para que un lenguaje / marco no pueda definir constructores separados para su uso en newexpresiones, frente a la invocación de constructores de subtipos, pero no conozco ningún lenguaje que haga esa distinción.
supercat
1
Un constructor protegido y / o interno sería tal señal; este constructor solo estaría disponible para consumir código, ya sea en una subclase o dentro del mismo ensamblaje. C # no tiene una combinación de palabras clave para "protegido e interno", lo que significa que solo los subtipos dentro del ensamblado podrían usarlo, pero MSIL tiene un identificador de alcance para ese tipo de visibilidad, por lo que es concebible que la especificación de C # se pueda ampliar para proporcionar una forma de utilizarla. . Pero, esto realmente no tiene mucho que ver con el uso de fábricas (a menos que use la restricción de visibilidad para hacer cumplir el uso de una fábrica).
KeithS
2
Respuesta perfecta. Justo en el punto con la parte "la teoría se encuentra con la realidad" Solo piense en cuántos miles de desarrolladores y el tiempo humano pasaron el culto de la carga alabando, justificando, implementando, usándolos para luego caer en la misma situación que describió, es enloquecedor. Después de YAGNI, nunca he identificado la necesidad de implementar una fábrica
Breno Salgado
Estoy programando en una base de código donde, debido a que la implementación de OOP no permite sobrecargar el constructor, el uso de constructores públicos está efectivamente prohibido. Esto ya es un problema menor en los idiomas que lo permiten. Como crear temperatura. Tanto Fahrenheit como Celsius son carrozas. Sin embargo, puede encuadrarlos y luego resolver el problema.
jgmjgm
33

Los constructores están bien cuando contienen código corto y simple.

Cuando la inicialización se convierte en algo más que asignar algunas variables a los campos, una fábrica tiene sentido. Estos son algunos de los beneficios:

  • El código largo y complicado tiene más sentido en una clase dedicada (una fábrica). Si se coloca el mismo código en un constructor que llama a un montón de métodos estáticos, esto contaminará la clase principal.

  • En algunos idiomas y en algunos casos, lanzar excepciones en los constructores es una muy mala idea , ya que puede introducir errores.

  • Cuando invoca un constructor, usted, la persona que llama, necesita saber el tipo exacto de la instancia que desea crear. Este no es siempre el caso (como a Feeder, solo necesito construir el Animalpara alimentarlo; no me importa si es a Dogo a Cat).

Arseni Mourzenko
fuente
2
Las opciones no son solo "fábrica" ​​o "constructor". A Feederpodría no usar ninguno, y en su lugar llamarlo es Kennelel getHungryAnimalmétodo del objeto .
DougM
44
+1 Me parece que adherirse a una regla de absolutamente ninguna lógica en un constructor no es una mala idea. Un constructor solo debe usarse para establecer el estado inicial del objeto asignando sus valores de argumentos a variables de instancia. Si se necesita algo más complicado, al menos cree un método de fábrica (clase) para construir la instancia.
KaptajnKold
Esta es la única respuesta satisfactoria que he visto aquí, me salvó de tener que escribir la mía. Las otras respuestas solo se refieren a conceptos abstractos.
TheCatWhisperer
Pero este argumento también puede ser válido Builder Patterncomo válido . ¿No es así?
soufrk
Regla de los constructores
Breno Salgado
10

Si trabaja con interfaces, puede permanecer independiente de la implementación real. Se puede configurar una fábrica (a través de propiedades, parámetros o algún otro método) para instanciar una de varias implementaciones diferentes.

Un ejemplo simple: desea comunicarse con un dispositivo pero no sabe si será a través de Ethernet, COM o USB. Define una interfaz y 3 implementaciones. En el tiempo de ejecución, puede seleccionar qué método desea y la fábrica le proporcionará la implementación adecuada.

Úselo a menudo ...

Pablo
fuente
55
Agregaría que es excelente usarlo cuando hay múltiples implementaciones de una interfaz y el código de llamada no sabe o no debe elegir cuál elegir. Pero cuando un método de fábrica es simplemente una envoltura estática alrededor de un solo constructor como en la pregunta, ese es el antipatrón. No tiene que haber múltiples implementaciones de la que para recoger la fábrica o se interponen en el camino y la adición de una complejidad innecesaria.
Ahora tiene la flexibilidad adicional y, en teoría, su aplicación puede usar Ethernet, COM, USB y serie, está lista para Fireloop o lo que sea, en teoría. En realidad, su aplicación solo se comunicará a través de Ethernet ... nunca.
Pieter B
7

Es un síntoma de una limitación en los sistemas de módulos de Java / C #.

En principio, no hay razón para que no pueda cambiar una implementación de una clase por otra con las mismas firmas de constructor y método. No son lenguajes que permiten esto. Sin embargo, Java y C # insisten en que cada clase tenga un identificador único (el nombre completo) y el código del cliente termina con una dependencia codificada en él.

Puede especie de evitar esto jugando con el sistema de archivos y opciones del compilador de manera que com.example.Foose asigna a un archivo diferente, pero esto es sorprendente y poco intuitivo. Incluso si lo hace, su código seguirá vinculado a una sola implementación de la clase. Es decir, si escribe una clase Fooque depende de una clase MySet, puede elegir una implementación MySeten tiempo de compilación pero aún no puede crear instancias Foousando dos implementaciones diferentes de MySet.

Esta desafortunada decisión de diseño obliga a las personas a usar interfaces innecesariamente para proteger su código en el futuro contra la posibilidad de que luego necesiten una implementación diferente de algo, o para facilitar las pruebas unitarias. Esto no siempre es factible; Si tiene algún método que observe los campos privados de dos instancias de la clase, no podrá implementarlos en una interfaz. Por eso, por ejemplo, no se ve unionen la Setinterfaz de Java . Aún así, fuera de los tipos numéricos y las colecciones, los métodos binarios no son comunes, por lo que generalmente puede salirse con la suya.

Por supuesto, si llama new Foo(...)aún tiene una dependencia de la clase , por lo que necesita una fábrica si desea que una clase pueda instanciar una interfaz directamente. Sin embargo, generalmente es una mejor idea aceptar la instancia en el constructor y dejar que otra persona decida qué implementación usar.

Depende de usted decidir si vale la pena inflar su base de código con interfaces y fábricas. Por un lado, si la clase en cuestión es interna a su base de código, la refactorización del código para que use una clase diferente o una interfaz en el futuro es trivial; podría invocar a YAGNI y refactorizar más tarde si surge la situación. Pero si la clase es parte de la API pública de una biblioteca que ha publicado, no tiene la opción de corregir el código del cliente. Si no usa interfacey luego necesita múltiples implementaciones, se quedará atrapado entre una roca y un lugar difícil.

Doval
fuente
2
Desearía que Java y derivados como .NET tuvieran una sintaxis especial para una clase que creara instancias de sí misma, y ​​de lo contrario newsimplemente hubiera sido un azúcar sintáctico para llamar a un método estático especialmente nombrado (que se generaría automáticamente si un tipo tuviera un "constructor público" "pero no incluyó explícitamente el método). En mi humilde opinión, si el código solo quiere una cosa aburrida por defecto que implemente List, debería ser posible que la interfaz le dé uno sin que el cliente tenga que saber de ninguna implementación en particular (por ejemplo ArrayList).
supercat
2

En mi opinión, solo están usando Simple Factory, que no es un patrón de diseño adecuado y no debe confundirse con Abstract Factory o el Método Factory.

Y dado que han creado una "gran clase de tela con miles de métodos estáticos CreateXXX", eso suena como un antipatrón (¿una clase de Dios tal vez?).

Creo que Simple Factory y los métodos creadores estáticos (que no requieren una clase externa) pueden ser útiles en algunos casos. Por ejemplo, cuando la construcción de un objeto requiere varios pasos, como crear instancias de otros objetos (por ejemplo, favorecer la composición).

Ni siquiera llamaría a eso una Fábrica, sino solo un montón de métodos encapsulados en alguna clase aleatoria con el sufijo "Fábrica".

FranMowinckel
fuente
1
Simple Factory tiene su lugar. Imagine que una entidad toma dos parámetros de constructor, int xy IFooService fooService. No quiere estar pasando por fooServicetodas partes, por lo que crea una fábrica con un método Create(int x)e inyecta el servicio dentro de la fábrica.
AlexFoxGill
44
@AlexG Y luego, tienes que pasar por IFactorytodas partes en lugar de hacerlo IFooService.
Eufórico el
3
Estoy de acuerdo con Euphoric; en mi experiencia, los objetos que se inyectan en un gráfico desde la parte superior tienden a ser del tipo que necesitan todos los objetos de nivel inferior, por lo que pasar IFooServices no es gran cosa. Reemplazar una abstracción con otra no logra nada más que ofuscar aún más la base de código.
KeithS
esto parece totalmente irrelevante para la pregunta formulada: "¿Por qué debería usar una clase de fábrica en lugar de la construcción directa de objetos? ¿Cuándo debo usar constructores públicos y cuándo debo usar fábricas?" Vea cómo responder
mosquito
1
Ese es solo el título de la pregunta, creo que te perdiste el resto. Ver último punto de enlace provisto;).
FranMowinckel
1

Como usuario de una biblioteca, si la biblioteca tiene métodos de fábrica, entonces debe usarlos. Asumiría que el método de fábrica le da al autor de la biblioteca la flexibilidad para hacer ciertos cambios sin afectar su código. Por ejemplo, podrían devolver una instancia de alguna subclase en un método de fábrica, que no funcionaría con un constructor simple.

Como creador de una biblioteca, usaría métodos de fábrica si desea usar esa flexibilidad usted mismo.

En el caso que describa, parece tener la impresión de que reemplazar los constructores con métodos de fábrica no tiene sentido. Ciertamente fue un dolor para todos los involucrados; una biblioteca no debería eliminar nada de su API sin una muy buena razón. Entonces, si hubiera agregado métodos de fábrica, habría dejado disponibles los constructores existentes, quizás en desuso, hasta que un método de fábrica ya no llame a ese constructor y el código que usa el constructor simple funcione menos de lo que debería. Su impresión bien podría ser correcta.

gnasher729
fuente
También tenga en cuenta; Si el desarrollador necesita proporcionar una clase derivada con funcionalidad adicional (propiedades adicionales, inicialización), el desarrollador puede hacerlo. (suponiendo que los métodos de fábrica son reemplazables). El autor de API también puede proporcionar una solución para las necesidades especiales de los clientes.
Eric Schneider
-4

Esto parece ser obsoleto en la era de Scala y la programación funcional. Una base sólida de funciones reemplaza a millones de clases.

También para tener en cuenta que el doble de Java {{ya no funciona cuando se usa una fábrica, es decir

someFunction(new someObject() {{
    setSomeParam(...);
    etc..
}})

Lo que puede permitirle crear una clase anónima y personalizarla.

En el dilema del espacio de tiempo, el factor tiempo se ha reducido tanto gracias a las CPU rápidas que la programación funcional que permite la reducción del espacio, es decir, el tamaño del código, ahora es práctico.

Bagazo
fuente
2
esto se parece más a un comentario tangencial (ver Cómo responder ), y no parece ofrecer nada sustancial sobre los puntos formulados y explicados en las respuestas anteriores 9
mosquito