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 CreateXXX
mé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?
Respuestas:
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:
Considera esta situación.
Asamblea A (-> medios depende de):
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:
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
new
palabra clave.fuente
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
CreateXXX
métodos que solo llaman a constructores. Y especialmente no se llama Fábrica (ni Fábrica abstracta).fuente
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".
fuente
Foo
especifique que un constructor público debería ser utilizable para la creación deFoo
instancias, 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 ennew
expresiones, frente a la invocación de constructores de subtipos, pero no conozco ningún lenguaje que haga esa distinción.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 elAnimal
para alimentarlo; no me importa si es aDog
o aCat
).fuente
Feeder
podría no usar ninguno, y en su lugar llamarlo esKennel
elgetHungryAnimal
método del objeto .Builder Pattern
como válido . ¿No es así?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 ...
fuente
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.Foo
se 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 claseFoo
que depende de una claseMySet
, puede elegir una implementaciónMySet
en tiempo de compilación pero aún no puede crear instanciasFoo
usando dos implementaciones diferentes deMySet
.Esta desafortunada decisión de diseño obliga a las personas a usar
interface
s 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 veunion
en laSet
interfaz 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
interface
y luego necesita múltiples implementaciones, se quedará atrapado entre una roca y un lugar difícil.fuente
new
simplemente 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 implementeList
, debería ser posible que la interfaz le dé uno sin que el cliente tenga que saber de ninguna implementación en particular (por ejemploArrayList
).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".
fuente
int x
yIFooService fooService
. No quiere estar pasando porfooService
todas partes, por lo que crea una fábrica con un métodoCreate(int x)
e inyecta el servicio dentro de la fábrica.IFactory
todas partes en lugar de hacerloIFooService
.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.
fuente
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 decirLo 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.
fuente