Capacitación de Kotlin para programadores 4: programación orientada a objetos

Este codelab es parte del curso Capacitación de Kotlin para programadores. Aprovecharás al máximo este curso si trabajas con los codelabs en secuencia. Según tus conocimientos, es posible que puedas cambiar algunas secciones. Este curso está dirigido a los programadores que conocen un lenguaje orientado a objetos y desean aprender Kotlin.

Introducción

En este codelab, crearás un programa en Kotlin y aprenderás sobre las clases y los objetos de este lenguaje. Gran parte de este contenido te resultará familiar si conoces otro lenguaje orientado a objetos, pero Kotlin tiene algunas diferencias importantes para reducir la cantidad de código que debes escribir. También aprenderás sobre clases abstractas y la delegación de interfaces.

En lugar de compilar una sola app de muestra, las lecciones de este curso están diseñadas para aumentar tu conocimiento, pero son semiindependientes entre sí a fin de que puedas leer las secciones con las que estás familiarizado. Para vincularlos, muchos de los ejemplos usan un tema de acuario. Si deseas ver la historia completa del acuario, consulta el curso de Udacity Capacitación de Kotlin para programadores.

Conocimientos que ya deberías tener

  • Conceptos básicos de Kotlin, incluidos tipos, operadores y bucles
  • Sintaxis de la función de Kotlin
  • Conceptos básicos de la programación orientada a objetos
  • Conceptos básicos de un IDE, como IntelliJ IDEA o Android Studio

Qué aprenderás

  • Cómo crear clases y acceder a propiedades en Kotlin
  • Cómo crear y usar constructores de clases en Kotlin
  • Cómo crear una subclase y cómo funciona la herencia
  • Acerca de las clases abstractas, las interfaces y la delegación de interfaces
  • Cómo crear y usar las clases de datos
  • Cómo usar singletons, enumeraciones y clases selladas

Actividades

  • Cómo crear una clase con propiedades
  • Cómo crear un constructor para una clase
  • Crea una subclase
  • Examina ejemplos de interfaces y clases abstractas
  • Cómo crear una clase de datos simple
  • Más información sobre singletons, enumeraciones y clases selladas

Debes conocer los siguientes términos de programación:

  • Las clases son planos para los objetos. Por ejemplo, una clase Aquarium es el plano para crear un objeto de acuario.
  • Los objetos son instancias de clases; un objeto de acuario es una Aquarium real.
  • Las propiedades son características de clases, como la longitud, el ancho y la altura de un Aquarium.
  • Los métodos, también llamados funciones de miembro, son la funcionalidad de la clase. Los métodos son lo que puedes hacer con el objeto. Por ejemplo, puedes fillWithWater() un objeto Aquarium.
  • Una interfaz es una especificación que una clase puede implementar. Por ejemplo, la limpieza es común a los objetos, excepto los acuarios, y la limpieza suele ocurrir de formas similares para los diferentes objetos. Podrías tener una interfaz llamada Clean que defina un método clean(). La clase Aquarium podría implementar la interfaz Clean para limpiar el acuario con una esponja suave.
  • Los paquetes son una forma de agrupar código relacionado para mantenerlo organizado o crear una biblioteca de código. Una vez que se crea un paquete, puedes importar su contenido a otro archivo y reutilizar el código y las clases que contiene.

En esta tarea, crearás un paquete y una clase nuevos con algunas propiedades y un método.

Paso 1: Crea un paquete

Los paquetes pueden ayudarte a mantener tu código organizado.

  1. En el panel Project, debajo del proyecto Hello Kotlin, haz clic con el botón derecho en la carpeta src.
  2. Selecciona New > Package y llámalo example.myapp.

Paso 2: Crea una clase con propiedades

Las clases se definen con la palabra clave class y los nombres de las clases, por convención, comienzan con mayúscula.

  1. Haz clic con el botón derecho en el paquete example.myapp.
  2. Selecciona New > Kotlin File / Class.
  3. En Tipo, selecciona Clase y asígnale el nombre Aquarium. IntelliJ IDEA incluye el nombre del paquete en el archivo y crea una clase Aquarium vacía para ti.
  4. Dentro de la clase Aquarium, define e inicializa las propiedades var para el ancho, el alto y la longitud (en centímetros). Inicializa las propiedades con los valores predeterminados.
package example.myapp

class Aquarium {
    var width: Int = 20
    var height: Int = 40
    var length: Int = 100
}

En un nivel profundo, Kotlin crea métodos get y métodos set de forma automática para las propiedades que definiste en la clase Aquarium. Por lo tanto, puedes acceder a las propiedades directamente, por ejemplo, myAquarium.length.

Paso 3: Crea una función main()

Crea un archivo nuevo llamado main.kt para conservar la función main().

  1. En el panel Project del lado izquierdo, haz clic con el botón derecho en el paquete example.myapp.
  2. Selecciona New > Kotlin File / Class.
  3. En el menú desplegable Tipo, mantén la selección como Archivo y asígnale el nombre main.kt. IntelliJ IDEA incluye el nombre del paquete, pero no incluye una definición de clase para un archivo.
  4. Define una función buildAquarium() y, dentro de ella, crea una instancia de Aquarium. Para crear una instancia, haz referencia a la clase como si fuera una función, Aquarium(). Esto llama al constructor de la clase y crea una instancia de la clase Aquarium, similar a usar new en otros lenguajes.
  5. Define una función main() y llama a buildAquarium().
package example.myapp

fun buildAquarium() {
    val myAquarium = Aquarium()
}

fun main() {
    buildAquarium()
}

