Clases y herencia en Kotlin

1. Antes de comenzar

Requisitos previos

  • Estar familiarizado con el uso del Playground de Kotlin para editar programas de Kotlin
  • Conocer los conceptos básicos de programación en Kotlin que se explican en la unidad 1 de este curso (en particular, el programa main() tiene funciones con argumentos que muestran valores, variables, tipos de datos y operaciones, así como sentencias if/else)
  • Ser capaz de definir una clase en Kotlin, crear una instancia de objeto desde ella y acceder a sus propiedades y métodos

Qué aprenderás

  • Cómo crear un programa de Kotlin que use la herencia a fin de implementar una jerarquía de clases
  • Cómo extender una clase, anular su funcionalidad existente y agregar una funcionalidad nueva
  • Cómo elegir el modificador de visibilidad correcto para las variables

Qué compilarás

  • Un programa de Kotlin con diferentes tipos de viviendas que se implementan como una jerarquía de clases

Requisitos

2. ¿Qué es una jerarquía de clases?

Es natural que los seres humanos clasifiquen elementos que tienen propiedades y comportamientos similares en grupos, y que incluso establezcan algún tipo de jerarquía entre ellos. Por ejemplo, puedes tener una categoría amplia, como vegetales, y en ella puedes tener un tipo más específico, como legumbres. Dentro de las legumbres, puedes tener tipos aún más específicos, como guisantes, frijoles, lentejas, garbanzos y porotos, por ejemplo.

Esto se puede representar como una jerarquía porque las legumbres contienen o heredan todas las propiedades de los vegetales (p. ej., son plantas y se pueden consumir). Del mismo modo, los guisantes, los frijoles y las lentejas tienen las propiedades de las legumbres y sus propias propiedades únicas.

Veamos cómo representarías esta relación en términos de programación. Si haces que Vegetable sea una clase en Kotlin, puedes crear Legume como elemento secundario o subclase de la clase Vegetable. Eso significa que la clase Legume hereda todas las propiedades y los métodos de la clase Vegetable (lo que significa que también están disponibles en ella).

Puedes representar esto en un diagrama de jerarquía de clases, como se muestra a continuación. Puedes hacer referencia a Vegetable como el elemento superior o la superclase de la clase Legume.

87e0a5eb0f85042d.png

Podrías continuar y expandir la jerarquía de clases creando subclases de Legume como Lentil y Chickpea. Esto hace que Legume sea tanto un elemento secundario o subclase de Vegetable como un elemento principal o superclase de Lentil y Chickpea. Vegetable es la clase raíz o de nivel superior (o base) de esta jerarquía.

638655b960530d9.png

Herencia en clases de Android

Si bien puedes escribir código Kotlin sin usar clases (que es lo que hiciste en codelabs anteriores), muchas partes de Android se te proporcionan en forma de clases, incluidas las actividades, las vistas y los grupos de vistas. Por lo tanto, comprender las jerarquías de clases resulta fundamental para el desarrollo de apps para Android y te permitirá aprovechar las funciones que ofrece el framework de Android.

Por ejemplo, hay una clase View en Android que representa un área rectangular en la pantalla y es responsable de dibujar y manejar eventos. La clase TextView es una subclase de la clase View, lo cual significa que TextView hereda todas las propiedades y funciones de la clase View, y agrega lógica específica para mostrarle texto al usuario.

c39a8aaa5b013de8.png

Si vamos un paso más allá, vemos que las clases EditText y Button son elementos secundarios de la clase TextView. Ambas heredan todas las propiedades y los métodos de las clases TextView y View, y también agregan su propia lógica específica. Por ejemplo, EditText agrega su propia funcionalidad en relación con la capacidad de editar texto en la pantalla.

En lugar de tener que copiar y pegar toda la lógica de las clases View y TextView en la clase EditText, alcanza con que EditText sea una subclase de la clase TextView, que, a su vez, es la subclase de la clase View. Luego, el código de la clase EditText puede enfocarse específicamente en lo que hace que este componente de IU sea diferente de otras vistas.

En la parte superior de la página de la documentación de una clase de Android en el sitio web developer.android.com, puedes ver el diagrama de jerarquía de clases. Si ves kotlin.Any en la parte superior de la jerarquía, se debe a que, en Kotlin, todas las clases tienen una superclase Any en común. Obtén más información aquí.

1ce2b1646b8064ab.png

Como puedes ver, aprender a aprovechar la herencia entre clases puede hacer que tu código sea más fácil de escribir, reutilizar, leer y probar.

3. Crea una clase base

Jerarquía de clases de viviendas

En este codelab, compilarás un programa de Kotlin que demuestra cómo funcionan las jerarquías de clases usando viviendas (alojamientos donde viven las personas) con superficies, pisos y habitantes a modo de ejemplo.

A continuación, se muestra un diagrama de la jerarquía de clases que compilarás. En la raíz, tienes una Dwelling que especifica las propiedades y funcionalidades que son verdaderas para todas las viviendas, similar a un plano técnico. Luego, tienes clases para una cabaña cuadrada (SquareCabin); una caseta redonda (RoundHut); y una torre redonda (RoundTower), que es una RoundHut con varios pisos.

de1387ca7fc26c81.png

