C++
Imagen de una rueda encima de una mano con un fondo de cubos de Rubik simbolizando la moldeabilidad de la programación orientada a objetos

El paradigma de la programación orientada a objetos

La programación orientada a objetos no solo facilita visualmente el trabajo del programador, sino que también permite reutilizar código y aumentar la fiabilidad del mismo.


| Actualizado:
4.562

Una de las características más significativas del lenguaje de programación C++ es la orientación a objetos. Junto con la capacidad de manejar la memoria lo convierten en uno de los lenguajes más eficientes y útiles. Por ello, es totalmente necesario que cualquier programador tenga conocimiento de cómo aprovechar dicha funcionalidad. En este artículo se pretende iniciar al lector en el paradigma de la programación orientada a objetos (POO -OOP en inglés-) partiendo de una problemática inicial con un ejemplo real.

Ejemplo práctico

Pantalla del juego Chicken Invaders

Para este caso, se hará uso de un ejemplo del videojuego Chicken Invaders, juego desarrollado por InterAction studios el año 1999. Este juego, basado en la idea original de Space Invaders, para aquellos que lo desconozcan, consiste en eliminar a un conjunto de enemigos (pollos en el caso de Chicken Invaders y naves espaciales en Space Invaders). En la mayoría de estos niveles los enemigos están colocados formando una matriz bidimensional, formando filas y columnas.

Para un programador experimentado, fácilmente detectaría que es un caso claro de programación orientada a objetos donde cada enemigo es una instancia de una clase. No obstante, se partirá de la base donde el programador desconoce dicho paradigma. Por este motivo, lo más probable que este diseñe sea un array bidimensional para cada una de las propiedades que el enemigo tendrá. Se supondrá que cada enemigo tendrá unos puntos de vida, munición, valor de ataque, su posición x y su posición y (en este artículo se trabajará con memoria estática para simplificar el ejemplo):


#define   MAX_ENEMIES_ROW   10
#define   MAX_ENEMIES_COL   5
#define   MAX_ENEMIES   50

int enemiesHP[MAX_ENEMIES_ROW][MAX_ENEMIES_COL];
int enemiesAmmo[MAX_ENEMIES_ROW][MAX_ENEMIES_COL];
int enemiesAtk[MAX_ENEMIES_ROW][MAX_ENEMIES_COL];
int enemiesPosX[MAX_ENEMIES_ROW][MAX_ENEMIES_COL];
int enemiesPosY[MAX_ENEMIES_ROW][MAX_ENEMIES_COL];

De este modo, cada enemigo tendrá sus propiedades almacenadas de forma ordenada en el array bidimensional. No obstante, utilizando este método ya se empiezan a encontrar los primeros problemas: la estructura planteada es poco visual para el programador, la reutilización del código será bastante más complicada que con POO y, además, todas las propiedades de los enemigos quedan visibles (más adelante se entenderá este concepto). Sin embargo, se supondrá que se quiere seguir con este diseño. De este modo, se van a definir funciones como la siguiente:


void initEnemies()
{
	int i, j;
	
	for (j = 0; j < MAX_ENEMIES_COL; j++)
		for (i = 0; i < MAX_ENEMIES_ROW; i++)
		{
			// Initial HP = 100
			enemiesHP[i][j] = 100;
			// Initial ammo = 10
			enemiesAmmo[i][j] = 10;
			// Initial Atk = 8
			enemiesAtk[i][j] = 8;
			// Initial x position = 10, 11, ... 19
			enemiesPosX[i][j] = i + 10;
			// Initial y position = j
			enemiesPosY[i][j] = j;
		}
}

void checkEnemiesStats()
{
	int i, j;
	
	cout << "Enemies:\n";
	
	for (j = 0; j < MAX_ENEMIES_COL; j++)
		for (i = 0; i < MAX_ENEMIES_ROW; i++)
			cout << "Enemy " << (i+j*MAX_ENEMIES_ROW+1) << ": PosX: " << enemiesPosX[i][j] << " PosY: " << enemiesPosY[i][j] << " HP=" << enemiesHP[i][j] << " Ammo=" << enemiesAmmo[i][j] << " Atk=" << enemiesAtk[i][j] << "\n";
	
	cout << "\n";
}