Paso 4: Agrega un método

  1. En la clase Aquarium, agrega un método para imprimir las propiedades de dimensión del acuario.
    fun printSize() {
        println("Width: $width cm " +
                "Length: $length cm " +
                "Height: $height cm ")
    }
  1. En main.kt, en buildAquarium(), llama al método printSize() en myAquarium.
fun buildAquarium() {
    val myAquarium = Aquarium()
    myAquarium.printSize()
}
  1. Para ejecutar el programa, haz clic en el triángulo verde junto a la función main(). Observe el resultado.
⇒ Width: 20 cm Length: 100 cm Height: 40 cm 
  1. En buildAquarium(), agrega código para establecer la altura en 60 y, luego, imprime las propiedades de la dimensión modificada.
fun buildAquarium() {
    val myAquarium = Aquarium()
    myAquarium.printSize()
    myAquarium.height = 60
    myAquarium.printSize()
}
  1. Ejecuta el programa y observa el resultado.
⇒ Width: 20 cm Length: 100 cm Height: 40 cm 
Width: 20 cm Length: 100 cm Height: 60 cm 

En esta tarea, creará un constructor para la clase y continuará trabajando con propiedades.

Paso 1: Crea un constructor

En este paso, agregarás un constructor a la clase Aquarium que creaste en la primera tarea. En el ejemplo anterior, cada instancia de Aquarium se crea con las mismas dimensiones. Puedes cambiar las dimensiones después de crearlas configurando las propiedades, pero sería más fácil crearla con el tamaño correcto para comenzar.

En algunos lenguajes de programación, el constructor se define creando un método dentro de la clase que tenga el mismo nombre que la clase. En Kotlin, se define el constructor directamente en la declaración de la clase y se especifican los parámetros dentro de los paréntesis como si la clase fuera un método. Al igual que con las funciones de Kotlin, esos parámetros pueden incluir valores predeterminados.

  1. En la clase Aquarium que creaste antes, cambia la definición de clase a fin de incluir tres parámetros de constructor con valores predeterminados para length, width y height, y asígnalos a las propiedades correspondientes.
class Aquarium(length: Int = 100, width: Int = 20, height: Int = 40) {
   // Dimensions in cm
   var length: Int = length
   var width: Int = width
   var height: Int = height
...
}
  1. La forma más compacta de Kotlin es definir las propiedades directamente con el constructor, mediante var o val, y Kotlin también crea los métodos get y set de forma automática. Luego, puedes quitar las definiciones de propiedad en el cuerpo de la clase.
class Aquarium(var length: Int = 100, var width: Int = 20, var height: Int = 40) {
...
}
  1. Cuando creas un objeto Aquarium con ese constructor, puedes especificar que no haya argumentos y obtener los valores predeterminados, o especificar solo algunos de ellos o especificarlos todos, y crear un Aquarium de tamaño completamente personalizado. En la función buildAquarium(), prueba diferentes formas de crear un objeto Aquarium con parámetros con nombre.
fun buildAquarium() {
    val aquarium1 = Aquarium()
    aquarium1.printSize()
    // default height and length
    val aquarium2 = Aquarium(width = 25)
    aquarium2.printSize()
    // default width
    val aquarium3 = Aquarium(height = 35, length = 110)
    aquarium3.printSize()
    // everything custom
    val aquarium4 = Aquarium(width = 25, height = 35, length = 110)
    aquarium4.printSize()
}
  1. Ejecuta el programa y observa el resultado.
⇒ Width: 20 cm Length: 100 cm Height: 40 cm 
Width: 25 cm Length: 100 cm Height: 40 cm 
Width: 20 cm Length: 110 cm Height: 35 cm 
Width: 25 cm Length: 110 cm Height: 35 cm 

Ten en cuenta que no tuviste que sobrecargar el constructor y escribir una versión diferente para cada uno de estos casos (además de algunas más para las otras combinaciones). Kotlin crea lo que se necesita de los valores predeterminados y los parámetros con nombre.

Paso 2: Agrega bloques init

Los constructores de ejemplo anteriores solo declaran las propiedades y les asignan el valor de una expresión. Si tu constructor necesita más código de inicialización, puedes colocarlo en uno o más bloques init. En este paso, agregarás algunos bloques init a la clase Aquarium.

  1. En la clase Aquarium, agrega un bloque init para imprimir que el objeto se está inicializando y un segundo bloque para imprimir el volumen en litros.
class Aquarium (var length: Int = 100, var width: Int = 20, var height: Int = 40) {
    init {
        println("aquarium initializing")
    }
    init {
        // 1 liter = 1000 cm^3
        println("Volume: ${width * length * height / 1000} l")
    }
}
  1. Ejecuta el programa y observa el resultado.
aquarium initializing
Volume: 80 l
Width: 20 cm Length: 100 cm Height: 40 cm 
aquarium initializing
Volume: 100 l
Width: 25 cm Length: 100 cm Height: 40 cm 
aquarium initializing
Volume: 77 l
Width: 20 cm Length: 110 cm Height: 35 cm 
aquarium initializing
Volume: 96 l
Width: 25 cm Length: 110 cm Height: 35 cm 

Observa que los bloques init se ejecutan en el orden en que aparecen en la definición de la clase, y todos se ejecutan cuando se llama al constructor.

Paso 3: Obtén información sobre los constructores secundarios

