Usar classes e objetos no Kotlin

1. Antes de começar

Este codelab ensina a usar classes e objetos no Kotlin.

As classes fornecem instruções para construir objetos. Um objeto é uma instância de uma classe que consiste em dados específicos desse objeto. Você pode usar objetos ou instâncias de classe como sinônimos.

Como analogia, imagine que você vai construir uma casa. Uma classe é semelhante ao plano de design de um arquiteto, também conhecido como planta. A planta não é a casa em si, mas sim instruções sobre como a construir. A casa é o objeto real que é construído com base na planta.

Assim como a planta da casa especifica várias salas, cada uma com o próprio design e finalidade, cada classe também tem um design e propósito próprios. Para aprender a projetar suas classes, você precisa se familiarizar com a programação orientada a objetos (OOP, na sigla em inglês), um framework que mostra como incluir dados, lógica e comportamento em objetos.

A OOP ajuda a simplificar problemas complexos em objetos menores. Você vai aprender sobre os quatro conceitos básicos de OOP mais adiante neste codelab:

  • Encapsulamento. Une as propriedades e os métodos relacionados que executam ações nessas propriedades em uma classe. Por exemplo, pense no smartphone. Ele encapsula uma câmera, uma tela, cartões de memória e vários outros componentes de hardware e software. Você não precisa se preocupar com a forma como os componentes são conectados internamente.
  • Abstração. Uma extensão do encapsulamento. A ideia é ocultar ao máximo a lógica de implementação interna. Por exemplo, para tirar uma foto com o smartphone, basta abrir o app da câmera, enquadrar a cena que você quer capturar e tocar em um botão. Você não precisa saber como o app foi criado ou como o hardware da câmera no smartphone realmente funciona. Em resumo, a mecânica interna do app e a forma como uma câmera móvel captura as fotos são abstraídas para você.
  • Herança. Permite criar uma classe com base nas características e no comportamento de outras classes ao estabelecer uma relação pai-filho. Por exemplo, os fabricantes produzem vários dispositivos móveis com o Android, mas a interface deles é diferente. Em outras palavras, os fabricantes herdam o recurso do SO Android e criam personalizações com base nele.
  • Polimorfismo. Essa palavra é uma adaptação da raiz grega poli, que significa "muitas", e morphos, que significa "formas". O polimorfismo é a capacidade de usar objetos diferentes de uma maneira única e comum. Por exemplo, quando você conecta um alto-falante Bluetooth a um smartphone, a única informação necessária é que há um dispositivo que pode tocar áudio por Bluetooth. No entanto, há várias opções de alto-falantes Bluetooth disponíveis, e o smartphone não precisa saber trabalhar com cada um deles especificamente.

Por fim, você vai aprender sobre delegados de propriedade que fornecem código reutilizável para gerenciar valores de propriedade com uma sintaxe concisa. Neste codelab, você vai aprender esses conceitos ao criar uma estrutura de classes para um app de casa inteligente.

Pré-requisitos

  • Saber abrir, editar e executar código no Playground Kotlin.
  • Conhecimentos básicos de programação em Kotlin, incluindo variáveis e funções, em especial println() e main().

O que você vai aprender

  • Uma visão geral da OOP.
  • O que são classes.
  • Como definir uma classe com construtores, funções e propriedades.
  • Como instanciar um objeto.
  • O que é herança.
  • A diferença entre as relações IS-A e HAS-A.
  • Como substituir propriedades e funções.
  • O que são modificadores de visibilidade.
  • O que é um delegado e como usar o delegado by.

O que você vai criar

  • Uma estrutura de classe de casa inteligente.
  • Classes que representam dispositivos inteligentes, como uma smart TV e uma iluminação inteligente.

O que é necessário

  • Um computador com acesso à Internet e um navegador da Web.

2. Definir uma classe

Na definição de uma classe, você especifica as propriedades e os métodos que todos os objetos dela precisam ter.

Esse processo começa com a palavra-chave class, seguida de um nome e um conjunto de chaves. A parte da sintaxe antes da chave de abertura também é conhecida como cabeçalho da classe. Dentro das chaves, você pode especificar propriedades e funções para a classe. Você vai aprender sobre propriedades e funções em breve. Confira a sintaxe de uma definição de classe neste diagrama:

Ela começa com uma palavra-chave de classe seguida de um nome e de chaves de abertura e fechamento. As chaves contêm o corpo que descreve a planta da classe.

Estas são as convenções de nomenclatura recomendadas para uma classe:

  • Você pode escolher qualquer nome de classe que quiser, mas não use palavras-chave (link em inglês) do Kotlin como nome de classe. Por exemplo, a palavra-chave fun.
  • Como o nome da classe é escrito em PascalCase, cada palavra começa com uma letra maiúscula e não há espaços entre elas. Por exemplo, em SmartDevice, a primeira letra de cada palavra é maiúscula e não há espaço entre elas.

Uma classe consiste em três partes principais:

  • Propriedades: variáveis que especificam os atributos dos objetos da classe.
  • Métodos: funções que contêm os comportamentos e as ações da classe.
  • Construtores: funções de membro especiais que criam instâncias da classe em todo o programa em que são definidas.

Esta não é a primeira vez que você trabalha com classes. Nos codelabs anteriores, você aprendeu sobre os tipos de dados, como Int, Float, String e Double. Eles são definidos como classes no Kotlin. Na definição de uma variável, você cria um objeto da classe Int que é instanciada com um valor 1, como mostrado neste snippet de código:

val number: Int = 1

Defina uma classe SmartDevice:

  1. No Playground Kotlin, substitua o conteúdo por uma função main() vazia:
fun main() {
}
  1. Na linha antes da função main(), defina uma classe SmartDevice com corpo que inclua um comentário // empty body:
class SmartDevice {
    // empty body
}

fun main() {
}

3. Criar uma instância de uma classe

Como você aprendeu, uma classe é uma planta para construir um objeto. O ambiente de execução do Kotlin usa a classe, ou a planta, para criar um objeto desse tipo específico. Com a classe SmartDevice, você tem uma planta de um dispositivo inteligente. Para usar um dispositivo inteligente real no programa, é necessário criar uma instância de objeto SmartDevice. A sintaxe de instanciação começa com o nome da classe seguido de um conjunto de parênteses, como mostrado neste diagrama:

