Treinamento do Kotlin para programadores 4: programação orientada a objetos

Este codelab faz parte do curso Treinamento do Kotlin para programadores. Você aproveitará mais o curso se fizer os codelabs em sequência. Dependendo do seu conhecimento, talvez você possa passar mais rápido por algumas seções. Este curso é destinado a programadores que conhecem uma linguagem orientada a objetos e querem aprender Kotlin.

Introdução

Neste codelab, você criará um programa em Kotlin e aprenderá sobre classes e objetos em Kotlin. Grande parte desse conteúdo já será conhecida se você conhecer outra linguagem orientada a objetos, mas o Kotlin tem algumas diferenças importantes para reduzir a quantidade de código que precisa ser escrita. Você também aprenderá sobre classes abstratas e delegação de interface.

Em vez de criar um único app de exemplo, as lições deste curso foram desenvolvidas para aumentar seu conhecimento, mas são semi-independentes umas das outras para que você possa ler as seções que já conhece. Para conectá-los, muitos dos exemplos usam um tema de aquário. Se você quiser ver a história completa do aquário, confira o curso Bootcamp de Kotlin para programadores na Udacity.

O que você já precisa saber

  • os conceitos básicos do Kotlin, incluindo tipos, operadores e repetições;
  • Sintaxe da função do Kotlin
  • Noções básicas da programação orientada a objetos
  • Princípios básicos de um ambiente de desenvolvimento integrado, como o IntelliJ IDEA ou o Android Studio

O que você vai aprender

  • Como criar classes e acessar propriedades em Kotlin.
  • Como criar e usar construtores de classe no Kotlin.
  • Como criar uma subclasse e como a herança funciona
  • Sobre classes abstratas, interfaces e delegação de interface
  • Como criar e usar classes de dados.
  • Como usar Singletons, enumerações e classes seladas.

Atividades do laboratório

  • Criar uma classe com propriedades
  • Criar um construtor para uma classe
  • Criar uma subclasse
  • Examinar exemplos de classes e interfaces abstratas
  • Criar uma classe de dados simples
  • Saiba mais sobre Singletons, enumerações e classes seladas

Você já conhece os seguintes termos de programação:

  • As classes são modelos de objetos. Por exemplo, uma classe Aquarium é a planta para criar um objeto de aquário.
  • Objetos são instâncias de classes. Um objeto de aquário é uma Aquarium real.
  • Propriedades são características de classes, como comprimento, largura e altura de uma Aquarium.
  • Métodos, também chamados de funções de membro, são a funcionalidade da classe. Os métodos são o que você pode fazer com o objeto. Por exemplo, é possível usar fillWithWater() um objeto Aquarium.
  • Uma interface é uma especificação que uma classe pode implementar. Por exemplo, a limpeza é comum para objetos diferentes dos aquários e a limpeza geralmente acontece de maneiras semelhantes para objetos diferentes. Dessa forma, é possível ter uma interface chamada Clean que define um método clean(). A classe Aquarium poderia implementar a interface Clean para limpar o aquário com uma esponja macia.
  • Os pacotes são uma forma de agrupar códigos relacionados para manter a organização ou criar uma biblioteca de códigos. Depois que um pacote é criado, você pode importar o conteúdo dele para outro arquivo e reutilizar o código e as classes contidas nele.

Nesta tarefa, você criará um novo pacote e uma classe com algumas propriedades e um método.

Etapa 1: criar um pacote

Os pacotes podem ajudar você a manter seu código organizado.

  1. No painel Project, no projeto Hello Kotlin, clique com o botão direito do mouse na pasta src.
  2. Selecione New > Package e chame-o de example.myapp.

Etapa 2: criar uma classe com propriedades

As classes são definidas com a palavra-chave class. Os nomes das classes por convenção começam com uma letra maiúscula.

  1. Clique com o botão direito no pacote example.myapp.
  2. Selecione New > Kotlin File / Class.
  3. Em Kind, selecione Class e nomeie a classe Aquarium. O IntelliJ IDEA inclui o nome do pacote no arquivo e cria uma classe Aquarium vazia para você.
  4. Na classe Aquarium, defina e inicialize as propriedades var para largura, altura e comprimento (em centímetros). Inicialize as propriedades com valores padrão.
package example.myapp

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

Internamente, o Kotlin cria automaticamente getters e setters para as propriedades definidas na classe Aquarium, para que você possa acessá-las diretamente, por exemplo, myAquarium.length.

Etapa 3: criar uma função main()

Crie um novo arquivo com o nome main.kt para manter a função main().

  1. No painel Project à esquerda, clique com o botão direito do mouse no pacote example.myapp.
  2. Selecione New > Kotlin File / Class.
  3. Na lista suspensa Kind, mantenha a seleção como File e nomeie o arquivo como main.kt. O IntelliJ IDEA inclui o nome do pacote, mas não inclui uma definição de classe para um arquivo,
  4. Defina uma função buildAquarium() e, dentro dela, crie uma instância de Aquarium. Para criar uma instância, faça referência à classe como se ela fosse uma função, Aquarium(). Isso chama o construtor da classe e cria uma instância da classe Aquarium, semelhante ao uso de new em outras linguagens.
  5. Defina uma função main() e chame buildAquarium().
