0

11 dicas (não tão) rápidas sobre Kotlin

#Kotlin
Francisco Souza
Francisco Souza

Pessoal estava pesquisando sobre a linguagem kotlin e vi essas dicas e achei importante compartilha. att Francisco.


Vamos às dicas:

1. Validação de argumentos

As vezes temos funções que queremos validar nossas entradas. Geralmente escrevemos alguns códigos do tipo


fun something(url: String): String {

if (url.startsWith("http://"))

{ throw new IllegalArgumentException("URL deve ser https") }}


Observe que se neste caso tivéssemos várias validações, repetiríamos várias vezes if (...) e o throw new IllegalArgumentException("...").

Uma forma que Kotlin nos provê nativamente para essas tratativas são as funções require e check. A diferença básica entre as duas é que require deve ser utilizada para validar se uma entrada é válida ou não, lançando sempre a exceção IllegalArgumentException, enquanto o check lança exceção IllegalStateException e deve ser utilizado para validações de estados, como por exemplo, validar se a idade de uma pessoa condiz com o fato de ela poder beber. 

Veja o exemplo:

fun podeBeber(nome: String, idade: Int, podeBeberValidator: (Int) -> Boolean)

{ require(nome.isNotEmpty()) { "Nome não pode ser vazio" }

require(idade > 0) { "Idade não pode ser menor que zero" }

check(podeBeberValidator(idade)) { "Idade insuficiente para consumir bebidas" }}


2. Sealed Class


Sealed classes, ou classes seladas, são classes que representam uma hierarquia de classes muito restrita, por exemplo quando você tem um conjunto muito restrito de tipos. 

Uma vantagem de se utilizar classes seladas é que além de proibir desenvolvedores de criarem novas subclasses, quando utilizada com o pattern matching when em forma de expressão do Kotlin, ele precisa ser exaustivo, sendo que como é um subconjunto muito bem definido, não existe a necessidade do else, sendo necessário implementar todos os casos.

Alguns usos comuns de sealed classes em Kotlin podem ser feitos ao executar chamadas Http, onde, em teoria, você poderia retornar 3 tipos de valores: HttpSuccess para retornos da família 2xx, ClientError para retornos da família 4xx, ServerError para retornos da família 5xx, etc.

Outro uso que pode ser destacado seria quando se utilizado com alguns conceitos de programação funcional, onde podemos encapsular uma chamada de função em um Success ou um Error

Por exemplo:


sealed class Resultdata class Success<R>(val result: R) : Result()data class Error(val error: Throwable) : Result()

fun <R> execute(action: () -> R): Result = try { Success(action())} catch (e: Throwable) { Error(e)}

fun main(args: Array<String>) { when (val result = execute { check(args.size > 2) { "Missing arguments" }; "OK" }) { is Success<*> -> println(result.result) is Error -> println("Failed due: ${result.error.message}") }}


3. Lazy initialization

As vezes queremos executar alguma ação em nosso código, mas essa ação pode ser demorada e gostaríamos de postergar isso o máximo possível. 

Uma forma de evitar essa chamada é utilizando o recurso de lazy initialization (inicialização preguiçosa). Vamos supor que tenham duas classes, LazyHolder e VeryHeavyClass, onde eu precise criar uma instância de LazyHolder, mas quero evitar a execução de VeryHeavyClass até algum momento que seja mais propício. 

Um exemplo prático poderia ser na construção de um framework, onde o container de injeção de dependência não criasse as dependências na inicialização da aplicação, fazendo ela subir mais rápido, mas apenas no ato do uso, fazendo a primeira chamada ser um pouco mais lenta, mas com uma carga que poderia ser dividida ao longo do ciclo de vida do projeto. 

Exemplo:


import java.util.stream.Collectorsimport kotlin.system.measureTimeMillis

class VeryHeavyClass { val someProperty: String

init { Thread.sleep(1000) someProperty = "done" }}

class LazyHolder { val heavyClass: VeryHeavyClass by lazy { VeryHeavyClass() }}

class EagerHolder { val heavyClass: VeryHeavyClass = VeryHeavyClass()}

fun logBuilder(prefix: String): (String) -> Unit = { message -> println("[${System.currentTimeMillis()} - $prefix] $message") }

fun main(args: Array<String>) { val tLazy = Thread { val log = logBuilder("LAZY") log("Creating") val lazyHolder = LazyHolder() log("Ready to action") log(lazyHolder.heavyClass.someProperty) }

val tEager = Thread { val log = logBuilder("EAGER") log("Creating") val eagerHolder = EagerHolder() log("Ready to action") log(eagerHolder.heavyClass.someProperty) }

val threads = listOf(tEager, tLazy) threads.parallelStream().map { it.run() }.collect(Collectors.toList()) threads.map { it.join() } println("All done")}


Como você pode ver, ambos terminam ao mesmo tempo, entretanto o `Ready to action` do block Lazy acontece um pouco antes.