void moveEnemies()
{
	int i, j;
	
	for (j = 0; j < MAX_ENEMIES_COL; j++)
		for (i = 0; i < MAX_ENEMIES_ROW; i++)
		{
			if (j % 2 == 0)
				enemiesPosX[i][j]++;
			else
				enemiesPosX[i][j]--;
		}
	
	cout << "Movement completed!\n\n";
}

int main()
{
	initEnemies();
	checkEnemiesStats();
	moveEnemies();
	checkEnemiesStats();

	return 0;
}

Con la función moveEnemies(), todos los enemigos de las filas impares se mueven hacia la izquierda y, los de las filas pares, hacia la derecha (se ha considerado que la primera fila es la fila 0 y que este número es par). Una vez más, para simplificar el código, se ha omitido código como el de mover gráficamente un elemento en C++, ya que no es el objetivo de este artículo.

La problemática

Cómo se puede observar en el diseño del código, este no parece el más indicado para resolver el desarrollo del juego, ya que las propiedades de cada enemigo no quedan del todo claras y, como se ha citado anteriormente, este código resulta bastante difícil de volver a usar y todos sus valores quedan al descubierto. Para poder solucionar estos problemas, se usará la programación orientada a objetos.

La programación orientada a objetos

Para poder entender en qué consiste este paradigma, simplemente hace falta entender que consiste en generar moldes para cada una de las piezas que se quieran crear de ese tipo. Más formalmente, consiste en diseñar clases con sus propios atributos y métodos y, cada instancia de esta, actuará de la forma que se ha definido en sus métodos. Antes de seguir, hay que definir ciertas palabras que se acaban de usar:

  • Clase: es ese molde donde se especifican todas las propiedades y funcionalidades que tendrá ese elemento.
  • Instancia: es cada una de las declaraciones que se haga de una clase.
  • Atributo: es cada una de las propiedades que toda instancia de esa clase tendrá (equivale a una variable).
  • Método: es cada una de las funcionalidades que tendrá toda instancia de esa clase (equivale a una función).

Definidos estos conceptos clave de la POO, se puede plantear el siguiente diseño de una clase con el ejemplo anterior:


class Enemy
{
	private:
		int hp;
		int ammo;
		int atk;
		int posX;
		int posY;
	
	public:
		Enemy() {}
		
		Enemy(int x, int y)
		{
			// Initial HP = 100
			this->hp = 100;
			// Initial ammo = 10
			this->ammo = 10;
			// Initial Atk = 8
			this->atk = 8;
			// Initial x position = 10, 11, ... 19
			this->posX = x + 10;
			// Initial y position = j
			this->posY = y;
		}
		
		void move()
		{
			if (this->posY % 2 == 0)
				this->posX--;
			else
				this->posX++;
		}
		
		void checkStatus()
		{
			cout << "Enemy: PosX: " << this->posX << " PosY: " << this->posY << " HP=" << this->hp << " Ammo=" << this->ammo << " Atk=" << this->atk << "\n";
		}
};

Para comprobar su funcionamiento, se puede usar la siguiente función main().


int main()
{
	cout << "\n\n=========================================\n\n";
	cout << "POO";
	cout << "\n\n=========================================\n\n";
	
	int i, j;
	int num;
	
	num = 0;
	Enemy * enemies = new Enemy[MAX_ENEMIES];
	
	// Initialize all the instances
	for (j = 0; j < MAX_ENEMIES_COL; j++)
		for (i = 0; i < MAX_ENEMIES_ROW; i++)
		{
			enemies[num] = Enemy(i, j);
			num++;
		}
	
	cout << "Check before the move\n\n";
	
	// Check status before move
	num = 0;
	for (j = 0; j < MAX_ENEMIES_COL; j++)
		for (i = 0; i < MAX_ENEMIES_ROW; i++)
		{
			enemies[num].checkStatus();
			num++;
		}
	
	// Move all the instances
	num = 0;
	for (j = 0; j < MAX_ENEMIES_COL; j++)
		for (i = 0; i < MAX_ENEMIES_ROW; i++)
		{
			enemies[num].move();
			num++;
		}
	
	cout << "\nCheck after the move\n\n";
	
	// Check status after move
	num = 0;
	for (j = 0; j < MAX_ENEMIES_COL; j++)
		for (i = 0; i < MAX_ENEMIES_ROW; i++)
		{
			enemies[num].checkStatus();
			num++;
		}
	
	return 0;
}