Las clases que implementarás son las siguientes:

  • Dwelling: Una clase base que representa un alojamiento no específico que contiene información común para todas las viviendas
  • SquareCabin: Una cabaña cuadrada de madera con una superficie de planta cuadrada
  • RoundHut: Una caseta redonda hecha de paja con una superficie de planta circular; el elemento superior de RoundTower
  • RoundTower: Una torre redonda hecha de piedra con una superficie de planta circular y varios pisos

Crea una clase Dwelling abstracta

Cualquier clase puede ser la clase base de una jerarquía de clases o un elemento superior de otras clases.

Una clase "abstracta" es una clase de la que no se puede crear una instancia porque no se implementó por completo. Podemos pensar que es como un esbozo. Un esbozo incorpora las ideas y los planos para un elemento, pero en general no tiene información suficiente para compilarlo. Usarás un esbozo (clase abstracta) a fin de crear un plano (clase) a partir del cual crear la instancia real del objeto.

Un beneficio común de crear una superclase es que esta contiene propiedades y funciones comunes a todas sus subclases. Si no se conocen los valores de las propiedades y las implementaciones de funciones, haz que la clase sea abstracta. Por ejemplo, Vegetables tiene muchas propiedades comunes a todos los vegetales, pero no puedes crear una instancia de un vegetal inespecífico, ya que no sabes, por ejemplo, su forma o color. Por lo tanto, Vegetable es una clase abstracta que deja que las subclases determinen los detalles específicos de cada vegetal.

La declaración de una clase abstracta comienza con la palabra clave abstract.

Dwelling será una clase abstracta como Vegetable. Tendrá propiedades y funciones comunes a muchos tipos de viviendas, pero no se conocen los valores exactos de las propiedades ni los detalles de la implementación de las funciones.

  1. Accede al Playground de Kotlin en https://developer.android.com/training/kotlinplayground.
  2. En el editor, borra println("Hello, world!") dentro de la función main().
  3. Luego, agrega este código, debajo de la función main(), a fin de crear una clase abstract llamada Dwelling.
abstract class Dwelling(){
}

Agrega una propiedad para materiales de construcción

En esta clase Dwelling, defines cosas que son verdaderas para todas las viviendas, incluso aunque puedan ser diferentes para distintas viviendas. Todas las viviendas están hechas de materiales de construcción.

  1. Dentro de Dwelling, crea una variable buildingMaterial de tipo String para representar el material de construcción. Dado que este material no cambiará, usa val para convertirlo en una variable inmutable.
val buildingMaterial: String
  1. Ejecuta tu programa. Se mostrará el siguiente error:
Property must be initialized or be abstract

La propiedad buildingMaterial no tiene un valor. De hecho, NO PUEDES asignarle un valor, porque un edificio inespecífico no está hecho de nada en particular. Por lo tanto, como se indica en el mensaje de error, puedes agregar un prefijo a la declaración de buildingMaterial con la palabra clave abstract a fin de indicar que esta no se definirá aquí.

  1. Agrega la palabra clave abstract a la definición de la variable.
abstract val buildingMaterial: String
  1. Ejecuta tu código. Aunque no hace nada, ahora se compila sin errores.
  2. Crea una instancia de Dwelling en la función main() y ejecuta tu código.
val dwelling = Dwelling()
  1. Se mostrará un error porque no puedes crear una instancia de la clase abstracta Dwelling.
Cannot create an instance of an abstract class
  1. Borra este código incorrecto.

Hasta ahora, el código terminado es el siguiente:

abstract class Dwelling(){
    abstract val buildingMaterial: String
}

Agrega una propiedad para la capacidad

Otra propiedad de una vivienda es la capacidad, es decir, cuántas personas pueden vivir en ella.

Todas las viviendas tienen una capacidad que no cambia. Sin embargo, la capacidad no se puede establecer dentro de la superclase Dwelling. Debe definirse en subclases para tipos específicos de viviendas.

  1. En Dwelling, agrega un número entero val abstract llamado capacity.
abstract val capacity: Int

Agrega una propiedad privada para la cantidad de habitantes

Todas las viviendas tendrán una cantidad de residents que residan en la vivienda (que puede ser menor o igual que la capacity). Por lo tanto, define la propiedad residents en la superclase Dwelling para que todas las subclases hereden de ella y la usen.

  1. Puedes hacer que residents sea un parámetro que se pase al constructor de la clase Dwelling. La propiedad residents es una var, ya que la cantidad de habitantes puede cambiar después de creada la instancia.
abstract class Dwelling(private var residents: Int) {

Ten en cuenta que la propiedad residents está marcada con la palabra clave private. Private es un modificador de visibilidad en Kotlin, lo que significa que la propiedad residents solo es visible en esta clase (y se puede usar dentro de ella). No se puede acceder a ella desde ningún otro lugar del programa. Puedes marcar las propiedades o los métodos con la palabra clave private. De lo contrario, cuando no se especifica un modificador de visibilidad, las propiedades y los métodos son public de forma predeterminada y se puede acceder a ellos desde otras partes del programa. Dado que la cantidad de personas que viven en una vivienda suele ser información privada (en comparación con la información del material de construcción o la capacidad del edificio), esta es una decisión razonable.

Con la capacity de la vivienda y la cantidad actual de residents definida, puedes crear una función hasRoom() a fin de determinar si hay espacio para otro habitante en la vivienda. Puedes definir e implementar la función hasRoom() en la clase Dwelling porque la fórmula para calcular si hay espacio es igual para todas las viviendas. Habrá espacio en una Dwelling si la cantidad de residents es menor a la capacity, y la función debe mostrar true o false según esta comparación.