4. Destructuring

Em Kotlin, assim como em algumas outras linguagens, como Javascript, é possível atribuir valores de variáveis a partir de um objeto, utilizando **desestruturação**. Um exemplo de uso seria para facilitar um pouco a leitura quando fazemos uso intensivo o objeto em questão. 

Observe os 2 métodos, imprimirPessoa e imprimirPessoaDesestruturacao para ver como o print ficou um pouco mais limpo e legível.


data class Pessoa(val nome: String, val idade: Int, val documento: String)

fun imprimirPessoa(pessoa: Pessoa) { println("Imprimindo a pessoa ${pessoa.nome}, de idade ${pessoa.idade} e documento ${pessoa.documento}")}

fun imprimirPessoaDesestruturacao(pessoa: Pessoa) { val (nome, idade, documento) = pessoa println("Imprimindo a pessoa $nome, de idade $idade e documento $documento")}

fun main(args: Array<String>) { val pessoa = Pessoa("Alguém", 30, "1230129301293") imprimirPessoa(pessoa) imprimirPessoaDesestruturacao(pessoa)}


5. Operator overloading (Sobrecarga de operador)

Já trabalhou com BigInteger ou BigDecimal em Java e achou super fora de mão ter que fazer valor1.add(valor2), ou algo do tipo. 

Em Kotlin, suas classes podem ter os operadores sobrecarregados, ou seja, você pode customizar o comportamento de alguns operadores. Um exemplo com a classe Moeda:

data class Moeda(val reais: Int, val centavos: Int) { operator fun plus(outraMoeda: Moeda): Moeda { val totalCentavos = (centavos + outraMoeda.centavos) val novosCentavos = totalCentavos % 100 val adicionalReais = totalCentavos.div(100)

return Moeda(this.reais + outraMoeda.reais + adicionalReais, novosCentavos) }

override fun toString(): String { return "R$ $reais,${centavos.toString().padStart(2, '0')}" }}

fun main(args: Array<String>)

{ val valor1 = Moeda(99, 99) val valor2 = Moeda(10, 10) val soma = valor1 + valor2

println(valor1)

println(valor2)

println(soma)}


6. Property delegation

Vamos supor que você queira mudar o comportamento padrão de como o getter e o setter de uma classe funciona em Kotlin, você poderia fazer algo do tipo:


class ExemploDelegate { var minhaProperty: Int = 0 get() { return field }

set(value) { require(value > 10) { "minhaProperty deve ser maior que 10" } field = value }}

fun main(args: Array<String>)

{ val exemploDelegate = ExemploDelegate() exemploDelegate.minhaProperty = 100 println(exemploDelegate.minhaProperty)}


Agora imagine que você tenha isso para muitos campos, sua classe ficaria com centenas de linhas de códigos, dependendo de como sua propriedade funcionasse. Ou até milhares. Isso tudo pode ser minimizado se você tiver uma classe que fique responsável apenas por definir como funcionam as regras de get e set. 

Essa classe deve ter os operadores getValue e setValue definidos. 

Veja o exemplo:


import kotlin.reflect.KProperty

class ExemploDelegate { var minhaProperty: Int by ValidateMinimunDelegate(10, 0) fun doSomething() { println(this.minhaProperty) }}

class ValidateMinimunDelegate(val minimun: Int, var value: Int) { operator fun getValue(thisRef: ExemploDelegate, property: KProperty<*>) = value operator fun setValue(thisRef: ExemploDelegate, property: KProperty<*>, value: Int) { require(value > minimun) { "minhaProperty deve ser maior que $minimun" } this.value = value thisRef.doSomething() }}

fun main(args: Array<String>) { val exemploDelegate = ExemploDelegate() exemploDelegate.minhaProperty = 5 println(exemploDelegate.minhaProperty)}


7. Sequences

As vezes queremos trabalhar com listas, e não nos atentamos para a performance da execução de nossos métodos. 

Ignorando que já temos vários métodos built-ins, vamos supor o seguinte cenário: temos uma lista de inteiros, na qual eu quero executar uma operação de mapeamento para todos os valores, mas essa operação é pesada. Também queremos fazer uma operação de filtro, que também pode ser muito pesada. 

Por fim, do resultado, gostaria de pegar apenas os n primeiros elementos, e essa operação seria até bem rápida.  

Na prática, precisaríamos executar o mapeamento apenas nos n primeiros elementos do qual o filtro for bem sucedido. Assumindo que cada operação demore 100 milissegundos, se tivéssemos uma lista com 1_000_000 de itens, e quiséssemos executar essa ação, teríamos um mapeamento de 1_000_000 de elementos, um filtro sobre 1_000_000 de elementos para no final pegarmos apenas os n primeiros.  

Nosso consumo de memória seria 1_000_000 da lista original, mais 1_000_000 de itens do mapeamento (que foi lento), até 1_000_000 elementos resultantes do filter e depois outra lista com n elementos que queremos de fato. Isso claro, no modelo tradicional.

