lunes, 16 de diciembre de 2013

Patrones de creación: Abstract Factory

Este post corresponde a la serie Patrones de diseñoPatrones de creación

Abstract Factory

Oficialmente el patrón Abstract Factory: "Provee una interfaz para crear familias de objetos dependientes o relacionados sin conocer sus clases concretas".

Como ya mencioné en el post de introducción de la serie "Patrones de diseño" un patrón es una guía para solucionar un problema. Así que lo primero será definir el problema para el que aplicar este patrón será útil.

Vamos a empezar por analizar la definición del patrón. Lo primero que me pregunto es ¿por qué querríamos crear una familia de objetos? Y después, ¿cómo es posible que no conozca sus clases concretas? Si voy a crear una familia de objetos necesitaré conocer que tipos de objetos voy a crear ¿no? Vamos a aclarar estas dudas planteando el problema que queremos resolver.



El problema

Estamos implementando una aplicación para una tienda de venta de equipos informáticos. La tienda configura presupuestos para dos tipos de ordenadores. La opción A es un equipo de bajo precio y la opción B es un equipo de alto rendimiento. Vamos a crear un módulo que muestre los componentes de estos equipos. 

Una primera aproximación podría ser la siguiente:
public class Celeron
{
    public Celeron()
    {
        Name = "Intel Celeron 2.60GHz";
    }

    public string Name { get; set; }
}
public class Xeon
{
    public Xeon()
    {
        Name = "Intel Xeon Phi 5110P";
    }


    public string Name { get; set; }
}


public class LowCostRam
{
    public LowCostRam()
    {
        GB = 4;
    }

    public int GB { get; set; }
}

public class HighPerformanceRam
{
    public HighPerformanceRam()
    {
        GB = 32;
    }

    public int GB { get; set; }
}


class Program
{
    static void Main(string[] args)
    {
        string computerType = args.GetUpperBound(0) >= 0 ? args[0] : "lowcost";
        if (computerType == "lowcost")
        {
            Console.WriteLine("LowCost Computer Components");
            Console.WriteLine(string.Format("CPU: {0}", new Celeron().Name));
            Console.WriteLine(string.Format("RAM: {0}GB", new LowCostRam().GB));
        }
        else
        {
            Console.WriteLine("High Performance Computer Components");
            Console.WriteLine(string.Format("CPU: {0}", new Xeon().Name));
            Console.WriteLine(string.Format("RAM: {0}GB", new HighPerformanceRam().GB));
        }

        Console.ReadLine();
    }

}

A pesar de ser un ejemplo muy sencillo podemos ver algunos problemas en este código.



  • Además de esto, a nivel de lógica de negocio, es fácil equivocarse y mezclar componentes de ordenadores distintos. Es decir, por error podríamos mostrar un ordenador con CPU Xeon y Ram 4GB. Cosa que por nuestra lógica de negocio no deberíamos hacer. 

Este segundo punto contesta a una de las preguntas que nos hacíamos al principio del post. ¿Por qué querríamos crear una familia de objetos relacionados? La respuesta obvia es que si creamos los objetos (en este caso los componentes del ordenador) en familias en lugar de crearlos de forma aislada nos aseguramos que siempre creamos objetos que están relacionados entre sí, disminuyendo la probabilidad de equivocarnos. En nuestro caso lo más evidente será crear la familia de componentes "LowCost" y la familia de componentes "HighPerformance". 

La respuesta a la pregunta sobre ¿cómo crear una familia de objetos sin conocer los tipos concretos de los que se trata? como habréis adivinado es mediante una factoría abstracta (Abstract Factory). Así que vamos a ver el patrón y luego volveremos al ejemplo para aplicarlo.

Abstract Factory 

La siguiente es la estructura UML del patrón Abstract Factory