1d25bc4f71c31fc9.png

Para usar um objeto, crie-o e o atribua a uma variável, como você faria na definição de uma variável. Use a palavra-chave val para criar uma variável imutável e a palavra-chave var para uma variável mutável. A palavra-chave val ou var é seguida pelo nome da variável, depois por um operador de atribuição = e pela instanciação do objeto de classe. Confira a sintaxe neste diagrama:

f58430542f2081a9.png

Instancie a classe SmartDevice como um objeto:

  • Na função main(), use a palavra-chave val para criar uma variável com o nome smartTvDevice e a inicialize como uma instância da classe SmartDevice:
fun main() {
    val smartTvDevice = SmartDevice()
}

4. Definir métodos de classe

Na Unidade 1, você aprendeu que:

  • A definição de uma função usa a palavra-chave fun seguida de um conjunto de parênteses e um conjunto de chaves. As chaves contêm o código, que são as instruções necessárias para executar uma tarefa.
  • Chamar uma função faz com que todo o código contido nela seja executado.

As ações que a classe pode realizar são definidas como funções. Por exemplo, imagine que você tem um dispositivo inteligente, uma smart TV ou uma iluminação inteligente que pode ser ativada e desativada com o smartphone. O dispositivo inteligente é convertido para a classe SmartDevice na programação, e a ação para ativar e desativar é representada pelas funções turnOn() e turnOff().

A sintaxe para definir uma função em uma classe é idêntica à que você aprendeu antes. A única diferença é que a função é colocada no corpo da classe. Quando você define uma função no corpo da classe, ela é uma função de membro ou método e representa o comportamento da classe. No restante deste codelab, as funções são chamadas de métodos sempre que aparecem no corpo de uma classe.

Defina métodos turnOn() e turnOff() na classe SmartDevice:

  1. No corpo da classe SmartDevice, defina um método turnOn() com um corpo vazio:
class SmartDevice {
    fun turnOn() {

    }
}
  1. No corpo do método turnOn(), adicione uma instrução println() e transmita uma string "Smart device is turned on." a ela:
class SmartDevice {
    fun turnOn() {
        println("Smart device is turned on.")
    }
}
  1. Depois do método turnOn(), adicione outro método turnOff() que mostra uma string "Smart device is turned off.":
class SmartDevice {
    fun turnOn() {
        println("Smart device is turned on.")
    }

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

Chamar um método em um objeto

Até agora, você definiu uma classe que serve como uma planta para um dispositivo inteligente, criou uma instância da classe e atribuiu a instância a uma variável. Agora você vai usar os métodos da classe SmartDevice para ligar e desligar o dispositivo.

A chamada de um método em uma classe é semelhante à chamada de funções na função main() do codelab anterior. Por exemplo, se você precisar chamar o método turnOff() no método turnOn(), escreva algo semelhante a este snippet 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 chamar um método de classe fora da classe, comece com o objeto de classe seguido pelo operador ., o nome da função e um conjunto de parênteses. Se aplicável, os parênteses podem conter argumentos exigidos pelo método. Confira a sintaxe neste diagrama:

fc609c15952551ce.png

Chame os métodos turnOn() e turnOff() no objeto:

  1. Na função main() na linha após a variável smartTvDevice, chame o método turnOn():
fun main() {
    val smartTvDevice = SmartDevice()
    smartTvDevice.turnOn()
}
  1. Na linha após o método turnOn(), chame o método turnOff():
fun main() {
    val smartTvDevice = SmartDevice()
    smartTvDevice.turnOn()
    smartTvDevice.turnOff()
}
  1. Execute o código.

A saída vai ser assim:

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

5. Definir propriedades de classe

Na Unidade 1, você aprendeu sobre variáveis, que são contêineres para dados únicos. Você aprendeu a criar uma variável somente leitura com a palavra-chave val e uma variável mutável com a palavra-chave var.

Enquanto os métodos definem as ações que uma classe pode realizar, as propriedades definem as características ou os atributos de dados dessa classe. Por exemplo, um dispositivo inteligente tem estas propriedades:

  • Nome: nome do dispositivo.
  • Categoria: tipo de dispositivo inteligente, como de entretenimento, utilitário ou culinário
  • Status do dispositivo: se o dispositivo está ligado, desligado, on-line ou off-line. O dispositivo é considerado on-line quando está conectado à Internet. Caso contrário, é considerado off-line.

Basicamente, as propriedades são variáveis definidas no corpo da classe em vez de no corpo da função. Isso significa que a sintaxe para definir propriedades e variáveis é idêntica. Defina uma propriedade imutável com a palavra-chave val e uma propriedade mutável com a palavra-chave var.

Implemente as características mencionadas acima como propriedades da classe SmartDevice:

  1. Na linha anterior ao método turnOn(), defina a propriedade name e a atribua a uma 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. Na linha após a propriedade name, defina a propriedade category e a atribua uma string "Entertainment". Em seguida, defina uma propriedade deviceStatus e a atribua a uma string "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. Na linha após a variável smartTvDevice, chame a função println() e transmita uma string "Device name is: ${smartTvDevice.name}" a ela:
fun main() {
    val smartTvDevice = SmartDevice()
    println("Device name is: ${smartTvDevice.name}")
    smartTvDevice.turnOn()
    smartTvDevice.turnOff()
}
  1. Execute o código.

A saída vai ser assim:

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

Funções getter e setter em propriedades

As propriedades podem fazer mais do que uma variável. Por exemplo, você criou uma estrutura de classes para representar uma smart TV. Uma das ações mais comuns é aumentar e diminuir o volume. Para representar essa ação na programação, você pode criar uma propriedade com o nome speakerVolume que contém o nível de volume atual definido no alto-falante da TV, mas há um intervalo em que o valor do volume pode estar. O volume mínimo que uma pessoa pode definir é 0, e o máximo é 100. Para a propriedade speakerVolume nunca exceder 100 ou ficar abaixo de 0, crie uma função setter. Ao atualizar o valor da propriedade, você precisa verificar se o valor está no intervalo de 0 a 100. Como outro exemplo, imagine que há uma exigência de letras maiúsculas para o nome. Implemente uma função getter para converter a propriedade name em letras maiúsculas.

Antes de se aprofundar na implementação dessas propriedades, você precisa entender a sintaxe completa de declaração. A sintaxe completa para definir uma propriedade mutável começa com a definição da variável seguida pelas funções opcionais get() e set(). Confira a sintaxe neste diagrama:

f2cf50a63485599f.png

Quando você não define as funções getter e setter para uma propriedade, o compilador do Kotlin cria as funções internamente. Por exemplo, se você usar a palavra-chave var para definir uma propriedade speakerVolume e atribuir a ela um valor 2, o compilador vai gerar automaticamente as funções getter e setter, como você pode notar neste snippet de código:

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

Essas linhas não estão visíveis no código porque são adicionadas pelo compilador em segundo plano.

A sintaxe completa de uma propriedade imutável tem duas diferenças:

  • Ela começa com a palavra-chave val.
  • As variáveis de tipo val são somente leitura, portanto, não têm funções set().

As propriedades do Kotlin usam um campo de apoio para armazenar um valor na memória. Em linhas gerais, um campo de apoio é uma variável de classe definida internamente nas propriedades. Um campo de apoio tem como escopo uma propriedade, o que significa que só pode ser acessado pelas funções de propriedade get() ou set().

Para ler o valor da propriedade na função get() ou atualizar o valor na função set(), você precisa usar o campo de apoio da propriedade. Ele é gerado automaticamente pelo compilador Kotlin e referenciado com um identificador field.

Por exemplo, quando você quiser atualizar o valor da propriedade na função set(), use o parâmetro da função set(), conhecido como parâmetro value, e o atribua ao field, como foi feito neste snippet de código:

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

Para o valor atribuído à propriedade speakerVolume ficar no intervalo de 0 a 100, implemente a função setter como neste snippet de código:

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

As funções set() verificam se o valor Int está em um intervalo de 0 a 100 usando a palavra-chave in seguida pelo intervalo de valor. Se o valor estiver no intervalo esperado, o valor de field é atualizado. Caso contrário, o valor da propriedade permanece inalterado.

Para não ter que adicionar a função setter ao código agora, inclua essa propriedade em uma classe na seção Implementar uma relação entre classes deste codelab.

6. Definir um construtor

O objetivo principal do construtor é especificar como os objetos da classe são criados. Em outras palavras, os construtores inicializam e preparam um objeto para uso. Você fez isso quando instanciou o objeto. O código dentro do construtor é executado quando o objeto da classe é instanciado. Você pode definir um construtor com ou sem parâmetros.

Construtor padrão

Um construtor padrão não tem parâmetros. Você pode definir um construtor padrão como mostrado neste snippet de código:

class SmartDevice constructor() {
    ...
}

O objetivo do Kotlin é ser conciso, portanto, é possível remover a palavra-chave constructor se não houver anotações ou modificadores de visibilidade no construtor. Você vai aprender sobre isso em breve. Também é possível remover os parênteses se o construtor não tiver parâmetros, conforme mostrado neste snippet de código:

class SmartDevice {
    ...
}

O compilador Kotlin gera automaticamente o construtor padrão. O construtor padrão gerado automaticamente não está visível no código porque foi adicionado pelo compilador em segundo plano.

Definir um construtor parametrizado

Na classe SmartDevice, as propriedades name e category são imutáveis. É preciso garantir que todas as instâncias da classe SmartDevice inicializem as propriedades name e category. Com a implementação atual, os valores das propriedades name e category estão fixados no código. Isso significa que todos os dispositivos inteligentes são nomeados pela string "Android TV" e categorizados com a string "Entertainment".

Para manter a imutabilidade, mas evitar valores fixados no código, use um construtor parametrizado para inicializar:

  • Na classe SmartDevice, mova as propriedades name e category para o construtor sem atribuir valores padrão:
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.")
    }
}

O construtor passa a aceitar parâmetros para configurar as propriedades, e a maneira de instanciar um objeto para essa classe também muda. A sintaxe completa para instanciar um objeto é mostrada neste diagrama:

bbe674861ec370b6.png

Esta é a representação do código:

SmartDevice("Android TV", "Entertainment")

Ambos os argumentos para o construtor são strings. Não está claro a qual parâmetro o valor precisa ser atribuído. Para corrigir isso, da mesma forma que você transmitiu argumentos de função, crie um construtor com argumentos nomeados, como mostrado neste snippet de código:

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

Há dois tipos principais de construtores no Kotlin:

  • Construtor principal: uma classe pode ter apenas um construtor principal, que é definido como parte do cabeçalho da classe. Um construtor principal pode ser um construtor padrão ou parametrizado. O construtor principal não tem corpo. Isso significa que ele não pode conter código.
  • Construtor secundário: uma classe pode ter vários construtores secundários. Você pode definir o construtor secundário com ou sem parâmetros. O construtor secundário pode inicializar a classe e tem um corpo que pode conter lógica de inicialização. Se a classe tiver um construtor principal, cada construtor secundário precisa inicializar o construtor principal.

É possível usar o construtor principal para inicializar propriedades no cabeçalho da classe. Os argumentos transmitidos ao construtor são atribuídos às propriedades. A sintaxe para definir um construtor principal começa com o nome da classe seguido pela palavra-chave constructor e um conjunto de parênteses. Os parênteses contêm os parâmetros do construtor principal. Se houver mais de um parâmetro, use vírgulas para separar as definições de parâmetro. No diagrama abaixo, confira a sintaxe completa para definir um construtor principal:

aa05214860533041.png

O construtor secundário está incluído no corpo da classe, e a sintaxe dele inclui três partes:

  • Declaração de construtor secundário: a definição do construtor secundário começa com a palavra-chave constructor seguida por parênteses. Se aplicável, os parênteses podem conter os parâmetros exigidos pelo construtor secundário.
  • Inicialização do construtor principal: a inicialização começa com dois pontos seguidos da palavra-chave this e um conjunto de parênteses. Se aplicável, os parênteses podem conter os parâmetros exigidos pelo construtor principal.
  • Corpo do construtor secundário: a inicialização do construtor principal é seguida por um conjunto de chaves que contêm o corpo do construtor secundário.