En este paso, aprenderás sobre los constructores secundarios y agregarás uno a tu clase. Además de un constructor principal, que puede tener uno o más bloques init, una clase de Kotlin también puede tener uno o más constructores secundarios para permitir la sobrecarga del constructor, es decir, constructores con diferentes argumentos.

  1. En la clase Aquarium, agrega un constructor secundario que tome una cantidad de peces como su argumento mediante la palabra clave constructor. Crea una propiedad de tanque val para el volumen calculado del acuario en litros en función de la cantidad de peces. Supón que se utilizan 2 litros (2,000 cm^3) de agua por pescado y un poco de espacio adicional para que el agua no se derrame.
constructor(numberOfFish: Int) : this() {
    // 2,000 cm^3 per fish + extra room so water doesn't spill
    val tank = numberOfFish * 2000 * 1.1
}
  1. Dentro del constructor secundario, mantén iguales la longitud y el ancho (que se establecieron en el constructor principal) y calcula la altura necesaria para que el tanque tenga el volumen dado.
    // calculate the height needed
    height = (tank / (length * width)).toInt()
  1. En la función buildAquarium(), agrega una llamada para crear un Aquarium mediante tu nuevo constructor secundario. Imprime el tamaño y el volumen.
fun buildAquarium() {
    val aquarium6 = Aquarium(numberOfFish = 29)
    aquarium6.printSize()
    println("Volume: ${aquarium6.width * aquarium6.length * aquarium6.height / 1000} l")
}
  1. Ejecuta el programa y observa el resultado.
⇒ aquarium initializing
Volume: 80 l
Width: 20 cm Length: 100 cm Height: 31 cm 
Volume: 62 l

Observa que el volumen se imprime dos veces, una vez mediante el bloque init en el constructor principal antes de que se ejecute el constructor secundario y una vez por el código en buildAquarium().

Podrías haber incluido la palabra clave constructor en el constructor principal, pero no es necesario en la mayoría de los casos.

Paso 4: Agrega un nuevo método get de propiedad

En este paso, agregarás un método get explícito de propiedad. Kotlin define de forma automática los métodos get y set cuando defines propiedades, pero, a veces, es necesario ajustar o calcular el valor de una propiedad. Por ejemplo, imprimiste el volumen de Aquarium. Puedes hacer que el volumen esté disponible como una propiedad si defines una variable y un método get para ella. Debido a que se debe calcular volume, el método get debe mostrar el valor calculado, lo que puedes hacer con una función de una línea.

  1. En la clase Aquarium, define una propiedad Int llamada volume y un método get() que calcule el volumen en la siguiente línea.
val volume: Int
    get() = width * height * length / 1000  // 1000 cm^3 = 1 l
  1. Quita el bloque init que imprime el volumen.
  2. Quita el código de buildAquarium() que imprime el volumen.
  3. En el método printSize(), agrega una línea para imprimir el volumen.
fun printSize() {
    println("Width: $width cm " +
            "Length: $length cm " +
            "Height: $height cm "
    )
    // 1 l = 1000 cm^3
    println("Volume: $volume l")
}
  1. Ejecuta el programa y observa el resultado.
⇒ aquarium initializing
Width: 20 cm Length: 100 cm Height: 31 cm 
Volume: 62 l

Las dimensiones y el volumen son los mismos que antes, pero el volumen solo se imprime una vez después de que el constructor principal y el secundario inicializan por completo el objeto.

Paso 5: Agrega un método set de propiedad

En este paso, crearás un nuevo método set para la propiedad del volumen.

  1. En la clase Aquarium, cambia volume por var para que se pueda configurar más de una vez.
  2. Agrega un método set para la propiedad volume agregando un método set() debajo del método get, que vuelva a calcular la altura según la cantidad de agua proporcionada. Por convención, el nombre del parámetro set es value, pero puedes cambiarlo si lo prefieres.
var volume: Int
    get() = width * height * length / 1000
    set(value) {
        height = (value * 1000) / (width * length)
    }
  1. En buildAquarium(), agrega código para establecer el volumen del acuario en 70 litros. Imprime el tamaño nuevo.
fun buildAquarium() {
    val aquarium6 = Aquarium(numberOfFish = 29)
    aquarium6.printSize()
    aquarium6.volume = 70
    aquarium6.printSize()
}
  1. Vuelve a ejecutar el programa y observa la altura y el volumen modificados.
⇒ aquarium initialized
Width: 20 cm Length: 100 cm Height: 31 cm 
Volume: 62 l
Width: 20 cm Length: 100 cm Height: 35 cm 
Volume: 70 l

No hay modificadores de visibilidad, como public o private, en el código hasta el momento. Eso se debe a que, de forma predeterminada, todo el contenido de Kotlin es público, lo que significa que se puede acceder a todo el contenido en todas partes, incluidas las clases, los métodos, las propiedades y las variables de miembro.

En Kotlin, las clases, los objetos, las interfaces, los constructores, las funciones, las propiedades y sus métodos set pueden tener modificadores de visibilidad:

  • public significa visible fuera de la clase. Todo es público de forma predeterminada, incluidas las variables y los métodos de la clase.
  • internal significa que solo será visible dentro de ese módulo. Un módulo es un conjunto de archivos Kotlin compilados de manera conjunta, por ejemplo, una biblioteca o una aplicación.
  • private significa que solo será visible en esa clase (o archivo de origen si trabajas con funciones).
  • protected es igual que private, pero también lo verá cualquier subclase.

Para obtener más información, consulta Modificadores de visibilidad en la documentación de Kotlin.

Variables del miembro

Las propiedades dentro de una clase, o variables de miembro, son public de forma predeterminada. Si los defines con var, son mutables, es decir, legibles y que admiten escritura. Si los defines con val, son de solo lectura después de la inicialización.

