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

Este codelab faz parte do curso de treinamento do Kotlin para programadores. Você vai aproveitar mais este 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ê vai criar um programa e aprender sobre classes e objetos no Kotlin. Grande parte desse conteúdo será familiar se você conhecer outra linguagem orientada a objetos, mas o Kotlin tem algumas diferenças importantes para reduzir a quantidade de código que você precisa escrever. Você também vai aprender sobre classes abstratas e delegação de interface.

Em vez de criar um único app de exemplo, as lições deste curso foram criadas para aumentar seu conhecimento, mas são semi-independentes umas das outras para que você possa ler as seções com que já está familiarizado. Para unificar os exemplos, muitos deles usam um tema de aquário. Se quiser conferir a história completa do aquário, acesse o curso da Udacity Treinamento do Kotlin para programadores (link em inglês).

O que você já precisa saber

  • Os conceitos básicos do Kotlin, incluindo tipos, operadores e loops
  • Sintaxe de função do Kotlin
  • Conceitos básicos da programação orientada a objetos
  • Os 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 no 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, enums e classes seladas

Atividades deste laboratório

  • Criar uma classe com propriedades
  • Criar um construtor para uma classe
  • Criar uma subclasse
  • Analisar exemplos de classes abstratas e interfaces
  • Criar uma classe de dados simples
  • Saiba mais sobre singletons, enums e classes seladas

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

  • As classes são plantas 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 é um Aquarium real.
  • Propriedades são características de classes, como comprimento, largura e altura de um Aquarium.
  • Os 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 fillWithWater() um objeto Aquarium.
  • Uma interface é uma especificação que uma classe pode implementar. Por exemplo, a limpeza é comum a objetos que não são aquários, e geralmente acontece de maneira semelhante para diferentes objetos. Assim, você pode ter uma interface chamada Clean que define um método clean(). A classe Aquarium pode implementar a interface Clean para limpar o aquário com uma esponja macia.
  • Pacotes são uma maneira de agrupar códigos relacionados para manter tudo organizado ou criar uma biblioteca de código. Depois que um pacote é criado, é possível importar o conteúdo dele para outro arquivo e reutilizar o código e as classes nele.

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

Etapa 1: criar um pacote

Os pacotes ajudam a manter seu código organizado.

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

Etapa 2: criar uma classe com propriedades

As classes são definidas com a palavra-chave class, e os nomes de classe, por convenção, começam com uma letra maiúscula.

  1. Clique com o botão direito do mouse no pacote example.myapp.
  2. Selecione New > Kotlin File / Class.
  3. Em Kind, selecione Class e nomeie a classe como Aquarium. O IntelliJ IDEA inclui o nome do pacote no arquivo e cria uma classe Aquarium vazia para você.
  4. Dentro da 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. Assim, é possível acessar as propriedades diretamente, por exemplo, myAquarium.length.

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

Crie um arquivo chamado main.kt para armazenar a função main().

  1. No painel Projeto à esquerda, clique com o botão direito do mouse no pacote example.myapp.
  2. Selecione New > Kotlin File / Class.
  3. No menu suspenso Tipo, mantenha a seleção como Arquivo 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 crie uma instância de Aquarium dentro dela. Para criar uma instância, faça referência à classe como se 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 imprimir 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. Para executar o programa, clique 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 imprimir as propriedades de dimensão alteradas.
fun buildAquarium() {
    val myAquarium = Aquarium()
    myAquarium.printSize()
    myAquarium.height = 60
    myAquarium.printSize()
}
  1. Execute o programa e observe a saída.
⇒ Width: 20 cm Length: 100 cm Height: 40 cm 
Width: 20 cm Length: 100 cm Height: 60 cm 

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

Etapa 1: criar um construtor

Nesta etapa, você adiciona um construtor à classe Aquarium criada na primeira tarefa. No exemplo anterior, todas as instâncias de Aquarium são criadas com as mesmas dimensões. É possível mudar as dimensões depois que ela é criada definindo as propriedades, mas seria mais simples criar com o tamanho correto desde o início.

