La banana que piden los programadores. Qué nos ofrece el paradigma POO.

Photo of a Gorilla Walking on Grass

En 2016, Joe Armstrong, el creador de Erlang, dijo:

«El problema con los lenguajes orientados a objetos es que contienen todo este ambiente implícito, que cargan con ellos. Tú querías una banana pero lo que obtuviste es Un gorila sosteniendo a la banana y al bosque completo».

– Joe Armstrong –

En resumen, nuestro amigo Joe, muestra su descontento, con la implementación de la POO (Programación orientada a objetos) que realizan los lenguajes que dicen soportarlo en la actualidad.

Este artículo es una interpretación personal, acompañado de códigos de ejemplo, que tratará de explicar lo que Armstrong quiso decir.

Una solución simple

La idea principal de la programación orientada a objetos (Ver artículo anterior) es que se unen, de forma sintáctica y conceptual, a los atributos y métodos, que antes, en la programación estructurada, estaban dispersos como variables y funciones. A esta nueva simbiosis de atributos y métodos es a lo que se llama «objeto». Y es el núcleo de todo el paradigma de la POO.

Para ilustrar la utilidad de la POO, consideremos el siguiente problema:

PROBLEMA: Se requiere representar números complejos y poder realizar operaciones de suma sobre esos números (No se me ocurre un mejor ejemplo).

En programación estructurada pura, un programador ordenado de C (No se me ocurre un mejor lenguaje), haría algo como esto:

typedef struct {
    float real;
    float imag;
} Complejo;

Complejo sumar(Complejo n1, Complejo n2) {
    Complejo r;
    r.real = n1.real + n2.real;
    r.imag = n1.imag + n2.imag;
    return r;
}
int main()
{   Complejo n1, n2, n3;
    n1.real = 1; n1.imag = 1;
    n2.real = 2; n2.imag = 3;
    n3 = sumar(n1, n2);
    return 0;
}

Y eso funciona bien. Inteligentemente se ha usado una «struct» para unir los dos valores del par ordenado (parte real y parte imaginaria) que representa a los números complejos. Se pudo haber hecho también sin crear un tipo «struct», guardando un número complejo en dos variables independientes, una para la parte real y otra para la parte imaginaria; solo que se tendría que manejar más variables y se perdería un poco la unidad de lo que significa un número complejo.

No se puede negar que el programa, de pocas líneas, resuelve el problema e inclusive incluye un ejemplo de uso.

No hay nada más que hacer. No se necesita más para lo que se pide. El programa y el lenguaje cumplen. Tenemos lo que necesitamos.

La banana

Observemos el código anterior, veremos que la orientación de este código está en las «funciones», como la que se usó para implementar la suma de números complejos. Se pueden crear más funciones para tareas similares, y todas ellas trabajarían de forma similar:

  • Recibirían números complejos, y,
  • Realizarían/ alguna operación sobre ellos.

Aquí, las estrellas del show son las funciones, y así eran los espectáculos antes de la llegada de la POO. Todo se pensaba en término de funciones que recibían un tipo de datos y hacían algo sobre estos datos, a veces, devolviendo otro tipo de datos.

Eso era todo lo que se necesitaba y el público no era tan exigente. Con mayor o menor esfuerzo, se podía pensar en funciones para resolver cualquier problema informático que se nos ocurriese.

La clave estaba en modelar los problemas de forma que se tuvieran los datos necesarios y las funciones apropiadas para trabajar sobre esos datos.

¿Cómo cambió el show cuando llegó la POO?

Pues es algo simple. Ahora debemos pensar en objetos y no ya en funciones que realizan algo sobre los objetos.

Si consideramos el mismo problema anterior, y se lo planteamos a alguien instruido en POO, el primer pensamiento que se le vendría a la mente sería que, los números complejos deben ser los objetos de nuestro modelo. Entonces, ¿Cómo cambia el panorama?

El siguiente código en C++ (Lo siento, no pude encontrar un lenguaje mejor) ilustra cómo implementaríamos el mismo problema:

class Complejo{
public:
    float real;
    float imag;
    Complejo sumar(Complejo n2) {
        Complejo r;
        r.real = this->real + n2.real;
        r.imag = this->imag + n2.imag;
        return r;
    }
};

int main()
{   Complejo n1, n2, n3;
    n1.real = 1; n1.imag = 1;
    n2.real = 2; n2.imag = 3;
    n3 = n1.sumar(n2);
    return 0;
}

Este sencillo código presenta una solución modelada desde la perspectiva de objetos, en donde el objeto principal es el número complejo definido en la clase «Complejo».

La solución es todavía compacta y cumplidora. Hace lo justo y necesario para cubrir la solución al problema planteado. Esta es una banana POO.

El gorila

Black Gorilla on Tree Branch

La solución anterior tiene todo lo que necesitamos, ni más ni menos. ¿Cuál es el problema entonces?

La verdad es que no habría ningún problema si los programadores implementaran una solución tan escueta como la que mostramos.

Pero en las escuelas de programación se nos adoctrina para pensar que tenemos que echarle algo más a la olla, algo que siempre hay que echar y entonces empiezan a moverse los dedos solos por el teclado y de pronto decimos algo como ¿Y dónde está el constructor?