Confira a sintaxe neste diagrama:

2dc13ef136009e98.png

Por exemplo, imagine que você quer integrar uma API desenvolvida por um provedor de dispositivos inteligentes. No entanto, a API retorna o código de status do tipo Int para indicar o status inicial do dispositivo. O valor retornado é 0 se o dispositivo estiver off-line e 1 se estiver on-line. Para qualquer outro valor inteiro, o status é considerado desconhecido. Você pode criar um construtor secundário na classe SmartDevice para converter esse parâmetro statusCode em uma representação de string, como neste snippet 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. Implementar uma relação entre classes

A herança permite criar uma classe com base nas características e no comportamento de outra classe. Esse é um mecanismo avançado que ajuda você a escrever código reutilizável e a estabelecer relações entre as classes.

Por exemplo, há muitos dispositivos inteligentes no mercado, como smart TVs, iluminação e interruptores inteligentes. Quando você representa dispositivos inteligentes na programação, eles compartilham algumas propriedades comuns, como nome, categoria e status. Além disso, eles têm comportamentos comuns, como estarem ligados ou desligados.

No entanto, a maneira de ligar ou desligar cada dispositivo inteligente é diferente. Por exemplo, para ligar uma TV, talvez seja necessário ligar a tela e, em seguida, configurar o último nível de volume e canal conhecidos. Por outro lado, para acender uma luz, talvez seja necessário apenas aumentar ou diminuir o brilho.

Além disso, todo dispositivo inteligente pode realizar mais funções e ações. Por exemplo, com uma TV, é possível ajustar o volume e mudar o canal. Com a iluminação, é possível ajustar o brilho ou a cor.

Em resumo, todos os dispositivos inteligentes têm recursos diferentes, mas compartilham algumas características comuns. Com a herança, é possível duplicar essas características comuns para cada classe de dispositivo inteligente ou tornar o código reutilizável.

Para fazer isso, você precisa criar uma classe mãe SmartDevice e definir as propriedades e os comportamentos comuns. Em seguida, você pode criar classes filhas, como SmartTvDevice e SmartLightDevice, que herdam as propriedades da classe mãe.

Em termos de programação, dizemos que as classes SmartTvDevice e SmartLightDevice estendem a classe mãe SmartDevice. A classe mãe também é chamada de superclasse e as classes filhas de subclasses. Entenda a relação entre elas neste diagrama:

Diagrama representando a relação de herança entre classes.

No Kotlin, todas as classes são finais por padrão, o que significa que não é possível estendê-las. É necessário definir as relações entre elas.

Defina a relação entre a superclasse SmartDevice e as subclasses:

  1. Na superclasse SmartDevice, adicione uma palavra-chave open antes da palavra-chave class para que ela possa ser estendida:
open class SmartDevice(val name: String, val category: String) {
    ...
}

A palavra-chave open informa ao compilador que essa classe pode ser estendida.

A sintaxe para criar uma subclasse começa com a criação do cabeçalho da classe, como foi feito até agora. O parêntese de fechamento do construtor é seguido por um espaço, dois pontos, outro espaço, o nome da superclasse e um conjunto de parênteses. Se necessário, os parênteses podem incluir os parâmetros exigidos pelo construtor da superclasse. Confira a sintaxe neste diagrama:

1ac63b66e6b5c224.png

  1. Crie uma subclasse SmartTvDevice que estenda a superclasse SmartDevice:
class SmartTvDevice(deviceName: String, deviceCategory: String) :
    SmartDevice(name = deviceName, category = deviceCategory) {
}

A definição do constructor para SmartTvDevice não especifica se as propriedades são mutáveis ou imutáveis. Isso significa que os parâmetros deviceName e deviceCategory são apenas parâmetros do constructor em vez de propriedades de classe. Eles não podem ser usados na classe, só transmitidos ao construtor da superclasse.

  1. No corpo da subclasse SmartTvDevice, adicione a propriedade speakerVolume que você criou quando aprendeu sobre as funções getter e setter:
class SmartTvDevice(deviceName: String, deviceCategory: String) :
    SmartDevice(name = deviceName, category = deviceCategory) {

    var speakerVolume = 2
        set(value) {
            if (value in 0..100) {
                field = value
            }
        }
}
  1. Defina uma propriedade channelNumber atribuída a um valor 1 com uma função setter que especifica um intervalo 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. Defina um método increaseSpeakerVolume() para aumentar o volume e mostrar uma 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. Adicione um método nextChannel() para aumentar o número do canal e mostrar uma 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. Na linha após a subclasse SmartTvDevice, defina uma subclasse SmartLightDevice para estender a superclasse SmartDevice:
class SmartLightDevice(deviceName: String, deviceCategory: String) :
    SmartDevice(name = deviceName, category = deviceCategory) {
}
  1. No corpo da subclasse SmartLightDevice, defina uma propriedade brightnessLevel atribuída a um valor 0 com uma função setter que especifica um intervalo 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. Defina um método increaseBrightness() para aumentar o brilho da luz e mostrar uma 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.")
    }
}

Relações entre classes

Com a herança, você estabelece uma relação entre duas classes com algo conhecido como relação IS-A. Um objeto também é uma instância da classe de que ele herda. Em uma relação HAS-A, um objeto pode ser proprietário de uma instância de outra classe sem ser realmente uma instância da classe em si. Neste diagrama, há uma visão geral dessas relações:

Visão geral das relações HAS-A e IS-A.

Relações IS-A

Quando você especifica uma relação IS-A entre a superclasse SmartDevice e a subclasse SmartTvDevice, isso significa que a subclasse SmartTvDevice pode fazer tudo que a superclasse SmartDevice faz. A relação é unidirecional. Portanto, você pode dizer que todas as smart TVs são dispositivos inteligentes, mas não é possível dizer que todos os dispositivos são smart TVs. Confira a representação do código para uma relação IS-A neste snippet:

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

Não use a herança apenas para reutilizar o código. Antes de decidir, verifique se as duas classes estão relacionadas entre si. Se estiverem, verifique se realmente estão qualificadas para a relação IS-A. Considere se a subclasse realmente é igual à superclasse. Por exemplo, o Android é um sistema operacional.

