32 - POO - herencia

Vimos en conceptos anteriores que dos clases pueden estar relacionadas por la colaboración (en una de ellas definimos una propiedad del tipo de la otra clase). Ahora veremos otro tipo de relación entre clases que es la Herencia.

La herencia significa que se pueden crear nuevas clases partiendo de clases existentes, que tendrá todas las propiedades y los métodos de su 'superclase' o 'clase padre' y además se le podrán añadir otras propiedades y métodos propios.

clase padre

Clase de la que desciende o deriva una clase. Las clases hijas (descendientes) heredan (incorporan) automáticamente las propiedades y métodos de la la clase padre.

Subclase

Clase descendiente de otra. Hereda automáticamente los atributos y métodos de su superclase. Es una especialización de otra clase.
Admiten la definición de nuevos atributos y métodos para aumentar la especialización de la clase.

Veamos algunos ejemplos teóricos de herencia:

1) Imaginemos la clase Vehículo. Qué clases podrían derivar de ella?

                            Vehiculo

   Colectivo                Moto                    Auto
                             
                                             FordK        Renault Fluence

Siempre hacia abajo en la jerarquía hay una especialización (las subclases añaden nuevas propiedades y métodos.

2) Imaginemos la clase Software. ¿Qué clases podrían derivar de ella?

                                           Software

             DeAplicacion                                        DeBase

ProcesadorTexto       PlanillaDeCalculo                          SistemaOperativo

Word   WordPerfect    Excel     Lotus123                         Linux    Windows       

El primer tipo de relación que habíamos visto entre dos clases, es la de colaboración. Recordemos que es cuando una clase contiene un objeto de otra clase como atributo.
Cuando la relación entre dos clases es del tipo "...tiene un..." o "...es parte de...", no debemos implementar herencia. Estamos frente a una relación de colaboración de clases no de herencia.

Si tenemos una ClaseA y otra ClaseB y notamos que entre ellas existe una relacion de tipo "... tiene un...", no debe implementarse herencia sino declarar en la clase ClaseA un atributo de la clase ClaseB.

Por ejemplo: tenemos una clase Auto, una clase Rueda y una clase Volante. Vemos que la relación entre ellas es: Auto "...tiene 4..." Rueda, Volante "...es parte de..." Auto; pero la clase Auto no debe derivar de Rueda ni Volante de Auto porque la relación no es de tipo-subtipo sino de colaboración. Debemos declarar en la clase Auto 4 atributos de tipo Rueda y 1 de tipo Volante.

Luego si vemos que dos clase responden a la pregunta ClaseA "..es un.." ClaseB es posible que haya una relación de herencia.

Por ejemplo:

Auto "es un" Vehiculo
Circulo "es una" Figura
Mouse "es un" DispositivoEntrada
Suma "es una" Operacion

Problema 1:

Plantear una clase Persona que contenga dos propiedades: nombre y edad. Definir como responsabilidades el constructor que reciba el nombre y la edad.
En la función main del programa definir un objeto de la clase Persona y llamar a sus métodos.

Declarar una segunda clase llamada Empleado que herede de la clase Persona y agregue una propiedad sueldo y muestre si debe pagar impuestos (sueldo superior a 3000)
También en la función main del programa crear un objeto de la clase Empleado.

Proyecto137 - Principal.kt

open class Persona(val nombre: String, val edad: Int) {
    open fun imprimir() {
        println("Nombre: $nombre")
        println("Edad: $edad")
    }
}

class Empleado(nombre: String, edad: Int, val sueldo: Double): Persona(nombre, edad) {
    override fun imprimir() {
        super.imprimir()
        println("Sueldo: $sueldo")
    }

    fun pagaImpuestos() {
        if (sueldo > 3000)
            println("El empleado $nombre paga impuestos")
        else
            println("El empleado $nombre no paga impuestos")
    }
}

fun main(parametro: Array<String>) {
    val persona1 = Persona("Jose", 22)
    println("Datos de la persona")
    persona1.imprimir()

    val empleado1 = Empleado("Ana", 30, 5000.0)
    println("Datos del empleado")
    empleado1.imprimir()
    empleado1.pagaImpuestos()
}