package example.myapp

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

fun main() {
    buildAquarium()
}

Etapa 4: adicionar um método

  1. Na classe Aquarium, adicione um método para exibir as propriedades de dimensão do aquário.
    fun printSize() {
        println("Width: $width cm " +
                "Length: $length cm " +
                "Height: $height cm ")
    }
  1. Em main.kt, em buildAquarium(), chame o método printSize() em myAquarium.
fun buildAquarium() {
    val myAquarium = Aquarium()
    myAquarium.printSize()
}
  1. Execute o programa clicando no triângulo verde ao lado da função main(). Observe o resultado.
⇒ Width: 20 cm Length: 100 cm Height: 40 cm 
  1. Em buildAquarium(), adicione o código para definir a altura como 60 e imprima as propriedades de dimensão alteradas.
fun buildAquarium() {
    val myAquarium = Aquarium()
    myAquarium.printSize()
    myAquarium.height = 60
    myAquarium.printSize()
}
  1. Execute o programa e observe o resultado.
⇒ Width: 20 cm Length: 100 cm Height: 40 cm 
Width: 20 cm Length: 100 cm Height: 60 cm 

Nesta tarefa, você criará um construtor para a classe e continuará trabalhando com as propriedades.

Etapa 1: criar um construtor

Nesta etapa, você adicionará um construtor à classe Aquarium criada na primeira tarefa. No exemplo anterior, cada instância de Aquarium é criada com as mesmas dimensões. É possível alterar as dimensões depois de criá-las, mas seria mais simples criar o tamanho correto para começar.

Em algumas linguagens de programação, o construtor é definido por meio da criação de um método dentro da classe que tenha o mesmo nome dela. Em Kotlin, você define o construtor diretamente na própria declaração de classe, especificando os parâmetros entre parênteses como se a classe fosse um método. Assim como nas funções do Kotlin, esses parâmetros podem incluir valores padrão.

  1. Na classe Aquarium que você criou anteriormente, altere a definição da classe para incluir três parâmetros de construtor com valores padrão para length, width e height e atribua-os às propriedades correspondentes.
class Aquarium(length: Int = 100, width: Int = 20, height: Int = 40) {
   // Dimensions in cm
   var length: Int = length
   var width: Int = width
   var height: Int = height
...
}
  1. A maneira mais compacta do Kotlin é definir as propriedades diretamente com o construtor usando var ou val. O Kotlin também cria os getters e setters automaticamente. Em seguida, remova as definições de propriedade no corpo da classe.
class Aquarium(var length: Int = 100, var width: Int = 20, var height: Int = 40) {
...
}
  1. Ao criar um objeto Aquarium com esse construtor, você não pode especificar argumentos e receber os valores padrão, especificar apenas alguns deles ou especificar todos eles e criar um Aquarium de tamanho totalmente personalizado. Na função buildAquarium(), teste maneiras diferentes de criar um objeto Aquarium usando os parâmetros nomeados.
fun buildAquarium() {
    val aquarium1 = Aquarium()
    aquarium1.printSize()
    // default height and length
    val aquarium2 = Aquarium(width = 25)
    aquarium2.printSize()
    // default width
    val aquarium3 = Aquarium(height = 35, length = 110)
    aquarium3.printSize()
    // everything custom
    val aquarium4 = Aquarium(width = 25, height = 35, length = 110)
    aquarium4.printSize()
}
  1. Execute o programa e observe a saída.
⇒ Width: 20 cm Length: 100 cm Height: 40 cm 
Width: 25 cm Length: 100 cm Height: 40 cm 
Width: 20 cm Length: 110 cm Height: 35 cm 
Width: 25 cm Length: 110 cm Height: 35 cm 

Você não precisou sobrecarregar o construtor e escrever uma versão diferente para cada um desses casos, além de algumas outras para as outras combinações. O Kotlin cria o que é necessário com base nos valores padrão e parâmetros nomeados.

Etapa 2: adicionar blocos de inicialização

Os construtores de exemplo acima apenas declaram propriedades e atribuem o valor de uma expressão a elas. Se o construtor precisar de mais código de inicialização, ele poderá ser colocado em um ou mais blocos init. Nesta etapa, você adicionará alguns blocos init à classe Aquarium.

  1. Na classe Aquarium, adicione um bloco init para exibir o objeto e um segundo bloco para exibir o volume em litros.
class Aquarium (var length: Int = 100, var width: Int = 20, var height: Int = 40) {
    init {
        println("aquarium initializing")
    }
    init {
        // 1 liter = 1000 cm^3
        println("Volume: ${width * length * height / 1000} l")
    }
}
  1. Execute o programa e observe a saída.
aquarium initializing
Volume: 80 l
Width: 20 cm Length: 100 cm Height: 40 cm 
aquarium initializing
Volume: 100 l
Width: 25 cm Length: 100 cm Height: 40 cm 
aquarium initializing
Volume: 77 l
Width: 20 cm Length: 110 cm Height: 35 cm 
aquarium initializing
Volume: 96 l
Width: 25 cm Length: 110 cm Height: 35 cm 