Relações HAS-A

Uma relação HAS-A é outra maneira de especificar o relacionamento entre duas classes. Por exemplo, talvez você use a smart TV na sua casa. Nesse caso, há uma relação entre a smart TV e a casa. A casa contém um dispositivo inteligente ou, em outras palavras, tem um dispositivo inteligente. A relação HAS-A entre duas classes também é conhecida como composição.

Até o momento, você criou alguns dispositivos inteligentes. Agora, você vai criar a classe SmartHome que contém dispositivos inteligentes. Você pode usar a classe SmartHome para interagir com os dispositivos inteligentes.

Use uma relação HAS-A para definir uma classe SmartHome:

  1. Entre a classe SmartLightDevice e a função main(), defina uma classe SmartHome:
class SmartLightDevice(deviceName: String, deviceCategory: String) :
    SmartDevice(name = deviceName, category = deviceCategory) {

    ...

}

class SmartHome {
}

fun main() { 
    ...
}
  1. No construtor da classe SmartHome, use a palavra-chave val para criar uma propriedade smartTvDevice do tipo SmartTvDevice:
// The SmartHome class HAS-A smart TV device.
class SmartHome(val smartTvDevice: SmartTvDevice) {

}
  1. No corpo da classe SmartHome, defina um método turnOnTv() que chame o método turnOn() na propriedade smartTvDevice:
class SmartHome(val smartTvDevice: SmartTvDevice) {

    fun turnOnTv() {
        smartTvDevice.turnOn()
    }
}
  1. Na linha após o método turnOnTv(), defina um método turnOffTv() que chame o método turnOff() na propriedade smartTvDevice:
class SmartHome(val smartTvDevice: SmartTvDevice) {

    fun turnOnTv() {
        smartTvDevice.turnOn()
    }

    fun turnOffTv() {
        smartTvDevice.turnOff()
    }

}
  1. Na linha após o método turnOffTv(), defina um método increaseTvVolume() que chame o método increaseSpeakerVolume() na propriedade smartTvDevice e, em seguida, defina um método changeTvChannelToNext() que chame o método nextChannel() na propriedade smartTvDevice:
class SmartHome(val smartTvDevice: SmartTvDevice) {

    fun turnOnTv() {
        smartTvDevice.turnOn()
    }

    fun turnOffTv() {
        smartTvDevice.turnOff()
    }

    fun increaseTvVolume() {
        smartTvDevice.increaseSpeakerVolume()
    }

    fun changeTvChannelToNext() {
        smartTvDevice.nextChannel()
    }
}
  1. No construtor da classe SmartHome, mova o parâmetro de propriedade smartTvDevice para a própria linha, seguido por uma vírgula:
class SmartHome(
    val smartTvDevice: SmartTvDevice,
) {

    ...

}
  1. Na linha após a propriedade smartTvDevice, use a palavra-chave val para definir uma propriedade smartLightDevice do tipo SmartLightDevice:
// The SmartHome class HAS-A smart TV device and smart light.
class SmartHome(
    val smartTvDevice: SmartTvDevice,
    val smartLightDevice: SmartLightDevice
) {

    ...

}
  1. No corpo de SmartHome, defina um método turnOnLight() que chame o método turnOn() no objeto smartLightDevice e um método turnOffLight() que chama o método turnOff() no objeto smartLightDevice:
class SmartHome(
    val smartTvDevice: SmartTvDevice,
    val smartLightDevice: SmartLightDevice
) {

    ...

    fun changeTvChannelToNext() {
        smartTvDevice.nextChannel()
    }

    fun turnOnLight() {
        smartLightDevice.turnOn()
    }

    fun turnOffLight() {
        smartLightDevice.turnOff()
    }
}
  1. Na linha após o método turnOffLight(), defina um método increaseLightBrightness() que chame o método increaseBrightness() na propriedade 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. Na linha após o método increaseLightBrightness(), defina um método turnOffAllDevices() que chame os métodos turnOffTv() e turnOffLight():
class SmartHome(
    val smartTvDevice: SmartTvDevice,
    val smartLightDevice: SmartLightDevice
) {

    ...

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

Substituir métodos da superclasse nas subclasses

Como discutido anteriormente, mesmo que os recursos de ativação e desativação tenham suporte de todos os dispositivos inteligentes, a forma como eles fazem isso é diferente. Para oferecer esse comportamento específico ao dispositivo, substitua os métodos turnOn() e turnOff() definidos na superclasse. Substituir significa interceptar a ação para ter controle manual. Quando você substitui um método, o método na subclasse interrompe a execução do definido na superclasse e fornece a própria execução.

Substitua os métodos turnOn() e turnOff() da classe SmartDevice:

  1. No corpo da superclasse SmartDevice antes da palavra-chave fun de cada método, adicione uma palavra-chave open:
open class SmartDevice(val name: String, val category: String) {

    var deviceStatus = "online"

    open fun turnOn() {
        // function body
    }

    open fun turnOff() {
        // function body
    }
}
  1. No corpo da classe SmartLightDevice, defina um método turnOn() com um corpo vazio:
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. No corpo do método turnOn(), defina o método deviceStatus como a string "on", defina a propriedade brightnessLevel como um valor 2 e adicione uma instrução println(). Em seguida, transmita uma string "$name turned on. The brightness level is $brightnessLevel." a ela:
    fun turnOn() {
        deviceStatus = "on"
        brightnessLevel = 2
        println("$name turned on. The brightness level is $brightnessLevel.")
    }
  1. No corpo da classe SmartLightDevice, defina um método turnOff() com um corpo vazio:
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. No corpo do método turnOff(), defina o método deviceStatus como a string "off", defina a propriedade brightnessLevel para um valor 0 e adicione uma instrução println(). Em seguida, transmita uma string "Smart Light turned off" a ela:
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. Na subclasse SmartLightDevice antes da palavra-chave fun dos métodos turnOn() e turnOff(), adicione a palavra-chave 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")
    }
}