  1. Agrega la función hasRoom() a la clase Dwelling.
fun hasRoom(): Boolean {
    return residents < capacity
}
  1. Puedes ejecutar este código. No debería haber errores. Todavía no hace nada visible.

El código terminado debería verse de la siguiente manera:

abstract class Dwelling(private var residents: Int) {

   abstract val buildingMaterial: String
   abstract val capacity: Int

   fun hasRoom(): Boolean {
       return residents < capacity
   }
}

4. Crea subclases

Crea una subclase SquareCabin

  1. Debajo de la clase Dwelling, crea una llamada SquareCabin.
class SquareCabin
  1. A continuación, debes indicar que SquareCabin está relacionada con Dwelling. En tu código, indica que SquareCabin se extiende de Dwelling (o es una subclase de Dwelling), porque SquareCabin proporcionará una implementación de las partes abstractas de Dwelling.

Indica esta relación de herencia agregando dos puntos (:) después del nombre de la clase SquareCabin, seguidos de una llamada a fin de inicializar la clase superior Dwelling. No olvides agregar paréntesis después del nombre de clase Dwelling.

class SquareCabin : Dwelling()
  1. Cuando extiendes desde una superclase, debes pasar los parámetros obligatorios que requiere la superclase. Dwelling requiere la cantidad de residents como entrada. Puedes pasar una cantidad fija de habitantes como 3.
class SquareCabin : Dwelling(3)

Sin embargo, quieres que tu programa sea más flexible y permita una cantidad variable de habitantes en SquareCabins. Por lo tanto, haz que residents sea un parámetro en la definición de la clase SquareCabin. No declares residents como val, porque volverías a usar una propiedad ya declarada en la clase superior Dwelling.

class SquareCabin(residents: Int) : Dwelling(residents)
  1. Ejecuta tu código.
  2. Esto generará errores. Echemos un vistazo:
Class 'SquareCabin' is not abstract and does not implement abstract base class member public abstract val buildingMaterial: String defined in Dwelling

Cuando declaras funciones y variables abstractas, lo que haces es como una promesa de que les darás valores e implementaciones más adelante. Para una variable, significa que cualquier subclase de esa clase abstracta debe darle un valor. Para una función, significa que cualquier subclase necesita implementar el cuerpo de la función.

En la clase Dwelling, definiste una variable abstract buildingMaterial. SquareCabin es una subclase de Dwelling, por lo que debe proporcionar un valor para buildingMaterial. Usa la palabra clave override a fin de indicar que esta propiedad se definió en una clase superior y que está por anularse en esta clase.

  1. En la clase SquareCabin, override la propiedad buildingMaterial y asígnale el valor "Wood".
  2. Haz lo mismo para la capacity e indica que 6 habitantes pueden vivir en una SquareCabin.
class SquareCabin(residents: Int) : Dwelling(residents) {
    override val buildingMaterial = "Wood"
    override val capacity = 6
}

El código finalizado debería verse de la siguiente manera:

abstract class Dwelling(private var residents: Int) {
    abstract val buildingMaterial: String
    abstract val capacity: Int

    fun hasRoom(): Boolean {
       return residents < capacity
   }
}

class SquareCabin(residents: Int) : Dwelling(residents) {
    override val buildingMaterial = "Wood"
    override val capacity = 6
}

Para probar tu código, crea una instancia de SquareCabin en tu programa.

Usa SquareCabin

  1. Inserta una función main() vacía antes de las definiciones de las clases Dwelling y SquareCabin.
fun main() {

}

abstract class Dwelling(private var residents: Int) {
    ...
}

class SquareCabin(residents: Int) : Dwelling(residents) {
    ...
}
  1. Dentro de la función main(), crea una instancia de SquareCabin llamada squareCabin con 6 habitantes. Agrega sentencias de impresión para el material de construcción, la capacidad y la función hasRoom().
fun main() {
    val squareCabin = SquareCabin(6)

    println("\nSquare Cabin\n============")
    println("Capacity: ${squareCabin.capacity}")
    println("Material: ${squareCabin.buildingMaterial}")
    println("Has room? ${squareCabin.hasRoom()}")
}

Observa que la función hasRoom() no se definió en la clase SquareCabin, pero sí se definió en la clase Dwelling. Debido a que SquareCabin es una subclase de la clase Dwelling, la función hasRoom() se heredó de forma gratuita. Ahora, se puede llamar a la función hasRoom() en todas las instancias de SquareCabin. Puedes verlo en el fragmento de código como squareCabin.hasRoom().