Si deseas una propiedad que tu código pueda leer o escribir, pero que el código exterior solo pueda leer, puedes dejar la propiedad y su método get como públicos y declarar el método set como privado, como se muestra a continuación.

var volume: Int
    get() = width * height * length / 1000
    private set(value) {
        height = (value * 1000) / (width * length)
    }

En esta tarea, aprenderás cómo funcionan las subclases y la herencia en Kotlin. Son similares a los que viste en otros idiomas, pero existen algunas diferencias.

En Kotlin, de forma predeterminada, no se pueden crear subclases de las clases. Del mismo modo, las propiedades y las variables de miembro no pueden anularse mediante subclases (aunque se puede acceder a ellas).

Debes marcar una clase como open para permitir que se cree una subclase. Del mismo modo, debes marcar las propiedades y las variables de miembro como open para anularlas en la subclase. La palabra clave open es obligatoria para evitar que se filtren accidentalmente detalles de implementación como parte de la interfaz de la clase.

Paso 1: Abre la clase del acuario

En este paso, crearás la clase Aquarium open, de modo que puedas anularla en el paso siguiente.

  1. Marca la clase Aquarium y todas sus propiedades con la palabra clave open.
open class Aquarium (open var length: Int = 100, open var width: Int = 20, open var height: Int = 40) {
    open var volume: Int
        get() = width * height * length / 1000
        set(value) {
            height = (value * 1000) / (width * length)
        }
  1. Agrega una propiedad shape abierta con el valor "rectangle".
   open val shape = "rectangle"
  1. Agrega una propiedad water abierta con un método get que muestre el 90% del volumen de Aquarium.
    open var water: Double = 0.0
        get() = volume * 0.9
  1. Agrega código al método printSize() para imprimir la forma y la cantidad de agua como un porcentaje del volumen.
fun printSize() {
    println(shape)
    println("Width: $width cm " +
            "Length: $length cm " +
            "Height: $height cm ")
    // 1 l = 1000 cm^3
    println("Volume: $volume l Water: $water l (${water/volume*100.0}% full)")
}
  1. En buildAquarium(), cambia el código para crear una Aquarium con width = 25, length = 25 y height = 40.
fun buildAquarium() {
    val aquarium6 = Aquarium(length = 25, width = 25, height = 40)
    aquarium6.printSize()
}
  1. Ejecuta el programa y observa el resultado nuevo.
⇒ aquarium initializing
rectangle
Width: 25 cm Length: 25 cm Height: 40 cm 
Volume: 25 l Water: 22.5 l (90.0% full)

Paso 2: Crea una subclase

  1. Crea una subclase de Aquarium llamada TowerTank, que implementa un tanque de cilindros redondeado en lugar de uno rectangular. Puedes agregar TowerTank debajo de Aquarium, ya que puedes agregar otra clase en el mismo archivo que la clase Aquarium.
  2. En TowerTank, anula la propiedad height, que se define en el constructor. Para anular una propiedad, usa la palabra clave override en la subclase.
  1. Haz que el constructor de TowerTank tome una diameter. Usa diameter para length y width cuando llames al constructor en la superclase Aquarium.
class TowerTank (override var height: Int, var diameter: Int): Aquarium(height = height, width = diameter, length = diameter) {
  1. Anula la propiedad de volumen para calcular un cilindro. La fórmula de un cilindro es pi multiplicado por el radio al cuadrado multiplicado por la altura. Debes importar la constante PI desde java.lang.Math.
    override var volume: Int
    // ellipse area = π * r1 * r2
    get() = (width/2 * length/2 * height / 1000 * PI).toInt()
    set(value) {
        height = ((value * 1000 / PI) / (width/2 * length/2)).toInt()
    }
  1. En TowerTank, anula la propiedad water para que sea el 80% del volumen.
override var water = volume * 0.8
  1. Anula el valor shape para que sea "cylinder".
override val shape = "cylinder"
  1. La clase TowerTank final debería verse como el siguiente código:

Aquarium.kt:

package example.myapp

import java.lang.Math.PI

... // existing Aquarium class

class TowerTank (override var height: Int, var diameter: Int): Aquarium(height = height, width = diameter, length = diameter) {
    override var volume: Int
    // ellipse area = π * r1 * r2
    get() = (width/2 * length/2 * height / 1000 * PI).toInt()
    set(value) {
        height = ((value * 1000 / PI) / (width/2 * length/2)).toInt()
    }

    override var water = volume * 0.8
    override val shape = "cylinder"
}
  1. En buildAquarium(), crea un TowerTank con un diámetro de 25 cm y una altura de 45 cm. Imprime el tamaño.

main.kt:

package example.myapp

fun buildAquarium() {
    val myAquarium = Aquarium(width = 25, length = 25, height = 40)
    myAquarium.printSize()
    val myTower = TowerTank(diameter = 25, height = 40)
    myTower.printSize()
}
  1. Ejecuta el programa y observa el resultado.
⇒ aquarium initializing
rectangle
Width: 25 cm Length: 25 cm Height: 40 cm 
Volume: 25 l Water: 22.5 l (90.0% full)
aquarium initializing
cylinder
Width: 25 cm Length: 25 cm Height: 40 cm 
Volume: 18 l Water: 14.4 l (80.0% full)

En ocasiones, quieres definir comportamientos o propiedades comunes que se compartirán entre algunas clases relacionadas. Kotlin ofrece dos maneras de hacerlo: interfaces y clases abstractas. En esta tarea, crearás una clase abstracta AquariumFish para las propiedades comunes a todos los peces. Crearás una interfaz llamada FishAction a fin de definir un comportamiento común para todos los peces.