A palavra-chave override diz ao ambiente de execução do Kotlin para executar o código incluído no método definido na subclasse.

  1. No corpo da classe SmartTvDevice, defina um método turnOn() com um corpo vazio:
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. No corpo do método turnOn(), defina a propriedade deviceStatus como a string "on" e adicione uma instrução println(). Em seguida, transmita uma string "$name is turned on. Speaker volume is set to $speakerVolume and channel number is " + "set to $channelNumber." a ela:
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. No corpo da classe SmartTvDevice, depois do método turnOn(), defina um método turnOff() com um corpo vazio:
class SmartTvDevice(deviceName: String, deviceCategory: String) : SmartDevice(name = deviceName, category = deviceCategory) {

    ...

    fun turnOn() {
        ...
    }

    fun turnOff() {
    }
}
  1. No corpo do método turnOff() defina a propriedade deviceStatus como a string "off" e adicione uma instrução println(). Em seguida, transmita uma string "$name turned off" a ela:
class SmartTvDevice(deviceName: String, deviceCategory: String) : SmartDevice(name = deviceName, category = deviceCategory) {

    ...

    fun turnOn() {
        ...
    }

    fun turnOff() {
        deviceStatus = "off"
        println("$name turned off")
    }
}
  1. Na classe SmartTvDevice antes da palavra-chave fun dos métodos turnOn() e turnOff(), adicione a palavra-chave 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. Na função main(), use a palavra-chave var para definir uma variável smartDevice do tipo SmartDevice que instancia um objeto SmartTvDevice que usa um argumento "Android TV" e um "Entertainment":
fun main() {
    var smartDevice: SmartDevice = SmartTvDevice("Android TV", "Entertainment")
}
  1. Na linha após a variável smartDevice, chame o método turnOn() no objeto smartDevice:
fun main() {
    var smartDevice: SmartDevice = SmartTvDevice("Android TV", "Entertainment")
    smartDevice.turnOn()
}
  1. Execute o código.

A saída vai ser assim:

Android TV is turned on. Speaker volume is set to 2 and channel number is set to 1.
  1. Na linha após a chamada para o método turnOn(), reatribua a variável smartDevice para instanciar uma classe SmartLightDevice que usa um argumento "Google Light" e um argumento "Utility". Em seguida, chame o método turnOn() na referência do objeto smartDevice:
fun main() {
    var smartDevice: SmartDevice = SmartTvDevice("Android TV", "Entertainment")
    smartDevice.turnOn()
    
    smartDevice = SmartLightDevice("Google Light", "Utility")
    smartDevice.turnOn()
}
  1. Execute o código.

A saída vai ser assim:

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.

Esse é um exemplo de polimorfismo. O código chama o método turnOn() em uma variável do tipo SmartDevice e, dependendo do valor real da variável, diferentes implementações do método turnOn() podem ser executadas.

Reutilizar o código de superclasse em subclasses com a palavra-chave super

Ao observar mais de perto os métodos turnOn() e turnOff(), há uma semelhança na forma como a variável deviceStatus é atualizada sempre que os métodos são chamados nas subclasses SmartTvDevice e SmartLightDevice: o código é duplicado. É possível atualizar o status na classe SmartDevice para reutilizar o código.

Para chamar o método substituído na superclasse da subclasse, use a palavra-chave super. Chamar um método da superclasse é semelhante a chamar o método de fora dela. Em vez de usar um operador . entre o objeto e o método, você precisa usar a palavra-chave super que diz ao compilador Kotlin para chamar o método na superclasse em vez de na subclasse.

A sintaxe para chamar o método na superclasse começa com uma palavra-chave super seguida pelo operador ., pelo nome da função e por um conjunto de parênteses. Se aplicável, os parênteses podem incluir os argumentos. Confira a sintaxe neste diagrama:

18cc94fefe9851e0.png

Reutilize o código da superclasse SmartDevice:

  1. Remova as instruções println() dos métodos turnOn() e turnOff() e mova o código duplicado das subclasses SmartTvDevice e SmartLightDevice para a superclasse SmartDevice:
open class SmartDevice(val name: String, val category: String) {

    var deviceStatus = "online"

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

    open fun turnOff() {
        deviceStatus = "off"
    }
}
  1. Use a palavra-chave super para chamar os métodos da classe SmartDevice nas subclasses SmartTvDevice e 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")
    }
}

Substituir propriedades da superclasse nas subclasses

Assim como os métodos, também é possível substituir as propriedades seguindo as mesmas etapas.

Substitua a propriedade deviceType:

  1. Na superclasse SmartDevice na linha após a propriedade deviceStatus, use as palavras-chave open e val para definir uma propriedade deviceType definida que tem uma string "unknown":
open class SmartDevice(val name: String, val category: String) {

    var deviceStatus = "online"

    open val deviceType = "unknown"
    ...
}
  1. Na classe SmartTvDevice, use as palavras-chave override e val para definir uma propriedade deviceType que tem uma string "Smart TV":
class SmartTvDevice(deviceName: String, deviceCategory: String) :
    SmartDevice(name = deviceName, category = deviceCategory) {

    override val deviceType = "Smart TV"

    ...
}
  1. Na classe SmartLightDevice, use as palavras-chave override e val para definir uma propriedade deviceType que tem uma string "Smart Light":
class SmartLightDevice(deviceName: String, deviceCategory: String) :
    SmartDevice(name = deviceName, category = deviceCategory) {

    override val deviceType = "Smart Light"

    ...

}

8. Modificadores de visibilidade

Os modificadores de visibilidade têm um papel importante para o encapsulamento:

  • Em uma classe, eles permitem esconder propriedades e métodos de acessos não autorizados fora da classe.
  • Em um pacote, eles permitem ocultar as classes e interfaces de acesso não autorizado fora do pacote.

O Kotlin oferece quatro modificadores de visibilidade:

  • public: modificador de visibilidade padrão. Torna a declaração acessível em qualquer lugar. As propriedades e os métodos que você quer usar fora da classe são marcados como públicos.
  • private: torna a declaração acessível no mesmo arquivo de classe ou origem.

