Âmbito Kotlin e funções de extensão. Como não prejudicar seu código.

Tempo de leitura: 5 minutes

As funções de escopo no Kotlin são ferramentas poderosas que podem tornar o código mais conciso, mas, como qualquer ferramenta poderosa, também podem arruiná-lo se não forem usadas corretamente. Este artigo cobre os problemas típicos da função de escopo e dicas e oferece práticas de mitigação.

0. Visão geral das funções de escopo

Primeiro, vamos fazer uma recapitulação.

Função de escopo é a função que executa código arbitrário (lambda que você passa) no escopo do objeto de contexto.

Função de extensão é a função que pega um receptor, que se torna dentro da função e serve como contexto.

1. Definição do problema

Então, qual é o problema com eles e como podem prejudicar o código?

  • Cada função de extensão muda o contexto de execução e o contexto do leitor
content.map { it.first() }
       .map { it.toString() }
       .filter { it.startsWith("_") }
       .toCollection(result)
  • Cada função de escopo adiciona contexto àquele que já existe (contexto de nossa classe ou função externa). Quanto mais precisamos memorizar, mais difícil é ler o código. Também precisamos lembrar quais funções de escopo retornam o resultado e quais retornam o próprio objeto de escopo.
with(content) {
  result.add(first())
  result.add(this.last())
  subList(1, size-2)
  filter { it != "" }
}

Isso pode parecer fácil, mas tente lembrar quais duas funções nesta imagem pegam lambda com um receptor (T. () -> R) e quais duas retornam o resultado do lambda?

Resposta: com e aplicar leva lambda com um receptor, com e deixa retornar o resultado de lambda (a mesma função de extensão é executada, não listada na imagem)

Em alguns casos extremos, o uso acidental do escopo errado ou função de extensão pode até levar a erro, durante a compilação ou em tempo de execução.

(null as Int?).plus(1)      // syntax errada
(null as String?).plus("1") // código 100% valido

Chega de teoria, vamos verificar alguns códigos de produção!

 

3. Exemplos da vida real

a) Aquela que encontrei no artigo médio sobre as “melhores” práticas, que sugere várias melhorias, como as seguintes

// Abordagem normal
fun makeDir(path: String): File {
    val result = File(path)
    result.mkdirs()
    return result
}

// Abordagem Aprimorada
fun makeDir(path: String) = path.let{ File(it) }.also{ it.mkdir() }

como você pode ver, a abordagem “aprimorada” na verdade é muito mais curta, mas não está claro se ela retorna o arquivo, o caminho ou nada. O que também não é óbvio é que a parte path.let não é necessária, a função pode ser reduzida a fun makeDir (path: String) = File (it) .also {it.mkdirs ()}

b) e aqui está o do aplicativo em que estou trabalhando

with(loadOfferComposer.loanFeesDetailsFor(loanTerms)) {this: Pair<String, String>
    loan_fee_name.text = first
    loan_fee_value.text = second
}

está tudo bem, mas apenas por causa da dica do IDE, durante a revisão do código, fica assim (agora é difícil dizer o que é o primeiro e o segundo)

with(loadOfferComposer.loanFeesDetailsFor(loanTerms)) {
    loan_fee_name.text = first 
    loan_fee_value.text = second 
}

que pode ser ligeiramente melhorado se usarmos lambda com parâmetro em vez de lambda com receptor

with(loadOfferComposer.loanFeesDetailsFor(loanTerms)).let { 
    loan_fee_name.text = it.first 
    loan_fee_value.text = it.second 
}

e ainda mais com o nome explícito e a reestruturação de um par

with(loadOfferComposer.loanFeesDetailsFor(loanTerms)).let { (feeName, feeValue) ->
   loan_fee_name.text = feeName
   loan_fee_value.text = feeValue
}

mas vamos compará-lo com a versão sem funções de escopo. Na verdade, é mais curto e tem uma leitura igualmente boa.

val (feeName, feeValue) = loadOfferComposer.loanFeesDetailsFor(loanTerms)
loan_fee_name.text = feeName 
loan_fee_value.text = feeValue

c) isso não quer dizer que ninguém deva usar truques inteligentes com escopo ou função de extensão. Aqui está o teste JUnit4 parametrizado, que não parece ótimo

companion object {
  @JvmStatic
  @Parameterized.Parameters(nome = "'{0} + {1} gives {3}"}
  fun testData() = listOf(
     arrayOf(null, "a", "a"),
     arrayOf("a", null, "a"),
     arrayOf("a", "a", "aa")
   }
}

mas com as funções costmand e shouldReturn agora é 100% claro para ler os dados de teste.

