Usa clases y objetos en Kotlin

1. Antes de comenzar

En este codelab, aprenderás a usar clases y objetos en Kotlin.

Las clases proporcionan planos a partir de cuales se pueden construir objetos. Un objeto es una instancia de una clase que consiste en datos específicos de ese objeto. Puedes usar instancias de clase y objetos indistintamente.

Como analogía, imagina que construyes una casa. Una clase es similar al plan de diseño de un arquitecto, también conocido como plano. El plano no es la casa, sino las instrucciones para construirla. La casa es el objeto real que se construye a partir de ese plano.

Al igual que el plano de una casa especifica varias habitaciones, y cada una tiene su propio diseño y propósito, cada clase tiene su propio diseño y propósito. Para saber cómo diseñar tus clases, debes familiarizarte con la programación orientada a objetos (OOP), un framework que te enseña a encerrar datos, lógica y comportamiento en los objetos.

OOP ayuda a simplificar problemas complejos del mundo real en objetos más pequeños. Hay cuatro conceptos básicos de OOP, cada uno de los cuales aprenderás más adelante en este codelab:

  • Encapsulamiento. Envuelve las propiedades y los métodos relacionados que realizan acciones en esas propiedades en una clase. Por ejemplo, considera tu teléfono móvil. Encapsula una cámara, una pantalla, tarjetas de memoria y varios componentes de hardware y software. No tienes que preocuparte por la forma en que los componentes se conectan de forma interna.
  • Abstracción. Es una extensión del encapsulamiento. La idea es ocultar la lógica de implementación interna tanto como sea posible. Por ejemplo, para tomar una foto con tu teléfono móvil, solo debes abrir la app de la cámara, apuntar el teléfono hacia la escena que deseas capturar y hacer clic en un botón para capturar la foto. No necesitas saber cómo se compila la app de la cámara ni cómo funciona realmente el hardware de la cámara del teléfono. En resumen, los mecanismos internos de la aplicación de cámara y la forma en que una cámara móvil captura las fotos se abstraen para permitirte realizar las tareas importantes.
  • Herencia. Permite crear una clase en función de las características y el comportamiento de otras clases estableciendo una relación de superior y secundario. Por ejemplo, hay distintos fabricantes que producen una variedad de dispositivos móviles que ejecutan el SO Android, pero la IU de cada uno es diferente. En otras palabras, los fabricantes heredan la función del SO Android y compilan sus personalizaciones a partir de ella.
  • Polimorfismo. La palabra es una adaptación de la raíz griega poly-, que significa muchas, y -morphism, que significa formas. El polimorfismo es la capacidad de usar objetos diferentes de una manera común. Por ejemplo, cuando conectas una bocina Bluetooth a tu teléfono móvil, este solo necesita saber si hay un dispositivo que puede reproducir audio mediante Bluetooth. Sin embargo, puedes elegir entre una variedad de bocinas Bluetooth, y tu teléfono no necesita saber cómo trabajar con cada una de ellas a nivel específico.

Por último, aprenderás sobre los delegados de propiedad, que proporcionan código reutilizable para administrar valores de propiedad con una sintaxis concisa. En este codelab, aprenderás estos conceptos cuando compiles una estructura de clase para una app de casa inteligente.

Requisitos previos

  • Saber abrir, editar y ejecutar un código en el Playground de Kotlin.
  • Conocimientos de los conceptos básicos de programación de Kotlin, incluidas variables, funciones, y las funciones println() y main()

Qué aprenderás

  • Descripción general de OOP
  • Qué son las clases
  • Cómo definir una clase con constructores, funciones y propiedades
  • Cómo crear una instancia de un objeto
  • Qué es la herencia
  • Cuál es la diferencia entre las relaciones IS-A y HAS-A
  • Cómo anular propiedades y funciones
  • Qué son los modificadores de visibilidad
  • Qué es un delegado y cómo usar el delegado by

Qué compilarás

  • Una estructura de clases para casa inteligente
  • Clases que representan dispositivos inteligentes, como una smart TV y una lámpara inteligente

Qué necesitarás

  • Una computadora con acceso a Internet y un navegador web

2. Define una clase

Cuando defines una clase, especificas las propiedades y los métodos que deben tener todos los objetos de esa clase.

La definición de una clase comienza con la palabra clave class, seguida de un nombre y un conjunto de llaves. La parte de la sintaxis anterior a la llave de apertura también se conoce como encabezado de clase. Entre llaves, puedes especificar las propiedades y funciones de la clase. Pronto aprenderás sobre las propiedades y funciones. Puedes ver la sintaxis de una definición de clase en este diagrama:

Comienza con la palabra clave de la clase, seguida de un nombre y un conjunto de llaves de apertura y cierre. Las llaves contienen el cuerpo de la clase que describe su diseño azul.

Estas son las convenciones de nombres recomendadas para una clase:

  • Puedes elegir el nombre de clase que desees, pero no uses las palabras clave de Kotlin como nombre de clase (por ejemplo, fun).
  • El nombre de la clase está escrito en PascalCase, por lo que cada palabra comienza con mayúscula y no hay espacios entre ellas. Por ejemplo, en SmartDevice, la primera letra de cada palabra aparece en mayúscula y no hay un espacio entre ellas.

Una clase consta de tres partes principales:

  • Propiedades. Son variables que especifican los atributos de los objetos de la clase.
  • Métodos. Son funciones que contienen los comportamientos y las acciones de la clase.
  • Constructores. Una función de miembro especial que crea instancias de la clase a lo largo del programa en el que se define.

Esta no es la primera vez que trabajas con clases. En codelabs anteriores, aprendiste sobre tipos de datos, como Int, Float, String y Double. Estos tipos de datos se definen como clases en Kotlin. Cuando definas una variable como se muestra en este fragmento de código, crea un objeto de la clase Int, en el que se creará una instancia con un valor 1:

val number: Int = 1

Define una clase SmartDevice:

  1. En el Playground de Kotlin, reemplaza el contenido por una función main() vacía:
fun main() {
}
  1. En la línea anterior a la función main(), define una clase SmartDevice con un cuerpo que incluya un comentario // empty body:
class SmartDevice {
    // empty body
}

fun main() {
}

3. Crea una instancia de una clase

Como aprendiste, una clase es un plano para un objeto. El tiempo de ejecución de Kotlin usa la clase, o plano, para crear un objeto de ese tipo en particular. Con la clase SmartDevice, tienes un plano de un dispositivo inteligente. Para tener un dispositivo inteligente real en tu programa, debes crear una instancia de objeto SmartDevice. La sintaxis de la creación de una instancia comienza con el nombre de la clase seguido de un conjunto de paréntesis, como se puede ver en este diagrama:

1d25bc4f71c31fc9.png

Para usar un objeto, debes crear ese objeto y asignarlo a una variable, de manera similar a como se define una variable. Usa la palabra clave val a fin de crear una variable inmutable y la palabra clave var para una variable mutable. A las palabras clave val o var les siguen el nombre de la variable, un operador de asignación = y la creación de instancias del objeto de clase. Puedes ver la sintaxis en este diagrama:

f58430542f2081a9.png

Crea una instancia de la clase SmartDevice como objeto:

  • En la función main(), usa la palabra clave val para crear una variable llamada smartTvDevice e inicializarla como una instancia de la clase SmartDevice:
fun main() {
    val smartTvDevice = SmartDevice()
}

4. Define métodos de clase

En la Unidad 1, aprendiste lo siguiente:

  • La definición de una función usa la palabra clave fun seguida de un conjunto de paréntesis y un conjunto de llaves. Las llaves contienen código, que son las instrucciones necesarias para ejecutar una tarea.
  • La llamada a una función hace que se ejecute el código contenido en ella.

Las acciones que la clase puede realizar se definen como funciones en ella. Por ejemplo, imagina que tienes un dispositivo inteligente, una smart TV o una lámpara inteligente que puedes encender y apagar con tu teléfono móvil. El dispositivo inteligente se traduce a la clase SmartDevice en programación, y la acción para encenderlo y apagarlo se representa con las funciones turnOn() y turnOff(), que permiten activar y desactivar el comportamiento.

La sintaxis para definir una función en una clase es idéntica a la que aprendiste anteriormente. La única diferencia es que la función se coloca en el cuerpo de la clase. Cuando defines una función en el cuerpo de la clase, se la conoce como una función de miembro o método, y representa el comportamiento de la clase. En el resto de este codelab, a las funciones se las denominará métodos siempre que aparezcan en el cuerpo de una clase.

Define un método turnOn() y turnOff() en la clase SmartDevice:

  1. En el cuerpo de la clase SmartDevice, define un método turnOn() con un cuerpo vacío:
class SmartDevice {
    fun turnOn() {

    }
}
  1. En el cuerpo del método turnOn(), agrega una sentencia println() y pásale una string "Smart device is turned on.":
class SmartDevice {
    fun turnOn() {
        println("Smart device is turned on.")
    }
}
  1. Después del método turnOn(), agrega un método turnOff() que imprima una string "Smart device is turned off.":
class SmartDevice {
    fun turnOn() {
        println("Smart device is turned on.")
    }

    fun turnOff() {
        println("Smart device is turned off.")
    }
}

Llama a un método en un objeto.

Hasta ahora, definiste una clase que funciona como plano para un dispositivo inteligente, creaste una instancia de la clase y asignaste esa instancia a una variable. Ahora, usarás los métodos de la clase SmartDevice para encender y apagar el dispositivo.

La llamada a un método en una clase es similar a la llamada a otras funciones de la función main() del codelab anterior. Por ejemplo, si necesitas llamar al método turnOff() desde el método turnOn(), puedes escribir algo similar a este fragmento de código:

class SmartDevice {
    fun turnOn() {
        // A valid use case to call the turnOff() method could be to turn off the TV when available power doesn't meet the requirement.
        turnOff()
        ...
    }

    ...
}

Para llamar a un método de clase fuera de la clase, comienza con el objeto de clase, seguido del operador ., el nombre de la función y un conjunto de paréntesis. Si corresponde, los paréntesis contendrán los argumentos que requiere el método. Puedes ver la sintaxis en este diagrama:

fc609c15952551ce.png

Llama a los métodos turnOn() y turnOff() en el objeto:

  1. En la función main() de la línea después de la variable smartTvDevice, llama al método turnOn():
fun main() {
    val smartTvDevice = SmartDevice()
    smartTvDevice.turnOn()
}
  1. En la línea después del método turnOn(), llama al método turnOff():
fun main() {
    val smartTvDevice = SmartDevice()
    smartTvDevice.turnOn()
    smartTvDevice.turnOff()
}
  1. Ejecuta el código.

Este es el resultado:

Smart device is turned on.
Smart device is turned off.

5. Define las propiedades de la clase

En la Unidad 1, aprendiste sobre las variables, que son contenedores de datos individuales. Aprendiste a crear una variable de solo lectura con la palabra clave val y una variable mutable con var.

Mientras que los métodos definen las acciones que puede realizar una clase, las propiedades definen las características o los atributos de los datos de la clase. Por ejemplo, un dispositivo inteligente tiene las siguientes propiedades:

  • Nombre. Indica el nombre del dispositivo.
  • Categoría. Indica un tipo de dispositivo inteligente, como entretenimiento, utilidad o cocina.
  • Estado del dispositivo. Indica si el dispositivo está encendido, apagado, en línea o sin conexión. El dispositivo se considera en línea cuando está conectado a Internet. De lo contrario, se considera como sin conexión.

Básicamente, las propiedades son variables que se definen en el cuerpo de la clase, y no el cuerpo de la función. Esto significa que la sintaxis para definir las propiedades y las variables es idéntica. Debes definir una propiedad inmutable con la palabra clave val y una propiedad mutable con la palabra clave var.

Implementa las características mencionadas anteriormente como propiedades de la clase SmartDevice:

  1. En la línea anterior al método turnOn(), define la propiedad name y asígnala a una string "Android TV":
class SmartDevice {

    val name = "Android TV"

    fun turnOn() {
        println("Smart device is turned on.")
    }

    fun turnOff() {
        println("Smart device is turned off.")
    }
}
  1. En la línea que sigue a la propiedad name, define la propiedad category y asígnala a una cadena "Entertainment". Luego, define una propiedad deviceStatus y asígnala a una cadena "online":
class SmartDevice {

    val name = "Android TV"
    val category = "Entertainment"
    var deviceStatus = "online"

    fun turnOn() {
        println("Smart device is turned on.")
    }

    fun turnOff() {
        println("Smart device is turned off.")
    }
}
  1. En la línea después de la variable smartTvDevice, llama a la función println() y pásale una string "Device name is: ${smartTvDevice.name}":
fun main() {
    val smartTvDevice = SmartDevice()
    println("Device name is: ${smartTvDevice.name}")
    smartTvDevice.turnOn()
    smartTvDevice.turnOff()
}
  1. Ejecuta el código.

Este es el resultado:

Device name is: Android TV
Smart device is turned on.
Smart device is turned off.

Funciones get y set en propiedades

Las propiedades pueden hacer más de lo que hace una variable. Por ejemplo, imagina que creas una estructura de clase para representar una smart TV. Una de las acciones comunes que realizarás será aumentar y disminuir el volumen. Para representar esta acción en la programación, puedes crear una propiedad llamada speakerVolume, que contenga el nivel de volumen actual establecido en la bocina de la TV, pero ese valor de volumen pertenece a un rango. El volumen mínimo que puedes establecer es 0, mientras que el máximo es 100. Para asegurarte de que la propiedad speakerVolume nunca supere los 100 ni caiga debajo 0, puedes escribir una función set. Cuando actualices el valor de la propiedad, debes verificar si está en el rango de 0 a 100. Como otro ejemplo, imagina que uno de los requisitos es garantizar que el nombre esté siempre en mayúsculas. Puedes implementar una función get para convertir la propiedad name en mayúsculas.

Antes de profundizar en cómo implementar estas propiedades, debes comprender la sintaxis completa para declararlas. La sintaxis completa para definir una propiedad mutable comienza con la definición de la variable seguida de las funciones get() y set() opcionales. Puedes ver la sintaxis en este diagrama:

f2cf50a63485599f.png

Cuando no defines la función de método get y set para una propiedad, el compilador de Kotlin crea las funciones a nivel interno. Por ejemplo, si usas la palabra clave var para definir una propiedad speakerVolume y asignarle un valor 2, el compilador genera automáticamente las funciones de método get y set, como puedes ver en este fragmento de código:

var speakerVolume = 2
    get() = field
    set(value) {
        field = value
    }

No verás estas líneas en tu código porque el compilador las agrega en segundo plano.

La sintaxis completa de una propiedad inmutable tiene dos diferencias:

  • Comienza con la palabra clave val.
  • Las variables de tipo val son variables de solo lectura, por lo que no tienen funciones set().

Las propiedades de Kotlin usan un campo de copia de seguridad para conservar un valor en la memoria. Un campo de copia de seguridad es básicamente una variable de clase definida internamente en las propiedades. Un campo de copia de seguridad tiene alcance en una propiedad, lo que significa que solo puedes acceder a él a través de las funciones de propiedad get() o set().

Para leer el valor de la propiedad en la función get() o actualizarlo en la función set(), debes usar el campo de copia de seguridad de la propiedad. El compilador de Kotlin lo genera automáticamente y se hace referencia a él con un identificador field.

Por ejemplo, cuando deseas actualizar el valor de la propiedad en la función set(), debes usar el parámetro de la función set(), que se denomina value, y asignarlo a la variable field como se ve en este fragmento de código:

var speakerVolume = 2
    set(value) {
        field = value
    }

Por ejemplo, para asegurarte de que el valor asignado a la propiedad speakerVolume esté en el rango de 0 a 100, puedes implementar la función set, como se muestra en este fragmento de código:

var speakerVolume = 2
    set(value) {
        if (value in 0..100) {
            field = value
        }
    }

Las funciones set() verifican si el valor Int está en un rango de 0 a 100 usando la palabra clave in seguida del rango de valor. Si el valor está en el rango esperado, se actualiza el valor de field. De lo contrario, el valor de la propiedad no se modifica.

Debes incluir esta propiedad en una clase de la sección Implementa una relación entre clases de este codelab, por lo que no necesitas agregar la función set al código ahora.

6. Define un constructor

El objetivo principal del constructor es especificar cómo se crean los objetos de la clase. En otras palabras, los constructores inicializan un objeto y lo preparan para su uso. Tú lo hiciste cuando creaste una instancia del objeto. El código dentro del constructor se ejecuta cuando se crea una instancia del objeto de la clase. Puedes definir un constructor con o sin parámetros.

Constructor predeterminado

Un constructor predeterminado es aquel que no tiene parámetros. Puedes definir un constructor predeterminado como se muestra en este fragmento de código:

class SmartDevice constructor() {
    ...
}

Kotlin tiene como objetivo ser conciso, por lo que puedes quitar la palabra clave constructor si no hay anotaciones ni modificadores de visibilidad, sobre los que aprenderás pronto. También puedes quitar los paréntesis si el constructor no tiene parámetros, como se muestra en este fragmento de código:

class SmartDevice {
    ...
}