Provavelmente, há algumas propriedades e métodos que são usados apenas dentro de uma classe e que você não quer que outras usem. Essas propriedades e métodos podem ser marcados com o modificador de visibilidade private para garantir que outra classe não os acesse acidentalmente.

  • protected: torna a declaração acessível em subclasses. Além das subclasses, as propriedades e os métodos usados na classe que os define são marcados com o modificador de visibilidade protected.
  • internal: torna a declaração acessível no mesmo módulo. O modificador interno é semelhante ao particular, mas você pode acessar propriedades e métodos internos de fora da classe, desde que eles sejam acessados no mesmo módulo.

Quando você define uma classe, ela é visível publicamente e pode ser acessada por qualquer pacote que a importe, o que significa que ela é pública por padrão, a menos que você especifique um modificador de visibilidade. Da mesma forma, quando você define ou declara propriedades e métodos na classe, por padrão eles podem ser acessados fora dela pelo objeto da classe. É fundamental definir uma visibilidade adequada para o código, principalmente para ocultar propriedades e métodos que outras classes não precisam acessar.

Por exemplo, considere o que um motorista pode acessar em um carro. As especificações sobre quais partes compõem o carro e como ele funciona internamente ficam ocultas por padrão. O objetivo é que operar um carro seja algo intuitivo. O ideal é que o carro não tenha uma operação tão complexa quanto uma aeronave comercial, da mesma forma que é ideal que outro desenvolvedor ou você no futuro não confunda as propriedades e os métodos de uma classe.

Os modificadores de visibilidade ajudam a mostrar as partes relevantes do código a outras classes do projeto e garantem que a implementação não seja usada acidentalmente. Isso torna o código fácil de entender e menos propenso a bugs.

O modificador de visibilidade precisa ser colocado antes da sintaxe da declaração quando você for declarar a classe, o método ou as propriedades, como mostrado no diagrama:

dcc4f6693bf719a9.png

Especificar um modificador de visibilidade para propriedades

A sintaxe para especificar um modificador de visibilidade para uma propriedade começa com o modificador private, protected ou internal, seguido pela sintaxe que define uma propriedade. Confira a sintaxe neste diagrama:

47807a890d237744.png

Por exemplo, confira como tornar a propriedade deviceStatus particular neste snippet de código:

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

    ...

    private var deviceStatus = "online"

    ...
}

Também é possível definir os modificadores de visibilidade para funções setter. O modificador é colocado antes da palavra-chave set. Confira a sintaxe neste diagrama:

cea29a49b7b26786.png

Para a classe SmartDevice, é necessário que os objetos de classe possam ler o valor da propriedade deviceStatus fora da classe. No entanto, somente a classe e os filhos dela podem atualizar ou gravar o valor. Para implementar esse requisito, é necessário usar o modificador protected na função set() da propriedade deviceStatus.

Use o modificador protected na função set() da propriedade deviceStatus:

  1. Na propriedade deviceStatus da superclasse SmartDevice, adicione o modificador protected à função set():
open class SmartDevice(val name: String, val category: String) {

    ...

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

    ...
}

Você não está realizando ações ou verificações na função set(). Você está atribuindo o parâmetro value à variável field. Como aprendeu anteriormente, isso é semelhante à implementação padrão para setters de propriedade. É possível omitir os parênteses e o corpo da função set() neste caso:

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

    ...

    var deviceStatus = "online"
        protected set

    ...
}
  1. Na subclasse SmartHome, defina um conjunto de propriedades deviceTurnOnCount com um valor 0 usando uma função setter particular:
class SmartHome(
    val smartTvDevice: SmartTvDevice,
    val smartLightDevice: SmartLightDevice
) {

    var deviceTurnOnCount = 0
        private set

    ...
}
  1. Adicione a propriedade deviceTurnOnCount seguida pelo operador aritmético ++ aos métodos turnOnTv() e turnOnLight(). Depois, adicione a propriedade deviceTurnOnCount seguida pelo operador aritmético -- aos métodos turnOffTv() e 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 visibilidade para métodos

A sintaxe para especificar um modificador de visibilidade de método começa com os modificadores private, protected ou internal, seguidos pela sintaxe que define um método. Confira a sintaxe neste diagrama:

e0a60ddc26b841de.png

Por exemplo, este snippet de código mostra como especificar um modificador protected para o método nextChannel() na classe SmartTvDevice:

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

    ...

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

    ...
}

Modificadores de visibilidade para construtores

A sintaxe para especificar um modificador de visibilidade de construtor é semelhante à definição do construtor principal, mas tem algumas diferenças:

  • O modificador é especificado após o nome da classe, mas antes da palavra-chave constructor.
  • Se você precisar especificar o modificador do construtor principal, é necessário manter a palavra-chave constructor e os parênteses mesmo quando não houver parâmetros.

Confira a sintaxe neste diagrama:

6832575eba67f059.png

Por exemplo, este snippet de código mostra como adicionar um modificador protected ao construtor SmartDevice:

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

    ...

}

Modificadores de visibilidade para classes

A sintaxe para especificar um modificador de visibilidade de classe começa com os modificadores private, protected ou internal, seguidos pela sintaxe que define uma classe. Confira a sintaxe neste diagrama:

3ab4aa1c94a24a69.png

Por exemplo, este snippet de código mostra como especificar um modificador internal para a classe SmartDevice:

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

    ...

}

O ideal é ter uma visibilidade restrita das propriedades e métodos. Portanto, use o modificador private (particular) sempre que possível nas declarações. Se não for possível manter a visibilidade particular, use o modificador protected. Se não for possível manter a visibilidade protegida, use o modificador internal. Se não for possível manter a visibilidade interna, use o modificador public.

Especificar os modificadores de visibilidade adequados

Esta tabela ajuda a determinar os modificadores de visibilidade adequados com base naquilo que pode ter acesso à propriedade ou aos métodos de uma classe ou um construtor:

Modificador

Acessível na mesma classe

Acessível na subclasse

Acessível no mesmo módulo

Acessível fora do módulo

private

𝗫

𝗫

𝗫

protected

𝗫

𝗫

internal

𝗫

public

Na subclasse SmartTvDevice, não permita que as propriedades speakerVolume e channelNumber sejam controladas de fora da classe. Apenas os métodos increaseSpeakerVolume() e nextChannel() podem controlar essas propriedades.

