lunes, 20 de enero de 2014

Patrones de creación: Factory Method

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

Factory Method

"Define una interfaz para crear un objeto, pero deja que sean las subclases las que deciden que objeto instanciar. Factory Method permite a la clase diferir la instanciación del objeto a las subclases".

En el post sobre Abstract Factory ya vimos, aunque de pasada, el patrón Factory Method. Abstract Factory se centra en crear familias de objetos y cada uno de los objetos de la familia se crean habitualmente mediante Factory Method.


Es importante entender y distinguir entre Abstract Factory y Factory Method.
  • Abstract Factoy: Crea familias de productos.
  • Factory Method: Crea los objetos concretos.
Aunque es habitual que Abstract Factory implemente sus métodos mediante Factory Method. Factory Method no tiene por qué aparecer solamente como métodos de Abstract Factory, veremos infinidad de veces Factory Method fuera de una factoría abstracta.

El problema

Veamos el problema que intenta resolver el patrón Factory Method y cómo lo resuelve. Vamos a basarnos en el ejemplo que ofrecen GoF en su libro Design Patterns: Elements of Reusable Object-Oriented Software.

Supongamos que vamos a crear un framework de aplicaciones que muestran documentos al usuario. Es decir, si un desarrollador quiere crear una aplicación de edición de textos usaría nuestro framework que le facilitará la tarea.

Nuestro framework tendrá dos entidades clave, la entidad Application y la entidad Document. Ambas entidades son abstractas y los desarrolladores deberán heredar de estas clases para implementar sus aplicaciones. Para crear una aplicación de dibujo, por ejemplo, el desarrollador que use nuestro framework creará las clases DrawingApplication y DrawingDocument.

La clase DrawingApplication es la responsable de gestionar los documentos DrawingDocumet, por ejemplo creará un documento cuando el usuario seleccione Nuevo o Abrir desde el menú de la aplicación.

Veamos el diagrama UML del patrón FactoryMethod antes de seguir:



El siguiente es el diagrama de la adaptación del patrón Factory Method a nuestro problema concreto:



Ya que el tipo de documento (DrawingDocument) es específico de cada tipo de aplicación (DrawingApplication), cuando desarrollemos nuestro framework al implementar la clase Application no sabemos qué clase concreta de Document debemos crear. La clase Application sólo sabe cuándo debe crear un objeto Document, pero no que tipo de documento crear. 

Este es el problema que nos encontramos al desarrollar nuestro framework. En nuestra clase Application debemos crear un objeto de tipo Document pero no podemos saber cuál.

El patrón Factory Method ofrece una solución a este problema. Encapsula la responsabilidad de la creación del documento concreto, en nuestro caso en el método abstracto CreateDocument de la clase Application, y mueve esa responsabilidad fuera de nuestro framework a la clase DrawingApplication.

Veamos cómo sería el código del framework y del cliente:
namespace Framework
{
    public class Document
    {
        public virtual void Open()
        {
            // Lógica para abrir el documento.
            // Este método puede ser abstracto o 
            // como en este caso virtual e
            // implementar una lógica razonable 
            // por defecto del tipo
            // System.Windows.Forms.OpenFileDialog();            
        }
    }

    /// 
    /// Clase abstracta para crear aplicaciones concretas.   
    /// 
    public abstract class Application
    {
        /// 
        /// Colección de documentos en la aplicación.
        /// 
        private List<Document> documetList;

        public Application()
        {
            documetnList = new List<Document>();
        }

        /// 
        /// Método abstracto para crear un documento específico (Factory Method).         
        /// 
        /// 
        protected abstract Document CreateDocument();

        /// 
        /// Crea un nuevo documeto, 
        /// lo agrega a la lista de documentos de la aplicación y lo muestra.
        /// 
        public void NewDocument()
        {
            Document newDocument = CreateDocument();
            documetList.Add(newDocument);
            newDocument.Open();
        }

        public void OpenDocument()
        {
            // Lógica para abrir un documento.

        }
    }
}

namespace Client
{
    public class DrawingDocument : Framework.Document
    {
        public override void Open()
        {
            // Podemos sobrescribir este método y 
            // crear una lógica específica
            // para abrir un DrawingDocument
            // o no sobrescribirlo y usar la lógica
            // por defecto de la clase base.
        }
    }

    public class DrawingApplication : Framework.Application
    {
        protected override Framework.Document CreateDocument()
        {
            // Creamos y devolvemos un documento 
            // específico de nuestra aplicación
            // cliente del framework
            return new DrawingDocument();
        }
    }
}