El compilador de Kotlin genera automáticamente el constructor predeterminado. No verás el constructor predeterminado generado automáticamente en tu código porque lo agrega el compilador en segundo plano.

Define un constructor parametrizado

En la clase SmartDevice, las propiedades name y category son inmutables. Debes asegurarte de que todas las instancias de la clase SmartDevice inicialicen las propiedades name y category. Con la implementación actual, los valores de las propiedades name y category están codificados. Esto significa que todos los dispositivos inteligentes se nombran con la string "Android TV" y se categorizan con la string "Entertainment".

A fin de mantener la inmutabilidad y evitar los valores codificados, usa un constructor parametrizado para inicializarlos:

  • En la clase SmartDevice, mueve las propiedades name y category al constructor sin asignar valores predeterminados:
class SmartDevice(val name: String, val category: String) {

    var deviceStatus = "online"

    fun turnOn() {
        println("Smart device is turned on.")
    }

    fun turnOff() {
        println("Smart device is turned off.")
    }
}

Ahora, el constructor acepta parámetros para configurar sus propiedades, por lo que también cambia la forma de crear una instancia de un objeto para esa clase. En este diagrama, se puede ver la sintaxis completa para crear una instancia de un objeto:

bbe674861ec370b6.png

Esta es la representación del código:

SmartDevice("Android TV", "Entertainment")

Ambos argumentos del constructor son strings. No se sabe con exactitud a qué parámetro se debe asignar el valor. Para solucionarlo, similar a como pasaste los argumentos de las funciones, puedes crear un constructor con argumentos nombrados, como se muestra en este fragmento de código:

SmartDevice(name = "Android TV", category = "Entertainment")

Existen dos tipos principales de constructores en Kotlin:

  • Constructor principal. Una clase solo puede tener un constructor principal, que se define como parte del encabezado de la clase. Un constructor principal puede ser un constructor predeterminado o parametrizado. El constructor principal no tiene un cuerpo, lo que significa que no puede contener código.
  • Constructor secundario. Una clase puede tener varios constructores secundarios. Puedes definir el constructor secundario con o sin parámetros. El constructor secundario puede inicializar la clase y tiene un cuerpo, que puede contener lógica de inicialización. Si la clase tiene un constructor principal, cada constructor secundario debe inicializarlo.

Puedes usar el constructor principal para inicializar propiedades en el encabezado de la clase. Los argumentos que se pasan al constructor se asignan a las propiedades. La sintaxis para definir un constructor principal comienza con el nombre de la clase seguido de la palabra clave constructor y un conjunto de paréntesis. Los paréntesis contienen los parámetros del constructor principal. Si hay más de un parámetro, las comas separan las definiciones de cada uno. En este diagrama, puedes ver la sintaxis completa para definir un constructor principal:

aa05214860533041.png

El constructor secundario se encierra en el cuerpo de la clase y su sintaxis incluye tres partes:

  • Declaración del constructor secundario. La definición del constructor secundario comienza con la palabra clave constructor, seguida de paréntesis. Si corresponde, los paréntesis contienen los parámetros que requiere el constructor secundario.
  • Inicialización del constructor principal. La inicialización comienza con dos puntos, seguidos de la palabra clave this y un conjunto de paréntesis. Si corresponde, los paréntesis contienen los parámetros que requiere el constructor principal.
  • Cuerpo del constructor secundario. A la inicialización del constructor principal le sigue un conjunto de llaves, que contienen el cuerpo del constructor secundario.

Puedes ver la sintaxis en este diagrama:

2dc13ef136009e98.png

Por ejemplo, imagina que deseas integrar una API desarrollada por un proveedor de dispositivos inteligentes. Sin embargo, esta muestra el código de estado de tipo Int para indicar el estado inicial del dispositivo. La API muestra un valor 0 si el dispositivo no tiene conexión y un valor 1 si el dispositivo está en línea. Para cualquier otro valor de número entero, el estado se considera desconocido. Puedes crear un constructor secundario en la clase SmartDevice a fin de convertir este parámetro statusCode en una representación de string, como se puede ver en este fragmento de código:

class SmartDevice(val name: String, val category: String) {
    var deviceStatus = "online"

    constructor(name: String, category: String, statusCode: Int) : this(name, category) {
        deviceStatus = when (statusCode) {
            0 -> "offline"
            1 -> "online"
            else -> "unknown"
        }
    }
    ...
}

7. Implementa una relación entre clases

La herencia te permite compilar una clase en función de las características y el comportamiento de otra clase. Es un mecanismo potente que te ayuda a escribir código reutilizable y establecer relaciones entre clases.

Por ejemplo, hay muchos dispositivos inteligentes en el mercado, como smart TVs e interruptores y lámparas inteligentes. Cuando representas dispositivos inteligentes en programación, estos comparten algunas propiedades comunes, como el nombre, la categoría y el estado. También tienen comportamientos comunes, como la capacidad de encenderse y apagarse.

Sin embargo, la manera de encender o apagar cada dispositivo inteligente es diferente. Por ejemplo, para encender una TV, es posible que debas encender la pantalla y, luego, configurar el último canal y nivel del volumen conocidos. Por otro lado, para encender una lámpara, es posible que solo necesites aumentar o disminuir el brillo.

Además, cada dispositivo inteligente cuenta con más funciones y acciones que puede realizar. Por ejemplo, con una TV, puedes ajustar el volumen y cambiar de canal. Con una luz, puedes ajustar el brillo o el color.

En resumen, todos los dispositivos inteligentes tienen diferentes características, pero también comparten algunas. Puedes duplicar estas características comunes en cada una de las clases de dispositivos inteligentes o hacer que el código sea reutilizable con herencia.

Para ello, debes crear una clase superior SmartDevice y definir estas propiedades y comportamientos comunes. Luego, podrás crear clases secundarias, como SmartTvDevice y SmartLightDevice, que heredarán las propiedades de la clase superior.

En términos de programación, se dice que las clases SmartTvDevice y SmartLightDevice extienden la clase superior SmartDevice. La clase superior también se conoce como superclase, y la clase secundaria como subclase. Puedes ver la relación entre ellas en este diagrama:

Diagrama que representa la relación de herencia entre clases

Sin embargo, en Kotlin, todas las clases son definitivas de manera predeterminada, lo que significa que no puedes extenderlas y, por ende, debes definir las relaciones entre ellas.

Define la relación entre la superclase SmartDevice y sus subclases:

  1. En la superclase SmartDevice, agrega una palabra clave open antes de la palabra clave class para que se pueda extender:
open class SmartDevice(val name: String, val category: String) {
    ...
}

La palabra clave open informa al compilador que esta clase se puede extender, por lo que ahora otras clases pueden extenderla.

La sintaxis para crear una subclase comienza con la creación del encabezado de clase, como lo hiciste hasta ahora. El paréntesis de cierre del constructor va seguido de un espacio, dos puntos, otro espacio, el nombre de la superclase y un conjunto de paréntesis. Si es necesario, los paréntesis incluyen los parámetros que requiere el constructor de la superclase. Puedes ver la sintaxis en este diagrama:

1ac63b66e6b5c224.png

  1. Crea una subclase SmartTvDevice que extienda la superclase SmartDevice:
class SmartTvDevice(deviceName: String, deviceCategory: String) :
    SmartDevice(name = deviceName, category = deviceCategory) {
}

La definición de constructor para SmartTvDevice no especifica si las propiedades son mutables o inmutables. Esto significa que los parámetros deviceName y deviceCategory son meramente constructor, en lugar de propiedades de clase. No podrás usarlas en la clase, sino que simplemente las pasarás al constructor de la superclase.

  1. En el cuerpo de la subclase SmartTvDevice, agrega la propiedad speakerVolume que creaste cuando aprendiste sobre las funciones get y set:
class SmartTvDevice(deviceName: String, deviceCategory: String) :
    SmartDevice(name = deviceName, category = deviceCategory) {

    var speakerVolume = 2
        set(value) {
            if (value in 0..100) {
                field = value
            }
        }
}
  1. Define una propiedad channelNumber asignada a un valor 1 con una función set que especifique un rango 0..200:
class SmartTvDevice(deviceName: String, deviceCategory: String) :
    SmartDevice(name = deviceName, category = deviceCategory) {

    var speakerVolume = 2
        set(value) {
            if (value in 0..100) {
                field = value
            }
        }

    var channelNumber = 1
        set(value) {
            if (value in 0..200) {
                field = value
            }
        }
}
  1. Define un método increaseSpeakerVolume() que aumente el volumen y que imprima una string "Speaker volume increased to $speakerVolume.":