  1. Ejecuta el código. Se debería imprimir lo siguiente:
Square Cabin
============
Capacity: 6
Material: Wood
Has room? false

Creaste squareCabin con 6 habitantes, lo que equivale a la capacity, por lo que hasRoom() muestra false. Puedes experimentar con la inicialización de SquareCabin con una cantidad menor de residents y, cuando vuelvas a ejecutar tu programa, hasRoom() debería mostrar true.

Usa with para simplificar tu código

En las sentencias println(), cada vez que hagas referencia a una propiedad o función de squareCabin, observa que debes repetir squareCabin.. Esto se vuelve repetitivo y puede ser una fuente de errores cuando copias y pegas sentencias de impresión.

Cuando trabajas con una instancia específica de una clase y necesitas acceder a varias propiedades y funciones de esa instancia, puedes decir "haz todas las siguientes operaciones con este objeto de instancia" usando una sentencia with. Comienza con la palabra clave with, seguida del nombre de la instancia entre paréntesis, seguido de llaves que contienen las operaciones que deseas realizar.

with (instanceName) {
    // all operations to do with instanceName
}
  1. En la función main(), cambia las instrucciones de impresión para usar with.
  2. Borra squareCabin. de las sentencias de impresión.
with(squareCabin) {
    println("\nSquare Cabin\n============")
    println("Capacity: ${capacity}")
    println("Material: ${buildingMaterial}")
    println("Has room? ${hasRoom()}")
}
  1. Vuelve a ejecutar el código para asegurarte de que se ejecute sin errores y muestre el mismo resultado.
Square Cabin
============
Capacity: 6
Material: Wood
Has room? false

Este es el código completado:

fun main() {
    val squareCabin = SquareCabin(6)

    with(squareCabin) {
        println("\nSquare Cabin\n============")
        println("Capacity: ${capacity}")
        println("Material: ${buildingMaterial}")
        println("Has room? ${hasRoom()}")
    }
}

abstract class Dwelling(private var residents: Int) {
    abstract val buildingMaterial: String
    abstract val capacity: Int

    fun hasRoom(): Boolean {
       return residents < capacity
   }
}

class SquareCabin(residents: Int) : Dwelling(residents) {
    override val buildingMaterial = "Wood"
    override val capacity = 6
}

Crea una subclase RoundHut

  1. De la misma manera que con SquareCabin, agrega otra subclase, RoundHut, a Dwelling.
  2. Anula buildingMaterial y asígnale un valor de "Straw".
  3. Anula capacity y establécelo en 4.
class RoundHut(residents: Int) : Dwelling(residents) {
    override val buildingMaterial = "Straw"
    override val capacity = 4
}
  1. En main(), crea una instancia de RoundHut con 3 habitantes.
val roundHut = RoundHut(3)
  1. Agrega el código que se indica a continuación a fin de imprimir información sobre roundHut.
with(roundHut) {
    println("\nRound Hut\n=========")
    println("Material: ${buildingMaterial}")
    println("Capacity: ${capacity}")
    println("Has room? ${hasRoom()}")
}
  1. Ejecuta el código. El resultado para todo el programa debería ser el siguiente:
Square Cabin
============
Capacity: 6
Material: Wood
Has room? false

Round Hut
=========
Material: Straw
Capacity: 4
Has room? true

Ahora, tienes una jerarquía de clases similar a esta, con Dwelling como la clase raíz y SquareCabin y RoundHut como las subclases de Dwelling.

c19084f4a83193a0.png

Crea una subclase RoundTower

La última clase de esta jerarquía de clases es una torre redonda. Puedes pensar en una torre redonda como una caseta redonda de varios pisos y hecha de piedra. Por lo tanto, puedes hacer que RoundTower sea una subclase de RoundHut.

  1. Crea una clase RoundTower que sea una subclase de RoundHut. Agrega el parámetro residents al constructor de RoundTower y, luego, pasa ese parámetro al constructor de la superclase RoundHut.
  2. Anula el valor buildingMaterial para que sea "Stone".
  3. Establece la capacity en 4.
class RoundTower(residents: Int) : RoundHut(residents) {
    override val buildingMaterial = "Stone"
    override val capacity = 4
}
  1. Ejecuta este código. Se mostrará un error.
This type is final, so it cannot be inherited from

Este error significa que no se puede crear (ni heredar) una subclase de la clase RoundHut. De manera predeterminada, en Kotlin, las clases son definitivas y no pueden crearse en subclases de ellas. Solo podrás heredar de las clases abstract o clases marcadas con la palabra clave open. Por lo tanto, debes marcar la clase RoundHut con la palabra clave open a fin de permitir que se pueda heredar de ella.

  1. Agrega la palabra clave open al principio de la declaración RoundHut.
open class RoundHut(residents: Int) : Dwelling(residents) {
   override val buildingMaterial = "Straw"
   override val capacity = 4
}
  1. En main(), crea una instancia de roundTower y, luego, imprime información sobre ella.
 val roundTower = RoundTower(4)
with(roundTower) {
    println("\nRound Tower\n==========")
    println("Material: ${buildingMaterial}")
    println("Capacity: ${capacity}")
    println("Has room? ${hasRoom()}")
}

Este es el código completo.

fun main() {
    val squareCabin = SquareCabin(6)
    val roundHut = RoundHut(3)
    val roundTower = RoundTower(4)

    with(squareCabin) {
        println("\nSquare Cabin\n============")
        println("Capacity: ${capacity}")
        println("Material: ${buildingMaterial}")
        println("Has room? ${hasRoom()}")
    }

    with(roundHut) {
        println("\nRound Hut\n=========")
        println("Material: ${buildingMaterial}")
        println("Capacity: ${capacity}")
        println("Has room? ${hasRoom()}")
    }

    with(roundTower) {
        println("\nRound Tower\n==========")
        println("Material: ${buildingMaterial}")
        println("Capacity: ${capacity}")
        println("Has room? ${hasRoom()}")
    }
}

abstract class Dwelling(private var residents: Int) {
    abstract val buildingMaterial: String
    abstract val capacity: Int