Porque clases sin constructores es algo que no se ve bien en POO. Así que empecemos poniéndole un constructor para inicializar al objeto. Mejor le ponemos dos por si no queremos inicializar explícitamente. Y así ahora tenemos:

class Complejo{
public:
    float real;
    float imag;
    Complejo sumar(Complejo n2);
    Complejo(float, float); 
    Complejo(); 
};

Complejo Complejo::sumar(Complejo n2) {
    Complejo r;
    r.real = this->real + n2.real;
    r.imag = this->imag + n2.imag;
    return r;
}

Complejo::Complejo(float r, float i) {
    this->real = r;
    this->imag = i;
}
Complejo::Complejo() {
    this->real = 0;
    this->imag = 0;
}

int main()
{   Complejo n1(1,1);
    Complejo n2(2,3);
    Complejo n3;
    n3 = n1.sumar(n2);
    return 0;
}

Lo que sucede ahora es que tenemos expuestos los atributos de nuestra clase Complejo, y por encapsulamiento, debemos ocultar el acceso directo así que procedemos a crearle unos bonitos «getters» y «setters». Lo que queda es esto:

class Complejo{
private:
    float real;
    float imag;
public:  //"Getters" y "Setter".
    void setReal(float r) {
      real = r;
    }
    float getReal() {
      return real;
    }
    void setImag(float i) {
      imag = i;
    }
    float getImag() {
      return imag;
    }
public:  //Operaciones
    Complejo sumar(Complejo n2);
public:  //Constructores
    Complejo(float, float); 
    Complejo(); 
};

Complejo Complejo::sumar(Complejo n2) {
    Complejo r;
    r.real = this->real + n2.real;
    r.imag = this->imag + n2.imag;
    return r;
}

Complejo::Complejo(float real, float imag) {
    this->real = real;
    this->imag = imag;
}

Complejo::Complejo() {
    this->real = 0;
    this->imag = 0;
}

int main()
{   Complejo n1(1,1);
    Complejo n2(2,3);
    Complejo n3;
    n3 = n1.sumar(n2);
    return 0;
}

El bosque

area covered with green leafed plants

Y ahora que lo pensamos, y porque así nos lo enseñaron en las clases de POO, podemos identificar a la clase Complejo como una derivación de una clase numérica más simple a la que podemos llamar Real y así podemos construir un nivel de herencia y de paso creamos a los objetos dinámicamente, como es la «moda» en estos tiempos, lo que obliga a usar los destructores:

class Real{
protected:
    float real;
    
public:  //Operaciones
    float sumar(float n2);
    
public:  //"Getters" y "Setter".
    void setReal(float r) {
      real = r;
    }
    float getReal() {
      return real;
    }
};

float Real::sumar(float n2) {
    return real + n2;
}

class Complejo: public Real{
private:
    float imag;
public:  //"Getters" y "Setter".
    void setImag(float i) {
      imag = i;
    }
    float getImag() {
      return imag;
    }
public:  //Operaciones
    Complejo sumar(Complejo n2);
    
public:  //Constructores
    Complejo(float, float); 
    Complejo(); 
};

Complejo Complejo::sumar(Complejo n2) {
    Complejo r;
    r.real = this->real + n2.real;
    r.imag = this->imag + n2.imag;
    return r;
}

Complejo::Complejo(float r, float i) {
    this->real = r;
    this->imag = i;
}

Complejo::Complejo() {
    this->real = 0;
    this->imag = 0;
}

int main()
{   
    Complejo *n1 = new Complejo(1,1);
    Complejo *n2 = new Complejo(2,3);
    Complejo *n3 = new Complejo;
    
    *n3 = n1->sumar(*n2);
    cout << n3->getReal() << "," << n3->getImag();
    
    delete n1;
    delete n2;
    delete n3;
    return 0;
}

Aún podemos seguir llenando el código de características de la POO que nos enseñaron que podemos (debemos) usar, aún cuando realmente no lo necesitemos.

La conclusión

El concepto puro de POO, como la unión semántica de atributos y métodos, es realmente útil como forma de pensamiento para el modelamiento y la solución de problemas informáticos.

El problema viene luego cuando a esta dualidad perfecta, le adornamos con herencia, polimorfismo, mensajes, encapsulamiento, constructores/destructores, sobrecarga de funciones, clases abstractas, métodos virtuales, memoria dinámica, recolección de basura y otros conceptos que no tienen nada que ver con la POO. Y el problema no es tanto que nos lo vendan así, el problema es que no nos la quieren vender sola.

¿Conoce algún lenguaje que solamente incluya objetos y no incluya encapsulamiento, herencia, o polimorfismo?

Esa capacidad de poder encapsular sintáctica y conceptualmente a atributos y métodos en una estructura de programación es la banana que piden los programadores. Pero lo que nos ofrecen lenguajes como Java, PHP, C++, o Python, es la banana con el gorila y el bosque completo. Y no es solo que nos ofrezcan algo que tal vez no necesitamos, sino que nos hacen pensar que en verdad lo necesitamos.


Sé el primero en comentar

Dejar una contestacion

Tu dirección de correo electrónico no será publicada.


*