class SmartTvDevice(deviceName: String, deviceCategory: String) :
    SmartDevice(name = deviceName, category = deviceCategory) {

    var speakerVolume = 2
        set(value) {
            if (value in 0..100) {
                field = value
            }
        }

     var channelNumber = 1
        set(value) {
            if (value in 0..200) {
                field = value
            }
        }

    fun increaseSpeakerVolume() {
        speakerVolume++
        println("Speaker volume increased to $speakerVolume.")
    }
}
  1. Agrega un método nextChannel() que aumente el número del canal y que imprima una string "Channel number increased to $channelNumber.":
class SmartTvDevice(deviceName: String, deviceCategory: String) :
    SmartDevice(name = deviceName, category = deviceCategory) {

    var speakerVolume = 2
        set(value) {
            if (value in 0..100) {
                field = value
            }
        }

    var channelNumber = 1
        set(value) {
            if (value in 0..200) {
                field = value
            }
        }

    fun increaseSpeakerVolume() {
        speakerVolume++
        println("Speaker volume increased to $speakerVolume.")
    }

    fun nextChannel() {
        channelNumber++
        println("Channel number increased to $channelNumber.")
    }
}
  1. En la línea que sigue a la subclase SmartTvDevice, define una subclase SmartLightDevice que extienda la superclase SmartDevice:
class SmartLightDevice(deviceName: String, deviceCategory: String) :
    SmartDevice(name = deviceName, category = deviceCategory) {
}
  1. En el cuerpo de la subclase SmartLightDevice, define una propiedad brightnessLevel asignada a un valor 0 con un método set que especifique un rango 0..100:
class SmartLightDevice(deviceName: String, deviceCategory: String) :
    SmartDevice(name = deviceName, category = deviceCategory) {

    var brightnessLevel = 0
        set(value) {
            if (value in 0..100) {
                field = value
            }
        }
}
  1. Define un método increaseBrightness() que aumente el brillo de la luz y que imprima una string "Brightness increased to $brightnessLevel.":
class SmartLightDevice(deviceName: String, deviceCategory: String) :
    SmartDevice(name = deviceName, category = deviceCategory) {

    var brightnessLevel = 0
        set(value) {
            if (value in 0..100) {
                field = value
            }
        }

    fun increaseBrightness() {
        brightnessLevel++
        println("Brightness increased to $brightnessLevel.")
    }
}

Relaciones entre clases

Cuando usas la herencia, estableces una relación entre dos clases en algo llamado relación IS-A. Un objeto también es una instancia de la clase de la cual se hereda. En una relación HAS-A, un objeto puede tener una instancia de otra clase sin ser realmente una instancia de esa clase en sí. En este diagrama, se puede ver una representación general de estas relaciones:

Representación general de las relaciones HAS-A y IS-A.

Relaciones IS-A

Cuando especifica una relación de IS-A entre la superclase SmartDevice y la subclaseSmartTvDevice, significa que cualquier tarea que pueda hacer la superclase SmartDevice, la SmartTvDevice subclase también la puede hacer. La relación es unidireccional, por lo que todas las smart TVs son dispositivos inteligentes, pero no se puede decir que todos los dispositivos inteligentes son smart TVs. La representación de código para una relación IS-A se muestra en este fragmento de código:

// Smart TV IS-A smart device.
class SmartTvDevice : SmartDevice() {
}

No uses la herencia solo para lograr la reutilización del código. Antes de decidir, verifica si las dos clases están relacionadas entre sí. Si es así, verifica si realmente califica para la relación de IS-A. Pregúntate si puedes decir que una subclase es una superclase. Por ejemplo, Android es un sistema operativo.

Relaciones HAS-A

Una relación HAS-A es otra forma de especificar la relación entre dos clases. Por ejemplo, es probable que uses la smart TV de tu casa. En este caso, existe una relación entre la smart TV y la casa. Dentro de la casa, hay un dispositivo inteligente o, en otras palabras, la casa tiene un dispositivo inteligente. La relación HAS-A entre dos clases también se conoce como composición.

Hasta ahora, creaste algunos dispositivos inteligentes. Ahora, crearás la clase SmartHome, que contiene dispositivos inteligentes. La clase SmartHome te permite interactuar con dispositivos inteligentes.

Usa una relación HAS-A para definir una clase SmartHome:

  1. Entre la clase SmartLightDevice y la función main(), define una clase SmartHome:
class SmartLightDevice(deviceName: String, deviceCategory: String) :
    SmartDevice(name = deviceName, category = deviceCategory) {

    ...

}

class SmartHome {
}

fun main() {
    ...
}
  1. En el constructor de la clase SmartHome, usa la palabra clave val para crear una propiedad smartTvDevice de tipo SmartTvDevice:
// The SmartHome class HAS-A smart TV device.
class SmartHome(val smartTvDevice: SmartTvDevice) {

}
  1. En el cuerpo de la clase SmartHome, define un método turnOnTv() que llame al método turnOn() en la propiedad smartTvDevice:
class SmartHome(val smartTvDevice: SmartTvDevice) {

    fun turnOnTv() {
        smartTvDevice.turnOn()
    }
}
  1. En la línea después del método turnOnTv(), define un método turnOffTv() que llame al método turnOff() en la propiedad smartTvDevice:
class SmartHome(val smartTvDevice: SmartTvDevice) {

    fun turnOnTv() {
        smartTvDevice.turnOn()
    }

    fun turnOffTv() {
        smartTvDevice.turnOff()
    }

}
  1. En la línea después del método turnOffTv(), define un método increaseTvVolume() que llame al método increaseSpeakerVolume() en la propiedad smartTvDevice y, luego, un método changeTvChannelToNext() que llame al método nextChannel(). en la propiedad smartTvDevice:
class SmartHome(val smartTvDevice: SmartTvDevice) {

    fun turnOnTv() {
        smartTvDevice.turnOn()
    }

    fun turnOffTv() {
        smartTvDevice.turnOff()
    }

    fun increaseTvVolume() {
        smartTvDevice.increaseSpeakerVolume()
    }

    fun changeTvChannelToNext() {
        smartTvDevice.nextChannel()
    }
}
  1. En el constructor de clase SmartHome, mueve el parámetro de propiedad smartTvDevice a su propia línea, seguido de una coma:
class SmartHome(
    val smartTvDevice: SmartTvDevice,
) {

    ...

}
  1. En la línea que sigue a la propiedad smartTvDevice, usa la palabra clave val para definir una propiedad smartLightDevice de tipo SmartLightDevice:
// The SmartHome class HAS-A smart TV device and smart light.
class SmartHome(
    val smartTvDevice: SmartTvDevice,
    val smartLightDevice: SmartLightDevice
) {

    ...

}
  1. En el cuerpo de SmartHome, define un método turnOnLight() que llame al método turnOn() en el objeto smartLightDevice y un turnOffLight() que llame al método turnOff() en el objeto smartLightDevice:
class SmartHome(
    val smartTvDevice: SmartTvDevice,
    val smartLightDevice: SmartLightDevice
) {

    ...

    fun changeTvChannelToNext() {
        smartTvDevice.nextChannel()
    }

    fun turnOnLight() {
        smartLightDevice.turnOn()
    }

    fun turnOffLight() {
        smartLightDevice.turnOff()
    }
}
  1. En la línea después del método turnOffLight(), define un método increaseLightBrightness() que llame al método increaseBrightness() en la propiedad smartLightDevice:
class SmartHome(
    val smartTvDevice: SmartTvDevice,
    val smartLightDevice: SmartLightDevice
) {

    ...

    fun changeTvChannelToNext() {
        smartTvDevice.nextChannel()
    }

    fun turnOnLight() {
        smartLightDevice.turnOn()
    }

    fun turnOffLight() {
        smartLightDevice.turnOff()
    }

    fun increaseLightBrightness() {
        smartLightDevice.increaseBrightness()
    }
}
  1. En la línea después del método increaseLightBrightness(), define un método turnOffAllDevices() que llame a los métodos turnOffTv() y turnOffLight():
class SmartHome(
    val smartTvDevice: SmartTvDevice,
    val smartLightDevice: SmartLightDevice
) {

    ...

    fun turnOffAllDevices() {
        turnOffTv()
        turnOffLight()
    }
}

Anula métodos de superclase de subclases

Como se mencionó anteriormente, a pesar de que la funcionalidad de activación y desactivación es compatible con todos los dispositivos inteligentes, la manera en la que realizan la funcionalidad difiere. Para proporcionar este comportamiento específico del dispositivo, debes anular los métodos turnOn() y turnOff() definidos en la superclase. La anulación de un método implica interceptar la acción, generalmente para realizar un control manual. Cuando anulas un método, el método de la subclase interrumpe la ejecución del método definido en la superclase y proporciona su propia ejecución.

