Combinando herencia, restricciones y tipos genéricos de datos

Buenas a todos,

Os voy a contar como he combinado estos conceptos que nos proporciona C# para solucionar un requerimiento que se me ha presentado en uno de los proyectos en los que trabajo. El requerimiento era el siguiente:

Partiremos de una clase genérica con métodos cuyo tipo de valor devuelto es el de la propia clase y queremos que, el tipo de valor devuelto en los mismos métodos de las clases derivadas, sea el tipo de la propia clase derivada

Para aclarar este requerimiento, os lo explico un poco más. En mi proyecto, quiero abstraer toda la capa de acceso a datos y para ello he creado una clase base «modelo» de la que heredarán los distintos modelos del mismo. Dicha clase base tiene métodos de la siguiente forma.

class modelo{
    modelo FindOne(String query)
    ...
}

Y quiero poder definir clases derivadas (los modelos) de manera que pueda hacer lo siguiente

class modelo1 : modelo{}
class modelo2 : modelo{}

public class Main {
    public static int Main() {
        modelo1 m1 = new modelo1();
        modelo1 x = m1.FindOne("query");

        modelo2 m2 = new modelo2();
        modelo2 x2 = m2.FindOne("query");
    }
}

El objetivo, es crear un código más legible, sencillo de entender y estructurado. Evidentemente, aquí partimos del uso de las herencias de clase que nos proporcionan los lenguajes de programación orientada a objetos.

http://msdn.microsoft.com/es-es/library/ms173149.aspx
http://msdn.microsoft.com/es-es/library/ms228387(v=vs.90).aspx

Se nos puede ocurrir, junto al concepto de herencia, usar override para sobre-escribir los métodos, de la siguiente forma.

public class modelo
{
    public abstract modelo FindOne(string query);
}

public class modelo1 : modelo
{
    public override modelo1 FindOne(string query){}
}

public class modelo2 : modelo
{
    public override modelo2 FindOne(string query){}
}

Esta aproximación, a parte de que nos es muy elegante, no es posible, porque no se pueden sobre-escribir métodos cambiando únicamente el valor devuelto en C#, ya que este no forma parte de la firma del método y el compilador tendría problemas para interpretar lo que se ha indicado y resolverlo adecuadamente.

Para poder cumplir el requerimiento que os he comentado, vamos a hacer uso de los Tipos de Datos Genéricos y Restricciones que nos proporciona C#, lo que nos proporcionará la solución posible.

  • Tipos de Datos Genéricos: Nos permite definir parámetros en métodos y propiedades de clases cuyo tipo va a ser especificado en el momento de instanciar un objeto de dicha clase. Toda la información detallada sobre tipos genéricos a continuación.
    http://msdn.microsoft.com/en-us/library/512aeb7t.aspx
  • Restricciones: Nos permiten, cuando definimos una clase base, establecer una serie de reglas para controlar la variedad de tipos que pueden ser usados como tipos de argumentos al instanciar dicha clase. Si no se cumplen esas reglas obtendremos un error en tiempo de compilación. El enlace de MSDN donde se detalla esto, os lo dejo a continuación.
    http://msdn.microsoft.com/en-us/library/d5x73970.aspx

A continuación os muestro el código que he implementado para la solución y que combina estos dos conceptos:

public abstract class modelo<T> where T : modelo<T>
{
    public T FindOne(string query)
    {
        //Do something

        return (T)this;
    }
}

public class modelo1 : modelo<modelo1>
{
}

public class modelo2 : modelo<modelo2>
{
}

De esta forma conseguimos lo que queríamos inicialmente. Podemos usar la misma función para las clases derivadas y que en cada caso devuelvan un objeto del mismo tipo de la clase derivada que llama a la misma, sin necesidad de reescribir código, y usando la función definida en la clase base.

Un saludo, espero que os resulte útil y como siempre, cualquier sugerencia o mejora, espero que me la hagáis llegar.