  • No se pueden crear instancias de una clase abstracta ni de una interfaz por sí sola, lo que significa que no puedes crear objetos de esos tipos directamente.
  • Las clases abstractas tienen constructores.
  • Las interfaces no pueden tener ninguna lógica de constructor ni almacenar ningún estado.

Paso 1: Cómo crear una clase abstracta

  1. En example.myapp, crea un archivo nuevo, AquariumFish.kt.
  2. Crea una clase, también llamada AquariumFish, y márcala con abstract.
  3. Agrega una propiedad String, color, y márcala con abstract.
package example.myapp

abstract class AquariumFish {
    abstract val color: String
}
  1. Crea dos subclases de AquariumFish, Shark y Plecostomus.
  2. Debido a que color es abstracto, las subclases deben implementarlo. Haz que Shark sea gris y Plecostomus dorado.
class Shark: AquariumFish() {
    override val color = "gray"
}

class Plecostomus: AquariumFish() {
    override val color = "gold"
}
  1. En main.kt, crea una función makeFish() para probar tus clases. Crea una instancia de Shark y Plecostomus, y, luego, imprime el color de cada una.
  2. Borra el código de prueba anterior en main() y agrega una llamada a makeFish(). El código debería ser similar al siguiente:

main.kt:

package example.myapp

fun makeFish() {
    val shark = Shark()
    val pleco = Plecostomus()

    println("Shark: ${shark.color}")
    println("Plecostomus: ${pleco.color}")
}

fun main () {
    makeFish()
}
  1. Ejecuta el programa y observa el resultado.
⇒ Shark: gray 
Plecostomus: gold

En el siguiente diagrama, se representan la clase Shark y la clase Plecostomus, que subclasifican la clase abstracta, AquariumFish.

Un diagrama que muestra la clase abstracta, AquariumFish y dos subclases, Shark y Plecostumus.

Paso 2: Crea una interfaz

  1. En AquariumFish.kt, crea una interfaz llamada FishAction con un método eat().
interface FishAction  {
    fun eat()
}
  1. Agrega FishAction a cada una de las subclases, e implementa eat() haciendo que imprima lo que hace el pez.
class Shark: AquariumFish(), FishAction {
    override val color = "gray"
    override fun eat() {
        println("hunt and eat fish")
    }
}

class Plecostomus: AquariumFish(), FishAction {
    override val color = "gold"
    override fun eat() {
        println("eat algae")
    }
}
  1. En la función makeFish(), llama a eat() para que cada pez que creaste consuma algo.
fun makeFish() {
    val shark = Shark()
    val pleco = Plecostomus()
    println("Shark: ${shark.color}")
    shark.eat()
    println("Plecostomus: ${pleco.color}")
    pleco.eat()
}
  1. Ejecuta el programa y observa el resultado.
⇒ Shark: gray
hunt and eat fish
Plecostomus: gold
eat algae

En el siguiente diagrama, se representan la clase Shark y la clase Plecostomus, las cuales están compuestas e implementadas por la interfaz FishAction.

Cuándo usar clases abstractas en lugar de interfaces

Los ejemplos anteriores son simples, pero cuando tienes muchas clases interrelacionadas, las interfaces y clases abstractas pueden ayudarte a mantener tu diseño más limpio, más organizado y fácil de mantener.

Como se indicó anteriormente, las clases abstractas pueden tener constructores, y las interfaces no, pero son muy similares. ¿Cuándo debería usar cada uno?

Cuando usas interfaces para componer una clase, la funcionalidad de la clase se extiende a través de las instancias de clase que contiene. La composición tiende a facilitar la reutilización del código y la razón por la que lo hace desde una clase abstracta. Además, puedes usar varias interfaces en una clase, pero solo puedes crear subclases de una clase abstracta.

La composición suele generar un mejor encapsulamiento, menores acoplamiento (interdependencia), interfaces más limpias y un código más utilizable. Por estos motivos, se prefiere el diseño de composición con interfaces. Por otro lado, la herencia de una clase abstracta tiende a ser una opción natural para algunos problemas. Por lo tanto, debes priorizar la composición, pero cuando la herencia tiene sentido que Kotlin también te lo permita.

  • Usa una interfaz si tienes muchos métodos y una o dos implementaciones predeterminadas, como en AquariumAction a continuación.
interface AquariumAction {
    fun eat()
    fun jump()
    fun clean()
    fun catchFish()
    fun swim()  {
        println("swim")
    }
}
  • Usa una clase abstracta cada vez que no puedas completar una clase. Por ejemplo, si regresas a la clase AquariumFish, puedes hacer que todos los AquariumFish implementen FishAction y proporcionar una implementación predeterminada para eat sin dejar que el color sea abstracto, ya que realmente no hay un color predeterminado para los peces.
interface FishAction  {
    fun eat()
}

abstract class AquariumFish: FishAction {
   abstract val color: String
   override fun eat() = println("yum")
}

La tarea anterior introdujo clases abstractas, interfaces y la idea de composición. La delegación de interfaz es una técnica avanzada en la que un objeto (o delegado) implementa los métodos de una interfaz, que luego utiliza una clase. Esta técnica puede ser útil cuando usas una interfaz en una serie de clases no relacionadas: agregas la funcionalidad de interfaz necesaria a una clase auxiliar independiente, y cada una de las clases usa una instancia de la clase auxiliar para implementar la funcionalidad.

En esta tarea, usarás la delegación de interfaces para agregar funcionalidad a una clase.

Paso 1: Crea una nueva interfaz