Em algumas linguagens de programação, o construtor é definido criando um método na classe com o mesmo nome dela. Em Kotlin, você define o construtor diretamente na declaração de classe, especificando os parâmetros entre parênteses como se a classe fosse um método. Assim como nas funções em Kotlin, esses parâmetros podem incluir valores padrão.

  1. Na classe Aquarium criada anteriormente, mude 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ê pode não especificar argumentos e receber os valores padrão, especificar apenas alguns deles ou especificar todos e criar um Aquarium de tamanho totalmente personalizado. Na função buildAquarium(), teste diferentes maneiras de criar um objeto Aquarium usando 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 

Não foi preciso sobrecarregar o construtor e escrever uma versão diferente para cada um desses casos (além de mais alguns para as outras combinações). O Kotlin cria o que é necessário com base nos valores padrão e nos 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ê vai adicionar alguns blocos init à classe Aquarium.

  1. Na classe Aquarium, adicione um bloco init para mostrar que o objeto está sendo inicializado e um segundo bloco para mostrar 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: saiba mais sobre construtores secundários

Nesta etapa, você vai aprender sobre construtores secundários e adicionar um à sua 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 receba um número de peixes como argumento, usando a palavra-chave constructor. Crie uma propriedade de val para o volume calculado do aquário em litros com base no número de peixes. Considere 2 litros (2.000 cm³) de água por peixe, além de um pouco mais de espaço para que a água não transborde.
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) iguais e calcule a altura necessária para que o tanque tenha o volume especificado.
    // 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. Mostre 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 a saída.
⇒ aquarium initializing
Volume: 80 l
Width: 20 cm Length: 100 cm Height: 31 cm 
Volume: 62 l

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

Você também poderia ter incluído 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ê adiciona 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ê imprimiu o volume do Aquarium. É possível disponibilizar o volume como uma propriedade definindo uma variável e um getter para ela. 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 chamada volume e um método get() que calcula 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 imprime o volume.
  2. Remova o código em buildAquarium() que imprime o volume.
  3. No método printSize(), adicione uma linha para imprimir 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 a saída.
⇒ 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 secundário.

Etapa 5: adicionar um setter de propriedade

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

  1. Na classe Aquarium, mude volume para um 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 de água fornecida. Por convenção, o nome do parâmetro setter é value, mas você pode mudar 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 em 70 litros. Mostre 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

Até agora, não houve modificadores de visibilidade, como public ou private, no código. Isso acontece 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 membro.

Em Kotlin, classes, objetos, interfaces, construtores, funções, propriedades e respectivos 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ó vai ficar visível dentro desse módulo. Um módulo é um conjunto de arquivos Kotlin compilados juntos, por exemplo, uma biblioteca ou um aplicativo.
  • private significa que ele só vai ficar visível nessa classe (ou arquivo de origem, se você estiver trabalhando com funções).
  • protected é igual a private, mas também fica visível para todas as subclasses.

Consulte Modificadores de visibilidade na documentação do Kotlin para mais informações.

Variáveis de membro

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

Se você quiser uma propriedade que seu código possa ler ou gravar, mas que um código externo só possa ler, deixe a propriedade e o getter como públicos 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ê vai aprender como as subclasses e a herança funcionam no Kotlin. Elas são parecidas com o que você já viu em outros idiomas, mas há algumas diferenças.

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

É necessário marcar uma classe como open para permitir 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ê vai tornar a classe Aquarium open para poder substituir 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 imprimir 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. Em buildAquarium(), mude o código para criar um 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 chamada TowerTank, que implementa um tanque cilíndrico arredondado em vez de um tanque retangular. Você pode adicionar TowerTank abaixo de Aquarium, porque é possível adicionar outra classe no mesmo arquivo da classe Aquarium.
  2. Em TowerTank, substitua a propriedade height, que é definida no construtor. Para substituir uma propriedade, use a palavra-chave override na subclasse.
  1. Faça com que o construtor de TowerTank use um diameter. Use o 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. Substitua a propriedade de volume para calcular um cilindro. A fórmula para um cilindro é pi vezes o raio ao quadrado vezes a altura. É necessário 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 para que ela seja 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 vai 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 um TowerTank com diâmetro de 25 cm e altura de 45 cm. Mostre 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 a saída.