Anula los métodos turnOn() y turnOff() de la clase SmartDevice:

  1. En el cuerpo de la superclase SmartDevice antes de la palabra clave fun de cada método, agrega una palabra clave open:
open class SmartDevice(val name: String, val category: String) {

    var deviceStatus = "online"

    open fun turnOn() {
        // function body
    }

    open fun turnOff() {
        // function body
    }
}
  1. En el cuerpo de la clase SmartLightDevice, define un método turnOn() con un cuerpo vacío:
class SmartLightDevice(deviceName: String, deviceCategory: String) :
    SmartDevice(name = deviceName, category = deviceCategory) {

    var brightnessLevel = 0
        set(value) {
            if (value in 0..100) {
                field = value
            }
        }

    fun increaseBrightness() {
        brightnessLevel++
        println("Brightness increased to $brightnessLevel.")
    }

    fun turnOn() {
    }
}
  1. En el cuerpo del método turnOn(), establece la propiedad deviceStatus a la cadena "on" y la propiedad brightnessLevel a un valor de 2, y agrega una sentencia println(). Luego, pásale una cadena "$name turned on. The brightness level is $brightnessLevel.":
    fun turnOn() {
        deviceStatus = "on"
        brightnessLevel = 2
        println("$name turned on. The brightness level is $brightnessLevel.")
    }
  1. En el cuerpo de la clase SmartLightDevice, define un método turnOff() con un cuerpo vacío:
class SmartLightDevice(deviceName: String, deviceCategory: String) :
    SmartDevice(name = deviceName, category = deviceCategory) {

    var brightnessLevel = 0
        set(value) {
            if (value in 0..100) {
                field = value
            }
        }

    fun increaseBrightness() {
        brightnessLevel++
        println("Brightness increased to $brightnessLevel.")
    }

    fun turnOn() {
        deviceStatus = "on"
        brightnessLevel = 2
        println("$name turned on. The brightness level is $brightnessLevel.")
    }

    fun turnOff() {
    }
}
  1. En el cuerpo del método turnOff(), establece la propiedad deviceStatus a la cadena "off" y la propiedad brightnessLevel a un valor de 0, y agrega una sentencia println(). Luego, pásale una cadena "Smart Light turned off":
class SmartLightDevice(deviceName: String, deviceCategory: String) :
    SmartDevice(name = deviceName, category = deviceCategory) {

    var brightnessLevel = 0
        set(value) {
            if (value in 0..100) {
                field = value
            }
        }

    fun increaseBrightness() {
        brightnessLevel++
        println("Brightness increased to $brightnessLevel.")
    }

    fun turnOn() {
        deviceStatus = "on"
        brightnessLevel = 2
        println("$name turned on. The brightness level is $brightnessLevel.")
    }

    fun turnOff() {
        deviceStatus = "off"
        brightnessLevel = 0
        println("Smart Light turned off")
    }
}
  1. En la subclase SmartLightDevice, antes de la palabra clave fun de los métodos turnOn() y turnOff(), agrega la palabra clave override:
class SmartLightDevice(deviceName: String, deviceCategory: String) :
    SmartDevice(name = deviceName, category = deviceCategory) {

    var brightnessLevel = 0
        set(value) {
            if (value in 0..100) {
                field = value
            }
        }

    fun increaseBrightness() {
        brightnessLevel++
        println("Brightness increased to $brightnessLevel.")
    }

    override fun turnOn() {
        deviceStatus = "on"
        brightnessLevel = 2
        println("$name turned on. The brightness level is $brightnessLevel.")
    }

    override fun turnOff() {
        deviceStatus = "off"
        brightnessLevel = 0
        println("Smart Light turned off")
    }
}

La palabra clave override indica al entorno de ejecución de Kotlin que ejecute el código incluido en el método definido en la subclase.

  1. En el cuerpo de la clase SmartTvDevice, define un método turnOn() con un cuerpo vacío:
class SmartTvDevice(deviceName: String, deviceCategory: String) : SmartDevice(name = deviceName, category = deviceCategory) {

    var speakerVolume = 2
        set(value) {
            if (value in 0..100) {
                field = value
            }
        }

    var channelNumber = 1
        set(value) {
            if (value in 0..200) {
                field = value
            }
        }

    fun increaseSpeakerVolume() {
        speakerVolume++
        println("Speaker volume increased to $speakerVolume.")
    }

    fun nextChannel() {
        channelNumber++
        println("Channel number increased to $channelNumber.")
    }

    fun turnOn() {
    }
}
  1. En el cuerpo del método turnOn(), establece la propiedad deviceStatus en la cadena "on", agrega una sentencia println() y, luego, pásale una cadena "$name is turned on. Speaker volume is set to $speakerVolume and channel number is " + "set to $channelNumber.":
class SmartTvDevice(deviceName: String, deviceCategory: String) : SmartDevice(name = deviceName, category = deviceCategory) {

    ...

    fun turnOn() {
        deviceStatus = "on"
        println(
            "$name is turned on. Speaker volume is set to $speakerVolume and channel number is " +
                "set to $channelNumber."
        )
    }
}
  1. En el cuerpo de la clase SmartTvDevice, después del método turnOn(), define un método turnOff() con un cuerpo vacío:
class SmartTvDevice(deviceName: String, deviceCategory: String) : SmartDevice(name = deviceName, category = deviceCategory) {

    ...

    fun turnOn() {
        ...
    }

    fun turnOff() {
    }
}
  1. En el cuerpo del método turnOff(), establece la propiedad deviceStatus en la cadena "off", agrega una sentencia println() y, luego, pásale una cadena "$name turned off":
class SmartTvDevice(deviceName: String, deviceCategory: String) : SmartDevice(name = deviceName, category = deviceCategory) {

    ...

    fun turnOn() {
        ...
    }

    fun turnOff() {
        deviceStatus = "off"
        println("$name turned off")
    }
}
  1. En la clase SmartTvDevice, antes de la palabra clave fun de los métodos turnOn() y turnOff(), agrega la palabra clave override:
class SmartTvDevice(deviceName: String, deviceCategory: String) :
    SmartDevice(name = deviceName, category = deviceCategory) {

    var speakerVolume = 2
        set(value) {
            if (value in 0..100) {
                field = value
            }
        }

    var channelNumber = 1
        set(value) {
            if (value in 0..200) {
                field = value
            }
        }

    fun increaseSpeakerVolume() {
        speakerVolume++
        println("Speaker volume increased to $speakerVolume.")
    }

    fun nextChannel() {
        channelNumber++
        println("Channel number increased to $channelNumber.")
    }

    override fun turnOn() {
        deviceStatus = "on"
        println(
            "$name is turned on. Speaker volume is set to $speakerVolume and channel number is " +
                "set to $channelNumber."
        )
    }

    override fun turnOff() {
        deviceStatus = "off"
        println("$name turned off")
    }
}
  1. En la función main(), usa la palabra clave var para definir una variable smartDevice de tipo SmartDevice que cree una instancia de un objeto SmartTvDevice que tome un argumento "Android TV" y uno "Entertainment":
fun main() {
    var smartDevice: SmartDevice = SmartTvDevice("Android TV", "Entertainment")
}
  1. En la línea que sigue a la variable smartDevice, llama al método turnOn() en el objeto smartDevice:
fun main() {
    var smartDevice: SmartDevice = SmartTvDevice("Android TV", "Entertainment")
    smartDevice.turnOn()
}
  1. Ejecuta el código.

Este es el resultado:

Android TV is turned on. Speaker volume is set to 2 and channel number is set to 1.
  1. En la línea posterior a la llamada al método turnOn(), reasigna la variable smartDevice para crear una instancia de una clase SmartLightDevice que tome un argumento "Google Light" y uno "Utility". Luego, llama al método turnOn() en la referencia del objeto smartDevice:
fun main() {
    var smartDevice: SmartDevice = SmartTvDevice("Android TV", "Entertainment")
    smartDevice.turnOn()

    smartDevice = SmartLightDevice("Google Light", "Utility")
    smartDevice.turnOn()
}
  1. Ejecuta el código.

Este es el resultado:

Android TV is turned on. Speaker volume is set to 2 and channel number is set to 1.
Google Light turned on. The brightness level is 2.

Este es un ejemplo de polimorfismo. El código llama al método turnOn() en una variable de tipo SmartDevice y, según cuál sea el valor real de la variable, se pueden ejecutar diferentes implementaciones del método turnOn().

Vuelve a usar el código de superclase en subclases con la palabra clave super.