companion object { 
  @JvmStatic 
  @Parameterized.Parameters(nome = "'{0} + {1} gives {3}"} 
  fun testData() = parametersOf( 
     (null and "a") shouldReturn "a", 
     ("a" and null) shouldReturn "a", 
     ("a" and "a") shouldReturn "aa" 
   } 
}

4. Recomendações

A documentação do próprio Kotlin oferece o guia para funções de escopo, mas é bastante vago e os exemplos demonstram principalmente os recursos, em vez do bom estilo, e não há nenhum guia para funções de extensão. Então aqui está meu guia proposto, que consiste em mnemônicos (para cada função) e regras (para casos gerais).

 

Guia não oficial de funções de escopo

Mnemônicos

  • [Se eu precisar] Deixe funcionar com um valor anulável → deixe.
    A chave é que nos permite usar o objeto que às vezes pode não estar disponível.
    nullable.let { print(it) }
  • [Se eu precisar] Aplicar configuração → aplicar.
    A chave é que mudamos algumas propriedades que não podemos definir por meio do construtor. ServerConfig(host).apply { port = 8888 }
  • [Se eu precisar] Retornar um objeto e também fazer algo → também.
    O fundamental é que fazemos todo o trabalho principal na função e prontos para retornar um objeto, mas também precisamos fazer algo auxiliar, como o log.
    return Something(withParameters).also { log("Created $it") }
  • [Se eu precisar] Faça várias coisas semelhantes com um objeto → com.
    A chave é que estamos trabalhando com um e apenas um objeto fazendo várias operações relacionadas com ele.
    with(service) { connect(); sendData(data); disconnect() }
  • Não use correr! Fuja disso!
    Realmente, não há razão para isso, outras funções cobrem tudo.

 

Regras

  • Se retornar o resultado – mantenha-o em uma linha.
    Essa regra ajuda muito com a legibilidade de lambdas longos, onde a última instrução passa a ser um valor de retorno, quando é uma linha, é muito mais fácil ver que algo é retornado sem “retorno” explícito.
  • Se funcionar com um objeto – use um único objeto.
    Vários receptores são difíceis de rastrear, e dois separam isso em um escopo é o suficiente.
  • Estabeleça padrões.
    Mesmo que algum truque inteligente possa não ser óbvio, torna-se claro e mundano quando usado com bastante frequência. Portanto, encontre casos de uso e use as mesmas funções para eles sempre: por exemplo, deixe para variáveis ​​anuláveis, aplique para construtores, também para registro, etc.
  • Não tente tornar o código mais curto.
    Esse nunca é o objetivo, mesmo o desenvolvedor e defensor de Kotlin, Roman Elizarov, em sua revisão das funções com o receptor, enfatiza que a brevidade não deve prejudicar a clareza.

 

Guia não oficial da função de extensão

  • aplicar ação ao receptor.
    A função de extensão não infixo não deve tratar os lados esquerdo (receptor) e direito (argumentos) igualmente, ela aplica uma ação à esquerda com parâmetros da direita. No exemplo abaixo, a segunda função tem a assinatura divertida String.connect (vararg params: String) que não faz muito sentido.
sharedPreferences.edit { putBoolean("key", value) } ---> Correto
"my.api.com/v1".connnect(parameters) ---> Errado
  • aninhe-se sob um objeto ou um pacote extra.
    A extensão quase sempre requer um nível de aninhamento extra que corresponda ao nome do arquivo em que foram definidos para que as importações façam sentido para o leitor. No exemplo abaixo, os nomes das funções são muito genéricos (e, ou), mas o pacote extra testutils esclarece o primeiro caso.
import com.github.fo2rist.kotlinscopeandextensionfunctions.testutils.and ---> Correto
import com.github.fo2rist.kotlinscopeandextensionfunctions.or ----> Errado
  • evite usar nomes existentes (por exemplo, toString, to, run).
    Deixar de fazer isso pode causar graves problemas em tempo real. As extensões integradas não exigem nenhuma importação, portanto, se o código contiver qualquer função personalizada com um nome como run, além disso, deixe-a funcionar até o momento em que for copiada em algum lugar ou o IDE reorganizar as importações, ponto em que provavelmente ainda permaneceria compilável, mas produzirá um resultado inesperado.
fun Command.execute(): Unit = Thread { this() }.start()  -----> Correto
fun Command.run() = Thread { this() }.start()  ---> Errado

5. Resumo

É fácil evitar o abuso de recursos do Kotlin com pouca disciplina e algumas regras. Concentre-se na tarefa de negócios e na legibilidade, em vez de na inteligência e brevidade, e o pode resolver esse problema inteiramente!

Codificação segura!

 

Aqui está o código completo desta artigo encontrado aqui.

 

 

 

Visits: 1 Visits: 1200486