Estos son los participantes en este patrón:

  • AbstractFactory provee una interfaz con operaciones para crear productos abstractos. En nuestro ejemplo crearemos ComputerComponentFactory con las operaciones CreateCpu y CreateRam. Podríamos usar interfaces en lugar de clases abstractas puras.
  • ConcreteFactory implementa las operaciones para crear familias de objetos concretos. En nuestro ejemplo implementaremos LowCostFactory e HighPerformanceFactory.
  • AbstractProduct declara una interfaz (o clase base) para los productos. En nuestro ejemplo declararemos las clases base Cpu y Ram para los productos. Podríamos usar interfaces en lugar de clases abstractas puras.
  • ConcreteProducts definen los productos que se crearán en las factorías concretas. Nuestra LowCostFatory devolverá un Celeron al llamar al método CreateCpu. Heredan de AbstractProduct
  • Client usa la factoría. Sólo usa la interfaz declarada por AbstractFactory y los tipos AbstractProduct.
El siguiente diagrama es el del patrón Abstract Factory como lo implementaremos en nuestro ejemplo:

El ejemplo anterior refactorizado para usar Abstract Factory quedaría más o menos así:
public abstract class Cpu
{
    public string Name { get; set; }
}
public class Celeron : Cpu
{
    public Celeron()
    {
        Name = "Intel Celeron 2.60GHz";
    }
}
public class Xeon : Cpu
{
    public Xeon()
    {
        Name = "Intel Xeon Phi 5110P";
    }
}

public abstract class Ram
{
    public int GB { get; set; }
}
public class LowCostRam : Ram
{
    public LowCostRam()
    {
        GB = 4;
    }

}
public class HighPerformanceRam : Ram
{
    public HighPerformanceRam()
    {
        GB = 32;
    }

}

public abstract class ComputerComponentFactory
{
    public abstract Cpu CreateCpu();
    public abstract Ram CreateRam();
}

public class LowCostFactory : ComputerComponentFactory
{
    public override Cpu CreateCpu()
    {
        return new Celeron();
    }

    public override Ram CreateRam()
    {
        return new LowCostRam();
    }
}

public class HighPerformanceFactory : ComputerComponentFactory
{
    public override Cpu CreateCpu()
    {
        return new Xeon();
    }

    public override Ram CreateRam()
    {
        return new HighPerformanceRam();
    }
}

class Program
{
    static void Main(string[] args)
    {
        string computerType = args.GetUpperBound(0) >= 0 ? args[0] : "lowcost";
        ComputerComponentFactory factory = CreateFactory(computerType);

        Console.WriteLine("Computer Components");
        Console.WriteLine(string.Format("CPU: {0}", factory.CreateCpu().Name));
        Console.WriteLine(string.Format("RAM: {0}GB", factory.CreateRam().GB));

        Console.ReadLine();
    }


    private static ComputerComponentFactory CreateFactory(string computerType)
    {
        if (computerType == "lowcost")
            return new LowCostFactory();
        else
            return new HighPerformanceFactory();
    }
}

Ahora obtenemos la factoría concreta a través del método CreateFactory. El cliente de la factoría (nuestro método Main) no trata directamente con las factorías concretas, sino que lo hace a través de la factoría abstracta ComputerComponentFactory. De la misma forma, también trata con las clases abstractas de productos (Cpu y Ram) en lugar de hacerlo con los productos concretos.

Es importante entender que lo fundamental a la hora de aplicar este patrón es que la factoría abstracta en ningún caso es responsable de crear productos concretos, sino que como hemos visto ya esa responsabilidad la delega en las clases hijas (factorías concretas).

Analicemos como ejemplo el código siguiente:
public class ComputerComponentFactory
{
    private string ComputerType;

    public ComputerComponentFactory(string computerType)
    {
        ComputerType = computerType;
    }
    public Cpu CreateCpu()
    {
        if (ComputerType == "lowcost")
            return new Celeron();
        else
            return new Xeon();
    }
    public Ram CreateRam()
    {
        if (ComputerType == "lowcost")
            return new LowCostRam();
        else
            return new HighPerformanceRam();
    }
}

Esta implementación de ComputerComponentFactory a pesar de que los métodos CreateCpu y CreateRam crean los productos correctos basándose en el parámetro computerType que recibe por el constructor no podemos considerarla una factoría abstracta ya que asume la responsabilidad de crear los objetos concretos en lugar de delegar esa responsabilidad en las subclases. A esta clase podríamos llamarla Factoría o Factoría Simple, pero no es en ningún caso Factoría Abstracta.

Consecuencias de usar Abstract Factory

