¿Clases base como fábricas?

14

Estaba escribiendo un código durante el fin de semana y me encontré con ganas de escribir una fábrica como método estático en una clase base.

Mi pregunta es simplemente saber si este es un enfoque idomático.

Mi sensación de que podría no ser proviene del hecho de que la clase base tiene conocimiento de la clase derivada.

Dicho esto, no estoy seguro de una forma más simple de obtener el mismo resultado. Toda otra clase de fábrica parece (al menos para mí) una complejidad innecesaria (?)

Algo como:

class Animal
{
  public static Animal CreateAnimal(string name)
  {
     switch(name)
     {
        case "Shark":
          return new SeaAnimal();
          break;
        case "Dog":
          return new LandAnimal();
          break;
        default:
          throw new Exception("unknown animal");
     }
  }
}
class LandAnimal : Animal
{
}

class SeaAnimal : Animal
{
}
Aaron Anodide
fuente
¿Cómo probarás tus fábricas?
con el código en esta pregunta, no lo haría. pero la respuesta me ayudó a centrarme en el camino de integrar más pruebas en mi estilo de codificación
Aaron Anodide el
Tenga en cuenta que habiendo atraído tanto el mundo marino como el mundo terrestre en su clase Animal, en general, es más difícil de manejar en un entorno aislado.

Respuestas:

13

Bueno, la ventaja de una clase de fábrica separada es que se puede burlar en pruebas unitarias.

Pero si no vas a hacer eso, o hacerlo polimórfico de otra manera, entonces un método Factory estático en la clase está bien.

pdr
fuente
gracias, ¿podrías dar un ejemplo de a qué te refieres cuando dices polimórfico de otra manera?
Aaron Anodide
Quiero decir que si alguna vez quieres poder reemplazar un método de fábrica con otro. Burlarse de él para propósitos de prueba es solo uno de los ejemplos más comunes de eso.
pdr
18

Puede usar Generics para evitar la instrucción switch y desacoplar las implementaciones posteriores de la clase base también .:

 public static T CreateAnimal<T>() where T: new, Animal
 {
    return new T();
 }

Uso:

LandAnimal rabbit = Animal.CreateAnimal();  //Type inference should should just figure out the T by the return indicated.

o

 var rabbit = Animal.CreateAnimal<LandAnimal>(); 
Nick Nieslanik
fuente
2
@PeterK. Fowler se refiere a las declaraciones de cambio en Code Smells, pero su solución es que deben extraerse a las clases Factory, en lugar de ser reemplazadas. Dicho esto, si siempre vas a conocer el tipo en el momento del desarrollo, los genéricos son una solución aún mejor. Pero si no lo hace (como implica el ejemplo original), entonces argumentaría que usar la reflexión para evitar un cambio en un método de fábrica puede ser una economía falsa.
pdr
las sentencias switch y las cadenas if-else-if son iguales. yo uso el anterior debido a la comprobación de tiempo de compilación que las fuerzas del caso por defecto a ser manejados
Aaron Anodide
44
@PeterK, en una declaración de cambio, el código está en un solo lugar. En OO completo, el mismo código puede extenderse en múltiples clases separadas. Hay un área gris donde la complejidad añadida de tener numerosos fragmentos es menos deseable que una declaración de cambio.
@Thorbjorn, he tenido ese pensamiento exacto, pero nunca tan sucintamente, gracias por ponerlo en palabras
Aaron Anodide el
5

Llevado al extremo, la fábrica también podría ser genérica.

interface IFactory<K, T> where K : IComparable
{
    T Create(K key);
}

Entonces uno puede crear cualquier tipo de fábrica de objetos, que a su vez podría crear cualquier tipo de objeto. No estoy seguro de si eso es más simple, ciertamente es más genérico.

No veo nada malo con una declaración de cambio para una implementación de fábrica pequeña. Una vez que te encuentres con una gran cantidad de objetos o posibles jerarquías de clase diferentes de objetos, creo que un enfoque más genérico es más adecuado.

Jon Raynor
fuente
1

Mala idea. Primero, viola el principio abierto-cerrado. Para cualquier animal nuevo, tendría que volver a meterse con su clase base y potencialmente lo rompería. Las dependencias irían por el camino equivocado.

Si necesita crear animales a partir de una configuración, una construcción como esta estaría bien, aunque usar una reflexión para obtener el tipo que coincida con el nombre e instanciarlo usando esa información de tipo obtenida sería una mejor opción.

Pero de todos modos, debe crear una clase de fábrica dedicada, desvinculada de la jerarquía de clases de animales, y devolver una interfaz IAnimal en lugar de un tipo base. Entonces sería útil, habrías logrado algo de desacoplamiento.

Martin Maat
fuente
0

Esta pregunta es oportuna para mí: ayer escribí casi exactamente ese código. Simplemente reemplace "Animal" con lo que sea relevante en mi proyecto, aunque me quedaré con "Animal" en aras de la discusión aquí. En lugar de una declaración de 'cambio', tuve una serie algo más compleja de declaraciones de 'si', que involucra más que solo comparar una variable con ciertos valores fijos. Pero eso es un detalle. Un método de fábrica estático parecía una forma ordenada de diseñar cosas, ya que el diseño surgió de la refactorización de desorden de código rápido y sucio.

Rechacé este diseño porque la clase base tenía conocimiento de la clase derivada. Si las clases LandAnimal y SeaAnimal son pequeñas, ordenadas y fáciles, pueden estar en el mismo archivo fuente. Pero tengo grandes métodos desordenados para leer archivos de texto que no se ajustan a ningún estándar oficialmente definido: quiero que mi clase LandAnimal esté en su propio archivo fuente.

Eso conduce a la dependencia del archivo circular: LandAnimal se deriva de Animal, pero Animal ya debe saber que existen LandAnimal, SeaAnimal y otras quince clases. Saqué el método de fábrica, lo puse en su propio archivo (en mi aplicación principal, no en mi biblioteca de animales) Tener un método de fábrica estático parecía lindo e inteligente, pero me di cuenta de que en realidad no resolvió ningún problema de diseño.

No tengo idea de cómo se relaciona esto con C # idiomático, ya que cambio mucho los idiomas, generalmente ignoro los modismos y las convenciones propios de los idiomas y las pilas de desarrolladores fuera de mi trabajo habitual. En todo caso, mi C # podría verse "pitónico" si eso es significativo. Apunto a la claridad general.

Además, no sé si sería ventajoso usar el método estático de fábrica en el caso de clases pequeñas y simples que probablemente no se extiendan en el trabajo futuro; tenerlo todo en un solo archivo fuente podría ser bueno en algunos casos.

DarenW
fuente