Os blocos init são executados na ordem em que aparecem na definição da classe, e todos eles são executados quando o construtor é chamado.

Etapa 3: saber mais sobre construtores secundários

Nesta etapa, você aprenderá sobre construtores secundários e adicionará um à classe. Além de um construtor principal, que pode ter um ou mais blocos init, uma classe Kotlin também pode ter um ou mais construtores secundários para permitir a sobrecarga de construtores, ou seja, construtores com argumentos diferentes.

  1. Na classe Aquarium, adicione um construtor secundário que aceita vários peixes como argumento, usando a palavra-chave constructor. Crie uma propriedade de tanque val para o volume calculado do aquário em litros com base no número de peixes. Considere 2.000 cm^3 de água por peixe, além de um pequeno espaço extra para que a água não derrame.
constructor(numberOfFish: Int) : this() {
    // 2,000 cm^3 per fish + extra room so water doesn't spill
    val tank = numberOfFish * 2000 * 1.1
}
  1. No construtor secundário, mantenha o comprimento e a largura (definidos no construtor principal) e calcule a altura necessária para fazer o tanque com o volume fornecido.
    // calculate the height needed
    height = (tank / (length * width)).toInt()
  1. Na função buildAquarium(), adicione uma chamada para criar um Aquarium usando o novo construtor secundário. Exiba o tamanho e o volume.
fun buildAquarium() {
    val aquarium6 = Aquarium(numberOfFish = 29)
    aquarium6.printSize()
    println("Volume: ${aquarium6.width * aquarium6.length * aquarium6.height / 1000} l")
}
  1. Execute o programa e observe o resultado.
⇒ aquarium initializing
Volume: 80 l
Width: 20 cm Length: 100 cm Height: 31 cm 
Volume: 62 l

Observe que o volume é impresso duas vezes, uma vez pelo bloco init no construtor principal antes da execução do construtor secundário e uma vez pelo código em buildAquarium().

Você também poderia incluir a palavra-chave constructor no construtor principal, mas isso não é necessário na maioria dos casos.

Etapa 4: adicionar um novo getter de propriedade

Nesta etapa, você adicionará um getter de propriedade explícito. O Kotlin define automaticamente getters e setters quando você define propriedades, mas, às vezes, o valor de uma propriedade precisa ser ajustado ou calculado. Por exemplo, acima, você exibiu o volume de Aquarium. Você pode disponibilizar o volume como uma propriedade definindo uma variável e um getter para ele. Como volume precisa ser calculado, o getter precisa retornar o valor calculado, o que pode ser feito com uma função de uma linha.

  1. Na classe Aquarium, defina uma propriedade Int com o nome volume e um método get() para calcular o volume na próxima linha.
val volume: Int
    get() = width * height * length / 1000  // 1000 cm^3 = 1 l
  1. Remova o bloco init que exibe o volume.
  2. Remova o código no buildAquarium() que exibe o volume.
  3. No método printSize(), adicione uma linha para exibir o volume.
fun printSize() {
    println("Width: $width cm " +
            "Length: $length cm " +
            "Height: $height cm "
    )
    // 1 l = 1000 cm^3
    println("Volume: $volume l")
}
  1. Execute o programa e observe o resultado.
⇒ aquarium initializing
Width: 20 cm Length: 100 cm Height: 31 cm 
Volume: 62 l

As dimensões e o volume são os mesmos de antes, mas o volume só é impresso uma vez depois que o objeto é totalmente inicializado pelo construtor principal e pelo construtor secundário.

Etapa 5: adicionar um setter de propriedade

Nesta etapa, você criará um novo setter de propriedade para o volume.

  1. Na classe Aquarium, altere volume para var para que ele possa ser definido mais de uma vez.
  2. Adicione um setter para a propriedade volume adicionando um método set() abaixo do getter, que recalcula a altura com base na quantidade fornecida de água. Por convenção, o nome do parâmetro setter é value, mas você pode alterá-lo, se preferir.
var volume: Int
    get() = width * height * length / 1000
    set(value) {
        height = (value * 1000) / (width * length)
    }
  1. Em buildAquarium(), adicione o código para definir o volume do aquário como 70 litros. Exiba o novo tamanho.
fun buildAquarium() {
    val aquarium6 = Aquarium(numberOfFish = 29)
    aquarium6.printSize()
    aquarium6.volume = 70
    aquarium6.printSize()
}
  1. Execute o programa novamente e observe a altura e o volume alterados.
⇒ aquarium initialized
Width: 20 cm Length: 100 cm Height: 31 cm 
Volume: 62 l
Width: 20 cm Length: 100 cm Height: 35 cm 
Volume: 70 l

Não houve modificadores de visibilidade, como public ou private, no código até agora. Isso ocorre porque, por padrão, tudo no Kotlin é público, o que significa que tudo pode ser acessado em qualquer lugar, incluindo classes, métodos, propriedades e variáveis de membros.