Si se compila el programa, se puede comprobar como el funcionamiento es exactamente el mismo de cara al usuario final, no obstante, el diseño del código cambia radicalmente. A pesar que la POO requiere algo más de recursos que el ejemplo anterior, merece totalmente la pena usar ese tipo de diseños.

Encapsulación de la información

Asimismo, hay otros aspectos a tener en cuenta de la programación orientada a objetos como por ejemplo la encapsulación de la información, también llamados modificadores de acceso. Si se presta atención, en el código de ejemplo se han usado palabras reservadas como public y private. Mediante estas palabras se puede determinar quién puede acceder a ver el contenido de ciertos atributos o invocar determinados métodos.

Entonces es muy común hacerse la siguiente pregunta: ¿para qué hay que ocultar la información? La realidad es que este es uno de los puntos fuertes de la POO. Muchos atributos y métodos no tienen porqué ser accesibles para todos, puesto que no siempre van a necesitar manipular esa información. Entonces, ¿para qué dejarlos visibles para todos? Normalmente, los atributos siempre suelen ser privados o protegidos (a continuación se definirán estos conceptos) y, mediante un método público, se puede acceder a él. La ventaja de usar métodos para acceder a esos valores es poder controlar previamente el valor que se va a mostrar o modificar.

Por ejemplo, en caso de querer asignar un valor al atributo que almacenaba la posición x del enemigo, se podría hacer mediante un método llamado setPosX() de la siguiente forma:


class Enemy
{
...
   void setPosX(int x)
   {
      if (x < 0 || x > 70)
         this->posX = x;
   }
...
}

Como se puede observar, si simplemente se asignara el valor al atributo de la posición x, podrían darse situaciones donde adoptara un valor que pueda desencadenar problemas (como por ejemplo que salga de la pantalla si x es inferior a 0). Si bien es cierto que esta condición se puede introdución en el paradigma del primer ejemplo, habría que añadirla cada vez que se quiera modificar el valor de esta propiedad. Por motivos evidentes, entonces, es mucho más cómodo hacerlo con un método dentro de la clase.

Finalmente, una vez conocida la finalidad de encapsular la información, se van a enumerar los modificadores de acceso que puede tener un atributo o un método dentro de una clase:

  • Público (public): es accesible desde dentro la clase y desde fuera de esta.
  • Privado (private): sólo es accesible desde la propia clase.
  • Protegido (protected): sólo es accesible desde la propia clase y desde clases que hereden a esta.
Ejemplo gráfico de cómo funcionan los modificadores de acceso con una casa, un jardín y la calle

En la definición del modificador de acceso protegido se ha hecho mención al hecho que una clase puede heredar otra, sin embargo, esta funcionalidad de la POO no se tratará en este artículo debido a su enorme extensión. Más adelante se hará referencia en futuros artículos.

Es importante tener presente que los modificadores de acceso se pueden asignar tanto a atributos, como métodos. Cada lenguaje tiene su propia sintaxis, en caso de C++ es la que se ha descrito en el ejemplo anterior.

Conclusión

De este modo, se ha podido desarrollar un programa que cumple con el propósito inicial y que sigue el paradigma de la programación orientada a objetos. Además, se han definido los términos necesarios para tener una base inicial de la POO. Si bien es cierto que este paradigma es mucho más extenso de lo que ha tratado en este artículo, el propósito de este era iniciarse en este mundo con un ejemplo simple y entender su importancia.

Si consideras que hay algún error en este artículo háznoslo saber mediante nuestro correo [email protected].


Comparte este artículo:
Foto de perfil de Carles Gallel

Carles Gallel

TaLeR

Computer scientist y profesor de la UOC (Universitat Oberta de Catalunya). Fundador de Last2, Undefined World, Erandic y Recursivity. Estudiante del máster en ingeniería informática en la UOC.