description |
---|
Explicación del concepto de polimorfismo, tipos y aplicaciones reales. |
Se ha añadido una clase SlimeFuego para trabajar éste tema:
val BAD_WORDS: Array<String> = arrayOf("Feo", "Tonto", "CuatroOjos")
open class Slime (name: String = "slimePorDefecto", hp: Int = 100, ap: Int = 5){
private val name: String
private var hp: Int
private var ap: Int
private var alive: Boolean = true
init {
if (name in BAD_WORDS) this.name = "BuenIntento" else this.name = name
if (hp <= 100) this.hp = hp else this.hp = Random.nextInt(0, 100)
if (ap <= 5) this.ap = ap else this.ap = Random.nextInt(0, 5)
}
fun getName(): String = this.name
fun getHp(): Int = this.hp
fun getAp(): Int = this.ap
fun getAlive(): Boolean = this.alive
fun setHp(value: Int) {
if (value > 100) this.hp = Random.nextInt(0, 100) else this.hp = value
}
fun setAp(value: Int) {
if (value > 5) this.ap = Random.nextInt(0, 5) else this.ap = value
}
fun sayHello() = println("¡Hola soy ${getName()}")
fun heal() {
this.hp = 100
}
fun attack() = println("¡¡Al Ataque!!")
fun evolve() {
this.ap *= 2
}
fun die() {
this.alive = false
}
}
class SlimeAgua(name: String, hp: Int, ap: Int, lungs: Int): Slime(name, hp, ap) {
private var waterResistance: Int
private var timeUnderWater: Int = 0
init{
this.waterResistance = lungs * 120
}
fun getWaterResistance(): Int = this.waterResistance
fun getTimeUnderWater(): Int = this.timeUnderWater
fun breathe() {
this.timeUnderWater = 0
}
fun immerse() {
this.timeUnderWater++
}
}
class SlimeFuego(name: String, hp: Int, ap: Int, ballTemperature: Int): Slime(name, hp, ap) {
private val ballTemperature: Int = ballTemperature
fun getBallTemperature(): Int = this.ballTemperature
}
Una clase abstracta es aquella que está marcada con el modificador abstract
en su declaración. Esto evita que se creen instancias de la clase, pero no impide que se creen subclases a partir de ella.
Las clases abstractas son una pieza fundamental del polimorfismo ya que implementan métodos y atributos genéricos para que las subclases los puedan heredar, facilitando así la abstracción.
{% hint style="warning" %} La diferencia entre utilizar una clase abstracta y una clase abierta para heredar es la capacidad de crear instancias de la primera. Sin embargo, ¿Cuándo utilizar una clase abstracta?
Toda clase abstracta debe contener al menos un método abstracto. {% endhint %}
Los miembros de una clase abstracta también pueden ser marcados como abstract
, lo que significa que no tendrán implementación, debido a que esta será exigida para las subclases.
abstract class ClaseAbstracta {
val atributoNormal: String
abstract val atributoAbstracto: Int
abstract fun metodoAbstracto(): Array<Int>
fun metodoNoAbstracto() {
// Cuerpo
}
}
{% embed url="https://www.linkedin.com/pulse/el-polimorfismo-y-principio-de-substituci%C3%B3n-liskov-en-luis-vespa/" %} Fuente: Luis Vespa (LinkedIn) {% endembed %}
El polimorfismo es una de las características más importantes de la POO. Permite utilizar una interfaz única para hacer referencia a objetos de diferentes tipos. Existen dos tipos:
Permite que una función se escriba de forma genérica, de modo que pueda manejar valores de manera uniforme sin depender de su tipo.
Este tipo de polimorfismo es tan común que se llama simplemente "Polimorfismo".
{% hint style="info" %} Funciones Polimórficas:
Una función que puede evaluarse o aplicarse a valores de diferentes tipos se conoce como función polimórfica. {% endhint %}
Para aplicar el polimorfismo paramétrico se puede hacer de dos maneras:
En este caso, vamos a implementar la función de combate "fight(Slime1, Slime2)
".
Sin embargo, si no existiera el polimorfismo paramétrico tendría que implementar dicha función varias veces, cada una de ellas con unos argumentos diferentes en función del tipo de los Slime que combatan.
Gracias al polimorfismo y a la clase padre Slime que engloba a todas las clases hijo de SlimeTipo, podemos implementar una sola vez la función:
fun fight(x: Slime, y: Slime) {
println("Comienza la batalla")
println("En la esquina azul: ${x.getName()}")
println("con un poder de: ${x.getAp()}")
println("y una vida de: ${x.getHp()}")
println()
println("En la esquina roja: ${y.getName()}")
println("con un poder de: ${y.getAp()}")
println("y una vida de: ${y.getHp()}")
println()
while (x.getHp() > 0 && y.getHp() > 0) {
x.attack()
y.setHp(y.getHp() - x.getAp())
y.attack()
x.setHp(y.getHp() - y.getAp())
}
if (x.getHp() <= 0) {
println("${y.getName()} es el ganador")
} else println("${x.getName()} es el ganador")
}
val azulito: SlimeAgua = SlimeAgua("Azulito", 1000000, 500, 2)
val rojito: SlimeFuego = SlimeFuego("Rojito", 100, 5, 80)
fight(azulito, rojito)
Como vemos, la función coge como argumentos dos objetos de tipo Slime
. Esto hace que todos los objetos que sean inferiores a Slime
en el árbol genealógico puedan entrar en esa definición.
Ese es el motivo por el que la llamada a la función fight no da error.
Lo mismo podemos hacer con una clase abstracta. De hecho, la clase Slime podría ser perfectamente una clase abstracta pues tiene un método "attack
" que podríamos considerar abstracto y no se espera que se generen instancias de ella. Vamos a verlo muy resumido para el ejemplo:
abstract class Slime (name: String = "slimePorDefecto", hp: Int = 100, ap: Int = 5){
/*...*/
abstract fun attack()
/*...*/
}
Con lo anterior, obligamos a cada una de las diferentes clases hijo a implementar su propia función attack
.
class SlimeAgua(name: String, hp: Int, ap: Int, lungs: Int): Slime(name, hp, ap) {
/*...*/
override fun attack() = println("Ataque con chorro.")
}
class SlimeFuego(name: String, hp: Int, ap: Int, ballTemperature: Int): Slime(name, hp, ap) {
/*...*/
override fun attack() = println("Ataque con bola de fuego.")
}
{% hint style="info" %}
Con los cambios que se han realizado, el ejemplo anterior funciona perfectamente, ya que en vez de heredar desde una clase open
, heredan desde una clase abstract
.
Sin embargo, ahora ya no se pueden crear objetos tipo Slime puesto que es una clase abstracta.
De la misma manera hemos aprendido a utilizar las funciones abstractas para generar funciones que aun llamándose igual, se comportan diferente en función del tipo del objeto. Esto es parte del polimorfismo de subtipo que vamos a ver ahora. {% endhint %}
Como ya hemos visto en el anterior ejemplo, puede darse el caso de que tenga un método de la clase padre que en una de las clases hijo se comporta diferente a como se comporta en la clase padre.
En este caso vamos a estudiar dos posibilidades:
La palabra reservada override
se utiliza en las clases hijo que desean cambiar el código de un método de la función padre por uno personalizado:
{% hint style="info" %} Para el ejemplo vamos a volver a poner la clase Slime como clase open. {% endhint %}
open class Slime (name: String = "slimePorDefecto", hp: Int = 100, ap: Int = 5){
/*...*/
open fun attack() = println("Al ataque!!")
fun evolve() {
this.ap *= 2
}
}
Como vemos, la función attack
ya no puede ser abstract
pues daría error, para que se pueda sobreescribir como hemos hecho antes, utilizamos la palabra reservada open
en la función tambien.
class SlimeAgua(name: String, hp: Int, ap: Int, lungs: Int): Slime(name, hp, ap) {
/*...*/
override fun attack() = println("Ataque con chorro.")
}
class SlimeFuego(name: String, hp: Int, ap: Int, ballTemperature: Int): Slime(name, hp, ap) {
/*...*/
override fun attack() = println("Ataque con bola de fuego.")
}
Al contrario que la palabra reservada override, la palabra reservada super llama a una función de la clase padre.
En el caso del ejemplo nos va a quedar muy claro:
class SlimeAgua(name: String, hp: Int, ap: Int, lungs: Int): Slime(name, hp, ap) {
/*...*/
override fun attack() {
super.attack()
println("Ataque con chorro.")
}
}
class SlimeFuego(name: String, hp: Int, ap: Int, ballTemperature: Int): Slime(name, hp, ap) {
/*...*/
override fun attack() {
super.attack()
println("Ataque con bola de fuego.")
}
}
De esta manera, cuando lancemos la función fight
, el log de la batalla quedará de la siguiente manera:
val azulito: SlimeAgua = SlimeAgua("Azulito", 1000000, 500, 2)
val rojito: SlimeFuego = SlimeFuego("Rojito", 100, 5, 80)
fight(azulito, rojito)
/* Respuesta
Comienza la batalla
En la esquina azul: Azulito
con un poder de: 0
y una vida de: 5
En la esquina roja: Rojito
con un poder de: 5
y una vida de: 100
Al ataque!!
Ataque con chorro.
Al ataque!!
Ataque con bola de fuego.
Rojito es el ganador */