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

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

Introducción

En este codelab, crearás un programa en Kotlin y aprenderás sobre clases y objetos en Kotlin. 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 necesitas escribir. También aprenderás sobre las clases abstractas y la delegación de interfaces.

En lugar de crear una sola app de ejemplo, las lecciones de este curso están diseñadas para ampliar tus conocimientos, pero son semiindependientes entre sí para que puedas revisar las secciones con las que ya estás familiarizado. Para unirlos, muchos de los ejemplos usan un tema de acuario. Si quieres ver la historia completa del acuario, consulta el curso de Udacity Kotlin Bootcamp for Programmers.

Conocimientos que ya deberías tener

  • Los conceptos básicos de Kotlin, incluidos los tipos, los operadores y los bucles
  • Sintaxis de funciones de Kotlin
  • Conceptos básicos de la programación orientada a objetos
  • Los 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 clases de datos
  • Cómo usar singletons, enumeraciones y clases selladas

Actividades

  • Crea una clase con propiedades
  • Cómo crear un constructor para una clase
  • Crea una subclase
  • Examina ejemplos de clases abstractas e interfaces
  • Crea una clase de datos simple
  • Obtén información sobre los singleton, los enums y las clases selladas

Ya deberías 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 un Aquarium real.
  • Las propiedades son características de las clases, como la longitud, el ancho y la altura de un Aquarium.
  • Los métodos, también llamados funciones 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 puede implementar una clase. Por ejemplo, la limpieza es común a objetos que no son acuarios, y la limpieza generalmente se realiza de manera similar para diferentes objetos. Por lo tanto, 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, en el 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 una letra mayúscula.

  1. Haz clic con el botón derecho en el paquete example.myapp.
  2. Selecciona New > Kotlin File / Class.
  3. En Kind, selecciona Class y asigna el nombre Aquarium a la clase. 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 y, luego, inicializa las propiedades var para el ancho, la altura y la longitud (en centímetros). Inicializa las propiedades con valores predeterminados.
package example.myapp

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

De forma interna, Kotlin crea automáticamente métodos get y set para las propiedades que definiste en la clase Aquarium, por lo que 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 contener la función main().

  1. En el panel Project de la izquierda, haz clic con el botón derecho en el paquete example.myapp.
  2. Selecciona New > Kotlin File / Class.
  3. En el menú desplegable Kind, mantén la selección como File y asigna el nombre main.kt al archivo. 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, de manera 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. A fin de ejecutar el programa, haz clic en el triángulo verde junto a la función main(). Observa 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 dimensión modificadas.
fun buildAquarium() {
    val myAquarium = Aquarium()
    myAquarium.printSize()
    myAquarium.height = 60
    myAquarium.printSize()
}
  1. Ejecuta tu 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ás un constructor para la clase y seguirás trabajando con las 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 una vez que se crea el objeto configurando las propiedades, pero sería más sencillo crearlo con el tamaño correcto desde el principio.

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

  1. En la clase Aquarium que creaste antes, cambia la definición de la clase para incluir tres parámetros del 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, usando var o val, y Kotlin también crea los métodos getter y setter automáticamente. Luego, puedes quitar las definiciones de propiedades del 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 no especificar ningún argumento y obtener los valores predeterminados, o bien especificar solo algunos de ellos, o bien especificar todos y crear un Aquarium con un 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 

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

Paso 2: Agrega bloques de inicialización

Los constructores de ejemplo anteriores solo declaran propiedades y les asignan el valor de una expresión. Si tu constructor necesita más código de inicialización, se puede colocar 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 

Ten en cuenta 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 de constructores, es decir, constructores con diferentes argumentos.

  1. En la clase Aquarium, agrega un constructor secundario que tome una cantidad de peces como argumento, usando la palabra clave constructor. Crea una propiedad de tanque val para el volumen calculado del acuario en litros según la cantidad de peces. Supón que necesitas 2 litros (2,000 cm³) de agua por pez, más un poco de espacio adicional para que no se derrame el agua.
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 la misma longitud y el mismo ancho (que se establecieron en el constructor principal) y calcula la altura necesaria para que el tanque tenga el volumen indicado.
    // calculate the height needed
    height = (tank / (length * width)).toInt()
  1. En la función buildAquarium(), agrega una llamada para crear un Aquarium con 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 tu 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 por el bloque init en el constructor principal antes de que se ejecute el constructor secundario y otra vez por el código en buildAquarium().