  1. En AquariumFish.kt, quita la clase AquariumFish. En lugar de heredar de la clase AquariumFish, Plecostomus y Shark implementarán interfaces para la acción de pez y su color.
  2. Crea una interfaz nueva, FishColor, que defina el color como una string.
interface FishColor {
    val color: String
}
  1. Cambia Plecostomus para implementar dos interfaces, FishAction y FishColor. Debes anular color de FishColor y eat() de FishAction.
class Plecostomus: FishAction, FishColor {
    override val color = "gold"
    override fun eat() {
        println("eat algae")
    }
}
  1. Cambia tu clase Shark para implementar también las dos interfaces, FishAction y FishColor, en lugar de heredar de AquariumFish.
class Shark: FishAction, FishColor {
    override val color = "gray"
    override fun eat() {
        println("hunt and eat fish")
    }
}
  1. El código finalizado debería verse de la siguiente manera:
package example.myapp

interface FishAction {
    fun eat()
}

interface FishColor {
    val color: String
}

class Plecostomus: FishAction, FishColor {
    override val color = "gold"
    override fun eat() {
        println("eat algae")
    }
}

class Shark: FishAction, FishColor {
    override val color = "gray"
    override fun eat() {
        println("hunt and eat fish")
    }
}

Paso 2: Crea una clase singleton

A continuación, implementarás la configuración de la parte de delegación creando una clase de ayuda que implemente FishColor. Creas una clase básica llamada GoldColor que implementa FishColor. Lo único que sucede es que su color es dorado.

No tiene sentido tener varias instancias de GoldColor, porque todas hacen lo mismo. Por lo tanto, Kotlin te permite declarar una clase en la que solo puedes crear una instancia de esta utilizando la palabra clave object en lugar de class. En Kotlin, se creará una instancia y se hará referencia a ella en el nombre de la clase. Luego, todos los demás objetos pueden usar esta única instancia; no hay forma de crear otras instancias de esta clase. Si estás familiarizado con el patrón singleton, así es como se implementan los singleton en Kotlin.

  1. En AquariumFish.kt, crea un objeto para GoldColor. Anula el color.
object GoldColor : FishColor {
   override val color = "gold"
}

Paso 3: Agrega la delegación de interfaz para FishColor

Ahora estás listo para usar la delegación de interfaces.

  1. En AquariumFish.kt, quita la anulación de color de Plecostomus.
  2. Cambia la clase Plecostomus para obtener su color de GoldColor. Para ello, agrega by GoldColor a la declaración de la clase y crea la delegación. Lo que esto dice es que, en lugar de implementar FishColor, usa la implementación proporcionada por GoldColor. Por lo tanto, cada vez que se accede a color, se delega a GoldColor.
class Plecostomus:  FishAction, FishColor by GoldColor {
   override fun eat() {
       println("eat algae")
   }
}

Con la clase tal como está, todos los Plecos estarán dorados, pero estos peces vienen en muchos colores. Puedes solucionar este problema agregando un parámetro de constructor para el color con GoldColor como el color predeterminado para Plecostomus.

  1. Cambia la clase Plecostomus para que pase un fishColor en su constructor y establece su valor predeterminado en GoldColor. Cambia la delegación de by GoldColor a by fishColor.
class Plecostomus(fishColor: FishColor = GoldColor):  FishAction,
       FishColor by fishColor {
   override fun eat() {
       println("eat algae")
   }
}

Paso 4: Agrega la delegación de interfaz para FishAction

De la misma manera, puedes usar la delegación de interfaz para FishAction.

  1. En AquariumFish.kt, crea una clase PrintingFishAction que implemente FishAction, que toma un String, food y, luego, imprime lo que come el pez.
class PrintingFishAction(val food: String) : FishAction {
    override fun eat() {
        println(food)
    }
}
  1. En la clase Plecostomus, quita la función de anulación eat(), ya que la reemplazarás por una delegación.
  2. En la declaración de Plecostomus, delega FishAction a PrintingFishAction y pasa "eat algae".
  3. Con toda esa delegación, no hay código en el cuerpo de la clase Plecostomus, por lo que debes quitar {}, ya que la anulación de todas las anulaciones se realiza mediante la delegación de interfaces.
class Plecostomus (fishColor: FishColor = GoldColor):
        FishAction by PrintingFishAction("eat algae"),
        FishColor by fishColor

En el siguiente diagrama, se representan las clases Shark y Plecostomus, las cuales están compuestas por las interfaces PrintingFishAction y FishColor, pero se delegan la implementación a ellas.

La delegación de interfaces es potente, y por lo general debes considerar cómo usarla cada vez que uses una clase abstracta en otro idioma. Te permite usar la composición para conectar comportamientos, en lugar de requerir muchas subclases, y cada una se especializa de una manera diferente.

Una clase de datos es similar a una struct en otros lenguajes (existe principalmente para contener algunos datos), pero un objeto de clase de datos sigue siendo un objeto. Los objetos de clase de datos de Kotlin tienen algunos beneficios adicionales, como utilidades para imprimir y copiar. En esta tarea, crearás una clase de datos simple y aprenderás sobre la compatibilidad que proporciona Kotlin para las clases de datos.

Paso 1: Crea una clase de datos

