Implementación de Polimorfismo en C# a través de la sobrecarga de métodos.

 

Fecha: 05/Ago/2005 (01 de Agosto de 2005)
Autor: Jorge Barrientos (jbarrientos@tutopia.com)

 


El presente articulo pretende ayudar a entender de la manera más sencilla y practica una de las técnicas con las que podemos implementar polimorfismo en nuestras clases creadas en .Net, como lo es la sobrecarga de métodos.
El lenguaje utilizado para los ejemplos es C# sin embargo estos son fáciles de traducir a código Visual Basic.
En primer lugar debemos comprender que el polimorfismo es una de las características mas poderosas e importantes de la programación orientada a objetos.
La palabra polimorfismo es una palabra que significa “múltiples formas”, por lo tanto, podríamos definir en el ámbito de programación que el polimorfismo es “la capacidad de poder enviar un mismo mensaje a objetos totalmente diferentes, y que cada uno de estos objetos responda de manera diferente”.
Antes que nada debemos comprender el concepto de clase base y clase derivada, la primera podríamos definirla como el padre y la segunda como el hijo que hereda del padre todos sus atributos y métodos a excepción de los definidos como privados (private), esto constituye lo que se denomina herencia de clase, que es la capacidad de poder agregar funcionalidad a una clase de la cual se hereda o extiende.
¿Que es sobrecarga de métodos?
Podemos definir la sobrecarga de métodos, como la reescritura de un método de una clase base en una clase derivada.
Para comenzar a entender como funciona la sobrecarga de métodos iniciaremos creando una clase base denominada Cuenta, que nos servirá de clase base para crear otras clases que heredaran de esta, las cuales veremos más adelante.

public class Cuenta
{
    private string idCuenta; // Número de la cuenta

    public Cuenta(string prmtIdCuenta)
    {
        this.idCuenta = prmtIdCuenta;
        System.Console.WriteLine(
        "Instancia creada para cuenta {0}",prmtIdCuenta);
    }

    public void CalcularIntereses()
    {
        System.Console.WriteLine("Cuenta.CalcularIntereses()");
    }
}

Como podemos apreciar la clase Cuenta es muy sencilla pero posee lo necesario para nuestros propósitos, posee un atributo o dato de uso privado denominado idCuenta, posee un constructor que recibe como parámetro el id de la cuenta (prmtIdCuenta), el constructor nada mas asigna el parámetro al atributo idCuenta y escribe una línea en la consola que notifica la creación de una nueva instancia Cuenta.
Además posee un método denominado CalcularIntereses() el cual escribirá en la consola el nombre del método, esto nos servirá para ubicarnos y saber que método estamos utilizando.

Para empezar a comprender mejor sobre la sobrecarga, crearemos una nueva clase que hereda de la clase base Cuenta, y a esta nueva clase la denominaremos CuentaCorriente, la idea es poder crear clases para un sistema bancario hipotético, este banco operara con cuentas corrientes (Cheques) y de ahorros, para ambos casos los criterios o reglas del negocio para el calculo de intereses son diferentes, mientras para las primeras los intereses se realizan a favor del banco debido a sobregiros o en algunos casos (la excepción no la regla) pueden ser a favor del cliente, mientras que para las cuentas de ahorro será un calculo a favor del cliente, no entraremos en detalles sobre estos procesos ya que no es la idea del articulo, pero es un buen esquema que nos ayudara a comprender de manera fácil y practica lo que estamos estudiando.
A continuación se detalla la definición de las clases CuentaCorriente y CuentaAhorro:

public class CuentaCorriente : Cuenta
{
    public CuentaCorriente(string prmtIdCuenta) : base(prmtIdCuenta)
    {
    }

    new public void CalcularIntereses()
    {
        System.Console.WriteLine(
        "CuentaCorriente.CalcularIntereses()");
    }
}

public class CuentaAhorro : Cuenta
{
    public CuentaAhorro(string prmtIdCuenta) : base(prmtIdCuenta)
    {
    }

    new public void CalcularIntereses()
    {
        System.Console.WriteLine(
        "CuentaAhorro.CalcularIntereses()");
    }
}