También 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 getter de propiedad

En este paso, agregarás un getter de propiedad explícito. Kotlin define automáticamente los métodos get y set cuando defines propiedades, pero, a veces, el valor de una propiedad debe ajustarse o calcularse. Por ejemplo, arriba, imprimiste el volumen del Aquarium. Puedes hacer que el volumen esté disponible como una propiedad definiendo una variable y un getter para ella. Como volume debe calcularse, el getter debe devolver el valor calculado, lo que puedes hacer con una función de una sola 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 tu 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 setter de propiedad

En este paso, crearás un nuevo establecedor de propiedades para el volumen.

  1. En la clase Aquarium, cambia volume a un var para que se pueda configurar más de una vez.
  2. Agrega un setter para la propiedad volume agregando un método set() debajo del getter, que vuelve a calcular la altura según la cantidad de agua proporcionada. Por convención, el nombre del parámetro setter 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

Hasta ahora, no hubo modificadores de visibilidad, como public o private, en el código. Esto se debe a que, de forma predeterminada, todo en Kotlin es público, lo que significa que se puede acceder a todo en cualquier lugar, incluidas las clases, los métodos, las propiedades y las variables miembro.

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

  • public significa que es 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 de Kotlin que se compilan juntos, por ejemplo, una biblioteca o una aplicación.
  • private significa que solo será visible en esa clase (o archivo fuente si trabajas con funciones).
  • protected es igual que private, pero también será visible para cualquier subclase.

Consulta Modificadores de visibilidad en la documentación de Kotlin para obtener más información.

Variables miembro

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

Si quieres una propiedad que tu código pueda leer o escribir, pero que el código externo solo pueda leer, puedes dejar la propiedad y su captador como públicos, y declarar el establecedor 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 hay algunas diferencias.

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

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

Paso 1: Abre la clase de Aquarium

En este paso, harás que la clase Aquarium sea open para que puedas anularla en el siguiente paso.

  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 getter que devuelva el 90% del volumen del 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 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 un 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 nuevo resultado.
⇒ 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 cilíndrico redondeado en lugar de un tanque 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 un 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 para un cilindro es pi por el radio al cuadrado por la altura. Debes importar la constante PI de 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. Tu 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 tu 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)

A veces, deseas definir un comportamiento o propiedades comunes para que se compartan entre algunas clases relacionadas. Kotlin ofrece dos formas de hacerlo: interfaces y clases abstractas. En esta tarea, crearás una clase abstracta AquariumFish para las propiedades que son comunes a todos los peces. Creas una interfaz llamada FishAction para definir el comportamiento común a todos los peces.

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

Paso 1: Crea 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. Como color es abstracta, las subclases deben implementarla. 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 instancias de un Shark y un Plecostomus, y, luego, imprime el color de cada uno.
  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 tu programa y observa el resultado.
⇒ Shark: gray 
Plecostomus: gold

En el siguiente diagrama, se representan las clases Shark y Plecostomus, que son subclases de 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 y, luego, implementa eat() para 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(), haz que cada pez que creaste coma algo llamando a eat().
fun makeFish() {
    val shark = Shark()
    val pleco = Plecostomus()
    println("Shark: ${shark.color}")
    shark.eat()
    println("Plecostomus: ${pleco.color}")
    pleco.eat()
}
  1. Ejecuta tu programa y observa el resultado.
⇒ Shark: gray
hunt and eat fish
Plecostomus: gold
eat algae

En el siguiente diagrama, se representan las clases Shark y Plecostomus, que se componen de la interfaz FishAction y la implementan.

Cuándo usar clases abstractas en lugar de interfaces

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

Como se mencionó anteriormente, las clases abstractas pueden tener constructores, y las interfaces no, pero, de lo contrario, son muy similares. Entonces, ¿cuándo deberías usar cada una?

Cuando usas interfaces para componer una clase, la funcionalidad de la clase se extiende por medio de las instancias de clase que contiene. La composición tiende a hacer que el código sea más fácil de reutilizar y razonar que la herencia de una clase abstracta. Además, puedes usar varias interfaces en una clase, pero solo puedes crear subclases a partir de una clase abstracta.