Utilizando sequences do Kotlin, podemos executar muito menos operações, pois uma sequence decora uma função que será executada item a item, ou seja, a cada item, eu executo todas as condições, e se no final tudo for verdadeiro, eu paro a execução. 

Se utilizamos o valor 5 para n no exemplo acima, teríamos em torno de 10 mapeamentos, 5 filtros e nenhuma lista intermediária para armazenar este valor. 

Exemplo:


import kotlin.system.measureTimeMillis

// Slow map functionfun mapNumber(number: Int): Int { Thread.sleep(100) return number * 2}

fun filterNumber(number: Int): Boolean { Thread.sleep(100) return (number / 2) % 2 == 1}

fun main(args: Array<String>) { val list = List(100) { it }

val timeAsList = measureTimeMillis { val response = list .map(::mapNumber) .filter(::filterNumber) .take(5) println(response) }

println("Took $timeAsList ms")

val timeAsSequence = measureTimeMillis { val response = list .asSequence() .map(::mapNumber) .filter(::filterNumber) .take(5) .toList()

println(response) }

println("Took $timeAsSequence ms")}


[2, 6, 10, 14, 18]Took 20037 ms[2, 6, 10, 14, 18]Took 2010 ms

8. Executando ação se variável não for nula

Essa dica é bem rápida mesmo. Muitas vezes se uma variável for nula, então geralmente fazemos algo do tipo:

doSomething(someVar!!) // Muito ruim, null pointer exception pulando na tua cara, confia!

if (someVar != null) { // Menos feio, mais seguro, ...verboso (só que mais expressivo). doSomething(someVar)}


Mas em Kotlin, você pode combinar o operador de nullsafe `?` e o operador de escopo `let` para executar um código apenas se o valor não for null. A leitura não é a melhor do mundo (até você se acostumar), mas é bem prático. 

Exemplo:

someVar?.let { doSomething(it) } // Usando lambdasomeVar?.let(::doSomething) // Usando method reference


9. Espaços em nomes de funções

Tá, vou confessar que usar essa em regras de domínio é pedir para ter problemas no futuro, mas sabe quando você está escrevendo aquele teste maroto e quer colocar o nome do método super descritivo? Então, em Java você faria algo do tipo:

`deve_salvar_no_banco_e_depois_publicar_em_uma_fila_kafka`. 

Certo? Bom, em Kotlin você não precisa do underscore, pode usar espaço direto se o nome do seu método estiver dentro de crases. 

Como por exemplo:

fun `deve salvar no banco e depois publicar em uma fila kafka`() { // assert de alguma coisa}


10. Infix functions

Infix functions são legais, mas o uso dela é mais para gerar códigos mais legíveis do que algo realmente prático. Mas diferente do nome com espaços, fica até legal utilizar nos códigos de domínio. Vamos direto ao exemplo:


data class Pessoa(val nome: String)data class Cachorro(val nome: String, val dono: Pessoa) { infix fun pertenceA(pessoa: Pessoa) { if (pessoa == dono) {

println("$nome realmente pertence à ${dono.nome}") } else {

println("$nome não pertence à ${pessoa.nome}. Ele pertence à ${dono.nome}") } }

infix fun `eh do`(pessoa: Pessoa): Boolean = dono == pessoa}

fun main() { val fulano = Pessoa("Fulano") val cicrano = Pessoa("Cicrano") val lupi = Cachorro("Lupi", fulano)

lupi pertenceA fulano // Mesmo que lupi.pertenceA(fulano) lupi pertenceA cicrano

if (lupi `eh do` fulano) {

println("Achei o dono!") }}


11. Local functions

Sabe aquelas vezes que você quer refatorar o código pois está muito grande mas você não acha que uma classe deveria ser exposta (é parte única e exclusivamente do meu método) e mesmo assim você quer separá-la do resto? 

Você pode criar funções dentro de funções, assim manterá suas funções bem concisas e não expõe API's desnecessárias pra fora do contexto necessário. Pois é, loucura não?

Olha só:

fun fazAMagica(valor: Int): Int { fun dobraONumero(numero: Int): Int = numero * 2 fun multiplicaPor3(numero: Int): Int = numero * 3

return multiplicaPor3(dobraONumero(valor))}

fun main() { val resultado = fazAMagica(10)

println("Resultado: $resultado") // Resultado: 60

val teste = dobraONumero(4)

// Se tiver essa linha vai dar erro de compilação, já que dobraONumero não existe no contexto de main}


FIM.

1
69

Comentários (1)

0
Jonatas Costa

Jonatas Costa

19/02/2021 00:07

Muito obrigado ! estou nesta jornada . iniciando em Kotlin


Prestativo, colaborativo, motivado, criativo, obstinado, inteligente, analítico, e é isso, quero voar além das nuvens, estou no ramo de tecnologia na prática a 7 anos.

Brasil