  1. Agrega un paquete nuevo decor en el paquete example.myapp para conservar el código nuevo. Haz clic con el botón derecho en example.myapp en el panel Project y selecciona File > New > Package.
  2. En el paquete, crea una nueva clase llamada Decoration.
package example.myapp.decor

class Decoration {
}
  1. Para hacer que Decoration sea una clase de datos, agrega la palabra clave data a la declaración de clase.
  2. Agrega una propiedad String llamada rocks para otorgarle algunos datos a la clase.
data class Decoration(val rocks: String) {
}
  1. En el archivo, fuera de la clase, agrega una función makeDecorations() para crear e imprimir una instancia de un Decoration con "granite".
fun makeDecorations() {
    val decoration1 = Decoration("granite")
    println(decoration1)
}
  1. Agrega una función main() para llamar a makeDecorations() y ejecuta tu programa. Observe el resultado razonable que se crea porque esta es una clase de datos.
⇒ Decoration(rocks=granite)
  1. En makeDecorations(), crea una instancia de dos objetos Decoration más que estén configurados e imprímelos.
fun makeDecorations() {
    val decoration1 = Decoration("granite")
    println(decoration1)

    val decoration2 = Decoration("slate")
    println(decoration2)

    val decoration3 = Decoration("slate")
    println(decoration3)
}
  1. En makeDecorations(), agrega una sentencia de impresión que imprima el resultado de la comparación de decoration1 con decoration2 y otra que compare decoration3 con decoration2. Usa el método equals() que proporcionan las clases de datos.
    println (decoration1.equals(decoration2))
    println (decoration3.equals(decoration2))
  1. Ejecuta tu código.
⇒ Decoration(rocks=granite)
Decoration(rocks=slate)
Decoration(rocks=slate)
false
true

Paso 2: Usa la desestructuración

Para obtener las propiedades de un objeto de datos y asignarlas a variables, puedes asignarlas de a una a la vez, de la siguiente manera.

val rock = decoration.rock
val wood = decoration.wood
val diver = decoration.diver

En su lugar, puede hacer variables, una para cada propiedad, y asignar el objeto de datos al grupo de variables. Kotlin coloca el valor de la propiedad en cada variable.

val (rock, wood, diver) = decoration

Esto se denomina desestructuración y es una abreviatura útil. La cantidad de variables debe coincidir con la cantidad de propiedades, y las variables se asignan en el orden en que se declaran en la clase. Aquí tienes un ejemplo completo que puedes probar en Decoration.kt.

// Here is a data class with 3 properties.
data class Decoration2(val rocks: String, val wood: String, val diver: String){
}

fun makeDecorations() {
    val d5 = Decoration2("crystal", "wood", "diver")
    println(d5)

// Assign all properties to variables.
    val (rock, wood, diver) = d5
    println(rock)
    println(wood)
    println(diver)
}
⇒ Decoration2(rocks=crystal, wood=wood, diver=diver)
crystal
wood
diver

Si no necesitas una o más de las propiedades, puedes omitirlas con _ en lugar de un nombre de variable, como se muestra en el siguiente código.

    val (rock, _, diver) = d5

En esta tarea, aprenderás sobre algunas de las clases de propósito especial en Kotlin, incluidas las siguientes:

  • Clases singleton
  • Enumeradores
  • Clases selladas

Paso 1: Recupera las clases singleton

Recuerda el ejemplo anterior con la clase GoldColor.

object GoldColor : FishColor {
   override val color = "gold"
}

Debido a que cada instancia de GoldColor hace lo mismo, se declara como object en lugar de como class para que sea un singleton. Solo puede haber una instancia.

Paso 2: Crea una enumeración

Kotlin también admite enumeraciones, que te permiten enumerar y hacer referencia a algo por su nombre, al igual que en otros lenguajes. Para declarar una enumeración, usa la palabra clave enum como prefijo. Una declaración básica de enumeración solo necesita una lista de nombres, pero también puedes definir uno o más campos asociados con cada nombre.

  1. En Decoration.kt, prueba un ejemplo de una enumeración.
enum class Color(val rgb: Int) {
   RED(0xFF0000), GREEN(0x00FF00), BLUE(0x0000FF);
}

Las enumeraciones se parecen a los singleton. Solo puede haber uno y cada valor de la enumeración. Por ejemplo, solo puede haber un Color.RED, un Color.GREEN y un Color.BLUE. En este ejemplo, los valores RGB se asignan a la propiedad rgb para representar los componentes de color. También puedes obtener el valor ordinal de una enumeración mediante la propiedad ordinal, y su nombre mediante la propiedad name.

  1. Prueba con otro ejemplo de una enumeración.
enum class Direction(val degrees: Int) {
    NORTH(0), SOUTH(180), EAST(90), WEST(270)
}

fun main() {
    println(Direction.EAST.name)
    println(Direction.EAST.ordinal)
    println(Direction.EAST.degrees)
}
⇒ EAST
2
90

Paso 3: Crea una clase sellada

Una clase sellada es una clase que se puede subclasificar, pero solo dentro del archivo en el que se declaró. Si intentas subclasificar la clase en un archivo diferente, obtendrás un error.

Debido a que las clases y subclases están en el mismo archivo, Kotlin conocerá todas las subclases de manera estática. Es decir, en el tiempo de compilación, el compilador ve todas las clases y subclases, y sabe que estas son todas, de manera que el compilador puede realizar verificaciones adicionales por ti.

  1. En AquariumFish.kt, prueba un ejemplo de una clase sellada, con el tema acuático.
sealed class Seal
class SeaLion : Seal()
class Walrus : Seal()

fun matchSeal(seal: Seal): String {
   return when(seal) {
       is Walrus -> "walrus"
       is SeaLion -> "sea lion"
   }
}

La clase Seal no se puede subclasificar en otro archivo. Si deseas agregar más tipos de Seal, debes agregarlos en el mismo archivo. Esto hace que las clases selladas sean una forma segura de representar una cantidad fija de tipos. Por ejemplo, las clases selladas son excelentes para devolver el éxito o el error desde una API de red.

Esta lección recorrió mucho terreno. Si bien gran parte debe conocer otros lenguajes de programación orientados a objetos, Kotlin agrega algunas funciones para que el código sea conciso y legible.

Clases y constructores