La composición suele generar una mejor encapsulación, un menor acoplamiento (interdependencia), interfaces más limpias y código más utilizable. Por estos motivos, usar la composición con interfaces es el diseño preferido. Por otro lado, la herencia de una clase abstracta tiende a ser una opción natural para algunos problemas. Por lo tanto, debes preferir la composición, pero cuando la herencia tiene sentido, Kotlin también te permite usarla.

  • Usa una interfaz si tienes muchos métodos y una o dos implementaciones predeterminadas, por ejemplo, como en AquariumAction a continuación.
interface AquariumAction {
    fun eat()
    fun jump()
    fun clean()
    fun catchFish()
    fun swim()  {
        println("swim")
    }
}
  • Usa una clase abstracta siempre que no puedas completar una clase. Por ejemplo, si volvemos a la clase AquariumFish, puedes hacer que todos los AquariumFish implementen FishAction y proporcionar una implementación predeterminada para eat mientras dejas color abstracto, ya que 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")
}

En la tarea anterior, se presentaron las clases abstractas, las interfaces y la idea de composición. La delegación de interfaces es una técnica avanzada en la que los métodos de una interfaz se implementan con un objeto auxiliar (o delegado) que luego usa 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 de ayuda separada, y cada una de las clases usa una instancia de la clase de ayuda para implementar la funcionalidad.

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

Paso 1: Crea una interfaz nueva

  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 del pez y su color.
  2. Crea una interfaz nueva, FishColor, que defina el color como una cadena.
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 que también implemente 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, implementa 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 hace es indicar que su color es dorado.

No tiene sentido crear varias instancias de GoldColor, ya que todas harían exactamente lo mismo. Por lo tanto, Kotlin te permite declarar una clase en la que solo puedes crear una instancia de ella usando la palabra clave object en lugar de class. Kotlin creará esa instancia, y el nombre de la clase hará referencia a ella. Luego, todos los demás objetos pueden usar esta única instancia. No hay forma de crear otras instancias de esta clase. Si conoces el patrón singleton, así es como se implementan los singletons 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 la interfaz para FishColor

Ahora puedes 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. Esto significa que, en lugar de implementar FishColor, debes usar la implementación que proporciona 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 serán dorados, pero estos peces en realidad vienen en muchos colores. Para solucionar este problema, agrega un parámetro de constructor para el color con GoldColor como color predeterminado para Plecostomus.

  1. Cambia la clase Plecostomus para que tome un fishColor pasado con 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 la interfaz para FishAction

Del mismo modo, puedes usar la delegación de la interfaz para FishAction.

  1. En AquariumFish.kt, crea una clase PrintingFishAction que implemente FishAction, que tome un String, food y, luego, imprima 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 se debe quitar {}, ya que todas las anulaciones se controlan con la delegación de la interfaz.
class Plecostomus (fishColor: FishColor = GoldColor):
        FishAction by PrintingFishAction("eat algae"),
        FishColor by fishColor

En el siguiente diagrama, se representan las clases Shark y Plecostomus, ambas compuestas por las interfaces PrintingFishAction y FishColor, pero delegando la implementación en ellas.

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

Una clase de datos es similar a un 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 Kotlin proporciona para las clases de datos.

Paso 1: Crea una clase de datos

  1. Agrega un paquete decor nuevo en el paquete example.myapp para contener 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 convertir Decoration en una clase de datos, antepón la palabra clave data a la declaración de la clase.
  2. Agrega una propiedad String llamada rocks para proporcionar 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. Observa el resultado útil que se crea porque se trata de una clase de datos.
⇒ Decoration(rocks=granite)
  1. En makeDecorations(), crea dos objetos Decoration más que sean "pizarra" y, luego, 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 comparar 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 acceder a las propiedades de un objeto de datos y asignarlas a variables, puedes asignarlas una por vez, de la siguiente manera.

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

En cambio, puedes crear 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 llama 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 usando _ 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
  • Enumeraciones
  • Clases selladas

Paso 1: Recuerda las clases singleton

Recuerda el ejemplo anterior con la clase GoldColor.

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

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

