La creación de subtipos describe relaciones de tipos, y el polimorfismo de subtipos permite que las operaciones definidas para los supertipos se sustituyan con seguridad por subtipos. Concretamente, imagina la relación entre una clase ‘Gato’ y una clase ‘Animal’. (Recuerde: las clases crean tipos de datos en JS++). En este caso, dentro del contexto de las relaciones de tipo, ‘Gato’ es el subtipo de ‘Animal’ y ‘Animal’ es el supertipo de ‘Gato’. En términos más simples, ‘Gato’ “es un” ‘Animal’, pero ‘Animal’ no es un ‘Gato’. En teoría, esto significa que todas las operaciones que se aplican al tipo de datos ‘Animal’ deberían aceptar operar en datos de tipo ‘Gato’; sin embargo, lo contrario no es cierto: las operaciones definidas para el tipo de datos ‘Gato’ no podrían operar con seguridad en datos de tipo ‘Animal’.
Si recuerdas el código del apartado anterior, los gatos y los perros son animales domesticados con nombre. Sin embargo, no todos los animales deben tener nombre, por lo que nuestra clase ‘Animal’ no tomó un parámetro de nombre. Por lo tanto, aunque podríamos haber definido y llamado un getter de ‘nombre’ en una instancia de ‘Gato’, no podríamos sustituir con seguridad la instancia de ‘Gato’ con una instancia de ‘Animal’. En JS++, incluso si intenta hacer esto, obtendrá un error.
El polimorfismo de subtipo nos permite escribir código de una manera más abstracta. Por ejemplo, dentro del contexto de tipos primitivos, un ‘byte’ representa números en el rango de 0 a 255. Mientras tanto, un ‘int’ representa números dentro de un rango mucho mayor: -2, 147, 483, 648 a 2, 147 , 483, 647. Por lo tanto, podemos sustituir números de tipo ‘byte’ donde se esperan números de tipo ‘int’:
int add(int a, int b) { return a + b; } byte a = 1; byte b = 1; add(a, b);
Por lo tanto, podemos expresar algoritmos y funciones de manera más «general» porque podemos aceptar una variedad más amplia de datos (categorizados por tipos de datos) para cualquier algoritmo o función determinada.
Es importante no confundir la creación de subtipos con la herencia, aunque los conceptos están estrechamente relacionados dentro de la programación orientada a objetos. La creación de subtipos describe las relaciones de tipos, mientras que la herencia se ocupa de las implementaciones (como extender la implementación de una clase base con una clase derivada). La subtipificación se puede aplicar a las interfaces, mientras que la «herencia» no. Este concepto se volverá más claro en secciones posteriores cuando cubramos las interfaces.
Como punto de partida para comprender prácticamente la creación de subtipos, podemos cambiar main.jspp para que el tipo de datos de todas nuestras variables se convierta en ‘Animal’ pero mantenemos la creación de instancias de clases como subtipos:
import Animals; Animal cat1 = new Cat("Kitty"); cat1.render(); Animal cat2 = new Cat("Kat"); cat2.render(); Animal dog = new Dog("Fido"); dog.render(); Animal panda = new Panda(); panda.render(); Animal rhino = new Rhino(); rhino.render();
Compila tu código. Debería compilarse correctamente porque, durante la creación de instancias, todos los datos que son un subtipo de ‘Animal’ se pueden asignar a una variable de tipo ‘Animal’. En este caso, los datos de ‘Gato’, ‘Perro’, ‘Panda’ y ‘Rinoceronte’ se pueden asignar de forma segura a las variables ‘Animal’.
Sin embargo, hay un pequeño «te pillé». Abra index.html. Verás todos los animales representados nuevamente, pero si pasas el mouse sobre cualquiera de los íconos de animales, ¡no verás ningún nombre! Para comprender por qué ocurre esto, debemos comprender el polimorfismo estático frente al dinámico, que se explicará en la siguiente sección. (En resumen, en realidad especificamos que queríamos polimorfismo «estático» o en tiempo de compilación mediante el uso de la palabra clave ‘sobrescribir’ en el método ‘renderizar’. Por lo tanto, obtuvimos el comportamiento especificado).