martes, 7 de agosto de 2007

Patrones creacionales.

Con este soberbio título comenzamos una series post que hace tiempo que tengo en el tintero.

Introducción

Los patrones creacionales son modos de crear objetos siguiendo ciertas reglas según el escenario que nos aseguran un comportamiento esperable. O sea, si lo hacemos así sabemos lo que vamos a obtener.

Creando Objetos

El modo básico de crear un objeto es el siguiente
Persona persona = new Persona().

Muy bien, esto es correcto, pero qué pasa si nuestra clase Persona necesita, al ser creada, un parámetro. Entonces hacemos esto

Persona persona = new Persona(“Leonardo”);
En este caso la clase Persona recibe un parámetro del tipo String que permite indicar el nombre de la persona, perfecto. Imaginemos que con ese nombre accedemos a una base de datos y recuperamos el resto de los datos de la persona… ¿cómo se resuelve?


   1:  public class Persona
   2:  {
   3:      private int _edad;
   4:      private string _nombre;
   5:      private string _apellido;
   6:      private DateTime _fechaNacimiento;
   7:   
   8:      public DateTime FechaNacimiento
   9:      {
  10:          get { return _fechaNacimiento; }
  11:          set { _fechaNacimiento = value; }
  12:      }
  13:   
  14:      public string Apellido
  15:      {
  16:          get { return _apellido; }
  17:          set { _apellido = value; }
  18:      }
  19:   
  20:      public string Nombre
  21:      {
  22:          get { return _nombre; }
  23:          set { _nombre = value; }
  24:      }
  25:   
  26:      public int Edad
  27:      {
  28:          get { return _edad; }
  29:          set { _edad = value; }
  30:      }
  31:   
  32:      public Persona() { }
  33:   
  34:      public Persona(string nombre)
  35:      {
  36:          string connectionString = ConfigurationManager.ConnectionStrings["local"].ConnectionString;
  37:          string sql = "SELECT Edad, Apellido, FechaNacimiento FROM Persona where Nombre='" + nombre + "'";
  38:   
  39:          using (SqlConnection connection = new SqlConnection(connectionString))
  40:          {
  41:              connection.Open();
  42:              SqlCommand cmd = new SqlCommand(sql, connection);
  43:   
  44:              using (SqlDataReader reader = cmd.ExecuteReader())
  45:              {
  46:                  while (reader.NextResult())
  47:                  {
  48:                      this.Apellido = (string) reader["Apellido"];
  49:                      this.FechaNacimiento= Convert.ToDateTime(reader["FechaNacimiento"]);
  50:                      this.Edad = (int)reader["Edad"];
  51:                  }
  52:                  reader.Close();
  53:              }
  54:              connection.Close();
  55:          }
  56:      }
  57:  }

Más allá de que no es el código ideal, es un lugar muuuuyyyy feo para ponerlo, ¿o no?, para quienes duden la respuesta es sí, es un lugar muy feo para esto, nuestra clase Persona no tiene que saber que hay un connectionString que hay una base de datos, un archivo de configuración, nada de eso. Vamos a ver cómo mejorarlo.

Primer acercamiento, Factory Method

