lunes, 7 de noviembre de 2011

Builders genéricos: Introducción (1)

Este post está basado en una publicación que encontré en Codeproject sobre el patrón Builder.

Es interesante ver el uso que el autor se le da a los Generics en C# para crear una clase que recibirá como parámetro genérico a la misma clase. Suena un poco confuso pero es bastante simple. Esto es muy común en programas que utilizan configuración Fluent en la cual un objeto, al llamarse uno de sus métodos, debe devolverse a sí mismo.

Veamos un ejemplo:

Si tuviesemos una clase llamada Configuration como la siguiente:

public class Configuration
{
    protected Dictionary<string, string> properties;
    public Configuration SetName(string name)
    {
        properties["Name"] = name;
        return this;
    }
    public Configuration Configure()
    {
        foreach (var item in properties)
        {
            System.Console.WriteLine("{0}: {1}", item.Key, item.Value);
        }
        return this;
    }
}

Por Fluent se configuraría de la siguiente manera:

Configuration config = new Configuration()
                .SetName("MyConfiguration")
                .Configure();

Ahora supongamos que queremos extender esta misma clase para poder configurar una Base de datos que necesita de un Connection String. La configuración sería la siguiente:

DBConfiguration config = new DBConfiguration()
                .SetName("MyConfiguration")
                .SetConnStringName("db1")
                .Configure();

Entonces la clase DBConfiguration debería quedar así:

public class DBConfiguration : Configuration
{
    public virtual DBConfiguration SetConnStringName(string connStringName)
    {
        properties["ConnStringName"] = connStringName;
        return this;
    }
}

Sin embargo tenemos los siguientes 2 errores al compilar:

'Pruebas.Configuration' does not contain a definition for 'SetConnStringName' and no extension method 'SetConnStringName' accepting a first argument of type 'Pruebas.Configuration' could be found (are you missing a using directive or an assembly reference?)

Cannot implicitly convert type 'Pruebas.Configuration' to 'Pruebas.DBConfiguration'. An explicit conversion exists (are you missing a cast?)

Estos errores aparecen porque estamos intentando llamar a un método de "DBConfiguration" cuando en realidad SetName devuelve un objeto de tipo "Configuration". Lo mismo sucede al intentar setear resultado a la variable config.

Un posible Fix a esto sería hacer "new" de los métodos SetName y Configure:

public new DBConfiguration SetName(string name)
{
    return (DBConfiguration)base.SetName(name);
}
public new DBConfiguration Configure()
{
    return (DBConfiguration)base.Configure();
}

En este punto, nuestro código ya compila y hace lo que debe hacer, sin embargo tuvimos que ocultar la implementación de la clase que heredamos y la volvimos a escribir. Es decir, la herencia de clases no nos sirvió para mucho.

¿Y si usamos Generics cómo podríamos resolverlo?

Fácilmente podríamos generar la siguiente clase abstracta:

public abstract class Configuration<TConfiguracion>
    where TConfiguracion : Configuration<TConfiguracion>
{
    protected Dictionary<string, string> properties;
    public TConfiguracion SetName(string name)
    {
        properties["Name"] = name;
        return (TConfiguracion)this;
    }
    public TConfiguracion Configure()
    {
        foreach (var item in properties)
        {
            System.Console.WriteLine("{0}: {1}", item.Key, item.Value);
        }
        return (TConfiguracion)this;
    }
}


Con la definición anterior lo que estamos diciendo es que la clase "Configuration<TConfiguration>" recibirá como tipo genérico una clase que hereda de "Configuration<TConfiguration>".

De esta manera, las clases Configuration (sin parámetros) y DBConfiguration simplemente quedarían así:

public class Configuration : Configuration<Configuration>
{
}

public class DBConfiguration : Configuration<DBConfiguration>
{
    public DBConfiguration SetConnStringName(string connStringName)
    {
        properties["ConnStringName"] = connStringName;
        return this;
    }
}


Ahora el código, aparte de compilar, es mucho más sencillo de extender. En un principio les puede costar entender esta sentencia: "class Configuration : Configuration<Configuration>", pero lo que debemos tener en cuenta es que "Configuration" y "Configuration<TConfiguration>" son en realidad dos clases diferentes.

Bueno, como este post me quedó un poco largo, lo dejaré como introducción al tema y dejaré mi explicación de los Builders en la segunda parte.

Cualquier duda o sugerencia es bienvenida.

2 comentarios:

  1. Felicitaciones Julián por el nuevo blog, esta muy bueno lo que estás posteando :)

    ResponderEliminar
  2. Gracias Jorge. Anteayer estuve pasando por tu Blog y me tomé prestadas algunas ideas que estaban buenas también. Voy a ver si me hago tiempo para seguir subiendo más cosas. Saludos!

    ResponderEliminar