No Kotlin, classes, objetos, interfaces, construtores, funções, propriedades e os setters podem ter modificadores de visibilidade:

  • public significa visível fora da classe. Tudo é público por padrão, incluindo variáveis e métodos da classe.
  • internal significa que ele só estará visível nesse módulo. Um módulo é um conjunto de arquivos Kotlin compilados juntos, por exemplo, uma biblioteca ou aplicativo.
  • private significa que ele só estará visível nessa classe (ou arquivo de origem se você estiver trabalhando com funções).
  • protected é igual a private, mas também ficará visível para qualquer subclasse.

Consulte Modificadores de visibilidade (link em inglês) na documentação do Kotlin para saber mais.

Variáveis de participante

As propriedades em uma classe ou variáveis de membro são public por padrão. Se você defini-los com var, eles serão mutáveis, ou seja, legíveis e graváveis. Se você defini-los com val, eles serão somente leitura após a inicialização.

Se você quiser uma propriedade que o código possa ler ou gravar, mas o código externo só pode ler, deixe a propriedade e o getter dela como públicas e declare o setter como privado, conforme mostrado abaixo.

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

Nesta tarefa, você aprenderá como subclasses e herança funcionam no Kotlin. Elas são semelhantes às que você vê em outros idiomas, mas existem algumas diferenças.

No Kotlin, as classes não podem ser transformadas em subclasses por padrão. Da mesma forma, propriedades e variáveis de membros não podem ser substituídas por subclasses (embora possam ser acessadas).

Marque uma classe como open para que ela seja transformada em subclasse. Da mesma forma, você precisa marcar propriedades e variáveis de membro como open para substituí-las na subclasse. A palavra-chave open é obrigatória para evitar o vazamento acidental de detalhes de implementação como parte da interface da classe.

Etapa 1: abrir a classe Aquarium

Nesta etapa, você definirá a classe Aquarium como open para substituí-la na próxima etapa.

  1. Marque a classe Aquarium e todas as propriedades dela com a palavra-chave open.
open class Aquarium (open var length: Int = 100, open var width: Int = 20, open var height: Int = 40) {
    open var volume: Int
        get() = width * height * length / 1000
        set(value) {
            height = (value * 1000) / (width * length)
        }
  1. Adicione uma propriedade shape aberta com o valor "rectangle".
   open val shape = "rectangle"
  1. Adicione uma propriedade water aberta com um getter que retorne 90% do volume do Aquarium.
    open var water: Double = 0.0
        get() = volume * 0.9
  1. Adicione código ao método printSize() para exibir a forma e a quantidade de água como uma porcentagem do volume.
fun printSize() {
    println(shape)
    println("Width: $width cm " +
            "Length: $length cm " +
            "Height: $height cm ")
    // 1 l = 1000 cm^3
    println("Volume: $volume l Water: $water l (${water/volume*100.0}% full)")
}
  1. No buildAquarium(), mude o código para criar uma Aquarium com width = 25, length = 25 e height = 40.
fun buildAquarium() {
    val aquarium6 = Aquarium(length = 25, width = 25, height = 40)
    aquarium6.printSize()
}
  1. Execute o programa e observe a nova saída.
⇒ aquarium initializing
rectangle
Width: 25 cm Length: 25 cm Height: 40 cm 
Volume: 25 l Water: 22.5 l (90.0% full)

Etapa 2: criar uma subclasse

  1. Crie uma subclasse de Aquarium com o nome TowerTank, que implementa um tanque de cilindro arredondado em vez de um tanque retangular. Você pode adicionar TowerTank abaixo de Aquarium porque pode adicionar outra classe no mesmo arquivo da classe Aquarium.
  2. Em TowerTank, substitua a propriedade height, que é definida no construtor. Para modificar uma propriedade, use a palavra-chave override na subclasse.
  1. Faça com que o construtor para TowerTank use um diameter. Use a diameter para length e width ao chamar o construtor na superclasse Aquarium.
class TowerTank (override var height: Int, var diameter: Int): Aquarium(height = height, width = diameter, length = diameter) {
  1. Modifique a propriedade de volume para calcular um cilindro. A fórmula para um cilindro é Pi vezes o raio ao quadrado vezes a altura. Você precisa importar a constante PI de java.lang.Math.
    override var volume: Int
    // ellipse area = π * r1 * r2
    get() = (width/2 * length/2 * height / 1000 * PI).toInt()
    set(value) {
        height = ((value * 1000 / PI) / (width/2 * length/2)).toInt()
    }
  1. Em TowerTank, substitua a propriedade water por 80% do volume.
override var water = volume * 0.8
  1. Modifique o shape para que seja "cylinder".
override val shape = "cylinder"
  1. A classe TowerTank final ficará parecida com o código abaixo.

Aquarium.kt:

package example.myapp

import java.lang.Math.PI

... // existing Aquarium class

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

    override var water = volume * 0.8
    override val shape = "cylinder"
}
  1. Em buildAquarium(), crie uma TowerTank com um diâmetro de 25 cm e uma altura de 45 cm. Exiba o tamanho.

main.kt:

package example.myapp

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

Às vezes, você quer definir comportamentos ou propriedades comuns a serem compartilhados entre algumas classes relacionadas. O Kotlin oferece duas maneiras de fazer isso: interfaces e classes abstratas. Nesta tarefa, você criará uma classe AquariumFish abstrata para propriedades comuns a todos os peixes. Você cria uma interface chamada FishAction para definir um comportamento comum a todos os peixes.