    fun hasRoom(): Boolean {
       return residents < capacity
   }
}

class SquareCabin(residents: Int) : Dwelling(residents) {
    override val buildingMaterial = "Wood"
    override val capacity = 6
}

open class RoundHut(residents: Int) : Dwelling(residents) {
   override val buildingMaterial = "Straw"
   override val capacity = 4
}

class RoundTower(residents: Int) : RoundHut(residents) {
    override val buildingMaterial = "Stone"
    override val capacity = 4
}
  1. Ejecuta tu código. Ahora debería funcionar sin errores y producir el siguiente resultado:
Square Cabin
============
Capacity: 6
Material: Wood
Has room? false

Round Hut
=========
Material: Straw
Capacity: 4
Has room? true

Round Tower
==========
Material: Stone
Capacity: 4
Has room? false

Agrega varios pisos a RoundTower

RoundHut, por inferencia, es un edificio de un solo piso. Las torres suelen tener varios pisos (plantas).

Si se tiene en cuenta la capacidad, mientras más pisos tenga una torre, más capacidad tendrá.

Puedes modificar RoundTower a fin de que tenga varios pisos y ajustar la capacidad según su cantidad.

  1. Actualiza el constructor de RoundTower de modo que tome un parámetro adicional de número entero val floors para la cantidad de pisos. Colócalo después de los residents. Observa que no necesitas pasar esto al constructor RoundHut superior, ya que floors se define aquí en RoundTower y RoundHut no tiene floors.
class RoundTower(
    residents: Int,
    val floors: Int) : RoundHut(residents) {

    ...
}
  1. Ejecuta tu código. Se produce un error cuando se crea roundTower en el método main(), ya que no proporcionaste un número para el argumento de floors. Puedes agregar el argumento que falta.

Como alternativa, en la definición de clase de RoundTower, puedes agregar un valor predeterminado de floors como se muestra a continuación. Luego, cuando no se pasa ningún valor de floors al constructor, se puede usar el valor predeterminado a fin de crear la instancia del objeto.

  1. En tu código, agrega = 2 después de la declaración de floors a efectos de asignarle un valor predeterminado de 2.
class RoundTower(
    residents: Int,
    val floors: Int = 2) : RoundHut(residents) {

    ...
}
  1. Ejecuta tu código. Debería compilarse, ya que RoundTower(4) ahora crea una instancia de objeto RoundTower con el valor predeterminado de 2 pisos.
  2. En la clase RoundTower, actualiza la capacity y multiplícala por la cantidad de pisos.
override val capacity = 4 * floors
  1. Ejecuta tu código y observa que la capacidad de RoundTower ahora es de 8 para 2 pisos.

Este es tu código terminado.

fun main() {

    val squareCabin = SquareCabin(6)
    val roundHut = RoundHut(3)
    val roundTower = RoundTower(4)

    with(squareCabin) {
        println("\nSquare Cabin\n============")
        println("Capacity: ${capacity}")
        println("Material: ${buildingMaterial}")
        println("Has room? ${hasRoom()}")
    }

    with(roundHut) {
        println("\nRound Hut\n=========")
        println("Material: ${buildingMaterial}")
        println("Capacity: ${capacity}")
        println("Has room? ${hasRoom()}")
    }

    with(roundTower) {
        println("\nRound Tower\n==========")
        println("Material: ${buildingMaterial}")
        println("Capacity: ${capacity}")
        println("Has room? ${hasRoom()}")
    }
}

abstract class Dwelling(private var residents: Int) {
    abstract val buildingMaterial: String
    abstract val capacity: Int

    fun hasRoom(): Boolean {
       return residents < capacity
   }
}

class SquareCabin(residents: Int) : Dwelling(residents) {
    override val buildingMaterial = "Wood"
    override val capacity = 6
}

open class RoundHut(residents: Int) : Dwelling(residents) {
    override val buildingMaterial = "Straw"
    override val capacity = 4
}

class RoundTower(
    residents: Int,
    val floors: Int = 2) : RoundHut(residents) {

    override val buildingMaterial = "Stone"
    override val capacity = 4 * floors
}

5. Modifica las clases de la jerarquía

Calcula la superficie de la planta

En este ejercicio, aprenderás a declarar una función abstracta en una clase abstracta y, luego, a implementar su funcionalidad en las subclases.

Todas las viviendas tienen una superficie de planta, pero, según la forma del edificio, esta se calcula de manera diferente.

Define floorArea() en la clase Dwelling

  1. Primero, agrega una función abstract floorArea() a la clase Dwelling. Muestra un Double. Double es un tipo de datos, como String y Int. Se utiliza para números de punto flotante, es decir, números que tienen un punto seguido de una parte decimal, como 5.8793.
abstract fun floorArea(): Double

Todos los métodos abstractos definidos en una clase abstracta deben implementarse en cualquiera de sus subclases. Para poder ejecutar tu código, debes implementar floorArea() en las subclases.

Implementa floorArea() para SquareCabin

Como sucede con buildingMaterial y capacity, ya que estás implementando una función abstract que se define en la clase superior, debes usar la palabra clave override.

