Estaba escribiendo unos DAOs y noté que tenía mucho código repetido. Y como soy un gran creyente del principio DRY. Me puse a buscarle la vuelta.
Este es mi código original.
public class PacienteDAO : BaseDAO<Paciente>, IPacienteDAO
{
private const string QUERY_SELECT = "SELECT * FROM [dbo].[Paciente]";
public Paciente Load(int id)
{
string query = QUERY_SELECT + " WHERE Id = @Id";
IDbParametersBuilder builder = CreateDbParametersBuilder();
builder.Create().Name("Id").Type(DbType.Int32).Value(id);
return LoadOne(query, new PacienteRowMapper(), builder.GetParameters());
}
public IList<Paciente> LoadAll()
{
this.Log.Debug("Getting all from Paciente");
return LoadList(QUERY_SELECT, new PacienteRowMapper(), null);
}
// (...)
}
public class HospitalDAO : BaseDAO<Hospital>, IHospitalDAO
{
private const string QUERY_SELECT = "SELECT * FROM [dbo].[Hospital]";
public Hospital Load(int id)
{
string query = QUERY_SELECT + " WHERE id = @Id";
IDbParametersBuilder builder = CreateDbParametersBuilder();
builder.Create().Name("Id").Type(DbType.Int32).Value(id);
return LoadOne(query, new HospitalRowMapper(), builder.GetParameters());
}
public IList<Hospital> LoadAll()
{
this.Log.Debug("Getting all from Hospital");
return LoadList(QUERY_SELECT, new HospitalRowMapper(), null);
}
// (...)
}
Estás 2 clases tienen mucho código en común. Además, si después tengo que agregar un DAO para otra entidad tendré que volver a copiar y pegar todo. Lo ideal sería que yo sólo diga la entidad de la que quiero tener un DAO.
Se me ocurrió generalizar un poco el caso haciendo un DAO que trate con una entidad genérica. Para eso necesito:
- Una clase entidad. Porque no quiero persistir cualquier clase. Además me permite agrupar las entidades y manejarlas con polimorfismo si lo necesito.
- Escribir un DAO genérico que sirva para cualquier entidad.
- Escribir un DAO particular para cada entidad. Este sólo definirá cosas particulares de esa clase.
Paso 1
Es fácil, sólo debo agrupar las características de las entidades en una clase. En mi caso es que tengan un Id.
public abstract class Entity
{
public int Id { get; set; }
}
La hago abstracta porque me parece que no tiene sentido instanciar una entidad genérica.
Luego, las entidades particulares quedan así.
public class Hospital : Entity
{
public string Direccion { get; set; }
}
// (...)
public class Investigador : Entity
{
public string Nombre { get; set; }
}
Paso 2
Esta es la parte donde tuve que pensar. Lo que hice fue recorrer uno de los DAOs originales e ir cambiando todas las apariciones de la entidad concreta por la entidad genérica.
public abstract class EntityDAO<EntityType> : BaseDAO<EntityType> where EntityType : Entity
{
public EntityType Load(int id)
{
string query = this.BuildSelectQuery() + " WHERE id = @Id";
IDbParametersBuilder builder = CreateDbParametersBuilder();
builder.Create().Name("Id").Type(DbType.Int32).Value(id);
return LoadOne(query, this.GetRowMapper(), builder.GetParameters());
}
public IList<EntityType> LoadAll()
{
this.Log.Debug(string.Format("Getting all from {0}", this.GetEntityName()));
return LoadList(this.BuildSelectQuery(), this.GetRowMapper(), null);
}
protected abstract string GetEntityName();
protected abstract IRowMapper<EntityType> GetRowMapper();
protected virtual string BuildSelectQuery()
{
return string.Format("SELECT * FROM [dbo].[{0}]", this.GetEntityName());
}
}
Acá hay varias cosas que me gustaría que se noten.
- Estoy usando una clase genérica EntityType, la costumbre es llamar a la clase con nombres como T o Type. Yo prefiero llamarla así porque me parece que ayuda a que el código se lea mejor.
- EntityType no es cualquier clase. Con poner where EntityType : Entity la fuerzo a que sea una entidad.
- Agregué 2 métodos abstractos que representan la información que es particular de cada entidad. Es responsabilidad de cada implementación de EntityDAO definirlos.
- El método BuildSelectQuery es virtual, lo que me permite cambiarlo si hay algún caso particular.
Paso 3
Ahora se puede ver dónde está el beneficio. Crear los DAOs particulares es fácil y muy extensible.
public class HospitalDAO : EntityDAO<Hospital>, IHospitalDAO
{
protected override string GetEntityName()
{
return "Hospital";
}
protected override IRowMapper<Hospital> GetRowMapper()
{
return new HospitalRowMapper();
}
protected override void CreateParameters(Hospital hospital, IDbParametersBuilder builder)
{
builder.Create().Name("Direccion").Type(DbType.String).Value(hospital.Direccion);
}
}
Algunas notas finales
En el ejemplo tengo un RowMapper para cada entidad, pero podría armar algún mecanismo con reflection para tener uno genérico que sirva para todos.
Una de las ventajas de tener este esquema es que; es menos probable que yo me equivoque creando un nuevo DAO e implementando 3 métodos, que copiando una clase más grande y modificando los nombres de cada entidad.
En realidad no lo comenté, pero esto funciona porque mi entidad se llama igual que la tabla. Si bien es normal, puede no ser el caso. Si no lo es, podemos separa el método GetEntityName en GetEntityName y GetTableName. Luego usamos cada uno donde corresponda en el EntitiDAO.
La mejor ventaja para mi es que si tenemos algún comportamiento particular del DAO de una entidad, como por ejemplo buscar hospitales por dirección, podemos agregarla al DAO particular y no se mezcla con otro código común de persistencia de cualquier entidad.
En realidad este ejemplo está simplificado. En el ejemplo real no sólo hay una consulta SELECT, también tengo INSERT y UPDATE pero es análogo a lo que hice acá.
Saludos.