  • Nem uma classe abstrata nem uma interface podem ser instanciadas por conta própria, o que significa que não é possível criar objetos desses tipos diretamente.
  • As classes abstratas têm construtores.
  • As interfaces não podem ter nenhuma lógica de construtor ou armazenar nenhum estado.

Etapa 1. Criar uma classe abstrata

  1. Em example.myapp, crie um novo arquivo, AquariumFish.kt.
  2. Crie uma classe, também chamada de AquariumFish, e marque-a com abstract.
  3. Adicione uma propriedade String, color, e marque-a com abstract.
package example.myapp

abstract class AquariumFish {
    abstract val color: String
}
  1. Crie duas subclasses de AquariumFish, Shark e Plecostomus.
  2. Como o color é abstrato, as subclasses precisam implementá-lo. Deixe o Shark cinza e Plecostomus dourado.
class Shark: AquariumFish() {
    override val color = "gray"
}

class Plecostomus: AquariumFish() {
    override val color = "gold"
}
  1. Em main.kt, crie uma função makeFish() para testar as classes. Instancie um Shark e um Plecostomus e exiba a cor de cada um.
  2. Exclua o código de teste anterior em main() e adicione uma chamada para makeFish(). O código ficará parecido com este:

main.kt:

package example.myapp

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

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

fun main () {
    makeFish()
}
  1. Execute o programa e observe o resultado.
⇒ Shark: gray 
Plecostomus: gold

O diagrama a seguir representa as classes Shark e Plecostomus, que têm como subclasse a classe abstrata AquariumFish.

Um diagrama mostrando a classe abstrata, AquariumFish e duas subclasses, Shark e Plecostumus.

Etapa 2: Criar uma interface

  1. Em AquariumFish.kt, crie uma interface chamada FishAction com um método eat().
interface FishAction  {
    fun eat()
}
  1. Adicione FishAction a cada uma das subclasses e implemente eat() fazendo com que ele exiba o que o peixe faz.
class Shark: AquariumFish(), FishAction {
    override val color = "gray"
    override fun eat() {
        println("hunt and eat fish")
    }
}

class Plecostomus: AquariumFish(), FishAction {
    override val color = "gold"
    override fun eat() {
        println("eat algae")
    }
}
  1. Na função makeFish(), faça com que cada peixe criado coma algo chamando eat().
fun makeFish() {
    val shark = Shark()
    val pleco = Plecostomus()
    println("Shark: ${shark.color}")
    shark.eat()
    println("Plecostomus: ${pleco.color}")
    pleco.eat()
}
  1. Execute o programa e observe o resultado.
⇒ Shark: gray
hunt and eat fish
Plecostomus: gold
eat algae

O diagrama a seguir representa a classe Shark e a classe Plecostomus, que são compostas e implementam a interface FishAction.

Quando usar classes abstratas x interfaces

Os exemplos acima são simples, mas quando você tem muitas classes interrelacionadas, as classes e interfaces abstratas podem ajudar a manter seu design mais limpo, mais organizado e mais fácil de manter.

Como mencionado acima, as classes abstratas podem ter construtores e as interfaces não, mas, caso contrário, são muito semelhantes. Quando você deve usar cada um?

Quando você usa interfaces para compor uma classe, a funcionalidade dela é estendida por meio das instâncias de classe que ela contém. A composição tende a facilitar a reutilização e o motivo do código em comparação à herança de uma classe abstrata. Além disso, você pode usar várias interfaces em uma classe, mas só pode criar uma subclasse a partir de uma classe abstrata.

A composição costuma melhorar o encapsulamento, reduzir a acoplamento (interdependência), interfaces mais limpas e criar códigos mais utilizáveis. Por isso, o melhor é usar a composição com interfaces. Por outro lado, a herança de uma classe abstrata tende a ser uma escolha natural para alguns problemas. Por isso, é melhor usar a composição, mas quando a herança faz sentido, o Kotlin também permite isso.

  • Use uma interface se você tiver muitos métodos e uma ou duas implementações padrão, como em AquariumAction abaixo.
interface AquariumAction {
    fun eat()
    fun jump()
    fun clean()
    fun catchFish()
    fun swim()  {
        println("swim")
    }
}
  • Use uma classe abstrata sempre que não for possível concluí-la. Por exemplo, de volta à classe AquariumFish, você pode fazer com que AquariumFish implemente FishAction e forneça uma implementação padrão para eat, deixando color abstrato, porque não há uma cor padrão para peixes.
interface FishAction  {
    fun eat()
}

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

A tarefa anterior introduziu classes abstratas, interfaces e a ideia de composição. A delegação de interface é uma técnica avançada em que os métodos de uma interface são implementados por um objeto auxiliar (ou delegado), que é usado por uma classe. Essa técnica pode ser útil ao usar uma interface em uma série de classes não relacionadas. Você adiciona a funcionalidade de interface necessária a uma classe auxiliar separada, e cada uma das classes usa uma instância da classe auxiliar para implementar a funcionalidade.

Nesta tarefa, você usará a delegação da interface para adicionar funcionalidades a uma classe.

Etapa 1: criar uma nova interface