⇒ 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 um comportamento ou propriedades comuns para serem compartilhados entre algumas classes relacionadas. O Kotlin oferece duas maneiras de fazer isso: interfaces e classes abstratas. Nesta tarefa, você vai criar uma classe abstrata AquariumFish para propriedades comuns a todos os peixes. Você cria uma interface chamada FishAction para definir um comportamento comum a todos os peixes.

  • Não é possível instanciar uma classe abstrata nem uma interface 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 lógica de construtor nem armazenar estados.

Etapa 1. Criar uma classe abstrata

  1. Em example.myapp, crie um arquivo chamado AquariumFish.kt.
  2. Crie uma classe, também chamada 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 color é abstrato, as subclasses precisam implementá-lo. Deixe 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 suas classes. Instancie um Shark e um Plecostomus e imprima a cor de cada um.
  2. Exclua o código de teste anterior em main() e adicione uma chamada para makeFish(). O código vai ficar assim:

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 a saída.
⇒ Shark: gray 
Plecostomus: gold

O diagrama a seguir representa as classes Shark e Plecostomus, que são subclasses da classe abstrata AquariumFish.

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

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() para que ele imprima 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 a saída.
⇒ Shark: gray
hunt and eat fish
Plecostomus: gold
eat algae

O diagrama a seguir representa as classes Shark e Plecostomus, ambas compostas e implementando a interface FishAction.

Quando usar classes abstratas em vez de interfaces

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

Como observado acima, as classes abstratas podem ter construtores, e as interfaces não, mas, de outra forma, elas são muito semelhantes. Então, quando usar cada uma?

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 raciocínio sobre o código do que a herança de uma classe abstrata. Além disso, é possível usar várias interfaces em uma classe, mas só é possível criar subclasses de uma classe abstrata.

A composição geralmente leva a um melhor encapsulamento, menor acoplamento (interdependência), interfaces mais limpas e código mais utilizável. Por esses motivos, usar composição com interfaces é o design preferido. Por outro lado, a herança de uma classe abstrata tende a ser uma opção natural para alguns problemas. Portanto, prefira a composição, mas quando a herança fizer sentido, o Kotlin também permite isso.

  • Use uma interface se você tiver muitos métodos e uma ou duas implementações padrão, por exemplo, 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 concluir uma classe. Por exemplo, voltando à classe AquariumFish, é possível fazer com que todos os AquariumFish implementem FishAction e fornecer 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 apresentou 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 quando você usa uma interface em uma série de classes não relacionadas: adicione 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ê vai usar a delegação de interface para adicionar funcionalidade a uma classe.

Etapa 1: criar uma nova interface

  1. Em AquariumFish.kt, remova a classe AquariumFish. Em vez de herdar da classe AquariumFish, Plecostomus e Shark vão implementar interfaces para a ação do peixe e a cor dele.
  2. Crie uma nova interface, FishColor, que define a cor como uma string.
interface FishColor {
    val color: String
}
  1. Mude Plecostomus para implementar duas interfaces, FishAction e um FishColor. É necessário substituir o color de FishColor e o eat() de FishAction.
class Plecostomus: FishAction, FishColor {
    override val color = "gold"
    override fun eat() {
        println("eat algae")
    }
}
  1. Mude a classe Shark para também implementar 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 vai ficar assim:
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 implementa FishColor. Você cria uma classe básica chamada GoldColor que implementa FishColor. Tudo o que ela faz é dizer que a cor é dourada.