El patrón Abstract Factory tiene las siguientes ventajas e inconvenientes:

Ventajas

  • Aísla las clases concretas. La factoría abstracta nos ayuda a controlar los objetos concretos que crea la aplicación, ya que el cliente manipula esos objetos siempre a través de la interfaz abstracta de los objetos.
  • Facilita el intercambio de familias de productos. Como hemos visto en los ejemplos la factoría concreta que crea los productos sólo aparece una vez en la aplicación, en el lugar en el que se instancia. Esto hace que sea fácil cambiar la factoría concreta que usa la aplicación. En el ejemplo anterior vemos que es muy fácil cambiar de la factoría "low cost" a la factoría "high performance" sin afectar al resto de la aplicación.
  • Promueve la consistencia entre productos. Cuando los objetos de una familia se diseñan para trabajar juntos es importante asegurarnos que la aplicación no mezcla objetos de distinta familia. A esto nos ayuda el patrón Abstract Factory.

Inconvenientes

  • Es difícil agregar nuevos productos. Este quizá sea el inconveniente más importante de Abstract Factory y hay que tenerlo muy en cuenta a la hora de valorar si es conveniente o no el uso del patrón. Si queremos añadir nuevos productos a la familia tendremos que reescribir la factoría abstracta y todas las factorías concretas que hayamos creado. En circunstancias donde preveamos que los cambios en las familias de objetos serán frecuentes y las factorías concretas numerosas deberíamos tratar de evitar el uso de factorías abstractas.

Una variante

Cómo ya he comentado en un post anterior, los patrones de diseño no son una solución directamente aplicable sino más bien una guía para solucionar un problema recurrente. Basándonos en esa guía podemos proponer soluciones concretar que aplican el patrón con más o menos adaptaciones. A continuación vamos a ver cómo aplicamos una adaptación del patrón Abstract Factory para solucionar un problema concreto.

Supongamos que vamos a desarrollar una aplicación web MVC. La aplicación consta de 2 páginas, la página principal y la página privada. Además hay dos tipos de usuarios en esta aplicación, el invitado y el administrador. De forma que nuestra aplicación mostrará distintas páginas en función del tipo de usuario según la siguiente tabla.

Usuario Acción Página a mostrar
Invitado Home GuestHomeView
Invitado Private NotAvailable
Administrador Home AdminHomeView
Administrador Private AdminPrivateView

Quien no conozca el patrón MVC que  no se asuste, lo que vamos a hacer es algo muy simple. 
Solo decir que en este patrón la V es de View, y aunque no es así, para nuestro ejemplo diremos que es el "archivo Html" que vamos a mostrar al usuario. C es de Controller y esta clase debe encargarse fundamentalmente de atender las peticiones del usuario y decidir que Vista se muestra al usuario.

En base al usuario y a través de una Abstract Factory vamos a obtener no una familia de objetos en el sentido estricto sino el nombre de las vistas que debemos renderizar para un usuario concreto. Es decir, nuestra factoría abstracta tendrá métodos del tipo string GetHomeViewName() que devuelve el nombre de la vista Home para el usuario actual.

Veamos el código:
/// 
/// factoría abstracta de vistas
/// 
public abstract class ViewsFactory
{
    public abstract string GetHomeViewName();
    public abstract string GetPrivateViewName();
}

/// 
/// Factoría concreta de las vistas del administrador.
/// 
public class AdminViewsFactory : ViewsFactory
{
    public override string GetHomeViewName()
    {
        return "AdminHomeView";
    }

    public override string GetPrivateViewName()
    {
        return "AdminPrivateView";
    }
}

/// 
/// Factoría concreta de las vistas del invitado
/// 
public class UserViewFactory : ViewsFactory
{
    public override string GetHomeViewName()
    {
        return "UserHomeView";
    }

    public override string GetPrivateViewName()
    {
        // El invitado no tiene acceso a la vista privada
        // en su lugar se muestra una vista que indica que no 
        // tiene el acceso permitido.
        return "NotAllowedView";
    }
}


/// 
/// Singleton para instanciar y acceder a la Abstract Factory
/// Veremos el patrón Singleton en un próximo post.
/// 
public class GlobalFactories
{
    private static GlobalFactories _instance;
    public static GlobalFactories Instance
    {
        get { return _instance ?? (_instance = new GlobalFactories()); }
    }