  1. No AquariumFish.kt, remova a classe AquariumFish. Em vez de herdar da classe AquariumFish, Plecostomus e Shark implementarão interfaces para a ação e a cor do peixe.
  2. Crie uma nova interface, FishColor, que defina a cor como uma string.
interface FishColor {
    val color: String
}
  1. Mude Plecostomus para implementar duas interfaces, FishAction e FishColor. Você precisa modificar a color da FishColor e a eat() da FishAction.
class Plecostomus: FishAction, FishColor {
    override val color = "gold"
    override fun eat() {
        println("eat algae")
    }
}
  1. Mude a classe Shark para implementar também as duas interfaces, FishAction e FishColor, em vez de herdar de AquariumFish.
class Shark: FishAction, FishColor {
    override val color = "gray"
    override fun eat() {
        println("hunt and eat fish")
    }
}
  1. O código finalizado ficará da seguinte forma:
package example.myapp

interface FishAction {
    fun eat()
}

interface FishColor {
    val color: String
}

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

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

Etapa 2: criar uma classe Singleton

Em seguida, implemente a configuração da parte de delegação criando uma classe auxiliar que implemente FishColor. Você cria uma classe básica chamada GoldColor que implementa FishColor. Ela só diz que a cor é dourada.

Não faz sentido utilizar várias instâncias de GoldColor porque todas fazem exatamente a mesma coisa. Portanto, o Kotlin permite declarar uma classe em que só é possível criar uma instância dela usando a palavra-chave object em vez de class. O Kotlin criará essa instância, que será referenciada pelo nome da classe. Dessa forma, todos os outros objetos poderão usar essa única instância. Não há como fazer outras instâncias dessa classe. Se você conhece o padrão Singleton, veja como implementar Singletons em Kotlin.

  1. Em AquariumFish.kt, crie um objeto para GoldColor. Substitua a cor.
object GoldColor : FishColor {
   override val color = "gold"
}

Etapa 3: adicionar a delegação de interface ao FishColor

Agora você está pronto para usar a delegação de interface.

  1. Em AquariumFish.kt, remova a substituição de color de Plecostomus.
  2. Mude a classe Plecostomus para ver a cor de GoldColor. Para fazer isso, adicione by GoldColor à declaração de classe, criando a delegação. O que diz é que, em vez de implementar FishColor, use a implementação fornecida pela GoldColor. Portanto, sempre que a color for acessada, ela será delegada a GoldColor.
class Plecostomus:  FishAction, FishColor by GoldColor {
   override fun eat() {
       println("eat algae")
   }
}

Com a classe como estão, todos os Plecos ficarão dourados, mas esses peixes vêm em diversas cores. Para resolver isso, adicione um parâmetro construtor para a cor com GoldColor como a cor padrão para Plecostomus.

  1. Mude a classe Plecostomus para receber uma transmissão em fishColor com o construtor e defina o padrão como GoldColor. Altere a delegação de by GoldColor para by fishColor.
class Plecostomus(fishColor: FishColor = GoldColor):  FishAction,
       FishColor by fishColor {
   override fun eat() {
       println("eat algae")
   }
}

Etapa 4: adicionar a delegação da interface para FishAction

Da mesma forma, você pode usar a delegação de interface para o FishAction.

  1. No AquariumFish.kt, crie uma classe PrintingFishAction que implemente FishAction, que usa um String, food e exibe o que o peixe consome.
class PrintingFishAction(val food: String) : FishAction {
    override fun eat() {
        println(food)
    }
}
  1. Na classe Plecostomus, remova a função de substituição eat(), porque ela será substituída por uma delegação.
  2. Na declaração de Plecostomus, delegue FishAction a PrintingFishAction, transmitindo "eat algae".
  3. Com toda essa delegação, não há código no corpo da classe Plecostomus. Portanto, remova o {}, porque todas as substituições são processadas pela delegação de interface.
class Plecostomus (fishColor: FishColor = GoldColor):
        FishAction by PrintingFishAction("eat algae"),
        FishColor by fishColor

O diagrama a seguir representa as classes Shark e Plecostomus, ambas compostas pelas interfaces PrintingFishAction e FishColor, mas delegando a implementação a elas.

A delegação de interface é poderosa e, geralmente, você deve considerar como usá-la sempre que puder usar uma classe abstrata em outra linguagem. Ele permite usar a composição para inserir comportamentos em vez de exigir muitas subclasses, cada uma especializada de uma forma diferente.

Em alguns outros idiomas, uma classe de dados é semelhante a uma struct. Ela existe principalmente para armazenar alguns dados, mas um objeto de classe de dados ainda é um objeto. Os objetos de classe de dados do Kotlin têm alguns outros benefícios, como utilitários para impressão e cópia. Nesta tarefa, você criará uma classe de dados simples e aprenderá sobre a compatibilidade do Kotlin com essas classes.

Etapa 1: criar uma classe de dados