Un segundo problema de esta implementación es que aún se pueden crear objetos de la clase Persona sin asignar nada porque tenemos el constructor definido y accesible, la solución es poner el constructor sin parámetros privado, quitar el segundo constructor y crear un método dentro de la clase que tenga todo ese código de inicialización, acepte el parámetro nombre y devuelva un objeto Persona, por supuesto que debe ser estático para poder accederlo, veámoslo.


   1:  public class Persona
   2:  {
   3:      private int _edad;
   4:      private string _nombre;
   5:      private string _apellido;
   6:      private DateTime _fechaNacimiento;
   7:   
   8:      public DateTime FechaNacimiento
   9:      {
  10:          get { return _fechaNacimiento; }
  11:          set { _fechaNacimiento = value; }
  12:      }
  13:   
  14:      public string Apellido
  15:      {
  16:          get { return _apellido; }
  17:          set { _apellido = value; }
  18:      }
  19:   
  20:      public string Nombre
  21:      {
  22:          get { return _nombre; }
  23:          set { _nombre = value; }
  24:      }
  25:   
  26:      public int Edad
  27:      {
  28:          get { return _edad; }
  29:          set { _edad = value; }
  30:      }
  31:   
  32:      public Persona() { }
  33:   
  34:      public static Persona Create(string nombre)
  35:      {
  36:          Persona2 persona = new Persona2();
  37:   
  38:          string connectionString = ConfigurationManager.ConnectionStrings["local"].ConnectionString;
  39:          string sql = "SELECT Edad, Apellido, FechaNacimiento FROM Persona where Nombre='" + nombre + "'";
  40:   
  41:          using (SqlConnection connection = new SqlConnection(connectionString))
  42:          {
  43:              connection.Open();
  44:              SqlCommand cmd = new SqlCommand(sql, connection);
  45:   
  46:              using (SqlDataReader reader = cmd.ExecuteReader())
  47:              {
  48:                  while (reader.NextResult())
  49:                  {
  50:                      persona.Apellido = (string)reader["Apellido"];
  51:                      persona.FechaNacimiento = Convert.ToDateTime(reader["FechaNacimiento"]);
  52:                      persona.Edad = (int)reader["Edad"];
  53:                  }
  54:                  reader.Close();
  55:              }
  56:              connection.Close();
  57:          }
  58:          return persona;
  59:      }
  60:  }

Mejor, esto se conoce como Factory Method, en una implementación más sólida pondríamos el factory method en otra clase la cual será la encargada de crear Personas, y la llamaremos PersonaFactory, con esto quitamos toda la responsabilidad a la clase Persona de hacer cosas que no debe, un detalle más es que la clase Persona debería tener el constructor internal para que lo pueda ver PersonaFactory dentro del mismo assembly pero que no se pueda desde fuera, Veamos el código final.


   1:  public class PersonaFactory
   2:  {
   3:      public static Persona Create(string nombre)
   4:      {
   5:          Persona persona = new Persona();
   6:   
   7:          string connectionString = ConfigurationManager.ConnectionStrings["local"].ConnectionString;
   8:          string sql = "SELECT Edad, Apellido, FechaNacimiento FROM Persona where Nombre='" + nombre + "'";
   9:   
  10:          using (SqlConnection connection = new SqlConnection(connectionString))
  11:          {
  12:              connection.Open();
  13:              SqlCommand cmd = new SqlCommand(sql, connection);
  14:   
  15:              using (SqlDataReader reader = cmd.ExecuteReader())
  16:              {
  17:                  while (reader.NextResult())
  18:                  {
  19:                      persona.Apellido = (string)reader["Apellido"];
  20:                      persona.FechaNacimiento = Convert.ToDateTime(reader["FechaNacimiento"]);
  21:                      persona.Edad = (int)reader["Edad"];
  22:                  }
  23:                  reader.Close();
  24:              }
  25:              connection.Close();
  26:          }
  27:   
  28:          return persona;
  29:      }
  30:  }
Conclusión

Este es un primer vistazo de los patrones creacionales, los cuales nos permiten crear objetos desacoplando el modelo, imaginemos si mañana en lugar de conectarnos a una base de datos recuperamos los datos de un archivo de texto, la clase Persona no se va a enterar de esto, y está bien que así sea. En fin, en la próxima vamos a ver cómo se complica las cosas cuando entra en escena una jerarquía de objetos. Hasta entonces.

7 comentarios:

Francisco dijo...

Me encanto tu articulo porque demuestra una buena metodologia de programación. Y un acercamiento en como programar a la "factory".
You are a groso.

LgEaObNrAiReDlO dijo...

Gracias Francisco, no es para tanto :P .

Juan Manuel Lombana dijo...

aunque la teoria es buena me parece que no es la filosofia de .net quemar codigo sql en el codigo del programa... yo utilizaria procedimientos almacenados y unos cuantos controles de enlace a datos como los tableadapters, bindingsource, datasets etc....
no crees lo mismo????..
Atentamente,

Juan Manuel Lombana
Medellín - Colombia

LgEaObNrAiReDlO dijo...

Si Juan Manuel, lo más adecuado es utilizar un procedimiento almacenado, este código es a modo de ejemplo; de todos modos prefiero utilizar DAAB que tableadapters o bindingsource, pero es un detalle, gracias por el comentario.