    private GlobalFactories()
    {

    }

    public ViewsFactory ViewsFactory { get; set; }
}

/// 
/// Clase que configura la vista en 
/// el inicio de la aplicación
/// 
public static class FactoriesConfig
{
    /// 
    /// Este método configura la ViewsFactory.
    /// Se llama al iniciar la aplicación
    /// 
    public static void ConfigFactory()
    {
        string userName;

        //
        // Aquí va la lógica que determine
        // el usuario actual y asigne UserName
        //

        userName = "Admin";
        if (userName == "Admin")
            GlobalFactories.Instance.ViewsFactory = new AdminViewsFactory();
        else
            GlobalFactories.Instance.ViewsFactory = new UserViewFactory();
    }
}

/// 
/// Controller de la página principal.
/// Decide qué vista renderizar cuando el 
/// usuario acceda a http://www.midemoabstractfactory.com
/// 
public class HomeController : Controller
{
    public ActionResult Index()
    {
        return View(GlobalFactories.Instance.ViewsFactory.GetHomeViewName());
    }
}

/// 
/// Controller de la página privada.
/// Decide qué vista renderizar cuando el 
/// usuario acceda a http://www.midemoabstractfactory.com/private
/// 
public class PrivateController : Controller
{
    public ActionResult Index()
    {
        return View(GlobalFactories.Instance.ViewsFactory.GetPrivateViewName());
    }

}

La solución que el código anterior aplica al problema de qué vista renderizar en cada momento está claramente inspirada en el patrón Abstract Factory, aunque tiene pequeñas adaptaciones para ajustar la solución al problema concreto.

Usos conocidos

El uso de factorías abstractas es muy habitual y podemos ver implementaciones de este patrón fácilmente. Como ejemplo veamos la forma en que ADO.NET conecta e interactúa con una base de datos:
  • DbProviderFactory (AbstractFactory): Define la interfaz para crear los objetos que conectan e interactuan con una base de datos.
  • SqlClientFactory (ConcreteFactory): Implementan los métodos que crean los objetos concretos para interactuar con SQL Server. Hay diversas factorías concretas para diversas bases de datos: OracleClientFactory, OleDbFactory, OdbcFactory, etc. Cualquiera puede crear una factoría para su propia base de datos.
  • DbConnection, DbCommand, etc (Abstract Product): Productos abstractos. Definen la interfaz para cada tipo de producto.
  • SqlConnection, SqlCommand, etc (Concrete Products): Productos concretos para cada sistema de base de datos. Igualmente existen OracleConnection, etc.
En ADO.NET usar un gestor de base de datos u otro se reduce a simplemente instanciar una u otra factoría concreta.

Patrones relacionados

Como ya hemos mencionado los patrones no suelen aparecer de forma aislada. Es decir, habitualmente están relacionados unos con otros. En el caso de Abstract Factory se suele implementar con el patrón Factory Method, también pueden ir junto a un patrón Prototype para la instanciación de los productos concretos.  Las factorías concretas suelen ser Singleton. 
Todos estos patrones relacionados iremos viéndolos en detalle en sus respectivos post.

Hasta aquí Abstract Factory. Hay muchas cosas más que decir de este patrón, pero creo que con lo visto hasta ahora nos podemos hacer una buena idea de cómo funciona este patrón.

El próximo post seguiremos con otro patrón de creación: Builder.