  1. Adicione um novo pacote decor no pacote example.myapp para armazenar o novo código. Clique com o botão direito do mouse em example.myapp no painel Project e selecione File > New > Package.
  2. No pacote, crie uma nova classe com o nome Decoration.
package example.myapp.decor

class Decoration {
}
  1. Para transformar Decoration em uma classe de dados, use a palavra-chave data como prefixo na declaração de classe.
  2. Adicione uma propriedade String com o nome rocks para fornecer alguns dados à classe.
data class Decoration(val rocks: String) {
}
  1. No arquivo, fora da classe, adicione uma função makeDecorations() para criar e imprimir uma instância de um Decoration com "granite".
fun makeDecorations() {
    val decoration1 = Decoration("granite")
    println(decoration1)
}
  1. Adicione uma função main() para chamar makeDecorations() e executar o programa. Observe o resultado gerado que é criado porque essa é uma classe de dados.
⇒ Decoration(rocks=granite)
  1. Em makeDecorations(), instancie mais dois objetos Decoration que são ambos "slate" e os exiba.
fun makeDecorations() {
    val decoration1 = Decoration("granite")
    println(decoration1)

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

    val decoration3 = Decoration("slate")
    println(decoration3)
}
  1. Em makeDecorations(), adicione uma instrução que exiba o resultado da comparação de decoration1 com decoration2 e uma segunda comparação com decoration3 com decoration2. Use o método equals() fornecido pelas classes de dados.
    println (decoration1.equals(decoration2))
    println (decoration3.equals(decoration2))
  1. Execute o código.
⇒ Decoration(rocks=granite)
Decoration(rocks=slate)
Decoration(rocks=slate)
false
true

Etapa 2: Como desestruturar

Para chegar às propriedades de um objeto de dados e atribuí-las a variáveis, atribua uma de cada vez, desta forma.

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

Em vez disso, é possível criar variáveis, uma para cada propriedade, e atribuir o objeto de dados ao grupo de variáveis. O Kotlin coloca o valor da propriedade em cada variável.

val (rock, wood, diver) = decoration

Isso é chamado de desestruturação e é uma abreviação útil. O número de variáveis precisa corresponder ao número de propriedades, e elas são atribuídas na ordem em que são declaradas na classe. Veja um exemplo completo em Decoration.kt.

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

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

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

Se você não precisar de uma ou mais propriedades, pode ignorá-las usando _ em vez de um nome de variável, como mostrado no código abaixo.

    val (rock, _, diver) = d5

Nesta tarefa, você aprenderá sobre algumas das classes específicas do Kotlin, incluindo as seguintes:

  • Aulas de Singleton
  • Enums
  • Aulas fechadas

Etapa 1: recuperar classes Singleton

Lembre-se do exemplo anterior com a classe GoldColor.

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

Como cada instância de GoldColor faz a mesma coisa, ela é declarada como object em vez de class para torná-la um Singleton. Só pode haver uma instância.

Etapa 2: criar uma enumeração

O Kotlin também é compatível com enumerações, que permitem enumerar e referir-se a elas pelo nome, assim como em outras linguagens. Declare uma enumeração prefixando a declaração com a palavra-chave enum. Uma declaração de enumeração básica só precisa de uma lista de nomes, mas também é possível definir um ou mais campos associados a cada nome.

  1. Em Decoration.kt, teste um exemplo de enumeração.
enum class Color(val rgb: Int) {
   RED(0xFF0000), GREEN(0x00FF00), BLUE(0x0000FF);
}

Enums são um pouco parecidos com Singletons. Pode haver apenas um e apenas um de cada valor na enumeração. Por exemplo, só pode haver Color.RED, Color.GREEN e Color.BLUE. Neste exemplo, os valores RGB são atribuídos à propriedade rgb para representar os componentes de cor. Também é possível descobrir o valor ordinal de uma enumeração usando a propriedade ordinal e o nome dela usando a propriedade name.

  1. Teste outro exemplo de enumeração.
enum class Direction(val degrees: Int) {
    NORTH(0), SOUTH(180), EAST(90), WEST(270)
}

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

Etapa 3: criar uma classe selada

Uma classe selada pode ser uma subclasse, mas somente no arquivo em que é declarada. Se você tentar criar uma subclasse em um arquivo diferente, receberá uma mensagem de erro.

Como as classes e subclasses ficam no mesmo arquivo, o Kotlin conhecerá todas as subclasses estaticamente. Ou seja, no tempo de compilação, o compilador vê todas as classes e subclasses e sabe que todas elas são para que ele possa fazer verificações adicionais para você.

  1. No AquariumFish.kt, teste um exemplo de classe fechada, com o tema aquático.
sealed class Seal
class SeaLion : Seal()
class Walrus : Seal()

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

A classe Seal não pode ser uma subclasse em outro arquivo. Se você quiser adicionar mais tipos de Seal, será necessário adicioná-los no mesmo arquivo. Isso torna as classes seladas uma maneira segura de representar um número fixo de tipos. Por exemplo, classes seladas são ótimas para retornar um sucesso ou um erro em uma API de rede.

Esta lição abordava muitos aspectos. Embora grande parte dele deva ser conhecida por outras linguagens de programação orientadas a objetos, o Kotlin adiciona alguns recursos para manter o código conciso e legível.

Classes e construtores