En Kotlin para que una clase sea heredable debemos anteceder la palabra clave open previo a class:

open class Persona(val nombre: String, val edad: Int) {

Cuando un programador defina una clase open debe pensar seriamente la definición de sus propiedades y métodos.

Si queremos que un método se pueda reescribir en una subclase debemos anteceder la palabra clave open:

    open fun imprimir() {
        println("Nombre: $nombre")
        println("Edad: $edad")
    }

Cuando definimos un objeto de la clase Persona en la función main por más que sea open y tenga métodos open no cambia en nada a como definimos objetos y accedemos a sus propiedades y métodos:

fun main(parametro: Array<String>) {
    val persona1 = Persona("Jose", 22)
    println("Datos de la persona")
    persona1.imprimir()

Lo nuevo aparece cuando declaramos la clase Empleado que recibe tres parámetros, es importante notar que en el tercero estamos definiendo una propiedad llamada sueldo (porque antecedemos la palabra clave val)

La herencia la indicamos después de los dos puntos indicando el nombre de la clase de la cual heredamos y pasando inmediatamente los datos del constructor de dicha clase:

class Empleado(nombre: String, edad: Int, val sueldo: Double): Persona(nombre, edad) {

Podemos decir que la clase Empleado tiene tres propiedades (nombre, edad y sueldo), una propia y dos heredadas.

Como no le antecedemos la palabra clave open a la declaración de la clase luego no se podrán declarar clases que hereden de la clase Empleado. Si queremos que de la clase Empleado puedan heredar otras clases la debemos definir con la sintaxis:

open class Empleado(nombre: String, edad: Int, val sueldo: Double): Persona(nombre, edad) {

La clase Empleado aparte del constructor define dos métodos, uno que imprime un mensaje si debe parar impuestos el empleado:

    fun pagaImpuestos() {
        if (sueldo > 3000)
            println("El empleado $nombre paga impuestos")
        else
            println("El empleado $nombre no paga impuestos")
    }

El método imprimir indicamos mediante la palabra clave override que estamos sobreescribiendo el método imprimir de la clase persona (dentro del método imprimir podemos llamar al método imprimir de la clase padre antecediendo la palabra clave super):

    override fun imprimir() {
        super.imprimir()
        println("Sueldo: $sueldo")
    }

Con la llamada al método imprimir de la clase que hereda podemos mostrar todos los datos del empleado que son su nombre, edad y sueldo.

Para definir un objeto de la clase Empleado en la función main lo hacemos con la sintaxis que ya conocemos:

    val empleado1 = Empleado("Ana", 30, 5000.0)
    println("Datos del empleado")
    empleado1.imprimir()
    empleado1.pagaImpuestos()

Problema 2:

Declarar una clase llamada Calculadora que reciba en el constructor dos valores de tipo Double. Hacer la clase abierta para que sea heredable
Definir las responsabilidades de sumar, restar, multiplicar, dividir e imprimir.

Declarar luego una clase llamada CalculadoraCientifica que herede de Calculadora y añada las responsabilidades de calcular el cuadrado del primer número y la raíz cuadrada.

Proyecto138 - Principal.kt

open class Calculadora(val valor1: Double, val valor2: Double ){
    var resultado: Double = 0.0
    fun sumar() {
        resultado = valor1 + valor2
    }

    fun restar() {
        resultado = valor1 - valor2
    }

    fun multiplicar() {
        resultado = valor1 * valor2
    }

    fun dividir() {
        resultado = valor1 / valor2
    }

    fun imprimir() {
        println("Resultado: $resultado")
    }
}

class CalculadoraCientifica(valor1: Double, valor2: Double): Calculadora(valor1, valor2) {
    fun cuadrado() {
        resultado = valor1 * valor1
    }

    fun raiz() {
        resultado = Math.sqrt(valor1)
    }
}

fun main(parametro: Array<String>) {
    println("Prueba de la clase Calculadora (suma de dos números)")
    val calculadora1 = Calculadora(10.0, 2.0)
    calculadora1.sumar()
    calculadora1.imprimir()
    println("Prueba de la clase Calculadora Cientrífica (suma de dos números y el cuadrado y la raiz del primero)")
    val calculadoraCientifica1 = CalculadoraCientifica(10.0, 2.0)
    calculadoraCientifica1.sumar()
    calculadoraCientifica1.imprimir()
    calculadoraCientifica1.cuadrado()
    calculadoraCientifica1.imprimir()
    calculadoraCientifica1.raiz()
    calculadoraCientifica1.imprimir()
}

Declaramos la clase Calculadora open para permitir que otras clases hereden de esta, llegan al constructor dos valores de tipo Double:

open class Calculadora(val valor1: Double, val valor2: Double ){

Definimos una tercer propiedad llamada resultado también de tipo Double:

    var resultado: Double = 0.0

Es importante tener en cuenta que la propiedad resultado es public, luego podemos tener acceso en sus subclases. Si la definimos de tipo private luego la subclase CalculadoraCientifica no puede acceder a su contenido:

    private var resultado: Double = 0.0

En la clase CalculadoraCientifica se genera un error sintáctico cuando queremos acceder a dicha propiedad:

    fun cuadrado() {
        resultado = valor1 * valor1
    }

Vimos que los modificadores de acceso pueden ser public (este es el valor por defecto) y private, existe un tercer modificador de acceso llamado protected que permite que una subclase tenga acceso a la propiedad pero no se tenga acceso desde donde definimos un objeto de dicha clase. Luego lo más conveniente para este problema es definir la propiedad resultado de tipo protected:

    protected var resultado: Double = 0.0

La clase Calculadora define los cuatro métodos con las operaciones básicas y la impresión del resultado.

La clase CalculadoraCientifica hereda de la clase Calculadora:

class CalculadoraCientifica(valor1: Double, valor2: Double): Calculadora(valor1, valor2) {

Una calculadora científica aparte de las cuatro operaciones básicas agrega la posibilidad de calcular el cuadrado y la raíz de un número:

    fun cuadrado() {
        resultado = valor1 * valor1
    }

    fun raiz() {
        resultado = Math.sqrt(valor1)
    }

En la función main creamos un objeto de la clase Calculadora y llamamos a varios de sus métodos:

fun main(parametro: Array<String>) {
    println("Prueba de la clase Calculadora (suma de dos números)")
    val calculadora1 = Calculadora(10.0, 2.0)
    calculadora1.sumar()
    calculadora1.imprimir()

Lo mismo hacemos definiendo un objeto de la clase CalculadoraCientifica y llamando a algunos de sus métodos:

    println("Prueba de la clase Calculadora Cientrífica (suma de dos números y el cuadrado y la raiz del primero)")
    val calculadoraCientifica1 = CalculadoraCientifica(10.0, 2.0)
    calculadoraCientifica1.sumar()
    calculadoraCientifica1.imprimir()
    calculadoraCientifica1.cuadrado()
    calculadoraCientifica1.imprimir()
    calculadoraCientifica1.raiz()
    calculadoraCientifica1.imprimir()

Problema propuesto

  • Declarar una clase Dado que genere un valor aleatorio entre 1 y 6, mostrar su valor.
    Crear una segunda clase llamada DadoRecuadro que genere un valor entre 1 y 6, mostrar el valor recuadrado en asteríscos.
    Utilizar la herencia entre estas dos clases.
Solución
Proyecto139

open class Dado{
    protected var valor: Int = 1
    fun tirar() {
        valor = ((Math.random() * 6) + 1).toInt()
    }

    open fun imprimir() {
        println("$valor")
    }
}

class DadoRecuadro: Dado() {
    override fun imprimir() {
        println("***")
        println("*$valor*")
        println("***")
    }
}

fun main(parametro: Array<String>) {
    val dado1 = Dado()
    dado1.tirar()
    dado1.imprimir()

    val dado2 = DadoRecuadro()
    dado2.tirar()
    dado2.imprimir()
}