El patrón Factory Method establece puntos de extensión de nuestro framework, el hecho de crear un objeto mediante Factory Method en lugar de crearlo directamente nos da más flexibilidad y heredando  podemos crear versiones extendidas de las clases del framework.

Por ejemplo, en la clase Document del framework podríamos crear un Factory Method llamado CreateFileDialog que nos permite crear un cuadro de dialogo para abrir un archivo con una implementación por defecto (método virtual en C#) que muestra el cuadro de dialogo OpenFileDialog de Windows. La clase DrawingDocument puede sobrescribir este método para crear un cuadro de dialogo específico que por ejemplo muestre una vista previa del documento.

Como ya hemos mencionado varias veces en esta serie de posts los patrones de diseño no son más que una guia para resolver un problema y a la hora de implementarlos tendremos que adaptarlos a nuestro problema concreto. En el caso anterior CreateFileDialog no es abstracto sino que ofrece una implementación razonable por defecto. Igualmente esto se ajusta al patrón Factory Method, ya que delega en las subclases la creación del cuadro de dialogo aunque ofrezca una implementación por defecto razonable.

Quiero destacar aquí el hecho de que Factory Method delega en la subclase la creación del objeto concreto, ya que es posible que veáis implementaciones de métodos para crear objetos pero en lugar de delegar en subclases la creación es el propio método quien crea el objeto concreto. Algo tal que así:

public class PlanTextDocument
{
    public PlanTextDocument CreateDocument()
    {
        return new PlanTextDocument();
    }
}

Esta implementación del método CreateDocument no correspondería con el patrón Template Method ya que no permite delegar la creación del documento a una subclase. Esta implementación corresponde a un método de creación y la podréis ver definida como Creation Method o Factory Function. En nuestro ejemplo el método CreateDocument de la clase DrawingApplication es un Creation Method. Gracias a mi amigo Iván Morales por su sugerencia de aclarar esto.

La trampa del constructor de C#

Creo que lo visto hasta ahora es suficiente para entender el patrón Factory Method. Sin embargo, antes de acabar este post me gustaría comentar una "trampa" en la que podemos caer al implementar este patrón en C#.

Supongamos que estamos creando un framework para que lo usen otros desarrolladores. Este framework tiene una clase UserNameWriter que es responsable de escribir en la consola el nombre de un usuario. La implementación de UserNameWriter es esta:

namespace Framework
{
   public abstract class UserNameWriter
   {
      public UserNameWriter()
      {
         var userName = GetUserName();
         Console.WriteLine(userName);
      }
      
      protected abstract string GetUserName();
   }
}

Como se puede ver claramente UserNameWriter escribe el nombre del usuario cuando se instancia.
Ahora nos pondremos en la piel de un desarrollador que usará nuestro framework. Implementaremos la clase GuestNameWriter que escribirá en la consola el nombre de un usuario con rol de invitado en mayúsculas. Este sería el código:
namespace Client
{
   public class GuestNameWriter : UserNameWriter
   {           
      protected override string GetUserName
      {
         return "GUEST";
      }
   }
   class Program
   {
      static void Main(string[] args)
      {
        var userWriter = new GuestWriter();
      }
   }
}

Evidentemente, este código el ejecutarlo escribirá en la consola el string "GUEST".

Crearemos ahora la clase LoggedUserNameWriter, esta clase como se puede suponer mostrará en la consola el nombre de un usuario conectado. Este es el código:
namespace Client
{
   public class LoggedUserNameWriter : UserNameWriter
   {   
      private string userName;      

      public LoggedUserNameWriter(string loggedUserName)
      {
         userName = loggedUserName;
      }        
      protected override string GetUserName
      {
         return userName.ToUpperInvariant();
      }
   }
   class Program
   {
      static void Main(string[] args)
      {
        var userWriter = new LoggedUserNameWriter("Miguel A. González");
      }
   }
}

¡Este código sorprendentemente al ejecutarse lanzará una excepción del tipo NullReferenceException! Pero, ¿qué hemos hecho mal al implementar LoggedUserNameWriter? la respuesta es simple: NADA.

No hay nada mal en la implementación de LoggedUserNameWriter. El problema está en la implementación de la clase UserNameWriter del framework, pero nosotros como "clientes" del framework vamos a obtener una excepción que nos va a dar más de un quebradero de cabeza depurándola.

Para explicar qué ha pasado realmente empezaremos por ver cómo funciona la instanciación de objetos. La instanciación de un objeto, aunque la escribimos con una sola instrucción (new), en tiempo de ejecución tiene un proceso complejo, no se trata simplemente de crear un objeto sin más.

En C#, en tiempo de ejecución, cuando instanciamos un objeto se siguen los siguientes pasos:
  1. Se instancian los campos privados estáticos de la clase hija.
  2. Se llama al constructor estático de la clase hija.
  3. Se instancian los campos privados estáticos de la clase padre.
  4. Se llama al constructor estático de la clase padre.
  5. Se instancian los campos privados de instancia de la clase padre.
  6. Se llama al constructor de instancia de la clase padre.
  7. Se instancian los campos privados de instancia de la clase hija.
  8. Se llama al constructor de instancia de la clase hija.

Ahora vamos a ver cómo se han aplicado estos pasos al instanciar nuestra clase LoggedUserNameWriter:
  1. Se instancian los campos privados estáticos de la clase LoggedUserNameWriter (en este caso no tiene).
  2. Se llama al constructor estático de la clase LoggedUserNameWriter (en este caso no tiene).
  3. Se instancian los campos privados estáticos de la clase UserNameWriter (en este caso no tiene).
  4. Se llama al constructor estático de la clase UserNameWriter (en este caso no tiene).
  5. Se instancian los campos privados de instancia de la clase UserNameWriter (en este caso no tiene).
  6. Se llama al constructor de instancia de la clase UserNameWriter . Aquí, en este caso, pasa lo siguiente:
    1. UserNameWriter llama al método GetUserName abstracto que implementa la clase LoggedUserNameWriter.
    2. ¡LoggedUserNameWriter lanza una excepción por que userName aún no se ha instanciado, tiene valor null! Evidentemente tampoco se le ha asignado el valor "Miguel A. González" porque todavía no se ha llamado al constructor de la clase LoggedUserNameWriter.
  7. Se instancian los campos privados de instancia de la clase hija (No se llega a realizar porque se ha lanzado una excepción en el paso 6).
  8. Se llama al constructor de instancia de la clase hija (No se llega a realizar porque se ha lanzado una excepción en el paso 6).
En resumen, la llamada al método abstracto GetUseName() en el constructor de la clase UserNameWriter provoca que se llame a la implementación el método en LoggedUserNameWriter incluso antes de que se haya ejecutado el constructor de LoggedUserNameWriter. Esto dependiendo de la implementación de la clase hija puede provocar una excepción como la que hemos visto.

Al heredar de UserNameWriter no tenemos por qué saber cómo está implementada esta clase, y no deberíamos obtener una excepción en nuestra clase hija si está bien escrita.

Como desarrolladores de la clase base debemos evitar a toda costa llamar a un método abstracto o virtual desde el constructor de nuestra clase para evitar esta trampa del constructor.

Una mejor implementación del patrón Factory Method para evitar esta trampa en C# sería la siguiente:

namespace Framework
{
   public abstract class UserNameWriter
   {
      public UserNameWriter()
      {
        
      }
      
      protected abstract string GetUserName();

      public void WriteUserName()
      {
        var userName = GetUserName();
        Console.WriteLine(userName);
      }
   }
}
namespace Client
{
   public class LoggedUserNameWriter : UserNameWriter
   {   
      private string userName;      

      public LoggedUserNameWriter(string loggedUserName)
      {
         userName = loggedUserName;
      }        
      protected override string GetUserName
      {
         return userName.ToUpperInvariant();
      }
   }
   
   class Program
   {
      static void Main(string[] args)
      {
        var userWriter = new LoggedUserNameWriter("Miguel A. González");
        userWriter.WriteUserName();
      }
   }
}

Con esta implementación de UserNameWriter nos aseguramos que las llamadas al método abstracto se hacen siempre después de que haya terminado completamente el proceso de instanciación.

Ahora si, termina este post sobre el patrón Factory Method. En el próximo post veremos un nuevo patrón de creación: Prototype

2 comentarios:

  1. Facil de entender y educativo. Echo de menos algun ejemplo mas elaborado, o un posible caso real. Ya que a la hora de la verdad nunca es tan sencillo de ver.

    ResponderEliminar
  2. Hola Alvaro.

    Gracias por tu comentario. Lo de crear ejemplos más reales ya lo sopesé en su momento pero temía que la explicación del patrón pudiera quedar confusa y mezclaba más conceptos de los estrictamente necesario. Aún así, tomo nota para intentar mejorar los ejemplos en posteriores post.

    De cualquier manera, si tienes alguna duda concreta en algún desarrollo tuyo estaré encantado de verlo contigo y ayudarte en lo que pueda.

    Un saludo.

    ResponderEliminar