Da mesma forma, na subclasse SmartLightDevice, a propriedade brightnessLevel precisa ser controlada apenas pelo método increaseLightBrightness().

Adicione os modificadores de visibilidade adequados às subclasses SmartTvDevice e SmartLightDevice:

  1. Na classe SmartTvDevice, adicione um modificador de visibilidade private às propriedades speakerVolume e 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. Na classe SmartLightDevice, adicione um modificador private à propriedade 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. Definir delegados de propriedade

Na seção anterior, você aprendeu que as propriedades no Kotlin usam um campo de apoio para armazenar os valores na memória. Use o identificador field para referenciar esse campo.

No que foi escrito até agora, você pode analisar o código duplicado para verificar se os valores estão dentro do intervalo das propriedades speakerVolume, channelNumber e brightnessLevel nas classes SmartTvDevice e SmartLightDevice. Você pode reutilizar o código de verificação de intervalo na função setter com delegados. Não é necessário usar um campo e uma função getter e setter para gerenciar o valor, já que o delegado pode fazer isso.

A sintaxe para criar delegados de propriedade começa com a declaração de uma variável seguida pela palavra-chave by e pelo objeto delegado que processa as funções setter e getter da propriedade. Confira a sintaxe neste diagrama:

928547ad52768115.png

Antes de implementar a classe a que você pode delegar a implementação, é necessário conhecer interfaces. Uma interface é um protocolo que precisa ser aderido pelas classes que a implementam. O foco da interface é o que fazer e não como fazer. Em resumo, uma interface ajuda você a alcançar a abstração.

Por exemplo, antes de construir uma casa, você fala para o arquiteto o que quer. Você quer um quarto, um quarto para crianças, uma sala de estar, uma cozinha e alguns banheiros. Isso significa que você diz o que quer, e o arquiteto diz como fazer isso. Este diagrama mostra a sintaxe para criar uma interface:

bfe3fd1cd8c45b2a.png

Você já aprendeu a estender uma classe e substituir a funcionalidade dela. A classe implementa as interfaces. A classe fornece detalhes de implementação dos métodos e propriedades declarados na interface. Para criar o delegado, você vai fazer algo semelhante na interface ReadWriteProperty (link em inglês). Você vai aprender mais sobre interfaces na próxima unidade.

Para criar a classe delegada do tipo var, é necessário implementar a interface ReadWriteProperty. Também é necessário implementar a interface ReadOnlyProperty para o tipo val (links em inglês).

Crie o delegado para o tipo var:

  1. Antes da função main(), crie uma classe RangeRegulator que implemente a interface ReadWriteProperty<Any?, Int>:
class RangeRegulator() : ReadWriteProperty<Any?, Int> {

}

fun main() {
    ...
}

Não se preocupe com os sinais de "maior e menor que" e o conteúdo dentro deles. Eles representam tipos genéricos, e você vai aprender sobre eles na próxima unidade.

  1. No construtor principal da classe RangeRegulator, adicione um parâmetro initialValue e propriedades particulares minValue e maxValue, todos os elementos do tipo Int:
class RangeRegulator(
    initialValue: Int,
    private val minValue: Int,
    private val maxValue: Int
) : ReadWriteProperty<Any?, Int> {

}
  1. No corpo da classe RangeRegulator, substitua os métodos getValue() e 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) {
    }
}

Esses métodos atuam como funções getter e setter das propriedades.

  1. Na linha antes da classe SmartDevice, importe as interfaces ReadWriteProperty e 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. Na classe RangeRegulator, na linha antes do método getValue(), defina uma propriedade fieldData e a inicialize com o 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) {
    }
}

Essa propriedade funciona como o campo de apoio da variável.

  1. No corpo do método getValue(), retorne a propriedade 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. No corpo do método setValue(), verifique se o parâmetro value que está sendo atribuído está no intervalo minValue..maxValue antes de o atribuir à propriedade 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. Na classe SmartTvDevice, use a classe delegada para definir as propriedades speakerVolume e 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. Na classe SmartLightDevice, use a classe delegada para definir a propriedade 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. Testar a solução

Você pode observar o código da solução neste snippet 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()
}

A saída vai ser assim:

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. Desafio

  • Na classe SmartDevice, defina um método printDeviceInfo() que mostra uma string "Device name: $name, category: $category, type: $deviceType".
  • Na classe SmartTvDevice, defina um método decreaseVolume() para diminuir o volume e um método previousChannel() que navega até o canal anterior.
  • Na classe SmartLightDevice, defina um método decreaseBrightness() que diminui o brilho.
  • Na classe SmartHome, faça com que todas as ações possam ser realizadas apenas quando a propriedade deviceStatus de cada dispositivo estiver definida como uma string "on". Além disso, verifique se a propriedade deviceTurnOnCount foi atualizada corretamente.

Depois de concluir a implementação:

  • Na classe SmartHome, defina um método decreaseTvVolume(), changeTvChannelToPrevious(), printSmartTvInfo(), printSmartLightInfo() e decreaseLightBrightness().
  • Chame os métodos apropriados das classes SmartTvDevice e SmartLightDevice na classe SmartHome.
  • Na função main(), chame os métodos adicionados para testar.

12. Conclusão

Parabéns! Você aprendeu a definir classes e instanciar objetos. Também mostramos como criar delegados de propriedade e relações entre classes.

Resumo

  • Há quatro princípios importantes de programação orientada a objetos: encapsulamento, abstração, herança e polimorfismo.
  • As classes são definidas com a palavra-chave class e contêm propriedades e métodos.
  • As propriedades são semelhantes às variáveis, mas podem ter getters e setters personalizados.
  • Um construtor especifica como instanciar objetos de uma classe.
  • Você pode omitir a palavra-chave constructor na definição de um construtor principal.
  • A herança facilita a reutilização de códigos.
  • A relação IS-A se refere à herança.
  • A relação HAS-A se refere à composição.
  • Os modificadores de visibilidade têm um papel importante para o encapsulamento.
  • O Kotlin oferece quatro modificadores de visibilidade: public, private, protected e internal.
  • Um delegado de propriedade permite reutilizar o código getter e setter em várias classes.

Saiba mais