Não faz sentido criar várias instâncias de GoldColor, porque todas fariam exatamente a mesma coisa. Assim, o Kotlin permite declarar uma classe em que só é possível criar uma instância usando a palavra-chave object em vez de class. O Kotlin vai criar essa instância, que é referenciada pelo nome da classe. Então, todos os outros objetos podem usar apenas essa instância. Não há como criar outras instâncias dessa classe. Se você conhece o padrão singleton, saiba 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 delegação de interface para FishColor

Agora você pode usar a delegação de interface.

  1. Em AquariumFish.kt, remova a substituição de color de Plecostomus.
  2. Mude a classe Plecostomus para receber a cor de GoldColor. Para fazer isso, adicione by GoldColor à declaração de classe, criando a delegação. Isso significa que, em vez de implementar FishColor, use a implementação fornecida por GoldColor. Assim, sempre que color é acessado, ele é delegado a GoldColor.
class Plecostomus:  FishAction, FishColor by GoldColor {
   override fun eat() {
       println("eat algae")
   }
}

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

  1. Mude a classe Plecostomus para receber uma fishColor transmitida com o construtor e defina o padrão como GoldColor. Mude 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 delegação de interface para FishAction

Da mesma forma, é possível usar a delegação de interface para o FishAction.

  1. Em AquariumFish.kt, crie uma classe PrintingFishAction que implemente FishAction, que usa um String, food e imprime o que o peixe come.
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 para 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 você geralmente deve considerar como usá-la sempre que usar uma classe abstrata em outra linguagem. Ela permite usar a composição para conectar comportamentos, em vez de exigir muitas subclasses, cada uma especializada de uma maneira diferente.

Uma classe de dados é semelhante a uma struct em algumas outras linguagens. 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 benefícios extras, como utilitários para impressão e cópia. Nesta tarefa, você vai criar uma classe de dados simples e aprender sobre o suporte que o Kotlin oferece para elas.

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 chamada Decoration.
package example.myapp.decor

class Decoration {
}
  1. Para transformar Decoration em uma classe de dados, adicione a palavra-chave data como prefixo à declaração da classe.
  2. Adicione uma propriedade String chamada rocks para dar 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 execute o programa. Observe a saída sensata criada porque esta é uma classe de dados.
⇒ Decoration(rocks=granite)
  1. Em makeDecorations(), crie mais dois objetos Decoration que sejam "slate" e imprima-os.
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 de impressão que mostre o resultado da comparação de decoration1 com decoration2 e outra que compare 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. Usar desestruturação

Para acessar as propriedades de um objeto de dados e atribuí-las a variáveis, você pode fazer isso uma de cada vez, assim:

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

Em vez disso, você pode 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 de propriedades, e as variáveis são atribuídas na ordem em que são declaradas na classe. Confira um exemplo completo que você pode testar 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, pule-as usando _ em vez de um nome de variável, conforme mostrado no código abaixo.

    val (rock, _, diver) = d5

Nesta tarefa, você vai aprender sobre algumas das classes de propósito especial em Kotlin, incluindo:

  • Classes singleton
  • Enums
  • Classes seladas

Etapa 1: relembrar classes singleton

Relembre o exemplo anterior com a classe GoldColor.

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

Como todas as instâncias de GoldColor fazem a mesma coisa, ela é declarada como um object em vez de um class para se tornar um singleton. Só pode haver uma instância dele.

Etapa 2: criar uma enumeração

O Kotlin também oferece suporte a enums, que permitem enumerar algo e se referir a ele por nome, assim como em outras linguagens. Declare uma enumeração adicionando a palavra-chave enum como prefixo à declaração. Uma declaração de enumeração básica só precisa de uma lista de nomes, mas você também pode definir um ou mais campos associados a cada nome.

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

Enums são um pouco como singletons: só pode haver um, e apenas um de cada valor na enumeração. Por exemplo, só pode haver um Color.RED, um Color.GREEN e um Color.BLUE. Neste exemplo, os valores RGB são atribuídos à propriedade rgb para representar os componentes de cor. Também é possível receber 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 sealed

