|
|
Artículo realizado por
Hasta ahora hemos tocado bastantes aspectos de forma puramente teórica, también hemos dado nuestros primeros pasos construyendo programas en C++ que son capaces de aprovecharse de las ventajas que este leguaje nos dispensa frente al C tradicional. Es ahora ya de empezar a llevar a la práctica todo el aspecto teórico o, mejor dicho, toda la nueva forma de pensar que impregna la metodología orientada a objetos. En este capítulo vamos a aprender a derivar clases y a utilizar la herencia en nuestros programas. Pronto veremos las grandes posibilidades que esto nos reporta para hacer diseños mucho más robustos y flexibles y, sobre todo, aprenderemos a ir pensado en C++ cosa fundamental para saber trabajar con la POO. Recordad que la metodología orientada a objetos no es sólo una nueva forma de programar es, sobre todo, una nueva forma de pensar. La derivación de clases viene a significar crear una clase a la que denominamos clase base. En ella, definimos un conjunto de propiedades (atributos y métodos) que consideramos que son los más generales y que no deberían de faltar. Una vez que tengamos la seguridad de haber dado con la clase general que andábamos buscando, podemos crear otra clase que, en cierto modo, supone una especialización de la anterior. Como es una especialización de la anterior, además de las propiedades definidas para la clase base tendrá las suyas propias que servirán para diferenciarla de la anterior y para hacer que consiga esa especialización que buscamos. ¿Para hacer que tenga los atributos de la clase base tenemos que volver a escribir todo?. ¡NO!, sólo hace falta heredar de la clase base los atributos que nos interesen. Lo único que tendremos que escribir nuevo serán los atributos que especializan a esta clase. A la clase que hereda de otra clase se le llama clase derivada. ¡¡¡Arggg!!!, vaya lío que te estarás formando en la cabeza, ¿no?. Te recomiendo que eches un vistazo al gráfico que voy a poner a continuación y que luego vuelvas a leer la parrafada anterior.
Fernando Rodríguez.
Capítulo 5.
Jerarquía de clases y herencia en la práctica.
Introducción a la jerarquía y herencia de clases
¿Deacuerdo?. Bueno, me supongo que no del todo así que voy a intentar ir aclarando todo esto poco a poco explicando qué son las jerarquías.
Cuando una clase se deriva de otra clase, llamada clase base, lo que hace es heredar un conjunto de funciones o métodos y variables de la clase base. No se heredan todos. Sólo se heredan los elementos que estén preparados para ser heredados. Así, si una función o variable en la clase base está declarada como privada no se podrá heredar desde la clase derivada, es decir, no podremos acceder a ella y si lo intentamos, obtendremos un error en tiempo de compilación.
Hasta ahora sólo hemos puesto el ejemplo de dos clases. Una la llamamos clase base y otra clase derivada. La segunda hereda los métodos y variables que no son declarados como privados en la clase base sin embargo, ¿puede la clase base ser heredada por más de una clase derivada?, ¿puede una clase derivada convertirse en clase base y que otras clases hereden de ella?. Ambas respuestas obtienen un gran SÍ. Podemos tener una clase base y montones de clases derivadas. Así mismo, puede darse el caso de una clase derivada de una clase base y una tercera clase que toma a la clase derivada como su clase base, derivándose de ella. Si esto lo llevamos a la práctica iremos obteniendo una jerarquía de clases.
¿Un ejemplo?. Voy a poner un caso que bien podría darse para un sistema de generación de personajes de un juego de rol (ahora bien, super primitivo...). Imaginaros que queremos hacer un sistema para la creación de multitud de personajes. Estos personajes podrán ser sólo humanos. Al ser humanos sólo existirán dos sexos, hombre y mujer. Por otro lado, existen dos tipos de personajes: los Guerreros y los Magos. ¿Cómo podríamos montarnos un sistema jerárquico con todo estos datos?.
Está claro que todos los personajes son humanos por lo tanto sería una buena idea partir de una clase llamada HUMANO que contuviera todos los atributos que tiene cualquier ser humano. Luego vemos que, al estar creando un sistema de generación de personajes humanos, tenemos dos sexos independientes el del hombre y el de la mujer. Las diferencias son obvias pero no por ello hace falta crear dos clases distintas llamadas HOMBRE y MUJER pues no nos hacen falta tomar atributos exclusivos de los sexos para trabajar en la generación de personajes. Basta con poner una variable en la clase HUMANO llamada sexo y dar el valor oportuno (por ejemplo si sexo se inicia con V significa que es un hombre y si se inicia con M significa que es una mujer. Internamente trabajaremos con 0 y 1).
Lo que seguro necesita de clases nuevas es el tema de las dos especialidades de los personajes que podamos crear. Está claro que tanto los guerreros como los magos son humanos. Vamos a preguntarnos a nosotros mismos para ver si nos aclaramos las diferencias que pueden existir en cada uno de los personajes.
¿Un guerrero tiene fuerza, inteligencia, destreza, energía, peso, sexo, edad y nombre?.
Sí, y además esto también lo tiene cualquier mago por lo tanto estas características ya han debido de ser incluidas en la clase HUMANO.
¿Un guerrero utiliza un libro de hechizos para luchar?.
No, un guerrero sólo puede utilizar armas. Pero un mago sí que puede utilizar libros de hechizos para luchar. Del mismo modo, un mago no puede utilizar armas para pelear.
¿Un mago puede vestir armaduras?.
No, los magos sólo pueden llevar túnicas de tres colores: negro, rojo o blanco (te suena ¿no?). Además, en el caso de los guerreros, esto sí pueden vestir armaduras pero no túnicas y las armaduras pueden ser de cuero o simplemente nada (el típico Conan...).
Podríamos seguir hasta desmenuzar totalmente a cada tipo de personaje pero, con estos datos, ya podemos saber que necesitamos dos clases que heredan de la clase base HUMANO. Estas clases serán la de GUERRERO y la de MAGO. La clase GUERRERO poseerá una variable llamada armas cuyo rango de valores podrá ir desde espadas a "simples" puños mientras que el MAGO poseerá una variable llamada libro de hechizos que trabajará con todos los distintos libros de hechizos que nosotros diseñemos. Así mismo, la clase GUERRERO dispondrá de una variable llamada armadura que tomara los valores de cuero o nada, indicando que no tiene armadura, y la clase MAGO dispondrá también de una variable similar llamada túnica y su rango será el de túnica roja, negra o blanca.
Y, ahora, el dibujo esquema para aclarar todo. Aconstumbraros a trazar esquemas similares antes de comenzar a codificar. Es muy importante sentar las bases de un diseño, si este va a tener cierta envergadura, pues de lo contrario os veréis modificando y ampliando vuestro programa hasta la eternidad. Estáis avisados, la metodología orientada a objetos es así...
Ahora os pongo cómo podría ser el esquema gráfico (sencillísimo) de lo que aquí se ha tratado de explicar:
Bueno, si no está más claro que antes si que deberíais de apreciar, aunque sea de refilón, lo que trato de mostraros con la jerarquía de clases. Trabajamos con especializaciones o derivaciones sucesivas de clases, vamos a poder utilizar toda la potencia del C++.
Cualquiera que programe con la metodología orientada a objetos utiliza la derivación de clases. Es algo básico cuando hacemos programas extensos y que requieren de montones de datos para la especialización. Lógicamente, se puede pasar por alto su aprovechamiento pero entonces, nos limitaríamos a programar en C (y rematadamente mal) pero utilizando estructuras y, para eso, sólo aprendemos C++ como ampliación del C y no como metodología orientada a objetos. La principal importancia de la metodología orientada a objetos radica en la posibilidad de utilizar el código que otras personas han hecho y trabajar en equipo para llevar a cabo proyectos de gran envergadura. Cuando se programa para entornos como el Windows, por ejemplo, una de las cosas a las que nos tenemos que acostumbrar es a aprender a leer el código que otras personas han hecho ya, a utilizar las clases ya diseñadas y codificadas y a adaptarlas a nuestras necesidades. Esto implica crearnos una jerarquía propia a partir de algo que ya está hecho por lo que se torna fundamental el aprender a pensar de esta nueva manera.
Si queremos implementar un programa, debemos de ponernos antes delante de un papel y diseñar a mano los posibles módulos que vamos a necesitar y, una vez establecidos, desmenuzar esos módulos en jerarquías de clases. Esto nos va a permitir que, en un futuro, nuestro diseño sea totalmente ampliable y mejorable y que otras personas puedan aprovecharlo para trabajar.
Intenta ir más allá del ejemplo anteriormente establecido. Imagina que hay ahora una clase de guerreros distinta que además también son magos. ¿Qué harías?. Crea una clase llamada GUERRERO_MAGO que herede de la clase GUERRERO y de la clase MAGO y ya está. Tu personaje ahora podrá tener todos los atributos exclusivos de un ser humano, de un guerrero y de un mago. Esto es lo que nos ofrece la herencia y la jerarquía de clases, poder reutilizar código de una forma inteligente.
A continuación vamos a ver, mediante un sencillo ejemplo, cómo implementar la derivación de una clase utilizando el concepto de herencia. Utilizaremos, para ello, el modelo ideado más atrás, es decir, tendremos una clase base llamada HUMANO y de ella se derivarán dos, la clase GUERRERO y la clase MAGO. En la clase HUMANO estarán todos los datos básicos y que son comunes a los dos tipos de personajes (Guerreros y Magos) mientras que, en la clases ya respectivas, pondremos características únicas y que les diferencian, como el caso de armas, hechizos, armaduras o túnicas. ¡Allá vamos!
#include <iostream.h> // Clase Base class Humano { public: // Constructor y destructor Humano(); {} ~Humano(); {} private: // Hago excesivo uso del tipo int porque es un ejemplo // en un caso real, aconstumbraros a utilizar el tipo unsigned char // si vuestros valores no van a rebasar el rango 0-255 char *m_nombre; int m_edad; int m_peso; int m_sexo; int m_inteligencia; int m_fuerza; int m_destreza; int m_energia; public: void PonNombre(char *nombre) { m_nombre = name; } void PonEdad(int edad) { m_edad = edad; } void PonSexo(char sexo) { if (sexo=='V') m_sexo =0; else m_sexo = 1; } void PonInteligencia(int intel) { m_inteligencia = intel; } void PonFuerza(int fuerza) { m_fuerza = fuerza; } void PonDestreza(float destreza) { m_destreza = destreza; } void PonEnergia (int energia) { m_energia = energia; } virtual void PonInformacion(void); }; // Primera clase derivada class Guerrero : public Humano { public: // Constructor y destructor Guerrero() {} ~Guerrero() {} private: int m_tipoArma; // 0 manos 1 espada int m_tipoArmadura; // 0 nada 1 cuero public: void PonArma(int arma) { m_tipoArma = arma; } void PonArmadura(int armadura) { m_tipoAramadura = armadura; } void PonInformacion(void); }; //Segunda clase derivada class Mago: public Humano { public: // Constructor y destructor Mago() {} ~Mago() {} private: int m_tipoLibroHechizos; // 0 ataque 1 curacion int m_tipoTunica; // 0 blanca 1 roja 2 negra public: void PonLibroHechizos(int libro) { m_tipoLibroHechizos = libro; } void PonTunica(int tunica) { m_tipoTunica = tunica; } void PonInformacion(void); }; // Ahora implementamos las funciones que no son // inline // Métodos de la clase Humano void Humano::PonInformacion(void) { cout << "\n Nombre: " << m_nombre; cout << "\n Edad: " << m_edad; switch(m_sexo) { case 0: cout << "\n Sexo: Hombre"; break; case 1: cout << "\n Sexo: Mujer"; break; }; cout << "\n Inteligencia: " << m_inteligencia; cout << "\n Fuerza: " << m_fuerza; cout << "\n Destreza: " << m_destreza; cout << "\n Energía: " << m_energia; } // Métodos de la clase Guerrero void Guerrero::PonInformacion(void) { Humano::PonInformacion(); switch(m_tipoArma) { case 0: cout << "n Arma: Puños"; break; case 1: cout << "\n Arma: Espada"; break; }; switch(m_tipoArmadura) { case 0: cout << "\n Armadura: Ninguna"; break; case 1: cout << "\n Armadura: Cuero"; break; }; } // Métodos de la clase Mago void Mago::PonInformacion(void) { Humano::PonInformacion(); switch(m_tipoLibroHechizo) { case 0: cout << "\n Libro: Hechizos de Ataque"; break; case 1: cout << "\n Libro: Hechizos de Curación"; break; }; switch(m_tipoTunica) { case 0: cout << "\n Túnica: Blanca"; break; case 1: cout << "\n Túnica: Roja"; break; case 2: cout << "\n Túnica: Negra"; break; }; } int main() { // Nota: Pese a que los nombres os sonarán (o, al menos, ¡deberían!) // sus datos los he puesto casi sin mirar... así que los puritanos // de las novelas, disculpen ;-) // Declaramos dos objetos. // Uno de tipo Mago y otro de tipo Guerrero Guerrero Caramon; Mago Raistlin; // Ponemos los datos de cada uno // Datos del guerrero Caramon.PonNombre("Caramon"); Caramon.PonEdad(26); Caramon.PonSexo('V'); Caramon.PonInteligencia(7); Caramon.PonFuerza(8); Caramon.PonDestreza(8); Caramon.PonEnergia(100); Caramon.PonArma(1); Caramon.PonArmadura(0); // Datos del Mago Raistlin.PonNombre("Raistlin"); Raistlin.PonEdad(26); Caramon.PonSexo('V'); Raistlin.PonInteligencia(9); Raistlin.PonFuerza(5); Raistlin.PonDestreza(6); Raistlin.PonEnergia(60); Raistlin.PonLibroHechizos(0); Raistlin.PonTunica(1); // Ahora Ponemos los datos del Guerrero por pantalla Caramon.PonInformacion(); // Ahora Ponemos los datos del Mago por pantalla Raistlin.PonInformacion(); return 0; }
¿Qué tal lo veis?. Espero que se os hayan aclarado bastantes más cosas ahora que tenéis toda la teoría codificada. Como podéis observar, utilizamos la clase base HUMANO y, a partir de ella, derivamos las clases GUERRERO y MAGO. Esto da mucha potencia ya que tenemos todo lo básico en la clase base y creamos clases específicas que se especializan, caso de las citadas GUERRERO y MAGO, ofreciendo esos datos u acciones concretas. Sería muy fácil crear una clase arquero, ladrón, bardo.... tan sólo hay que derivar de la clase base y añadir esas características únicas.
A modo de recordatorio, os pongo la clase base HUMANO que establece todas las características básicas así como los métodos para trabajar con ellas. Ahí va:
// Clase Base class Humano { public: // Constructor y destructor Humano(); {} ~Humano(); {} private: // Hago excesivo uso del tipo int porque es un ejemplo // en un caso real, aconstumbraros a utilizar el tipo unsigned char // si vuestros valores no van a rebasar el rango 0-255 char *m_nombre; unsigned char m_edad; int m_peso; int m_sexo; int m_inteligencia; int m_fuerza; int m_destreza; int m_energia; public: void PonNombre(char *nombre) { m_nombre = name; } void PonEdad(int edad) { m_edad = edad; } void PonSexo(char sexo) { if (sexo=='V') m_sexo =0; else m_sexo = 1; } void PonInteligencia(int intel) { m_inteligencia = intel; } void PonFuerza(int fuerza) { m_fuerza = fuerza; } void PonDestreza(float destreza) { m_destreza = destreza; } void PonEnergia (int energia) { m_energia = energia; } virtual void PonInformacion(void); };
La clase derivada GUERRERO, hereda todas las funciones o métodos y todas las variables o atributos de la clase base HUMANO. Además, añade dos funciones nuevas, dos variables nuevas y redefine una función de la clase base, la función PonInformación(). Es decir, la clase GUERRERO es la siguiente:
// Primera clase derivada class Guerrero : public Humano { public: // Constructor y destructor Guerrero() {} ~Guerrero() {} private: int m_tipoArma; // 0 manos 1 espada int m_tipoArmadura; // 0 nada 1 cuero public: void PonArma(int arma) { m_tipoArma = arma; } void PonArmadura(int armadura) { m_tipoAramadura = armadura; } void PonInformacion(void); };
Por su parte, la clase MAGO, viene a hacer algo similar. Hereda todas las funciones y variables públicas de la clase base HUMANO, al igual que la clase GUERRERO y, además, añade dos variables, dos funciones y redefine la, ya citada, función PonInformacion() de la clase base. La clase MAGO es esta:
//Segunda clase derivada class Mago: public Humano { public: // Constructor y destructor Mago() {} ~Mago() {} private: int m_tipoLibroHechizos; // 0 ataque 1 curacion int m_tipoTunica; // 0 blanca 1 roja 2 negra public: void PonLibroHechizos(int libro) { m_tipoLibroHechizos = libro; } void PonTunica(int tunica) { m_tipoTunica = tunica; } void PonInformacion(void); };
Vamos a mirar la cabecera de la clase GUERRERO que nos vale para ilustrar el proceso. Recordemos cómo era:
// Primera clase derivada class Guerrero : public Humano {
Si nos fijamos, después de poner class Guerrero, ponemos dos puntos ":" y, después, public Humano. Lo que debemos de hacer es declarar primero el nombre de la clase que, como todos sabemos, es Guerrero, después los dos puntos ":" y, por último, lo más importante, debemos de poner el nombre de la clase de la que vamos a derivar o heredar anteponiendo el formato public o private. Estos son los dos únicos especificadores de acceso que podemos poner a la hora de declarar una clase derivada. Con public, heredamos todos los elementos, tanto variables como funciones. Si pusiéramos private como especificador de acceso, lo único que conseguiríamos sería hacer oculta una clase derivada del resto del programa pero, como esto no se suele utilizar (nosotros, en principio, no lo vamos a hacer), no se suele indicar, es decir, se suele declarar todo como public a la hora de derivar.
Durante la utilización de los objetos Guerrero y Mago, utilizamos, mayormente, los métodos de la clase base, esto es, durante el establecimiento del nombre, edad, sexo, inteligencia, fuerza, destreza o energía, lo que se hace es "echar mano" de los métodos de la clase Humano y que son heredados. Sin embargo, métodos como PonArma() o PonTunica(), por poner algún ejemplo, ya pertenecen exclusivamente a los objetos derivados, es decir, estos métodos no se heredan de ninguna clase.
Otro aspecto muy importante es el de la redefinición de funciones. Cuando redefinimos funciones lo que venimos a hacer es coger una función ya hecha, que pertenece a una clase base, y añadirla una serie de características necesarias para el objeto derivado. Estas características pueden y suelen, ampliarlas. El ejemplo más claro es el del método PonInformacion() método que será el que estudiemos a continuación.
Retomando el párrafo anterior, el tema de la redefinición es una valiosa "arma" para poder concretar todo lo que heredamos aún más. Así, en nuestros ejemplos, tanto la clase Guerrero como la clase Mago, hacen uso de la redefinición. Vamos a recordar cómo era la función PonInformacion() en la clase base:
void Humano::PonInformacion(void) { cout << "\n Nombre: " << m_nombre; cout << "\n Edad: " << m_edad; switch(m_sexo) { case 0: cout << "\n Sexo: Hombre"; break; case 1: cout << "\n Sexo: Mujer"; break; }; cout << "\n Inteligencia: " << m_inteligencia; cout << "\n Fuerza: " << m_fuerza; cout << "\n Destreza: " << m_destreza; cout << "\n Energía: " << m_energia; }
Se puede observar perfectamente que lo que hace este método es volcar la información concerniente a las variables que contienen (o contendrán) los valores acerca del nombre, edad, sexo, inteligencia, fuerza, destreza y energía del personaje. La pregunta viene ahora. Si un Guerrero o Mago, tiene estos datos, que los hereda y, además, tiene otros que también deberá listar, ¿Tendremos que volver a escribir todo otra vez?. La respuesta es NO. Tan sólo hace falta mirar a la función PonInformacion() de la clase Guerrero o Mago. Echemos un vistazo a la clase Mago y, en concreto, a dicha función:
void Mago::PonInformacion(void) { Humano::PonInformacion(); switch(m_tipoLibroHechizo) { case 0: cout << "\n Libro: Hechizos de Ataque"; break; case 1: cout << "\n Libro: Hechizos de Curación"; break; }; switch(m_tipoTunica) { case 0: cout << "\n Túnica: Blanca"; break; case 1: cout << "\n Túnica: Roja"; break; case 2: cout << "\n Túnica: Negra"; break; }; }
Está más o menos claro, ¿no?. Lo que hacemos es llamar la función definida en la clase base y que se encarga de listar los valores básicos anteriormente citados. Para ello, ponemos el nombre de la clase base, seguida de los dos puntos de resolución de ámbito y, después, el nombre de la función, es decir, Humano::PonInformacion(); . Una vez hecho esto, definimos la parte de la función que nos hace falta y que no es otra que la concerniente al listado de los valores únicos de un Mago, esto es, tipo de libro de hechizos y tipo de túnica, es decir, completamos función PonInformacion() para que, además de listar los datos básicos sin necesidad de reescribir las instrucciones (llamamos a la función definida en la clase base), liste también los datos específicos.
Esto mismo es aplicable a la clase Guerrero.
Creo que con este capítulo debería de haber quedado claro qué es la herencia y cómo implementarla en nuestros programas en C++. Así mismo, debería de comenzar a tomar fuerza el concepto que entraña programar con una metodología orientada a objetos y es que, hasta que el programador no consigue pensar de esta nueva forma, es muy importante armarse de papel y lápiz y hacer un boceto de lo que debería de ser el diseño. Siempre lo agradeceréis cuando os metáis, en serio, a codificar.
El próximo número hablaremos del Polimorfismo y se desvelará esa palabreja de nombre virtual. Hasta que os suene esa palabreja, ya estáis en condiciones de ir haciendo cosas algo más serias. ¿Por qué no intentáis hacer un generador de personajes en plan serio?. Basta con diseñarlo en papel, escribir sus clases, variables y métodos. Si hacéis eso, y lo hacéis bien, aprovecharéis mucho más que si os lanzáis a codificar como locos. Rercordad que primero hay que saber diseñar. Luego ya vendrá lo de "machacar" el teclado.
|
|