Si observas de cerca los métodos turnOn() y turnOff(), notarás que hay similitud en cómo se actualiza la variable deviceStatus cada vez que se llama a los métodos en las subclases SmartTvDevice y SmartLightDevice: el código se duplica. Podrás reutilizar el código cuando actualices el estado de la clase SmartDevice.

Para llamar al método anulado en la superclase desde la subclase, debes usar la palabra clave super. Llamar a un método desde la superclase es similar a llamar al método desde fuera de la clase. En lugar de usar un operador . entre el objeto y el método, debes usar la palabra clave super, que le informa al compilador de Kotlin que llame al método en la superclase en lugar de en la subclase.

La sintaxis para llamar al método de la superclase comienza con una palabra clave super seguida del operador ., el nombre de la función y un conjunto de paréntesis. Si corresponde, los paréntesis incluyen los argumentos. Puedes ver la sintaxis en este diagrama:

18cc94fefe9851e0.png

Vuelve a usar el código de la superclase SmartDevice:

  1. Quita las sentencias println() de los métodos turnOn() y turnOff(), y mueve el código duplicado de las subclases SmartTvDevice y SmartLightDevice a la superclase SmartDevice:
open class SmartDevice(val name: String, val category: String) {

    var deviceStatus = "online"

    open fun turnOn() {
        deviceStatus = "on"
    }

    open fun turnOff() {
        deviceStatus = "off"
    }
}
  1. Usa la palabra clave super para llamar a los métodos de la clase SmartDevice en las subclases SmartTvDevice y SmartLightDevice:
class SmartTvDevice(deviceName: String, deviceCategory: String) :
    SmartDevice(name = deviceName, category = deviceCategory) {

    var speakerVolume = 2
        set(value) {
            if (value in 0..100) {
                field = value
            }
        }

     var channelNumber = 1
        set(value) {
            if (value in 0..200) {
                field = value
            }
        }

    fun increaseSpeakerVolume() {
        speakerVolume++
        println("Speaker volume increased to $speakerVolume.")
    }

    fun nextChannel() {
        channelNumber++
        println("Channel number increased to $channelNumber.")
    }

    override fun turnOn() {
        super.turnOn()
        println(
            "$name is turned on. Speaker volume is set to $speakerVolume and channel number is " +
                "set to $channelNumber."
        )
    }

    override fun turnOff() {
        super.turnOff()
        println("$name turned off")
    }
}
class SmartLightDevice(deviceName: String, deviceCategory: String) :
    SmartDevice(name = deviceName, category = deviceCategory) {

    var brightnessLevel = 0
        set(value) {
            if (value in 0..100) {
                field = value
            }
        }

    fun increaseBrightness() {
        brightnessLevel++
        println("Brightness increased to $brightnessLevel.")
    }

    override fun turnOn() {
        super.turnOn()
        brightnessLevel = 2
        println("$name turned on. The brightness level is $brightnessLevel.")
    }

    override fun turnOff() {
        super.turnOff()
        brightnessLevel = 0
        println("Smart Light turned off")
    }
}

Anula propiedades de superclase de subclases

Al igual que con los métodos, también puedes anular propiedades siguiendo los mismos pasos.

Anula la propiedad deviceType:

  1. En la superclase SmartDevice, en la línea que aparece después de la propiedad deviceStatus, usa las palabras clave open y val para definir una propiedad deviceType establecida en una string "unknown":
open class SmartDevice(val name: String, val category: String) {

    var deviceStatus = "online"

    open val deviceType = "unknown"
    ...
}
  1. En la clase SmartTvDevice, usa las palabras clave override y val para definir una propiedad deviceType establecida en una cadena "Smart TV":
class SmartTvDevice(deviceName: String, deviceCategory: String) :
    SmartDevice(name = deviceName, category = deviceCategory) {

    override val deviceType = "Smart TV"

    ...
}
  1. En la clase SmartLightDevice, usa las palabras clave override y val para definir una propiedad deviceType establecida en una cadena "Smart Light":
class SmartLightDevice(deviceName: String, deviceCategory: String) :
    SmartDevice(name = deviceName, category = deviceCategory) {

    override val deviceType = "Smart Light"

    ...

}

8. Modificadores de visibilidad

Los modificadores de visibilidad son importantes para lograr el encapsulamiento:

  • En una clase, te permiten ocultar tus propiedades y métodos para evitar el acceso no autorizado fuera de la clase.
  • En un paquete, te permiten ocultar las interfaces y clases para evitar el acceso no autorizado fuera del paquete.

Kotlin ofrece cuatro modificadores de visibilidad:

  • public. Es el modificador de visibilidad predeterminado. Permite que la declaración sea accesible en todas partes. Las propiedades y los métodos que deseas usar fuera de la clase se marcan como públicos.
  • private. Permite que se pueda acceder a la declaración en la misma clase o archivo de origen.

Es probable que algunos métodos y propiedades solo se usen dentro de la clase, y que no necesariamente quieras que se usen otras clases. Estos métodos y propiedades se pueden marcar con el modificador de visibilidad private para garantizar que otra clase no pueda acceder a ellos de forma accidental.

  • protected. Hace que la declaración sea accesible en subclases. Las propiedades y los métodos que deseas usar en la clase que los define y las subclases se marcan con el modificador de visibilidad protected.
  • internal. Permite que se pueda acceder a la declaración en el mismo módulo. } El modificador interno es similar al privado, pero puedes acceder a propiedades y métodos internos desde fuera de la clase, siempre y cuando lo hagas en el mismo módulo.

Cuando defines una clase, es visible de forma pública y puedes acceder a ella con cualquier paquete que la importe, lo que significa que es pública de forma predeterminada, a menos que especifiques un modificador de visibilidad. De manera similar, cuando defines o declaras propiedades y métodos en la clase, se puede acceder a ellos de forma predeterminada fuera de la clase a través del objeto de clase. Es esencial definir una visibilidad adecuada para el código, principalmente a fin de ocultar las propiedades y los métodos a los que otras clases no necesitan acceder.

Por ejemplo, considera cómo un conductor puede acceder a un automóvil. Los detalles sobre las partes que componen el vehículo y su funcionamiento interno están ocultos de forma predeterminada. El vehículo está diseñado para ser lo más intuitivo posible. Así como no se espera que un automóvil sea tan complejo de operar como un avión comercial, no quieres que otro desarrollador ni tú en el futuro se confundan respecto de qué clases y métodos deben usarse.

Los modificadores de visibilidad te ayudan a mostrar las partes relevantes del código para otras clases de tu proyecto y a garantizar que la implementación no se pueda usar de manera no intencional, lo que hace que el código sea fácil de entender y menos propenso a errores.

El modificador de visibilidad debe colocarse antes de la sintaxis de la declaración, durante la declaración de la clase, el método o las propiedades, como se puede ver en este diagrama:

dcc4f6693bf719a9.png

Especifica un modificador de visibilidad para propiedades

La sintaxis para especificar el modificador de visibilidad de una propiedad comienza con el modificador private, protected o internal, seguido de la sintaxis que define una propiedad. Puedes ver la sintaxis en este diagrama:

47807a890d237744.png

Por ejemplo, en este fragmento de código, puedes ver cómo convertir la propiedad deviceStatus en privada:

open class SmartDevice(val name: String, val category: String) {

    ...

    private var deviceStatus = "online"

    ...
}

También puedes configurar los modificadores de visibilidad para las funciones set. El modificador se coloca antes de la palabra clave set. Puedes ver la sintaxis en este diagrama:

cea29a49b7b26786.png

Para la clase SmartDevice, el valor de la propiedad deviceStatus debe ser legible fuera de la clase mediante los objetos de clase. Sin embargo, solo la clase y sus elementos secundarios deben poder actualizar o escribir el valor. Para implementar este requisito, debes usar el modificador protected en la función set() de la propiedad deviceStatus.

Usa el modificador protected en la función set() de la propiedad deviceStatus:

  1. En la propiedad deviceStatus de la superclase SmartDevice, agrega el modificador protected a la función set():
open class SmartDevice(val name: String, val category: String) {

    ...

    var deviceStatus = "online"
        protected set(value) {
           field = value
       }

    ...
}

No realizarás ninguna acción ni verificación de la función set(). Solo debes asignar el parámetro value a la variable field. Como aprendiste anteriormente, esto es similar a la implementación predeterminada de métodos set de propiedades. Puedes omitir los paréntesis y el cuerpo de la función set() en este caso:

open class SmartDevice(val name: String, val category: String) {

    ...

    var deviceStatus = "online"
        protected set

    ...
}
  1. En la clase SmartHome, define una propiedad deviceTurnOnCount establecida en un valor 0 con una función de método set privada:
class SmartHome(
    val smartTvDevice: SmartTvDevice,
    val smartLightDevice: SmartLightDevice
) {

    var deviceTurnOnCount = 0
        private set

    ...
}
  1. Agrega la propiedad deviceTurnOnCount, seguida del operador aritmético ++ a los métodos turnOnTv() y turnOnLight() y, luego, agrega la propiedad deviceTurnOnCount, seguida del operador aritmético -- a los métodos turnOffTv() y turnOffLight():
class SmartHome(
    val smartTvDevice: SmartTvDevice,
    val smartLightDevice: SmartLightDevice
) {

    var deviceTurnOnCount = 0
        private set

    fun turnOnTv() {
        deviceTurnOnCount++
        smartTvDevice.turnOn()
    }

    fun turnOffTv() {
        deviceTurnOnCount--
        smartTvDevice.turnOff()
    }

    ...

    fun turnOnLight() {
        deviceTurnOnCount++
        smartLightDevice.turnOn()
    }

    fun turnOffLight() {
        deviceTurnOnCount--
        smartLightDevice.turnOff()
    }

    ...

}

Modificadores de visibilidad para métodos

La sintaxis de especificación de un modificador de visibilidad para un método comienza con los modificadores private, protected o internal, seguidos de la sintaxis que define un método. Puedes ver la sintaxis en este diagrama:

e0a60ddc26b841de.png

Por ejemplo, puedes ver cómo especificar un modificador protected para el método nextChannel() en la clase SmartTvDevice de este fragmento de código:

class SmartTvDevice(deviceName: String, deviceCategory: String) :
    SmartDevice(name = deviceName, category = deviceCategory) {

    ...

    protected fun nextChannel() {
        channelNumber++
        println("Channel number increased to $channelNumber.")
    }

    ...
}

Modificadores de visibilidad para constructores

La sintaxis para especificar un modificador de visibilidad en un constructor es similar a definir el constructor principal, pero con algunas diferencias:

  • El modificador se especifica después del nombre de la clase, pero antes de la palabra clave constructor.
  • Si necesitas especificar el modificador para el constructor principal, es necesario mantener la palabra clave constructor y los paréntesis incluso cuando no haya parámetros.

Puedes ver la sintaxis en este diagrama:

6832575eba67f059.png

Por ejemplo, puedes ver cómo agregar un modificador protected al constructor SmartDevice en este fragmento de código:

open class SmartDevice protected constructor (val name: String, val category: String) {

    ...

}

Modificadores de visibilidad para clases

La sintaxis para especificar un modificador de visibilidad para una clase comienza con los modificadores private, protected o internal, seguidos de la sintaxis que define una clase. Puedes ver la sintaxis en este diagrama:

3ab4aa1c94a24a69.png

Por ejemplo, puedes ver cómo especificar un modificador internal para la clase SmartDevice en este fragmento de código:

internal open class SmartDevice(val name: String, val category: String) {

    ...

}

Lo ideal sería que busques una visibilidad estricta para las propiedades y los métodos; para ello, debes declararlos con el modificador private con la mayor frecuencia posible. Si no puedes mantenerlos privados, usa el modificador protected. Si no puedes mantenerlos protegidos, usa el modificador internal. Si no puedes mantenerlos internos, usa el modificador public.

Especifica los modificadores de visibilidad adecuados

Esta tabla te ayudará a determinar los modificadores de visibilidad adecuados según el lugar donde deberían ser accesibles la propiedad o los métodos de una clase o un constructor:

Modificador

Accesible en la misma clase

Accesible en subclase

Accesible en el mismo módulo

Accesible fuera del módulo

private

𝗫

𝗫

𝗫

protected

𝗫

𝗫

internal

𝗫

public

En la subclase SmartTvDevice, no deberías permitir que se controlen las propiedades speakerVolume y channelNumber desde fuera de la clase. Estas propiedades se deben controlar solo a través de los métodos increaseSpeakerVolume() y nextChannel().

Del mismo modo, en la subclase SmartLightDevice, la propiedad brightnessLevel solo se debe controlar a través del método increaseLightBrightness().

Agrega los modificadores de visibilidad adecuados a las subclases SmartTvDevice y SmartLightDevice:

  1. En la clase SmartTvDevice, agrega un modificador de visibilidad private a las propiedades speakerVolume y channelNumber:
class SmartTvDevice(deviceName: String, deviceCategory: String) :
    SmartDevice(name = deviceName, category = deviceCategory) {

    private var speakerVolume = 2
        set(value) {
            if (value in 0..100) {
                field = value
            }
        }

    private var channelNumber = 1
        set(value) {
            if (value in 0..200) {
                field = value
            }
        }

    ...
}
  1. En la clase SmartLightDevice, agrega un modificador private a la propiedad brightnessLevel:
class SmartLightDevice(deviceName: String, deviceCategory: String) :
    SmartDevice(name = deviceName, category = deviceCategory) {

    ...

    private var brightnessLevel = 0
        set(value) {
            if (value in 0..100) {
                field = value
            }
        }

    ...
}

9. Define delegados de propiedad

En la sección anterior, aprendiste que las propiedades en Kotlin usan un campo de copia de seguridad para contener sus valores en la memoria. Usa el identificador field para hacer referencia a él.

Si observas el código que tienes hasta el momento, podrás ver el código duplicado para verificar si los valores están dentro del rango para las propiedades speakerVolume, channelNumber y brightnessLevel en las clases SmartTvDevice y SmartLightDevice. Puedes reutilizar el código de verificación de rango en la función set con delegados. En lugar de usar un campo y funciones de métodos get y set para administrar el valor, el delegado lo administra.

La sintaxis para crear delegados de propiedad comienza con la declaración de una variable, seguida de la palabra clave by y el objeto delegado que controla las funciones de métodos get y set. Puedes ver la sintaxis en este diagrama:

928547ad52768115.png

Antes de implementar la clase a la que puedes delegarle la implementación, debes conocer bien las interfaces. Una interfaz es un contrato que deben cumplir las clases que la implementan. Se centra en qué hacer, en lugar de cómo hacer la acción. En resumen, una interfaz te ayuda a lograr la abstracción.

Por ejemplo, antes de construir una casa, debes informarle al arquitecto cómo quieres que sea. Quieres una habitación para ti, otra para los niños, una sala de estar, una cocina y un par de baños. En resumen, especificas qué quieres y el arquitecto especifica cómo lograrlo. Puedes ver la sintaxis para crear una interfaz en este diagrama:

bfe3fd1cd8c45b2a.png

Ya aprendiste a extender una clase y a anular su funcionalidad. En el caso de las interfaces, la clase implementa la interfaz. La clase proporciona detalles de implementación para los métodos y las propiedades declarados en la interfaz. Realizarás una acción similar con la interfaz ReadWriteProperty para crear el delegado. Obtén más información sobre las interfaces en la siguiente unidad.

A fin de crear la clase delegada para el tipo var, debes implementar la interfaz ReadWriteProperty. Del mismo modo, debes implementar la interfaz ReadOnlyProperty para el tipo val.

Crea el delegado para el tipo var:

  1. Antes de la función main(), crea una clase RangeRegulator que implemente la interfaz ReadWriteProperty<Any?, Int>:
class RangeRegulator() : ReadWriteProperty<Any?, Int> {

}

fun main() {
    ...
}

No te preocupes por los corchetes angulares ni el contenido que está dentro de ellos. Representan tipos genéricos, y aprenderás sobre ellos en la siguiente unidad.

  1. En el constructor principal de la clase RangeRegulator, agrega un parámetro initialValue, una propiedad privada minValue y otra maxValue, todas del tipo Int:
class RangeRegulator(
    initialValue: Int,
    private val minValue: Int,
    private val maxValue: Int
) : ReadWriteProperty<Any?, Int> {

}
  1. En el cuerpo de la clase RangeRegulator, anula los métodos getValue() y setValue():
class RangeRegulator(
    initialValue: Int,
    private val minValue: Int,
    private val maxValue: Int
) : ReadWriteProperty<Any?, Int> {

    override fun getValue(thisRef: Any?, property: KProperty<*>): Int {
    }

    override fun setValue(thisRef: Any?, property: KProperty<*>, value: Int) {
    }
}

Estos métodos actúan como las funciones get y set de las propiedades.

  1. En la línea anterior a la clase SmartDevice, importa las interfaces ReadWriteProperty y KProperty:
import kotlin.properties.ReadWriteProperty
import kotlin.reflect.KProperty

open class SmartDevice(val name: String, val category: String) {
    ...
}

...

