Polimorfismo y Function Overloading

Objetivos

  • Presentar el concepto de polimorfismo.
  • Comprender que es bastante mas sencillo e intuitivo de lo que semejante nombre parece indicar.
  • Conocer el concepto de sobrecarga de funciones o Function Overloading.
  • Modificar la clase Contador para incluir alguna de estas ideas.  
  •  

    Material requerido.

    Imagen de Arduino UNO Arduino Uno o similar.

     

    Paz Interior Mucha paz interior

    Centrando ideas

     

    Con el título que tiene esta sesión, probablemente vamos a leerla 4 gatos, y sin embargo Polimorfismo es un nombre extraño para un concepto muy sencillo que nos resulta natural de entender. Ya hablamos algo de ello en las sesiones anteriores, pero vamos a tocar el tema un poco más en profundidad en esta (Sin miedo que no hay para tanto)

    Y es que resulta que lleváis usando aspectos del concepto de Polimorfismo con desenvoltura, poco menos desde los primeros días que empezasteis a programar vuestros Arduinos.

    ¿Qué no? Ya lo creo que sí, pero no os habéis dado cuenta porque el concepto es tan natural que ni siquiera solemos percibirlo, salvo haciendo un esfuerzo mental.

    Y para que veáis que no os engaño vamos a empezar con algunos casos que deberían haberos disparado todas las alarmas y que sin embargo, os han parecido completamente normales desde el minuto uno.

    Quizás empezando así, os daréis cuenta de que aunque no sabías como se llamaba, me creeréis cuando os digo que habéis estado usando el Polimorfismo de un modo natural desde que empezasteis con Arduino C++.

    Por ejemplo lleváis mucho tiempo usando la función Serial.println(), que no es nada de sospechosa de veleidades extravagantes y sin embargo tiene un comportamiento sorprendente . ¿No veis nada raro en estas líneas?

    Serial.println( 5) ;
    Serial.println( 3.1416 ) ;
    Serial.println(“ Buenos días”) ;

    Insisto, ¿No veis nada sospechoso ahí? Eso es porque estáis tan acostumbrados a ello que no es fácil ver la trampa.

    Según lo que hemos aprendido hasta ahora, una función solo puede aceptar un tipo definido de parámetros. ¿Qué demonios es eso, de pasar a una función un int un float o un String según se me ocurra?

    ¿Si el parámetro es int…Porque me acepta que le pase un float o String? Aquí está pasando algo raro. ¿Por qué nuestro compilador, siempre tan amable el, no nos devuelve un ladrido diciendo que te den?

    Lo lleváis haciendo desde siempre pero es imposible, ¿Por qué funciona? ¿Serias capaz de programar una función así? ¿A que no?

    Y eso, queridos amigos, en una función que habéis estado usando hasta hartaros sin pensar ni por un momento que era imposible (¿Creías que os engañaba?)

    El misterio está precisamente en una característica inherente a C++ y que no existía en C, y no es otra que una característica  llamada function overloading.

     

    Function Overloading

     

    Ahora que he conseguido tu atención, podemos empezar a hablar en serio del Polimorfismo y de porque los println() anteriores funcionan, aunque todo indica que no deberían, porque va en contra de todo lo que hemos aprendido hasta ahora de las funciones.

    Y el misterio está en que no existe una única función println(), sino que las líneas anteriores invocan 3 funciones completamente diferentes… que se llaman igual.

    ¡ Queee ¡ ¡Venga ya!

    Normalmente aquí aparecen frases del tipo: “Todo el mundo sabe que dos funciones distintas no pueden llamarse igual, lo mismo que dos variables diferentes no pueden tener el mismo nombre”.

    Veamos. Si intento algo así:

    int var = 0 ;
    String var = "Buenos dias" ;

    El compilador enseguida me pone firme y parece estar de acuerdo con la idea general:

    Mensaje de error

    Pero hagamos un intento diferente. Imaginaros una función llamada Duplica() que si le paso un int me devuelve el doble claro, y lo mismo se le paso un float. Pero imagínate que quiero que si le paso un String me devuelva otro String con la cadena inicial duplicada, ¿Parece natural, No? Fíjate que hasta en la redacción de este párrafo no hago diferencia entre ambas ideas.

    Pero… ¿Y el compilador que va a decir? Veamos, intentemos definir tres funciones así: Prog133_1

     int Duplica( int j)
         { return (2 * j) ; }
    
    float Duplica ( float n)
         {   return( 2 * n) ;   }
    
    String Duplica( String s)
         {  return ( s + s) ;   }

    Y luego podemos intentar esto:

      Serial.println(Duplica(5));
      Serial.println(Duplica(3.1416 )) ;
      Serial.println(Duplica("Hola."));

    Lo lógico es que el compilador diga que ni de coña se traga esto. “Las funciones tienen que llamarse distinto y punto”. Pero resulta que no, ya ves. Siempre consiguen sorprendernos:

    Salida a consola

    Resulta que el compilador de C++ (Que no el de C) acepta que diferentes funciones tengan el mismo nombre, a condición inexcusable de que él pueda diferenciarlas implícitamente por el numero o tipo de parámetros que requiere cada una, lo que suele llamarse firma. ¿Qué te parece?

    A esta capacidad de definir varias funciones diferentes con el mimsos nombre, se le llama Function Overloading o sobrecarga de funcionesy de cada una de las funciones de igual nombre decimos que están sobrecargadas (Overloaded).

    A pesar de tan extravagante comportamiento y de algo que nos parece tan extraño al primer bote, llevas mucho tiempo usándolo y te parece tan normal, porque nuestro cerebro abstrae los conceptos mayores y le parece normal que el ordenador haga esto (Aunque nos suele dejar flasheados descubrirlo)

    De hecho la sobrecarga (Overloadding) de funciones es una operación tan intuitiva que nos permite desarrollar programas mucho más sencillos y menos proclives a error.

  • De no existir el Overloading, la función println() necesitaría al menos 3 funciones en sus lugar: Una para enteros, otra para float, otra apara Strings. Pero recuerda que también hay Bytes, Uints, longs, doubles y ….
  •  

    En cuanto nos recuperemos de la impresión sufrida, empezaremos a preguntarnos que si se puede hacer Overloading  de funciones … ¿Hay más cosas con las que se pueda hacer?

    Y la respuesta es que si, y os habéis hartado a usarlo sin daros cuenta tampoco. ¿Adivináis que puede ser? Os doy una pista: En el último programa usamos el Overloading de algo más que las funciones, pero de esto hablaremos en la próxima sesión.

    De momento quiero volver a la clase Contador que definimos en la sesión previa, para darle más vueltas.

     

    Jugando con la Clase Contador

     

    En nuestra última sesión estuvimos jugando con una pequeña clase ejemplo que llamamos Contador. La definimos así:

    class Contador
      {   private:
             int N ;
    
          public:
             Contador( ) ;               // Constructor
             void SetContador( int n) ;
             void Incrementar() ;
             int GetCont() ;
      } ;

    Y luego definimos sus funciones miembros o Métodos.

          Contador::Contador( )                // Constructor
                 { N = 0 ; }
    
          void Contador::SetContador( int n)
                 {  N = n ;    }
    
          void Contador::Incrementar()
                 {  N++ ; }
    
          int Contador::GetCont()
                 { return (N) ;}

    Bien, a lo nuestro. No está mal para ser nuestra primera Clase, pero es manifiestamente mejorable. Por ejemplo, todos nuestros contadores se ponen a cero mediante el Constructor, lo que ha sido una mejora con respecto a tener que inicializarlo a mano, pero… ¿Qué hago si necesito un contador que empiece en digamos 129 o cualquier otro valor, claro?

    Puedo usar el método SetContador(), pero nuestros amigos nos mirarán con desprecio por usar semejante solución, así que hay que discurrir algo más.

    La solución elegante y que hará suspirar a los freakys de tus colegas es hacer un Overloading del Constructor, que lo acepta sin rechistar como cualquier otra función.

    class Contador
       {  private:
             int N ;
    
          public:    
             Contador( ) ;               // Constructor
             Contador( int k ) ;         // Constructor
             void SetContador( int n) ;
             void Incrementar() ;
             int GetCont() ;
       } ;

    Y las funciones miembros podrían ser así:

          Contador::Contador( )              // Constructor
               { N = 0 ; }       
    
          Contador::Contador( int k)         // Constructor
               { N = k ; }   
    
          void Contador::SetContador( int n)
               {  N = n ;    }  
    
          void Contador::Incrementar()
               {  N++ ; } 
    
          int Contador::GetCont()
               { return (N) ;}

    Hemos hecho un Overloading del Constructor de la Clase, Que dicho así suena muy raro, pero que traducido significa, que podemos declarar dos Constructores diferentes siempre y cuando le pasemos diferente firma parámetros (En numero o tipo). Si hacemos dos constructores, uno sin parametros y otro que acepte un int: Prog133_2

    Contador C1, C2(23) ;
    
    void loop() 
    {    
         C1.Incrementar() ;
         Serial.print("C1 = "); Serial.println(C1.GetCont());
        
         C2.Incrementar() ; C2.Incrementar() ; C2.Incrementar() ;
         Serial.print("C2 = "); Serial.println(C2.GetCont());
    }

    Si no lleva parámetros ponemos a cero el contador interno, pero si recibe un parámetro hacemos que este sea el valor inicial del contador. ¿Qué fácil y hasta natural, No?

    Constructor Overloading

    Casi oigo como os crujen las neuronas. Las ideas involucradas son sencillas una a una, pero al ir construyendo una idea sobre otra, puede haber que dar un paso atrás para coger perspectiva (Y aire).

    Recapitulemos.

  • Definimos una clase llamado Contador que nos permite llevar la cuenta de lo que se nos ocurra.
  • Pero no queremos tener que inicializar el contador cada vez que instanciamos un nuevo Objeto (Forma pija de decir que creamos un contador)
  • Para evitarlo, definimos un Constructor, que se invoca siempre que creamos un Objeto del tipo Contador, poniendo el contador a 0.
  • Pero esto, aunque no está mal, no mola porque si quiero cambiar el valor del contador tengo que invocar un método, y como somos vagos, no queremos aprender tonterías, y preferimos evitarlo.
  • Para ello Hacemos un Constructor Overloading, o segundo constructor de modo que pueda aceptar un parámetro al instanciar el contador y poner a ese valor el contador interno.
  • De ese modo si instanciamos el objeto sin parámetro, el contador arranca desde cero, pero si le pasamos un parámetro, inicia el contador desde ahí.
  • Impresionamos a los colegas fijo (De ligar nada, no sirve para eso 
  •  

    La potencia que este tipo de unión entre las Clases y el Overloading nos proporciona es impresionante, no tanto para quedarnos con los colegas, sino para hacer programas más sencillos y comprensibles.

    En lugar de usar varias funciones que puedan hacer algo que para nosotros es lo mismo, nos basta con recordar una. Casi parece lo normal

    Vale, esto va cogiendo buena pinta, pero vaya asco de contador que hemos hecho. Solo se incrementa. ¿Y si a mí me apetece que decremente porque voy a hacer una cuenta atrás, que?

    Además C++ siempre ha tenido esa chulada del ++ o el – para variar el valor de una variable. ¿Podría hacer lo mismo con un objeto?

    O más aún, Si tengo dos contadores ¿Puedo sumarlos y obtener un contador con la suma neta de los dos contadores? ¿Y podría restarlos?

    Creo que ya adivináis la respuesta. Desde luego que sí, mediante un Operator Overloading en lugar de un Function Overloading.

    Pero esto, queridos amigos, será el tema de la próxima sesión que por hoy ya nos hemos complicado suficiente y conviene descansar el cerebro.

     

    Algo más sobre el Polimorfismo

     

    El function Overloading es un aspecto del Polimorfismo que nos permite manejar diferentes objetos con los mismos métodos o propiedades.

    Los lectores avispados se habrán percatado que he evitado referirme directamente al Polimorfismo per se, porque entraríamos en aguas pantanosas rápidamente y no es el momento, ni probablemente sea yo el más indicado para esa discusión.

    He preferido evitar el rigor conceptual en beneficio de una aproximación simple, presentando algunas ventajas más tangibles como el concepto de Funcion y Operator Overloading (Que veremos en la próxima sesión) y esquivar el tema central, porque requeriría otros conceptos adicionales que no hemos visto como la Herencia simple y múltiple, o las funciones virtuales, que serían complicadas de encajar con garantías en esta primera aproximación.

    Por eso me contentaré con decir aquí simplemente, que el Polimorfismo es una cualidad abstracta de los objetos que nos permite usar un interface único, de métodos y propiedades, en una colección de objetos de distintos tipos o Clases.

    Recordad el ejemplo que comentamos en alguna sesión previa, que existe un concepto abstracto llamado arrancar que nos resulta natural, para un motor eléctrico, de gasolina o de diésel.

    En la forma en que nuestro cerebro procesa el mundo, las tres objetos comparten ese método común, y para nosotros es de lo más natural considerarlos iguales, por más que comprendemos muy bien que el procedimiento físico que arranca esos tres motores es completamente diferente.

    Polimorfismo es un concepto abstracto que representa precisamente esa capacidad de modelizar diferentes sistemas físicos u Objetos, mediante métodos y propiedades comunes, en un concepto abstracto (Y jerárquicamente superior) de motor que comparten métodos como Arrancar, Frenar o Acelerar y propiedades como Potencia o Velocidad.

    La Clase Motor en abstracto, es independiente de la tecnología que se emplea en un caso concreto y sigue siendo válida cuando se desarrollen otros tipos de motores en el futuro.

    Si queréis profundizar en el tema, no tendréis dificultad en hallar documentación en Internet, pero os recomiendo que si esta es vuestro primera aproximación a la OOP, evitéis hacerlo hasta que hayáis asentado e interiorizado bastante más el  asunto

     

     

    Resumen de la sesión

     

  • Confiamos en que el Polimorfismo y Overloading parezcan un poco menos amenazantes ahora que sabeís lo que son.
  • Vimos como una sobrecarga de funciones como el Constructor, nos ayuda a escribir programas más sencillos de comprender y usar.
  • Hemos programado algún ejemplo de Function Overloading y parece que no era para tanto.
  •