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()
ymain()
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:
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
:
- En el Playground de Kotlin, reemplaza el contenido por una función
main()
vacía:
fun main() {
}
- En la línea anterior a la función
main()
, define una claseSmartDevice
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:
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:
Crea una instancia de la clase SmartDevice
como objeto:
- En la función
main()
, usa la palabra claveval
para crear una variable llamadasmartTvDevice
e inicializarla como una instancia de la claseSmartDevice
:
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
:
- En el cuerpo de la clase
SmartDevice
, define un métodoturnOn()
con un cuerpo vacío:
class SmartDevice {
fun turnOn() {
}
}
- En el cuerpo del método
turnOn()
, agrega una sentenciaprintln()
y pásale una string"Smart
device
is
turned
on."
:
class SmartDevice {
fun turnOn() {
println("Smart device is turned on.")
}
}
- Después del método
turnOn()
, agrega un métodoturnOff()
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:
Llama a los métodos turnOn()
y turnOff()
en el objeto:
- En la función
main()
de la línea después de la variablesmartTvDevice
, llama al métodoturnOn()
:
fun main() {
val smartTvDevice = SmartDevice()
smartTvDevice.turnOn()
}
- En la línea después del método
turnOn()
, llama al métodoturnOff()
:
fun main() {
val smartTvDevice = SmartDevice()
smartTvDevice.turnOn()
smartTvDevice.turnOff()
}
- 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
:
- En la línea anterior al método
turnOn()
, define la propiedadname
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.")
}
}
- En la línea que sigue a la propiedad
name
, define la propiedadcategory
y asígnala a una cadena"Entertainment"
. Luego, define una propiedaddeviceStatus
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.")
}
}
- En la línea después de la variable
smartTvDevice
, llama a la funciónprintln()
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()
}
- 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:
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 funcionesset()
.
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 propiedadesname
ycategory
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:
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:
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:
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:
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:
- En la superclase
SmartDevice
, agrega una palabra claveopen
antes de la palabra claveclass
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:
- Crea una subclase
SmartTvDevice
que extienda la superclaseSmartDevice
:
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.
- En el cuerpo de la subclase
SmartTvDevice
, agrega la propiedadspeakerVolume
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
}
}
}
- Define una propiedad
channelNumber
asignada a un valor1
con una función set que especifique un rango0..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
}
}
}
- 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.")
}
}
- 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.")
}
}
- En la línea que sigue a la subclase
SmartTvDevice
, define una subclaseSmartLightDevice
que extienda la superclaseSmartDevice
:
class SmartLightDevice(deviceName: String, deviceCategory: String) :
SmartDevice(name = deviceName, category = deviceCategory) {
}
- En el cuerpo de la subclase
SmartLightDevice
, define una propiedadbrightnessLevel
asignada a un valor0
con un método set que especifique un rango0..100
:
class SmartLightDevice(deviceName: String, deviceCategory: String) :
SmartDevice(name = deviceName, category = deviceCategory) {
var brightnessLevel = 0
set(value) {
if (value in 0..100) {
field = value
}
}
}
- 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:
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
:
- Entre la clase
SmartLightDevice
y la funciónmain()
, define una claseSmartHome
:
class SmartLightDevice(deviceName: String, deviceCategory: String) :
SmartDevice(name = deviceName, category = deviceCategory) {
...
}
class SmartHome {
}
fun main() {
...
}
- En el constructor de la clase
SmartHome
, usa la palabra claveval
para crear una propiedadsmartTvDevice
de tipoSmartTvDevice
:
// The SmartHome class HAS-A smart TV device.
class SmartHome(val smartTvDevice: SmartTvDevice) {
}
- En el cuerpo de la clase
SmartHome
, define un métodoturnOnTv()
que llame al métodoturnOn()
en la propiedadsmartTvDevice
:
class SmartHome(val smartTvDevice: SmartTvDevice) {
fun turnOnTv() {
smartTvDevice.turnOn()
}
}
- En la línea después del método
turnOnTv()
, define un métodoturnOffTv()
que llame al métodoturnOff()
en la propiedadsmartTvDevice
:
class SmartHome(val smartTvDevice: SmartTvDevice) {
fun turnOnTv() {
smartTvDevice.turnOn()
}
fun turnOffTv() {
smartTvDevice.turnOff()
}
}
- En la línea después del método
turnOffTv()
, define un métodoincreaseTvVolume()
que llame al métodoincreaseSpeakerVolume()
en la propiedadsmartTvDevice
y, luego, un métodochangeTvChannelToNext()
que llame al métodonextChannel()
. en la propiedadsmartTvDevice
:
class SmartHome(val smartTvDevice: SmartTvDevice) {
fun turnOnTv() {
smartTvDevice.turnOn()
}
fun turnOffTv() {
smartTvDevice.turnOff()
}
fun increaseTvVolume() {
smartTvDevice.increaseSpeakerVolume()
}
fun changeTvChannelToNext() {
smartTvDevice.nextChannel()
}
}
- En el constructor de clase
SmartHome
, mueve el parámetro de propiedadsmartTvDevice
a su propia línea, seguido de una coma:
class SmartHome(
val smartTvDevice: SmartTvDevice,
) {
...
}
- En la línea que sigue a la propiedad
smartTvDevice
, usa la palabra claveval
para definir una propiedadsmartLightDevice
de tipoSmartLightDevice
:
// The SmartHome class HAS-A smart TV device and smart light.
class SmartHome(
val smartTvDevice: SmartTvDevice,
val smartLightDevice: SmartLightDevice
) {
...
}
- En el cuerpo de
SmartHome
, define un métodoturnOnLight()
que llame al métodoturnOn()
en el objetosmartLightDevice
y unturnOffLight()
que llame al métodoturnOff()
en el objetosmartLightDevice
:
class SmartHome(
val smartTvDevice: SmartTvDevice,
val smartLightDevice: SmartLightDevice
) {
...
fun changeTvChannelToNext() {
smartTvDevice.nextChannel()
}
fun turnOnLight() {
smartLightDevice.turnOn()
}
fun turnOffLight() {
smartLightDevice.turnOff()
}
}
- En la línea después del método
turnOffLight()
, define un métodoincreaseLightBrightness()
que llame al métodoincreaseBrightness()
en la propiedadsmartLightDevice
:
class SmartHome(
val smartTvDevice: SmartTvDevice,
val smartLightDevice: SmartLightDevice
) {
...
fun changeTvChannelToNext() {
smartTvDevice.nextChannel()
}
fun turnOnLight() {
smartLightDevice.turnOn()
}
fun turnOffLight() {
smartLightDevice.turnOff()
}
fun increaseLightBrightness() {
smartLightDevice.increaseBrightness()
}
}
- En la línea después del método
increaseLightBrightness()
, define un métodoturnOffAllDevices()
que llame a los métodosturnOffTv()
yturnOffLight()
:
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
:
- En el cuerpo de la superclase
SmartDevice
antes de la palabra clavefun
de cada método, agrega una palabra claveopen
:
open class SmartDevice(val name: String, val category: String) {
var deviceStatus = "online"
open fun turnOn() {
// function body
}
open fun turnOff() {
// function body
}
}
- En el cuerpo de la clase
SmartLightDevice
, define un métodoturnOn()
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() {
}
}
- En el cuerpo del método
turnOn()
, establece la propiedaddeviceStatus
a la cadena "on
" y la propiedadbrightnessLevel
a un valor de2
, y agrega una sentenciaprintln()
. 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.")
}
- En el cuerpo de la clase
SmartLightDevice
, define un métodoturnOff()
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() {
}
}
- En el cuerpo del método
turnOff()
, establece la propiedaddeviceStatus
a la cadena "off
" y la propiedadbrightnessLevel
a un valor de0
, y agrega una sentenciaprintln()
. 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")
}
}
- En la subclase
SmartLightDevice
, antes de la palabra clavefun
de los métodosturnOn()
yturnOff()
, agrega la palabra claveoverride
:
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.
- En el cuerpo de la clase
SmartTvDevice
, define un métodoturnOn()
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() {
}
}
- En el cuerpo del método
turnOn()
, establece la propiedaddeviceStatus
en la cadena "on
", agrega una sentenciaprintln()
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."
)
}
}
- En el cuerpo de la clase
SmartTvDevice
, después del métodoturnOn()
, define un métodoturnOff()
con un cuerpo vacío:
class SmartTvDevice(deviceName: String, deviceCategory: String) : SmartDevice(name = deviceName, category = deviceCategory) {
...
fun turnOn() {
...
}
fun turnOff() {
}
}
- En el cuerpo del método
turnOff()
, establece la propiedaddeviceStatus
en la cadena "off
", agrega una sentenciaprintln()
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")
}
}
- En la clase
SmartTvDevice
, antes de la palabra clavefun
de los métodosturnOn()
yturnOff()
, agrega la palabra claveoverride
:
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")
}
}
- En la función
main()
, usa la palabra clavevar
para definir una variablesmartDevice
de tipoSmartDevice
que cree una instancia de un objetoSmartTvDevice
que tome un argumento"Android
TV"
y uno"Entertainment"
:
fun main() {
var smartDevice: SmartDevice = SmartTvDevice("Android TV", "Entertainment")
}
- En la línea que sigue a la variable
smartDevice
, llama al métodoturnOn()
en el objetosmartDevice
:
fun main() {
var smartDevice: SmartDevice = SmartTvDevice("Android TV", "Entertainment")
smartDevice.turnOn()
}
- 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.
- En la línea posterior a la llamada al método
turnOn()
, reasigna la variablesmartDevice
para crear una instancia de una claseSmartLightDevice
que tome un argumento"Google
Light"
y uno"Utility"
. Luego, llama al métodoturnOn()
en la referencia del objetosmartDevice
:
fun main() {
var smartDevice: SmartDevice = SmartTvDevice("Android TV", "Entertainment")
smartDevice.turnOn()
smartDevice = SmartLightDevice("Google Light", "Utility")
smartDevice.turnOn()
}
- 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:
Vuelve a usar el código de la superclase SmartDevice
:
- Quita las sentencias
println()
de los métodosturnOn()
yturnOff()
, y mueve el código duplicado de las subclasesSmartTvDevice
ySmartLightDevice
a la superclaseSmartDevice
:
open class SmartDevice(val name: String, val category: String) {
var deviceStatus = "online"
open fun turnOn() {
deviceStatus = "on"
}
open fun turnOff() {
deviceStatus = "off"
}
}
- Usa la palabra clave
super
para llamar a los métodos de la claseSmartDevice
en las subclasesSmartTvDevice
ySmartLightDevice
:
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
:
- En la superclase
SmartDevice
, en la línea que aparece después de la propiedaddeviceStatus
, usa las palabras claveopen
yval
para definir una propiedaddeviceType
establecida en una string"unknown"
:
open class SmartDevice(val name: String, val category: String) {
var deviceStatus = "online"
open val deviceType = "unknown"
...
}
- En la clase
SmartTvDevice
, usa las palabras claveoverride
yval
para definir una propiedaddeviceType
establecida en una cadena"Smart
TV"
:
class SmartTvDevice(deviceName: String, deviceCategory: String) :
SmartDevice(name = deviceName, category = deviceCategory) {
override val deviceType = "Smart TV"
...
}
- En la clase
SmartLightDevice
, usa las palabras claveoverride
yval
para definir una propiedaddeviceType
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 visibilidadprotected
.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:
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:
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:
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
:
- En la propiedad
deviceStatus
de la superclaseSmartDevice
, agrega el modificadorprotected
a la funciónset()
:
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
...
}
- En la clase
SmartHome
, define una propiedaddeviceTurnOnCount
establecida en un valor0
con una función de método set privada:
class SmartHome(
val smartTvDevice: SmartTvDevice,
val smartLightDevice: SmartLightDevice
) {
var deviceTurnOnCount = 0
private set
...
}
- Agrega la propiedad
deviceTurnOnCount
, seguida del operador aritmético++
a los métodosturnOnTv()
yturnOnLight()
y, luego, agrega la propiedaddeviceTurnOnCount
, seguida del operador aritmético--
a los métodosturnOffTv()
yturnOffLight()
:
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:
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:
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:
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 |
| ✔ | 𝗫 | 𝗫 | 𝗫 |
| ✔ | ✔ | 𝗫 | 𝗫 |
| ✔ | ✔ | ✔ | 𝗫 |
| ✔ | ✔ | ✔ | ✔ |
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
:
- En la clase
SmartTvDevice
, agrega un modificador de visibilidadprivate
a las propiedadesspeakerVolume
ychannelNumber
:
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
}
}
...
}
- En la clase
SmartLightDevice
, agrega un modificadorprivate
a la propiedadbrightnessLevel
:
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:
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:
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
:
- Antes de la función
main()
, crea una claseRangeRegulator
que implemente la interfazReadWriteProperty<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.
- En el constructor principal de la clase
RangeRegulator
, agrega un parámetroinitialValue
, una propiedad privadaminValue
y otramaxValue
, todas del tipoInt
:
class RangeRegulator(
initialValue: Int,
private val minValue: Int,
private val maxValue: Int
) : ReadWriteProperty<Any?, Int> {
}
- En el cuerpo de la clase
RangeRegulator
, anula los métodosgetValue()
ysetValue()
:
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.
- En la línea anterior a la clase
SmartDevice
, importa las interfacesReadWriteProperty
yKProperty
:
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) {
}
}
...
- En la clase
RangeRegulator
, en la línea antes del métodogetValue()
, define una propiedadfieldData
y, luego, inicialízala con el parámetroinitialValue
:
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.
- En el cuerpo del método
getValue()
, muestra la propiedadfieldData
:
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) {
}
}
- En el cuerpo del método
setValue()
, verifica si el parámetrovalue
que se está asignando está en el rangominValue..maxValue
antes de asignarlo a la propiedadfieldData
:
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
}
}
}
- En la clase
SmartTvDevice
, usa la clase delegada para definir las propiedadesspeakerVolume
ychannelNumber
:
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)
...
}
- En la clase
SmartLightDevice
, usa la clase delegada para definir la propiedadbrightnessLevel
:
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étodoprintDeviceInfo()
que imprima una string"Device
name:
$name,
category:
$category,
type:
$deviceType"
. - En la clase
SmartTvDevice
, define un métododecreaseVolume()
que disminuya el volumen y un métodopreviousChannel()
que navegue al canal anterior. - En la clase
SmartLightDevice
, define un métododecreaseBrightness()
que disminuya el brillo. - En la clase
SmartHome
, asegúrate de que todas las acciones solo se puedan realizar cuando la propiedaddeviceStatus
de cada dispositivo se establezca en una string"on"
. Además, asegúrate de que la propiedaddeviceTurnOnCount
esté actualizada correctamente.
Cuando finalices la implementación, haz lo siguiente:
- En la clase
SmartHome
, define un métododecreaseTvVolume()
,changeTvChannelToPrevious()
,printSmartTvInfo()
,printSmartLightInfo()
ydecreaseLightBrightness()
. - Llama a los métodos adecuados de las clases
SmartTvDevice
ySmartLightDevice
en la claseSmartHome
. - 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
yinternal
. - Un delegado de propiedad te permite reutilizar el código get y set en varias clases.