Unknown dijo...

Saludos,

Me gusto el ejemplo asi que decidi ponerlo en practica ya que estoy empezando con el .net, pero tengo el siguiente error.

PatronCreacional.ClienteFactory does not contain a constructor that takes '1' arguments

Y este el codigo:


public class Cliente
{
private int _clie_codigo;
private string _clie_nombres;
private string _clie_apellidos;
private string _clie_identifi;

public int clie_codigo
{
get { return _clie_codigo; }
set {_clie_codigo = value; }
}
public string clie_nombres
{
get { return _clie_nombres; }
set { _clie_nombres = value; }
}
public string clie_apellidos
{
get { return _clie_apellidos; }
set { _clie_apellidos = value; }
}
public string clie_identifi
{
get { return _clie_identifi; }
set { _clie_identifi = value; }
}

public Cliente() {}
}

public class ClienteFactory
{
public static Cliente Create(string cedula)
{
string strCnn = "";
string strSql = "";
Cliente cliente = new Cliente();
Configuracion config = new Configuracion();
strCnn = config.strCnn;
strSql = "SELECT clie_codigo,clie_nombres,clie_apellidos FROM mark_cliente WHERE clie_identifi = '" + cedula + "'";
using (SqlConnection cnn = new SqlConnection(strCnn))
{
cnn.Open();
SqlCommand cmd = new SqlCommand(strSql, cnn);
using (SqlDataReader reader = cmd.ExecuteReader())
{
while (reader.NextResult())
{
cliente.clie_codigo = (int)reader["clie_codigo"];
cliente.clie_nombres = (string)reader["clie_nombres"];
cliente.clie_apellidos = (string)reader["clie_apellidos"];
cliente.clie_identifi = (string)reader["clie_identifi"];
}
reader.Close();
}
cnn.Close();
}
return cliente;
}
}



La pregunta es cual es mi error porque no funciona, estoy utilizando Visual Studio Team 2008.

LgEaObNrAiReDlO dijo...

Gracias por tu comentario,
Imagino que estás haciendo algo como new ClienteFactory("algo") porque el error indica que está intentando instanciar un objeto ClienteFactory con un constructor con un parámetro cosa que no existe, pasá el código completo que lo vemos. Saludos, Leonardo

Unknown dijo...

Este es el código completo solo faltaba la parte donde instancio a la clase ClienteFactory.


private void btnTraerPersona_Click(object sender, EventArgs e)
{
ClienteFactory cliente = new ClienteFactory("1716061658");
}
}

public class Cliente
{
private int _clie_codigo;
private string _clie_nombres;
private string _clie_apellidos;
private string _clie_identifi;

public int clie_codigo
{
get { return _clie_codigo; }
set {_clie_codigo = value; }
}
public string clie_nombres
{
get { return _clie_nombres; }
set { _clie_nombres = value; }
}
public string clie_apellidos
{
get { return _clie_apellidos; }
set { _clie_apellidos = value; }
}
public string clie_identifi
{
get { return _clie_identifi; }
set { _clie_identifi = value; }
}

public Cliente() {}
}

public class ClienteFactory
{
public static Cliente Create(string cedula)
{
string strCnn = "";
string strSql = "";
Cliente cliente = new Cliente();
Configuracion config = new Configuracion();
strCnn = config.strCnn;
strSql = "SELECT clie_codigo,clie_nombres,clie_apellidos FROM mark_cliente WHERE clie_identifi = '" + cedula + "'";
using (SqlConnection cnn = new SqlConnection(strCnn))
{
cnn.Open();
SqlCommand cmd = new SqlCommand(strSql, cnn);
using (SqlDataReader reader = cmd.ExecuteReader())
{
while (reader.NextResult())
{
cliente.clie_codigo = (int)reader["clie_codigo"];
cliente.clie_nombres = (string)reader["clie_nombres"];
cliente.clie_apellidos = (string)reader["clie_apellidos"];
cliente.clie_identifi = (string)reader["clie_identifi"];
}
reader.Close();
}
cnn.Close();
}
return cliente;
}
}