class RangeRegulator(
    initialValue: Int,
    private val minValue: Int,
    private val maxValue: Int
) : ReadWriteProperty<Any?, Int> {

    override fun getValue(thisRef: Any?, property: KProperty<*>): Int {
    }

    override fun setValue(thisRef: Any?, property: KProperty<*>, value: Int) {
    }
}

...
  1. En la clase RangeRegulator, en la línea antes del método getValue(), define una propiedad fieldData y, luego, inicialízala con el parámetro initialValue:
class RangeRegulator(
    initialValue: Int,
    private val minValue: Int,
    private val maxValue: Int
) : ReadWriteProperty<Any?, Int> {

    var fieldData = initialValue

    override fun getValue(thisRef: Any?, property: KProperty<*>): Int {
    }

    override fun setValue(thisRef: Any?, property: KProperty<*>, value: Int) {
    }
}

Esta propiedad actúa como el campo de copia de seguridad de la variable.

  1. En el cuerpo del método getValue(), muestra la propiedad fieldData:
class RangeRegulator(
    initialValue: Int,
    private val minValue: Int,
    private val maxValue: Int
) : ReadWriteProperty<Any?, Int> {

    var fieldData = initialValue

    override fun getValue(thisRef: Any?, property: KProperty<*>): Int {
        return fieldData
    }

    override fun setValue(thisRef: Any?, property: KProperty<*>, value: Int) {
    }
}
  1. En el cuerpo del método setValue(), verifica si el parámetro value que se está asignando está en el rango minValue..maxValue antes de asignarlo a la propiedad fieldData:
class RangeRegulator(
    initialValue: Int,
    private val minValue: Int,
    private val maxValue: Int
) : ReadWriteProperty<Any?, Int> {

    var fieldData = initialValue

    override fun getValue(thisRef: Any?, property: KProperty<*>): Int {
        return fieldData
    }

    override fun setValue(thisRef: Any?, property: KProperty<*>, value: Int) {
        if (value in minValue..maxValue) {
            fieldData = value
        }
    }
}
  1. En la clase SmartTvDevice, usa la clase delegada para definir las propiedades speakerVolume y channelNumber:
class SmartTvDevice(deviceName: String, deviceCategory: String) :
    SmartDevice(name = deviceName, category = deviceCategory) {

    override val deviceType = "Smart TV"

    private var speakerVolume by RangeRegulator(initialValue = 2, minValue = 0, maxValue = 100)

    private var channelNumber by RangeRegulator(initialValue = 1, minValue = 0, maxValue = 200)

    ...

}
  1. En la clase SmartLightDevice, usa la clase delegada para definir la propiedad brightnessLevel:
class SmartLightDevice(deviceName: String, deviceCategory: String) :
    SmartDevice(name = deviceName, category = deviceCategory) {

    override val deviceType = "Smart Light"

    private var brightnessLevel by RangeRegulator(initialValue = 0, minValue = 0, maxValue = 100)

    ...

}

10. Prueba la solución

Puedes ver el código de la solución en este fragmento de código:

import kotlin.properties.ReadWriteProperty
import kotlin.reflect.KProperty

open class SmartDevice(val name: String, val category: String) {

    var deviceStatus = "online"
        protected set

    open val deviceType = "unknown"

    open fun turnOn() {
        deviceStatus = "on"
    }

    open fun turnOff() {
        deviceStatus = "off"
    }
}

class SmartTvDevice(deviceName: String, deviceCategory: String) :
    SmartDevice(name = deviceName, category = deviceCategory) {

    override val deviceType = "Smart TV"

    private var speakerVolume by RangeRegulator(initialValue = 2, minValue = 0, maxValue = 100)

    private var channelNumber by RangeRegulator(initialValue = 1, minValue = 0, maxValue = 200)

    fun increaseSpeakerVolume() {
        speakerVolume++
        println("Speaker volume increased to $speakerVolume.")
    }

    fun nextChannel() {
        channelNumber++
        println("Channel number increased to $channelNumber.")
    }

    override fun turnOn() {
        super.turnOn()
        println(
            "$name is turned on. Speaker volume is set to $speakerVolume and channel number is " +
                "set to $channelNumber."
        )
    }

    override fun turnOff() {
        super.turnOff()
        println("$name turned off")
    }
}

class SmartLightDevice(deviceName: String, deviceCategory: String) :
    SmartDevice(name = deviceName, category = deviceCategory) {

    override val deviceType = "Smart Light"

    private var brightnessLevel by RangeRegulator(initialValue = 0, minValue = 0, maxValue = 100)

    fun increaseBrightness() {
        brightnessLevel++
        println("Brightness increased to $brightnessLevel.")
    }

    override fun turnOn() {
        super.turnOn()
        brightnessLevel = 2
        println("$name turned on. The brightness level is $brightnessLevel.")
    }

    override fun turnOff() {
        super.turnOff()
        brightnessLevel = 0
        println("Smart Light turned off")
    }
}

class SmartHome(
    val smartTvDevice: SmartTvDevice,
    val smartLightDevice: SmartLightDevice
) {

    var deviceTurnOnCount = 0
        private set

    fun turnOnTv() {
        deviceTurnOnCount++
        smartTvDevice.turnOn()
    }

    fun turnOffTv() {
        deviceTurnOnCount--
        smartTvDevice.turnOff()
    }

    fun increaseTvVolume() {
        smartTvDevice.increaseSpeakerVolume()
    }

    fun changeTvChannelToNext() {
        smartTvDevice.nextChannel()
    }

    fun turnOnLight() {
        deviceTurnOnCount++
        smartLightDevice.turnOn()
    }

    fun turnOffLight() {
        deviceTurnOnCount--
        smartLightDevice.turnOff()
    }

    fun increaseLightBrightness() {
        smartLightDevice.increaseBrightness()
    }

    fun turnOffAllDevices() {
        turnOffTv()
        turnOffLight()
    }
}

class RangeRegulator(
    initialValue: Int,
    private val minValue: Int,
    private val maxValue: Int
) : ReadWriteProperty<Any?, Int> {

    var fieldData = initialValue

    override fun getValue(thisRef: Any?, property: KProperty<*>): Int {
        return fieldData
    }

    override fun setValue(thisRef: Any?, property: KProperty<*>, value: Int) {
        if (value in minValue..maxValue) {
            fieldData = value
        }
    }
}

fun main() {
    var smartDevice: SmartDevice = SmartTvDevice("Android TV", "Entertainment")
    smartDevice.turnOn()

    smartDevice = SmartLightDevice("Google Light", "Utility")
    smartDevice.turnOn()
}

Este es el resultado:

Android TV is turned on. Speaker volume is set to 2 and channel number is set to 1.
Google Light turned on. The brightness level is 2.

11. Prueba este desafío

  • En la clase SmartDevice, define un método printDeviceInfo() que imprima una string "Device name: $name, category: $category, type: $deviceType".
  • En la clase SmartTvDevice, define un método decreaseVolume() que disminuya el volumen y un método previousChannel() que navegue al canal anterior.
  • En la clase SmartLightDevice, define un método decreaseBrightness() que disminuya el brillo.
  • En la clase SmartHome, asegúrate de que todas las acciones solo se puedan realizar cuando la propiedad deviceStatus de cada dispositivo se establezca en una string "on". Además, asegúrate de que la propiedad deviceTurnOnCount esté actualizada correctamente.

Cuando finalices la implementación, haz lo siguiente:

  • En la clase SmartHome, define un método decreaseTvVolume(), changeTvChannelToPrevious(), printSmartTvInfo(), printSmartLightInfo() y decreaseLightBrightness().
  • Llama a los métodos adecuados de las clases SmartTvDevice y SmartLightDevice en la clase SmartHome.
  • En la función main(), llama a estos métodos agregados para probarlos.

12. Conclusión

¡Felicitaciones! Aprendiste a definir clases y crear instancias de objetos. También aprendiste a crear relaciones entre clases y delegados de propiedades.

Resumen

  • OOP consta de cuatro principios principales: encapsulamiento, abstracción, herencia y polimorfismo.
  • Las clases se definen con la palabra clave class y contienen propiedades y métodos.
  • Las propiedades son similares a las variables, excepto que pueden tener métodos get y set personalizados.
  • Un constructor especifica cómo crear instancias de los objetos de una clase.
  • Puedes omitir la palabra clave constructor cuando defines un constructor principal.
  • La herencia facilita la reutilización de código.
  • La relación IS-A se refiere a la herencia.
  • La relación HAS-A se refiere a la composición.
  • Los modificadores de visibilidad son importantes para lograr el encapsulamiento.
  • Kotlin ofrece cuatro modificadores de visibilidad: public, private, protected y internal.
  • Un delegado de propiedad te permite reutilizar el código get y set en varias clases.

Más información