  1. En la clase SquareCabin, comienza con la palabra clave override seguida de la implementación real de la función floorArea(), como se muestra a continuación.
override fun floorArea(): Double {

}
  1. Muestra la superficie calculada de la planta. La superficie de un rectángulo o cuadrado es la longitud de su lado multiplicada por la longitud de su otro lado. El cuerpo de la función será return length * length.
override fun floorArea(): Double {
    return length * length
}

La longitud no es una variable en la clase y es diferente para cada instancia, por lo que puedes agregarla como un parámetro constructor para la clase SquareCabin.

  1. Cambia la definición de clase de SquareCabin para agregar un parámetro length de tipo Double. Declara la propiedad como un val, ya que la longitud de un edificio no cambia.
class SquareCabin(residents: Int, val length: Double) : Dwelling(residents) {

Dwelling y, por lo tanto, todas sus subclases tienen residents como un argumento del constructor. Dado que es el primer argumento del constructor de Dwelling, es una práctica recomendada que también lo conviertas en el primer argumento en todos los constructores de las subclases y que coloques los argumentos en el mismo orden en todas las definiciones de clase. Por lo tanto, inserta el nuevo parámetro length después del parámetro residents.

  1. En main(), actualiza la creación de la instancia de squareCabin. Pasa 50.0 al constructor SquareCabin como la length.
val squareCabin = SquareCabin(6, 50.0)
  1. Dentro de la sentencia with para squareCabin, agrega una sentencia de impresión para la superficie de la planta.
println("Floor area: ${floorArea()}")

No se ejecutará tu código, ya que también deberás implementar floorArea() en RoundHut.

Implementa floorArea() para RoundHut

De la misma manera, implementa la superficie de la planta para RoundHut. RoundHut también es una subclase directa de Dwelling, por lo que debes usar la palabra clave override.

La superficie de la planta de una vivienda circular es igual a PI * radio * radio.

PI es un valor matemático. Se define en una biblioteca de matemática. Una biblioteca es una colección predefinida de funciones y valores definidos fuera de un programa que este puede usar. Para usar una función o un valor de una biblioteca, deberás indicarle al compilador que lo vas a usar. Para ello, importa la función o el valor a tu programa. Para usar PI en tu programa, debes importar kotlin.math.PI.

  1. Importa PI desde la biblioteca de matemática de Kotlin. Coloca esto en la parte superior del archivo, antes de main().
import kotlin.math.PI
  1. Implementa la función floorArea() para RoundHut.
override fun floorArea(): Double {
    return PI * radius * radius
}

Advertencia: Si no importas kotlin.math.PI, verás un error, por lo que debes importar esta biblioteca antes de usarla. Como alternativa, puedes escribir la versión completamente calificada de PI, como en kotlin.math.PI * radio * radio, y, luego, no se necesitará la sentencia de importación.