7 comentarios:

  1. Hola Miguel Ángel, muy bien desarrollado.

    Yo particularmente este patrón, en el contexto de los principios SOLID, lo asocio siempre al principio O(open/close). Generalmente se aplica este patrón como solución a este principio.

    ResponderEliminar
  2. Este comentario ha sido eliminado por el autor.

    ResponderEliminar
  3. ambién se puede asociar al concepto de inyección de dependencias, puesto que una fabrica concreta puede ser la encargada de inyectar las dependencias necesarias de la clase concreta en el momento o después de crearla y antes de retornarla.

    Con respecto a la implementación del ejemplo de componentes tengo una proposición que discutir con usted.

    Una clase enteramente abstracta (sin ninguna implementación publica ni privada) como la que usted ha escrito no tiene sentido puesto que lo único que define es una interfaz para el cliente; así que yo haría lo siguiente:

    Mover la implemetación de CreateFactory a la clase abstracta como un método estático. No tiene mucho sentido que el código de creación de la fabrica concreta esté en el Main puesto que incumple directamente el propio principio de inversión de dependencias que esta intentando cumplir ya que el método Main mantiene dependencia con las clases concretas aunque luego solo use las abstracciones.

    Cuando uno se encuentra que ha programado una clase completamente abstracta uno tiene que pararse y replanteárselo puesto que esto nunca es necesario. O tenemos que crear una interfaz en vez de una clase abstracta o no hemos encapsulado bien las responsabilidades y tenemos código que pertenece a esta clase implementada en otro sitio.

    Un saludo y felicidades por la buena calidad del post.

    ResponderEliminar
  4. Gracias por el trabajo que realizas en este blog, como siempre, muy útil e interesante

    ResponderEliminar
  5. Hola Jaume.
    Primero deciros que por algún motivo no funciona responder directamente a vuestros comentarios y el lugar de responder a cada uno en vuestro hilo os contestaré "sin hilo", trataré de solucionar este tema cuanto antes para que los comentarios se sigan mejor.

    Estoy de acuerdo contigo. Una forma que podemos usar para extender nuestras clases sin tener que cambiarlas es sin duda mediante el patrón Abstract Factory.

    En este sentido la mayoría de patrones nos permiten extender los diseños sin modificarlos. TemplateMethod nos permite implementar pasos concretos de un algoritmo en subclases, con Visitor podemos agregar funcionalidades a un arbol de objetos sin modificar los elementos del arbol, etc.

    Los patrones en general, y por supuesto Abstract Factory tambien, siguen ampliamente los principios de diseño.

    Gracias por tu comentario.

    ResponderEliminar
  6. Hola Nenaza Ramone.
    Gracias por tu comentario. Estoy de acuerdo con tu planteamiento aunque no en todo.
    Como he comentado antes a Jaume, en general los patrones de diseño sigue ampliamente los principios de diseño SOLID, . De ahí la flexibilidad que nos ofrecen.

    Como habrás observado en los dos post que hay por el momento sobre patrones de diseño algo en lo que me gusta hacer especial hincapié es que los patrones no son soluciones cerradas sino guías para solucionar un problema. Efectivamente la implementación de los ejemplos no es la "más correcta" seguramente, pero ten en cuenta que la finalidad es que se entienda bien el patrón y para esto hay que sacrificar la aplicación de principios que posiblemente "enturbiarían" el objetivo principal del ejemplo.

    Con respecto a la dicotomía entre clase abstracta pura e interfaz no estoy completamente de acuerdo contigo.

    Como habrás visto, cuando describo los elementos que forman parte del patrón en la clase abstracta digo que se podría sustituir por una interfaz. Sin embargo, no creo que cambiar una clase abstracta pura por una interfaz sea algo automático.

    Es cierto que la interfaz nos da mayor flexibilidad y si es posible yo prefiero usarla ya que promueve la composición de objetos (más flexible) sobre la herencia (más rígida).

    Sin embargo la clase abstracta además de ofrecernos la interfaz pública de la clase nos ofrece la posibilidad de encajar una clase dentro de una jerarquía de herencia, cosa que la interfaz no. En el ejemplo tal cual está podríamos sustituir la factoría abstracta por una interfaz, pero hay situaciones donde eso no seria lo adecuado.

    Intentaré describir una jerarquía donde una clase abstracta pura sea necesaria, espero que se entienda por que en los comentarios no es fácil formatear el texto.

    Imagina que creamos una jerarquía de animales. La clase base es Animal con el método comer. De aquí hereda Vaca, Gusano y los animales Cazadores que tienen el método Cazar. De Cazadores heredan Gato y Leon. La jerarquía será algo así:

    Animal
    |_ Vaca
    |_ Gusano
    |_ Cazadores (abstracta pura)
         |_ Gato
         |_ Leon

    Si sustituimos Cazadores por ICazadores perderíamos la jerarquía.

    Un saludo.

    ResponderEliminar