Uma classe selada é uma classe que pode ser subclassificada, mas apenas dentro do arquivo em que é declarada. Se você tentar criar uma subclasse em um arquivo diferente, vai receber um erro.

Como as classes e subclasses estão no mesmo arquivo, o Kotlin conhece todas as subclasses de forma estática. Ou seja, no momento da compilação, o compilador vê todas as classes e subclasses e sabe que são todas elas. Assim, ele pode fazer verificações extras para você.

  1. Em AquariumFish.kt, confira um exemplo de classe selada, mantendo 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 subclassificada em outro arquivo. Se você quiser adicionar mais tipos de Seal, faça isso no mesmo arquivo. Isso torna as classes seladas uma maneira segura de representar um número fixo de tipos. Por exemplo, classes sealed são ótimas para retornar sucesso ou erro de uma API de rede.

Esta lição abordou muita coisa. Embora grande parte seja familiar de 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 em 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 código adicional, escreva-o em um ou mais blocos init.
  • Uma classe pode definir um ou mais construtores secundários usando constructor, mas o estilo Kotlin é usar uma função de fábrica.

Modificadores de visibilidade e subclasses

  • Todas as classes e funções no 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 mãe precisa ser marcada como open.
  • Para substituir métodos e propriedades em uma subclasse, eles precisam ser marcados como open na classe pai.
  • Uma classe sealed só pode ser subclassificada no mesmo arquivo em que é definida. Crie uma classe selada adicionando sealed como prefixo à declaração.

Classes de dados, singletons e enums

  • Crie uma classe de dados adicionando data como prefixo à declaração.
  • A desestruturação é uma abreviação para atribuir as propriedades de um objeto data a variáveis separadas.
  • Crie uma classe singleton usando object em vez de class.
  • Defina uma enumeração usando enum class.

Classes abstratas, interfaces e delegação

  • Classes abstratas e interfaces 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 parte ou todo o comportamento.
  • 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 composição, mas também delega a implementação às classes de interface.
  • A composição é uma maneira 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 você quiser mais informações sobre algum tópico deste curso ou se tiver dúvidas, https://kotlinlang.org é o melhor ponto de partida.

Tutoriais do Kotlin

O site https://try.kotlinlang.org inclui tutoriais avançados chamados Kotlin Koans, um interpretador baseado na Web e um conjunto completo de documentação de referência com exemplos.

Curso Udacity

Para conferir o curso da Udacity sobre esse tema, consulte Treinamento do Kotlin para programadores (link em inglês).

IntelliJ IDEA

A documentação do IntelliJ IDEA está disponível no site da JetBrains.

Esta seção lista as possíveis atividades de dever de casa para os alunos que estão fazendo este codelab como parte de um curso ministrado por um professor. Cabe ao professor fazer o seguinte:

  • Atribuir o dever de casa, se necessário.
  • Informar aos alunos como enviar deveres de casa.
  • Atribuir nota aos deveres de casa.

Os professores podem usar essas sugestões o quanto quiserem, podendo passar os exercícios que acharem mais apropriados como dever de casa.

Se você estiver seguindo este codelab por conta própria, sinta-se à vontade para usar esses deveres de casa para testar seu conhecimento.

Responda estas perguntas

Pergunta 1

As classes têm um método especial que serve como modelo para criar objetos dessa classe. Qual é o nome do método?

▢ Um builder

▢ Um instanciador

▢ Um construtor

▢ Um blueprint

Pergunta 2

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

▢ Classes abstratas podem ter construtores.

▢ As interfaces não podem ter construtores.

▢ Interfaces e classes abstratas podem ser instanciadas diretamente.

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

Pergunta 3

Qual das opções a seguir 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 de cuidados. Qual das opções a seguir NÃO faz parte da implementação da proteção?

▢ Uma interface para diferentes tipos de alimentos que os animais comem.

▢ Uma classe abstract Caretaker em que é possível criar diferentes tipos de responsáveis.

▢ Um interface por dar água limpa a um animal.

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

Acesse a próxima lição: 5.1 Extensões

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