Paso 2: Crea una enumeración

Kotlin también admite enumeraciones, que te permiten enumerar algo y hacer referencia a ello por su nombre, de forma similar a otros lenguajes. Declara un enum agregando el prefijo de la declaración con la palabra clave enum. Una declaración de enumeración básica 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);
}

Los enums son un poco como los singleton: solo puede haber uno, y solo uno de cada valor en 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 un enum con la propiedad ordinal y su nombre con la propiedad name.

  1. Probemos con otro ejemplo de 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 puede tener subclases, pero solo dentro del archivo en el que se declara. Si intentas crear una subclase en un archivo diferente, recibirás un error.

Como las clases y las subclases están en el mismo archivo, Kotlin conocerá todas las subclases de forma estática. Es decir, en el momento de la compilación, el compilador ve todas las clases y subclases, y sabe que son todas, por lo que puede realizar verificaciones adicionales por ti.

  1. En AquariumFish.kt, prueba un ejemplo de una clase sellada, manteniendo 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 subclasear 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 ideales para devolver un éxito o un error desde una API de red.

En esta lección, se abordaron muchos temas. Si bien gran parte de este lenguaje te resultará familiar por otros lenguajes de programación orientada a objetos, Kotlin agrega algunas funciones para mantener el código conciso y legible.

Clases y constructores

  • Define una clase en Kotlin con class.
  • Kotlin crea automáticamente métodos get y set 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 en su lugar.

Modificadores de visibilidad y subclases

  • Todas las clases y funciones en 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 métodos y propiedades en una subclase, los métodos y las propiedades deben estar marcados como open en la clase principal.
  • Una clase sellada solo puede tener subclases 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

  • Crea una clase de datos agregando el prefijo data a la declaración.
  • La desestructuración es una abreviatura para asignar las propiedades de un objeto data a variables separadas.
  • Crea una clase singleton con object en lugar de class.
  • Define un enum con enum class.

Clases abstractas, interfaces y delegación

  • Las clases abstractas y las interfaces son dos formas de compartir un comportamiento común entre las clases.
  • Una clase abstracta define propiedades y comportamiento, pero deja la implementación a las subclases.
  • Una interfaz define el comportamiento y puede proporcionar implementaciones predeterminadas para parte o todo el comportamiento.
  • Cuando usas interfaces para componer una clase, la funcionalidad de la clase se extiende por medio 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 a través de la delegación de interfaces. En general, se prefiere la composición, pero la herencia de una clase abstracta se adapta mejor a algunos problemas.

Documentación de Kotlin

Si deseas obtener más información sobre algún tema de este curso o si te quedas atascado, https://kotlinlang.org es el 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 Capacitación de Kotlin para programadores.

IntelliJ IDEA

Encontrarás la documentación de IntelliJ IDEA en el sitio web de JetBrains.

En esta sección, se enumeran las posibles actividades para el hogar para los alumnos que trabajan en este codelab como parte de un curso dirigido por un instructor. Depende del instructor hacer lo siguiente:

  • Si es necesario, asigna una tarea.
  • Comunicarles a los alumnos cómo enviar las actividades para el hogar.
  • Califica las actividades para el hogar.

Los instructores pueden usar estas sugerencias en la medida que quieran y deben asignar cualquier otra actividad para el hogar que consideren apropiada.

Si estás trabajando en este codelab por tu cuenta, usa estas actividades para el hogar para probar 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.

▢ Las interfaces y las clases abstractas se pueden crear instancias directamente.

▢ Las propiedades abstractas deben ser implementadas por las subclases de la clase abstracta.

Pregunta 3

¿Cuál de las siguientes opciones NO es un modificador de visibilidad de Kotlin para propiedades, métodos, etcétera?

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 los siguientes códigos NO es 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 necesitan cuidados. ¿Cuál de las siguientes opciones NO formaría parte de la implementación del cuidado?

▢ Un interface para los diferentes tipos de alimentos que comen los animales

▢ Una clase abstract Caretaker desde la que puedes crear diferentes tipos de cuidadores.

▢ Un interface por darle 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 los vínculos a otros codelabs, consulta "Capacitación de Kotlin para programadores: Bienvenido al curso".