El patrón Factoría Simple en C# desde un punto de vista de mantenibilidad


Publicado el sábado, 17 de junio de 2017

Factoría simple en C#


¿Qué hay de malo en utilizar “new”? 

No hay nada malo en utilizar "new", pero en el momento que lo haces estás creando un acoplamiento entre tu clase y la clase que crea la instancia. Ambas quedan ligadas para siempre.

Los patrones de factoría están diseñados para contrarrestar este efecto. Encapsulan la creación de objetos.

Hay tres patrones factoría conocidos:

  • Patrón Factoría Simple
  • Patrón Método Factoría
  • Patrón Factoría Abstracta

En este post escribiré sobre el patrón Factoría Simple y sobre dos niveles de abstracción previos a utilizar este patrón.

Crear objetos con "new" - Nivel 1

El modo más sencillo de crear un objeto en C# es utilizando la cláusula “new”. Por ejemplo, así:

var servicio = new Servicio();  

En el inicio de los proyectos suelo crear los objetos así, sin pensar si es lo más correcto o lo más mantenible. Simplemente escribo “new”.

Si todo va bien y el proyecto crece es posible que empecemos a escribir “new Servicio()” en más lugares del código.

var servicio = new Servicio();  
// …
// …
var servicioGestion = new Servicio();  
//…
//…
var servicioPrincipal = new Servicio();  
//…
//…
var servicioSecundario = new Servicio();  
//…
//… 

Cuando el número de creaciones de Servicio() empieza a ser elevado, una buena práctica es encapsular la creación del objeto en un método.

Método estático que crea un objeto. Factoría a secas. Nivel 2

Utilizando un método estático que encapsula la creación del objeto, el ejemplo anterior quedaría así:

var servicio = FactoriaServicios.CrearServicio();

No parece que entre ambos haya gran diferencia ¿para qué molestarse entonces en aplicar una factoría, perdón, método estático de creación? En este ejemplo tan sencillo no merece la pena aplicar ningún cambio.

Pero pongamos por ejemplo que el servicio depende de una cadena de conexión y de otros dos servicios:

var servicio = new Servicio(“cadena conexión”, new Servicio2(), new Servicio3());  
//…
//…
var servicioGestion = new Servicio(“cadena conexión”, new Servicio2(), new Servicio3());
//…
//…
var servicioPrincipal = new Servicio(“cadena conexión”, new Servicio2(), new Servicio3());
//…
//…
var servicioSecundario = new Servicio(“cadena conexión”, new Servicio2(), new Servicio3());
//…
//…

Ahora la factoría empieza a cobrar sentido. Si en lugar de crear el objeto y sus dependencias directamente utilizando “new” encapsulamos la creación del objeto escondiendo los detalles de su creación en un método, por ejemplo así:

public static FactoriaServicios
{
  public static Servicio CrearServicio()
  {
    return new Servicio(“cadena conexión”, new Servicio2(), new Servicio3);
  }
}

Podríamos sustituir las líneas anteriores por:

var servicio = FactoriaServicios.CrearServicio();
//…
//…
var servicioGeation = FactoriaServicios.CrearServicio();
//…
//…
var servicioPrincipal = FactoriaServicios.CrearServicio();
//…
//…
var servicioSecundario = FactoriaServicios.CrearServicio();
//…
//…

El código queda más limpio. Hemos centralizado/encapsulado la creación del objeto en un método.

Si más adelante la clase “servicio” cambia y necesita una dependencia más, por ejemplo que dependa de un cuarto servicio:

// Constructor servicio
void Servicio(
     “cadena conexión”, 
     new Servicio2(), 
     new Servicio3(), 
     new Servicio4()
);


Sólo existirá una línea de código donde realizar el cambio: en el método de la factoría.

El código cliente no se verá afectado, al contrario del caso anterior, en el que hubiéramos tenido que ir a todos los lugares del código donde se estaba creando una instancia del servicio para añadirle un parámetro más.

Aclaración:

El ejemplo de factoría (a secas) que he utilizado se trata de un método estático dentro de una clase estática, cosa que no corresponde exactamente con la definición de ninguno de los tres patrones factoría conocidos (Simple, Method y Abstract). Sin embargo, esta manera de centralizar la creación de objetos es muy útil y muy utilizada, más incluso que los patrones factoría conocidos.

Por comodidad, y porque el objetivo del método es el mismo que el del patrón factoría, en lugar de llamarlo “método estático que encapsula la creación de un objeto” se le suele llamar factoría (a secas).

Factoría Simple. Nivel 3

El patrón de Factoría Simple sirve para que código como este:

if(tipoProcesador == TipoProcesadorEnum.OrdenCobro)
{
   if (EsModoEdicion(ModoEnum.Nuevo))
   {
     GenerarFechaSiguienteCobro();
   }

   if (HaCambiadoEstadoOrdenCobro() || HaCambiadoImporteOrdenCobroPagada())
   {
      RegistrarEnHistorialCobro();
   }
}
else if(tipoProcesador == TipoProcesadorEnum.Proyecto)
{
    if (HaCambiadoEstadoProyecto() && EsEstado(EstadoEnum.Replanificado))
    {
       ReplanificarProyecto();
     }
}
else if(tipoProcesador == TipoProcesadorEnum.FormaPago)
{
     //…
}

Pueda ser sustituido por código como este:

var procesador = FactoriaProcesadores.CrearPorcesador(tipoProcesador);
procesador.Procesar();

Definición de Factoría Simple:

Clase utilizada para crear nuevas instancias de objetos, cuyo objetivo es ahorrar la lógica de creación de las nuevas instancias al código cliente.

Dicho de otro modo, los else if (lógica) quedan encapsulados y centralizados dentro de la factoría.

Ejemplo de Factoría Simple:

public static FactoriaProcesadores
{
  public static IProcesador CrearProcesador(TipoProcesadorEnum tipoProcesador)
  {	
	    if(tipoProcesador == TipoProcesadorEnum.OrdenCobro)
        {
	       return new PorcesadorOrdenCobro();
        }
        else if(tipoProcesador == TipoProcesadorEnum.Proyecto)
        {
	       return new PorcesadorProyecto();
        }
        else if(tipoProcesador == TipoProcesadorEnum.FormaPago)
        {
           return new PorcesadorFormaPago();
        }
        else if(/*...*/)
  }
}

Y cada uno de los procesadores (PorcesadorOrdenCobro, PorcesadorProyecto, etc ) generados por la factoría debe implementar la interface IProcesador:

interface IProcesador
{
  void Procesar();
}

¿Cuándo utilizar el patrón Factoría Simple?

Cuando en nuestro código aparece un "switch" o un "else-if" con numerosos casos podemos empezar a sospechar. Si el "switch/else-if" solo aparece una vez en nuestro código, lo peor que puede pasar es que vaya creciendo más de lo que nos gustaría. Pero en cuanto a mantenimiento, si la clase no es muy grande, puede pasar.

Cuando realmente es útil este patrón es cuando los "switch/else-if" se empiezan repetir en varios lugares del código. Y ¡ojo, que los "switch/else-if" suelen ser muy contagiosos! Cuantos más haya en tu código más problemas de mantenibilidad tendrás.

Lo ideal es cortar cuanto antes por los sano y crear una Factoría Simple.

Como en todos los patrones, no siempre es fácil saber cuándo hay que aplicar una Factoría Simple. No siempre que veas un "switch/else-if" hay que aplicar este patrón, pero puedes planteártelo. Si nunca lo has aplicado, te animo a hacerlo. Para dominar el patrón tendrás que crearlo varias veces y equivocarte en alguna de ellas, pero si no lo haces, tarde o temprano empezarás a aborrecer tu propio código. Si logras que las cosas se simplifiquen tu código mejorará y además será más divertido. :)

El patrón Factoría Simple también es un anti-patrón

Dicho esto, también hay que ser consciente de que las factorías simples son consideradas por algunos programadores como un anti-patrón. Esto es porque no cumplen con algunos de los principios SOLID. En concreto con la “O” Open-Closed principle. Así pues, cada vez que las utilices asegúrate de que te están ayudando más que perjudicando.

En un código base ideal, cada vez que tienes que añadir una funcionalidad no deberías modificar las clases que has creado, simplemente deberías añadir nuevas clases, o extender las existentes, y todo funcionará perfectamente.

Al utilizar una Factoría Simple, si aparece un nuevo procesador, en nuestro ejemplo, tendremos que crear la nueva clase que lo implemente. Hasta ahí sin problemas, pero además de crear la clase, tendremos que ir a la clase factoría y crear un nuevo caso en el "switch/else-if". Y ahí es cuando ropmemos con el principio "Open-Closed" puesto que estamos modificando las tripas de una clase ya existente.

De hecho, lo más habitual que te puede pasar cuando trabajas con factorías simples es que andes tan preocupado con la nueva clase que te acabes olvidando de añadir el "switch/else-if" en la facroría. Por lo menos, a mí me ha pasado varias veces.

Sinceramente, no lo veo tan grave. Como he dicho antes, si consigues sacarle partido, valdrá la pena.