  • Define una clase en Kotlin usando class.
  • Kotlin crea métodos set y get de forma automática para las propiedades.
  • Define el constructor principal directamente en la definición de la clase. Por ejemplo:
    class Aquarium(var length: Int = 100, var width: Int = 20, var height: Int = 40)
  • Si un constructor principal necesita código adicional, escríbelo en uno o más bloques init.
  • Una clase puede definir uno o más constructores secundarios con constructor, pero el estilo de Kotlin es usar una función de fábrica.

Modificadores y subclases de visibilidad

  • Todas las clases y funciones de Kotlin son public de forma predeterminada, pero puedes usar modificadores para cambiar la visibilidad a internal, private o protected.
  • Para crear una subclase, la clase superior debe estar marcada como open.
  • Para anular los métodos y las propiedades de una subclase, los métodos y las propiedades deben marcarse como open en la clase superior.
  • Una clase sellada solo se puede subclasificar en el mismo archivo en el que se define. Para crear una clase sellada, agrega el prefijo sealed a la declaración.

Clases de datos, singletons y enumeraciones

  • Para hacer una clase de datos, agrega la prefijo data a la declaración.
  • Desestructurar es una abreviatura para asignar las propiedades de un objeto data a variables separadas.
  • Crea una clase singleton mediante object en lugar de class.
  • Define una enumeración mediante enum class.

Clases abstractas, interfaces y delegación

  • Las interfaces y las clases abstractas son dos formas de compartir el comportamiento común entre clases.
  • Una clase abstracta define las propiedades y el comportamiento, pero deja la implementación en subclases.
  • Una interfaz define el comportamiento y puede proporcionar implementaciones predeterminadas para parte o la totalidad del comportamiento.
  • Cuando usas interfaces para componer una clase, la funcionalidad de la clase se extiende a través de las instancias de clase que contiene.
  • La delegación de interfaces usa la composición, pero también delega la implementación a las clases de interfaz.
  • La composición es una forma eficaz de agregar funcionalidad a una clase mediante la delegación de interfaces. En general, se prefiere la composición, pero la herencia de una clase abstracta es una mejor opción para algunos problemas.

Documentación de Kotlin

Si necesitas más información sobre algún tema de este curso o si no puedes avanzar, https://kotlinlang.org es tu mejor punto de partida.

Instructivos de Kotlin

El sitio web https://try.kotlinlang.org incluye instructivos enriquecidos llamados Kotlin Koans, un intérprete basado en la Web, y un conjunto completo de documentación de referencia con ejemplos.

Curso de Udacity

Para ver el curso de Udacity sobre este tema, consulta el Capacitación de Kotlin para programadores.

IntelliJ IDEA

Puedes encontrar la documentación de IntelliJ IDEA en el sitio web de JetBrains.

En esta sección, se enumeran las posibles tareas para los alumnos que trabajan con este codelab como parte de un curso que dicta un instructor. Depende del instructor hacer lo siguiente:

  • Si es necesario, asigna la tarea.
  • Informa a los alumnos cómo enviar los deberes.
  • Califica las tareas.

Los instructores pueden usar estas sugerencias lo poco o lo que quieran, y deben asignar cualquier otra tarea que consideren apropiada.

Si estás trabajando en este codelab por tu cuenta, usa estas tareas para poner a prueba tus conocimientos.

Responde estas preguntas

Pregunta 1

Las clases tienen un método especial que sirve como plano para crear objetos de esa clase. ¿Cómo se llama el método?

▢ Un compilador

▢ Un instanciador

▢ Un constructor

▢ Un plano

Pregunta 2

¿Cuál de las siguientes afirmaciones sobre las interfaces y las clases abstractas NO es correcta?

▢ Las clases abstractas pueden tener constructores.

▢ Las interfaces no pueden tener constructores.

▢ Se pueden crear directamente instancias de las interfaces y las clases abstractas.

▢ Las propiedades abstractas se deben implementar por subclases de la clase abstracta.

Pregunta 3

¿Cuál de los siguientes NO es un modificador de visibilidad de Kotlin para propiedades, métodos y otros elementos?

internal

nosubclass

protected

private

Pregunta 4

Considera esta clase de datos:
data class Fish(val name: String, val species:String, val colors:String)
¿Cuál de las siguientes opciones NO es un código válido para crear y desestructurar un objeto Fish?

val (name1, species1, colors1) = Fish("Pat", "Plecostomus", "gold")

val (name2, _, colors2) = Fish("Bitey", "shark", "gray")

val (name3, species3, _) = Fish("Amy", "angelfish", "blue and black stripes")

val (name4, species4, colors4) = Fish("Harry", "halibut")

Pregunta 5

Supongamos que tienes un zoológico con muchos animales que se deben cuidar. ¿Cuál de las siguientes opciones NO formaría parte de la implementación del cuidado?

▢ Un interface para diferentes tipos de comida que consumen los animales.

▢ Una clase abstract Caretaker a partir de la cual puedes crear diferentes tipos de cuidadores.

▢ Una interface que le da agua limpia a un animal.

▢ Una clase data para una entrada en un programa de alimentación.

Continúa con la siguiente lección: 5.1 Extensiones

Para obtener una descripción general del curso, incluidos vínculos a otros codelabs, consulta "Capacitación de Kotlin para programadores: Bienvenido al curso."