  1. Actualiza el constructor de RoundHut a fin de pasar el radius.
open class RoundHut(
   residents: Int,
   val radius: Double) : Dwelling(residents) {
  1. En main(), actualiza la inicialización de roundHut pasando un radius de 10.0 al constructor de RoundHut.
val roundHut = RoundHut(3, 10.0)
  1. Agrega una sentencia de impresión dentro de la sentencia with de roundHut.
println("Floor area: ${floorArea()}")

Implementa floorArea() para RoundTower

Tu código aún no se ejecuta correctamente y genera este error:

Error: No value passed for parameter 'radius'

En RoundTower, a fin de que tu programa se compile, no necesitas implementar floorArea(), ya que se hereda de RoundHut, pero debes actualizar la definición de la clase RoundTower para que también tenga el mismo argumento de radius que su superior RoundHut.

  1. Cambia el constructor de RoundTower de modo que también tome el radius. Coloca el radius después de los residents y antes de los floors. Te recomendamos que las variables con valores predeterminados se enumeren al final. Recuerda pasar el radius al constructor de la clase superior.
class RoundTower(
    residents: Int,
    radius: Double,
    val floors: Int = 2) : RoundHut(residents, radius) {
  1. Actualiza la inicialización de roundTower en main().
val roundTower = RoundTower(4, 15.5)
  1. Además, agrega una sentencia de impresión que llame a floorArea().
println("Floor area: ${floorArea()}")
  1. Ahora puedes ejecutar tu código.
  2. Observa que el cálculo de RoundTower no es correcto porque se hereda de RoundHut y no tiene en cuenta la cantidad de floors.
  3. En RoundTower, override floorArea() de modo que puedas darle una implementación diferente que multiplique la superficie por la cantidad de pisos. Observa cómo puedes definir una función en una clase abstracta (Dwelling), implementarla en una subclase (RoundHut) y luego anularla de nuevo en una subclase de la subclase (RoundTower). Es lo mejor de ambos mundos: heredas la funcionalidad que deseas y puedes anular aquella que no quieras.
override fun floorArea(): Double {
    return PI * radius * radius * floors
}

Este código funciona, pero hay una forma de evitar repetir código que ya se encuentra en la clase superior RoundHut. Puedes llamar a la función floorArea() desde la clase superior RoundHut, que muestra PI * radius * radius. Luego, multiplica ese resultado por la cantidad de floors.

  1. En RoundTower, actualiza floorArea() a fin de usar la implementación de la superclase de floorArea(). Usa la palabra clave super para llamar a la función definida en el elemento superior.
override fun floorArea(): Double {
    return super.floorArea() * floors
}
  1. Vuelve a ejecutar el código. RoundTower muestra el espacio correcto de todos los pisos.

Este es tu código terminado:

import kotlin.math.PI

fun main() {

    val squareCabin = SquareCabin(6, 50.0)
    val roundHut = RoundHut(3, 10.0)
    val roundTower = RoundTower(4, 15.5)

    with(squareCabin) {
        println("\nSquare Cabin\n============")
        println("Capacity: ${capacity}")
        println("Material: ${buildingMaterial}")
        println("Has room? ${hasRoom()}")
        println("Floor area: ${floorArea()}")
    }

    with(roundHut) {
        println("\nRound Hut\n=========")
        println("Material: ${buildingMaterial}")
        println("Capacity: ${capacity}")
        println("Has room? ${hasRoom()}")
        println("Floor area: ${floorArea()}")
    }

    with(roundTower) {
        println("\nRound Tower\n==========")
        println("Material: ${buildingMaterial}")
        println("Capacity: ${capacity}")
        println("Has room? ${hasRoom()}")
        println("Floor area: ${floorArea()}")
    }
 }

abstract class Dwelling(private var residents: Int) {

    abstract val buildingMaterial: String
    abstract val capacity: Int

    fun hasRoom(): Boolean {
        return residents < capacity
}

    abstract fun floorArea(): Double
}

class SquareCabin(residents: Int,
    val length: Double) : Dwelling(residents) {

    override val buildingMaterial = "Wood"
    override val capacity = 6

    override fun floorArea(): Double {
       return length * length
    }
}

open class RoundHut(residents: Int,
    val radius: Double) : Dwelling(residents) {

    override val buildingMaterial = "Straw"
    override val capacity = 4

    override fun floorArea(): Double {
        return PI * radius * radius
    }
}

class RoundTower(residents: Int, radius: Double,
    val floors: Int = 2) : RoundHut(residents, radius) {

    override val buildingMaterial = "Stone"
    override val capacity = 4 * floors

    override fun floorArea(): Double {
        return super.floorArea() * floors
    }
}

El resultado debería ser el siguiente:

Square Cabin
============
Capacity: 6
Material: Wood
Has room? false
Floor area: 2500.0

Round Hut
=========
Material: Straw
Capacity: 4
Has room? true
Floor area: 314.1592653589793

Round Tower
==========
Material: Stone
Capacity: 8
Has room? true
Floor area: 1509.5352700498956

Permite que otra persona obtenga una habitación

Agrega la capacidad para que otra persona obtenga una habitación mediante una función getRoom() que aumente la cantidad de habitantes de uno en uno. Dado que esta lógica es la misma para todas las viviendas, puedes implementar la función en Dwelling y hacer que esté disponible para todas las subclases y sus elementos secundarios. ¡Bravo!

Notas:

  • Usa una sentencia if que solo agregue un habitante si hay capacidad restante.
  • Imprime un mensaje con el resultado.
  • Puedes usar residents++ como versión abreviada de residents = residents + 1 a fin de incrementar en 1 la variable residents.
  1. Implementa la función getRoom() en la clase Dwelling.
fun getRoom() {
    if (capacity > residents) {
        residents++
        println("You got a room!")
    } else {
        println("Sorry, at capacity and no rooms left.")
    }
}
  1. Agrega algunas sentencias de impresión al bloque de sentencias with de modo que roundHut detecte lo que sucede con getRoom() y hasRoom() usadas en conjunto.
println("Has room? ${hasRoom()}")
getRoom()
println("Has room? ${hasRoom()}")
getRoom()

Resultado de estas sentencias de impresión:

Has room? true
You got a room!
Has room? false
Sorry, at capacity and no rooms left.

Consulta el código de la solución para obtener más información.

Adapta una alfombra en una vivienda redonda

Supongamos que necesitas saber la longitud de un lado de la alfombra para colocarla en RoundHut o RoundTower. Coloca la función en RoundHut de modo que esté disponible para todas las viviendas redondas.

2e328a198a82c793.png

  1. Primero, importa la función sqrt() desde la biblioteca kotlin.math.
import kotlin.math.sqrt
  1. Implementa la función calculateMaxCarpetLength() en la clase RoundHut. La fórmula para calcular la longitud de la alfombra cuadrada que puede caber en un círculo es sqrt(2) * radius. Esto se explica en el diagrama anterior.
fun calculateMaxCarpetLength(): Double {

    return sqrt(2.0) * radius
}

Pasa un valor Double, 2.0 a la función matemática sqrt(2.0), ya que el tipo de datos que se muestra de la función es Double y no Integer.

  1. Ahora, se puede llamar al método calculateMaxCarpetLength() en instancias de RoundHut y RoundTower. Agrega sentencias de impresión a roundHut y roundTower en la función main().
println("Carpet Length: ${calculateMaxCarpetLength()}")

Consulta el código de la solución para obtener más información.

¡Felicitaciones! Ya creaste una jerarquía de clases completa con propiedades y funciones, y aprendiste todo lo que necesitas para crear clases más útiles.

6. Código de solución

Este es el código de solución completo para este codelab, incluidos los comentarios.

/**
* Program that implements classes for different kinds of dwellings.
* Shows how to:
* Create class hierarchy, variables and functions with inheritance,
* abstract class, overriding, and private vs. public variables.
*/

import kotlin.math.PI
import kotlin.math.sqrt

fun main() {
   val squareCabin = SquareCabin(6, 50.0)
   val roundHut = RoundHut(3, 10.0)
   val roundTower = RoundTower(4, 15.5)

   with(squareCabin) {
       println("\nSquare Cabin\n============")
       println("Capacity: ${capacity}")
       println("Material: ${buildingMaterial}")
       println("Floor area: ${floorArea()}")
   }

   with(roundHut) {
       println("\nRound Hut\n=========")
       println("Material: ${buildingMaterial}")
       println("Capacity: ${capacity}")
       println("Floor area: ${floorArea()}")
       println("Has room? ${hasRoom()}")
       getRoom()
       println("Has room? ${hasRoom()}")
       getRoom()
       println("Carpet size: ${calculateMaxCarpetLength()}")
   }

   with(roundTower) {
       println("\nRound Tower\n==========")
       println("Material: ${buildingMaterial}")
       println("Capacity: ${capacity}")
       println("Floor area: ${floorArea()}")
       println("Carpet Length: ${calculateMaxCarpetLength()}")
   }
}

/**
* Defines properties common to all dwellings.
* All dwellings have floorspace,
* but its calculation is specific to the subclass.
* Checking and getting a room are implemented here
* because they are the same for all Dwelling subclasses.
*
* @param residents Current number of residents
*/
abstract class Dwelling(private var residents: Int) {
   abstract val buildingMaterial: String
   abstract val capacity: Int

   /**
    * Calculates the floor area of the dwelling.
    * Implemented by subclasses where shape is determined.
    *
    * @return floor area
    */
   abstract fun floorArea(): Double

   /**
    * Checks whether there is room for another resident.
    *
    * @return true if room available, false otherwise
    */
   fun hasRoom(): Boolean {
       return residents < capacity
   }

   /**
    * Compares the capacity to the number of residents and
    * if capacity is larger than number of residents,
    * add resident by increasing the number of residents.
    * Print the result.
    */
   fun getRoom() {
       if (capacity > residents) {
           residents++
           println("You got a room!")
       } else {
           println("Sorry, at capacity and no rooms left.")
       }
   }

   }

/**
* A square cabin dwelling.
*
*  @param residents Current number of residents
*  @param length Length
*/
class SquareCabin(residents: Int, val length: Double) : Dwelling(residents) {
   override val buildingMaterial = "Wood"
   override val capacity = 6

   /**
    * Calculates floor area for a square dwelling.
    *
    * @return floor area
    */
   override fun floorArea(): Double {
       return length * length
   }

}

/**
* Dwelling with a circular floorspace
*
* @param residents Current number of residents
* @param radius Radius
*/
open class RoundHut(
       residents: Int, val radius: Double) : Dwelling(residents) {

   override val buildingMaterial = "Straw"
   override val capacity = 4

   /**
    * Calculates floor area for a round dwelling.
    *
    * @return floor area
    */
   override fun floorArea(): Double {
       return PI * radius * radius
   }

   /**
    *  Calculates the max length for a square carpet
    *  that fits the circular floor.
    *
    * @return length of square carpet
    */
    fun calculateMaxCarpetLength(): Double {
        return sqrt(2.0) * radius
    }

}

/**
* Round tower with multiple stories.
*
* @param residents Current number of residents
* @param radius Radius
* @param floors Number of stories
*/
class RoundTower(
       residents: Int,
       radius: Double,
       val floors: Int = 2) : RoundHut(residents, radius) {

   override val buildingMaterial = "Stone"

   // Capacity depends on the number of floors.
   override val capacity = floors * 4

   /**
    * Calculates the total floor area for a tower dwelling
    * with multiple stories.
    *
    * @return floor area
    */
   override fun floorArea(): Double {
       return super.floorArea() * floors
   }
}

7. Resumen

En este codelab aprendiste a hacer lo siguiente:

  • Crear una jerarquía de clases, que es un árbol de clases en las que los elementos secundarios heredan la funcionalidad de las clases superiores (las propiedades y funciones se heredan mediante subclases)
  • Crear una clase abstract en la que las subclases implementen algunas de sus funciones (por lo tanto, no se puede crear una instancia de una clase abstract)
  • Crear subclases de una clase abstract
  • Usar la palabra clave override a fin de anular las propiedades y funciones en las subclases
  • Usar la palabra clave super para hacer referencia a funciones y propiedades en la clase superior
  • Crear una clase open que pueda ser una subclase
  • Crear una propiedad private de modo que solo se pueda usar dentro de la clase
  • Usar la construcción with a efectos de realizar varias llamadas en la misma instancia de objeto
  • Importar funciones desde la biblioteca kotlin.math

8. Más información