Ahora exploraremos el código de la clase CuentaCorriente, en primer lugar podemos identificar que esta hereda de la clase Cuenta, también podemos apreciar un constructor que invoca el constructor de la clase base con el parámetro que contiene el número de cuenta.
También podemos apreciar la sobrecarga o sobre escritura del método CalcularIntereses() y podemos identificar el uso de la palabra new, esta es necesaria para la definición del método de la clase derivada, si se omite no se registraran errores de compilación ni de ejecución debido a que implícitamente es definido como default, sin embargo el compilador subrayara el método y el IntelliSence enviara un warning advirtiendo que la palabra new es requerida (The keyword new is required on 'winAppTest.CuentaCorriente.CalcularIntereses()' because it hides inherited member 'winAppTest.Cuenta.CalcularIntereses()’), por lo tanto es buena practica siempre utilizarla.
La misma explicación aplica para la clase CuentaAhorro.
Ahora estamos en condiciones de poder realizar pruebas y ver como funcionan la sobrecarga en .Net.
Para realizar nuestras pruebas crearemos una clase de testeo, denominada Test:

public class Test
{
    const string CUENTA = "100";

    public static void Main(string[] args)
    {
        // Creamos instancia de clase base
        Cuenta cuenta = new Cuenta(CUENTA);
        // Creamos instancia de clase derivada
        CuentaCorriente cuentaCorriente =
        new CuentaCorriente(CUENTA);
        // Creamos instancia de clase derivada
        CuentaAhorro cuentaAhorro =
        new CuentaAhorro(CUENTA);
        // Invocamos método de la clase base
        cuenta.CalcularIntereses();
        // Invocamos método de la clase derivada
        cuentaCorriente.CalcularIntereses();
        // Invocamos método de la clase derivada
        cuentaAhorro.CalcularIntereses();
    }
}

Ahora explicaremos este primer ejemplo, en el método Main podemos apreciar que creamos tres instancias a partir de las tres clases que hemos creado anteriormente, un objeto de la clase Cuenta (cuenta) y otros para la clases CuentaCorriente (cuentaCorriente) y CuentaAhorro (cuentaAhorro), para efectos de pruebas tenemos una constante para el número de la cuenta que se utiliza como parámetro para crear ambas instancias. Una vez creados los tres objetos invocamos el método CalcularIntereses() de todos los objetos. Al ejecutar esta clase obtenemos el siguiente resultado:

Constructor Clase Base para cuenta 100
Constructor Clase Base para cuenta 100
Constructor Clase Base para cuenta 100
Cuenta.CalcularIntereses()
CuentaCorriente.CalcularIntereses()
CuentaAhorro.CalcularIntereses()

Las primeras tres líneas escritas en la consola son las que el constructor de la clase base imprime en la consola (esto es debido a que las clases derivadas al momento de instanciarse ejecutan el método de la clase base), luego la tercera nos indica que el método que se ejecuta es el de la clase base Cuenta y por último tenemos la ejecución del método CalcularIntereses() de las clases derivadas.
En este caso no hay mucho problema y trabaja bien siempre y cuando se tenga una referencia al objeto derivado, sin embargo cuando se tiene que realizar una referencia a una clase derivada a partir de una clase base, como se puede ejecutar un método de la clase derivada y evitar la ejecución del método de la clase base?, esto lo veremos a continuación.
Ahora desarrollaremos cambios a las clases, simularemos un proceso unificado para calcular intereses tanto de cuentas corrientes como de ahorro. Para ello tendremos un proceso principal que simulara la carga de las cuentas en un arreglo y otro que procesara el cálculo de intereses de cada cuenta.
La clase Cuenta quedara después de los cambios como se detalla a continuación:


public class Cuenta
{
    private string idCuenta;

    public Cuenta(string prmtIdCuenta)
    {
        this.idCuenta = prmtIdCuenta;
        System.Console.WriteLine(
        "Constructor Clase Base para cuenta {0}", prmtIdCuenta);
    }

    public void CalcularIntereses()
    {
        System.Console.WriteLine(
        "Cuenta.CalcularIntereses() efectuado para la cuenta {0}",
        this.idCuenta);
    }

    public string getIdCuenta()
    {
        return this.idCuenta;
    }
}

Para las clases CuentaCorriente y CuentaAhorro, solo se haran cambios al método CalcularIntereses():

// CuentaCorriente
new public void CalcularIntereses()
{
    System.Console.WriteLine(
    "CuentaCorriente.CalcularIntereses() efectuado para " +
    "la cuenta {0}",    getIdCuenta());
}

// CuentaAhorro
new public void CalcularIntereses()
{
    System.Console.WriteLine(
    "CuentaAhorro.CalcularIntereses() efectuado para " +
    "la cuenta {0}",    getIdCuenta());
}

Ahora crearemos una clase de prueba denominada TestPolimorfismo, la cual debe quedar de la siguiente forma:


public class TestPolimorfismo
{
    protected Cuenta[] cuentas;

    public static void Main(string[] args){
        TestPolimorfismo test = new TestPolimorfismo();
        test.cargaCuentas();
        test.efectuarCalculoIntereses();
    }

    public void cargaCuentas()
    {
        // Este proceso deberia de cargar las cuentas 
        // de alguna base de datos.
        cuentas = new Cuenta[2];
        //
        cuentas[0] = new CuentaCorriente("100");
        cuentas[1] = new CuentaAhorro("200");
    }

    public void efectuarCalculoIntereses()
    {
        for(int i = 0;i < cuentas.GetLength(0); i++)
        {
            cuentas[i].CalcularIntereses();
        }
    }
}

Podemos apreciar que el método Main ejecuta el método de carga de las cuentas (cargaCuentas) y posteriormente el de calculo de Intereses (efectuarCalculoIntereses). Podemos observar que el método de carga de las cuentas crea dos instancias, una de la clase CuentaCorriente y otra de la clase CuentaAhorro.
Sin embargo si ejecutamos la clase TestPolimorfismo el resultado es el siguiente:

Constructor Clase Base para cuenta 100
Constructor Clase Base para cuenta 200
Cuenta.CalcularIntereses() efectuado para la cuenta 100
Cuenta.CalcularIntereses() efectuado para la cuenta 200

Esta ejecutando el método CalcularIntereses() de la clase base, algo que definitivamente no se desea, a este fenómeno se le denomina en ingles como early binding. Cuando el código compila, el compilador de C# observa la llamada al método CalcularIntereses() y determina la dirección en memoria necesaria para llegar cuando se ejecute la llamada. En nuestro caso será la ubicación en memoria del método Cuenta.CalcularIntereses().
Podemos determinar entonces que la llamada al método Cuenta.CalcularIntereses() es el problema que tenemos.
Asi como existe un early binding, existe un late binding que permite que el compilador seleccione el método a ejecutar hasta que este se ejecute y no se determine en tiempo de compilación.
Para forzar al compilador a que utilice el late binding, o que ejecute un método de una clase derivada a partir de una clase base, se utilizan dos palabras claves: virtual y override. Se debe usar virtual en el método de la clase base y override se debe colocar en la clase derivada.
Por lo tanto para que nuestro ejemplo funcione, debemos de colocar el calificador virtual al método ClacularIntereses() de nuestra clase base Cuenta, y en las clases derivadas se le debe agregar el calificador override en la implementación del mismo método.
Los métodos en todas las clases deben quedar como sigue:

// Cuenta
public virtual void CalcularIntereses()
{
    System.Console.WriteLine(
    "Cuenta.CalcularIntereses() efectuado para la cuenta {0}",
    this.idCuenta);
}

// CuentaCorriente
public override void CalcularIntereses()
{
    System.Console.WriteLine(
    "CuentaCorriente.CalcularIntereses() efectuado para " +
    "la cuenta {0}",    getIdCuenta());
}

// CuentaAhorro
public override void CalcularIntereses()
{
    System.Console.WriteLine(
    "CuentaAhorro.CalcularIntereses() efectuado para " +
    "la cuenta {0}",    getIdCuenta());
}

Con este simple cambio, ejecutemos nuevamente la clase TestPolimorfismo, y obtendremos el resultado siguiente:

Constructor Clase Base para cuenta 100
Constructor Clase Base para cuenta 200
CuentaCorriente.CalcularIntereses() efectuado para la cuenta 100
CuentaAhorro.CalcularIntereses() efectuado para la cuenta 200

Que es el resultado que esperábamos.

Buenos amigos, espero que estos pequeños ejemplos les sirvan, como me ha servido a mi a comprender lo versátil y dinámica que puede ser la programación orientada a objetos, utilizando la característica del polimorfismo, sino la más fácil de entender, personalmente la considero la más poderosa.
Espero pronto escribir un poco más sobre el tema, principalmente sobre la definición e implementación de interfaces, tema también relacionado con el polimorfismo.

Hasta luego y no olvides calificar este articulo.

Suerte.

Atte.

Jorge Barrientos (jbarrientos@tutopia.com)


ir al índice del Guille