  • Defina uma classe no Kotlin usando class.
  • O Kotlin cria automaticamente setters e getters para propriedades.
  • Defina o construtor principal diretamente na definição da classe. Exemplo:
    class Aquarium(var length: Int = 100, var width: Int = 20, var height: Int = 40)
  • Se um construtor principal precisar de mais código, grave-o em um ou mais blocos init.
  • Uma classe pode definir um ou mais construtores secundários usando constructor. No entanto, o estilo Kotlin é usar uma função de fábrica.

Modificadores e subclasses de visibilidade

  • Todas as classes e funções em Kotlin são public por padrão, mas você pode usar modificadores para mudar a visibilidade para internal, private ou protected.
  • Para criar uma subclasse, a classe pai precisa ser marcada como open.
  • Para substituir os métodos e propriedades em uma subclasse, eles precisam ser marcados como open na classe pai.
  • Uma classe selada só pode ser transformada em subclasse no mesmo arquivo em que é definida. Faça uma classe selada adicionando o prefixo sealed à declaração.

Classes de dados, Singletons e enumerações

  • Faça uma classe de dados prefixando a declaração com data.
  • Desestruturação é um atalho para atribuir as propriedades de um objeto data para separar variáveis.
  • Crie uma classe Singleton usando object em vez de class.
  • Definir uma enumeração usando enum class

Classes, interfaces e delegação abstratas

  • Classes e interfaces abstratas são duas maneiras de compartilhar comportamentos comuns entre classes.
  • Uma classe abstrata define propriedades e comportamento, mas deixa a implementação para subclasses.
  • Uma interface define o comportamento e pode fornecer implementações padrão para alguns ou todos os comportamentos.
  • Quando você usa interfaces para compor uma classe, a funcionalidade dela é estendida por meio das instâncias de classe que ela contém.
  • A delegação de interface usa a composição, mas também delega a implementação às classes de interface.
  • A composição é uma forma eficiente de adicionar funcionalidade a uma classe usando a delegação de interface. Em geral, a composição é preferível, mas a herança de uma classe abstrata é mais adequada para alguns problemas.

Documentação do Kotlin

Se quiser mais informações sobre qualquer assunto deste curso ou se tiver dificuldades, https://kotlinlang.org é seu melhor ponto de partida.

Tutoriais do Kotlin

O site https://try.kotlinlang.org (link em inglês) tem tutoriais elaborados chamados Kotlin Koans, um intérprete baseado na Web, e um conjunto completo de documentação de referência com exemplos.

Curso Udacity

Para ver o curso da Udacity sobre esse assunto, consulte Bootcamp de Kotlin para programadores (link em inglês).

IntelliJ IDEA

A documentação do IntelliJ IDEA (em inglês) pode ser encontrada no site da JetBrains.

Esta seção lista as possíveis atividades para os alunos que estão trabalhando neste codelab como parte de um curso ministrado por um instrutor. Cabe ao instrutor fazer o seguinte:

  • Se necessário, atribua o dever de casa.
  • Informe aos alunos como enviar o dever de casa.
  • Atribua nota aos trabalhos de casa.

Os professores podem usar essas sugestões o quanto quiserem, e eles devem se sentir à vontade para passar o dever de casa como achar adequado.

Se você estiver fazendo este codelab por conta própria, use essas atividades para testar seu conhecimento.

Responda a estas perguntas

Pergunta 1

As classes têm um método especial que serve como modelo para a criação de objetos dessa classe. Como se chama o método?

▢ Um criador

▢ Um instanciador

▢ Um construtor

▢ Um modelo

Pergunta 2

Qual das seguintes afirmações sobre interfaces e classes abstratas NÃO está correta?

▢ As classes abstratas podem ter construtores.

▢ As interfaces não podem ter construtores.

▢ As interfaces e classes abstratas podem ser instanciadas diretamente.

▢ As propriedades abstratas precisam ser implementadas por subclasses da classe abstrata.

Pergunta 3

Qual das seguintes opções NÃO é um modificador de visibilidade do Kotlin para propriedades, métodos etc.?

internal

nosubclass

protected

private

Pergunta 4

Considere esta classe de dados:
data class Fish(val name: String, val species:String, val colors:String)
Qual das opções a seguir NÃO é um código válido para criar e desestruturar um objeto Fish?

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

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

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

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

Pergunta 5

Digamos que você tenha um zoológico com muitos animais que precisam ser cuidadodos. Qual alternativa NÃO faz parte da implementação de cuidados?

interface: tipos de alimentos que os animais comem.

▢ Uma classe abstract Caretaker que permite criar diferentes tipos de cuidador.

▢ Uma interface para fornecer água limpa a um animal.

▢ Uma classe data para uma entrada em uma programação de alimentação.

Vá para a próxima lição: 5.1 Extensões

Para ter uma visão geral do curso, incluindo links para outros codelabs, consulte "Bootcamp de Kotlin para programadores: bem-vindo ao curso."