Adicionando Extension Functions no Kotlin

Alex Felipe
CollabCode
Published in
8 min readJan 3, 2018
Várias peças de lego

Em um sistema para um estoque, criei o seguinte modelo para representar um produto:

Modelo para um produto

Então, recebi uma lista com todos os produtos e fiz a impressão do valor dos mesmos:

val products = allProducts()
products.forEach { println(it.value) }

Entenda a função allProducts() como uma lista qualquer que pode estar alocada na memória ou por meio de um banco de dados.

Rodando esse trecho de código temos o seguinte resultado:

2150.99
1699.99
799.99
1899.99
1199.99

Algo um tanto quanto esperado, né? Entretanto, quando apresentamos valores, é muito comum aplicarmos um formato como uma moeda, certo?

Quer aprender mais sobre Kotlin tanto no mundo mobile como no back-end? Então confira este agregador de conteúdo onde listo todos os conteúdos que escrevi de Kotlin e os que serão publicados mais pra frente 😉

Formatando o valor do BigDecimal para moeda

Considerando o uso da API BigDecimal do Java, temos a capacidade de utilizar o DecimalFormat pra isso:

Formatando o valor para moeda brasileira para cada produto da lista

Perceba que primeiro criamos um formatador para moeda brasileira e, para cada produto na lista, pedimos para formatar a moeda e fazemos a impressão… Executando o código novamente, chegamos neste resultado:

R$ 2.150,99
R$ 1.699,99
R$ 799,99
R$ 1.899,99
R$ 1.199,99

Por mais que o código seja simples, ele foi implementado de uma maneira que não podemos reutilizá-lo em outros pontos do sistema, a não ser que façamos um copy and paste.

Aplicando a extração de código

Isso significa que faz todo o sentido considerarmos uma das técnicas mais famosas de refatoração de código que é a extração função:

fun formatForBrazilianCurrency(value: BigDecimal) : String {
val brazilianFormat = DecimalFormat
.getCurrencyInstance(Locale("pt", "br"))
return brazilianFormat.format(value)
}

Então, chamaríamos essa função da seguinte maneira:

val products = allProducts()
products.forEach {
val brazilianCurrency = formatForBrazilianCurrency(it.value)
println(brazilianCurrency)
}

Perceba que agora somos capazes de reutilizar o nosso código, pois o deixamos isolado em uma função. Também, é válido mencionar que ao invés de depender de um objeto do tipo Produto agora exigimos um tipo mais comum via parâmetro, o BigDecimal.

Delegando responsabilidade para classes utilitárias

Entretanto, visando os conceitos do paradigma OO, a boa prática para esse tipo de situação é delegar essa função em uma classe específica que vai ficar responsável em mantê-la e disponibilizá-la para todo o sistema!

Mas, a princípio, a nossa função possui um comportamento tão genérico que faz todo o sentido mantermos em uma classe utilitária assim como fazemos no Java. Um exemplo seria criando uma classe chamada CurrencyUtil:

package br.com.alexf.utilimport java.math.BigDecimal
import java.text.DecimalFormat
import java.util.*
class CurrencyUtil {
fun formatForBrazilianCurrency(value: BigDecimal) : String {
val brazilianFormat = DecimalFormat
.getCurrencyInstance(Locale("pt", "br"))
return brazilianFormat.format(value)
}
}

Então bastaria apenas chamá-la no momento que fosse necessário fazer a formatação:

Chamando o formatador de moeda brasileira a partir de um objeto do tipo CurrencyUtil

Se executarmos o nosso código, teremos exatamente o mesmo comportamento de antes, a diferença é que criamos uma instância de uma classe e chamamos a função que formata o BigDecimal para a moeda brasileira.

Sendo mais objetivo, delegamos a responsabilidade de formatar moeda para uma classe específica e que só fica responsável por isso.

Detalhes sobre classes utilitárias

Mas ainda existem alguns pontos neste tipo de implementação, pois, geralmente, classes utilitárias não exigem instância para chamar seus membros, ou seja, seus membros são implementados de maneira estática…

Portanto, faz todo o sentido modificarmos a função membro formatForBrazilianCurrency() em uma chamada estática, mas como fazemos para deixar um membro estático?

A primeira coisa que pensamos é em usar a keyword static, concorda? Porém, nem existe o static no Kotlin… E agora?

Criando chamadas estáticas

Pensando justamente em disponibilizar esse tipo de comportamento, como também por outros motivos, no Kotlin podemos utilizar a feature Companion Object:

Tornando uma função estática com Companion Object

Com essa mudança nem somos mais capazes de chamar a função formatForBrazilianCurrency() por meio de um objeto, ou seja, somos obrigados a chamá-la no modo estático:

val brazilianCurrency = CurrencyUtil.formatForBrazilianCurrency(it.value)

O nosso código ficou mais organizado e simples de reutilizar, porém, ainda mantém uma certa verbosidade que não é muito bem vinda para muitos desenvolvedores. Principalmente para aqueles que tiveram diversas experiências com classes utilitárias…

Analisando outras possibilidades de delegação de responsabilidade

Considerando a última refatoração que fizemos na função que formata a moeda, vimos que a única dependência dela é a referência da classe BigDecimal.

Em outras palavras, faria muito sentido a própria classe BigDecimal ter essa responsabilidade de se auto formatar para uma moeda brasileira, concorda?

Porém, como já sabemos, essa classe não é nossa, logo, não conseguimos abrir o código dela e adicionar esse comportamento…

Delegando responsabilidade com extension function

Embora no Java não somos capazes de realizar esse tipo de abordagem, no Kotlin, temos uma feature muito bacana que permite adicionar comportamentos em classes que não são nossas! Tecnicamente, conhecida como Extension Function.

Sendo assim, podemos fazer com que a função formatForBrazilianCurrency() seja uma extensão da classe BigDecimal:

Criando uma extensão para a classe BigDecimal

Repare que na assinatura, antes de nomear a função, foi adicionado o prefixo BigDecimal. que indica que essa função será chamada apenas por um objeto do tipo BigDecimal.

Entretanto, apenas declarar a função desta maneira não significa que podemos acessá-la em todo sistema!

Atualmente ela é acessível apenas pelos membros da classe CurrencyUtil e outros membros do arquivo CurrencyUtil.kt.

Isso significa que, para chamar a Extension Function da maneira como foi declarada, precisamos importá-la! Mas como podemos fazer esse import? Em primeiro momento, pensamos em um import próximo a este:

import br.com.alexf.util.CurrencyUtil.formatForBrazilianCurrency

Entretanto, ao tentarmos essa abordagem, temos um erro de compilação indicando que não sabe lidar com essa referência… E agora?

Entendendo sobre a estrutura do Companion Object

A princípio, vemos os membros do Companion Object como se fossem membros direto da classe que os implementam, certo?

Porém, na verdade, todos eles estão envolvidos dentro de uma classe chamada Companion que os mantém estáticos. Ao mesmo tempo, ela é acessível pela classe que a implementa!

Para testarmos essa teoria, podemos adicionar mais um membro dentro do Companion Object:

class CurrencyUtil {
companion object {
fun BigDecimal.formatForBrazilianCurrency(value: BigDecimal): String {
// rest of the code
}
val empty = ""
}
}

Então, podemos acessar essa property diretamente como uma chamada estática da classe CurrencyUtil:

CurrencyUtil.empty

Como também, como uma chamada estática a partir do objeto Companion:

CurrencyUtil.Companion.empty

“Mas o que isso resolve pra gente?”

Importando membros do Companion Object

Considerando que os membros dentro do Companion Object ficam acessíveis por meio deste objeto Companion, podemos utilizá-lo para importar a Extension Function:

import br.com.alexf.util.CurrencyUtil.Companion.formatForBrazilianCurrency

Então, temos acesso à Extension Function a partir de um objeto do tipo BigDecimal:

val products = allProducts()
products.forEach {
val brazilianCurrency = it.value.formatForBrazilianCurrency(it.value)
println(brazilianCurrency)
}

Legal, mas repara que essa chamada está um tanto quanto estranha, pois primeiro chamamos a Extension Function com o valor do produto e depois enviamos o mesmo valor do produto via parâmetro…

Utilizando a referência do objeto na Extension Function

Considerando que o próprio objeto está fazendo a chamada, somos capazes de utilizá-lo como referência por meio do this, ou seja, não precisamos mais receber um parâmetro já que o próprio objeto que será utilizado como referência:

fun BigDecimal.formatForBrazilianCurrency(): String {
val brazilianFormat = DecimalFormat
.getCurrencyInstance(Locale("pt", "br"))
return brazilianFormat.format(this)
}

Agora a chamada fica da seguinte maneira:

produtos.forEach {
val brazilianCurrency = it.valor.formatForBrazilianCurrency()
println(brazilianCurrency)
}

Ao executar a App novamente, temos o seguinte resultado:

R$ 2.150,99
R$ 1.699,99
R$ 799,99
R$ 1.899,99
R$ 1.199,99

Bem mais objetivo e elegante, concorda? Mas ainda existem alguns detalhes…

Detalhes e boas práticas com Extension Function

Por mais que o nosso código esteja funcionando, existem algumas boas práticas que valem muito a pena considerarmos quando usamos Extension Functions.

Sendo assim, o nosso foco será conhecer essas técnicas e realizar algumas refatorações para melhorar o código.

Definindo o local da Extension Function

A primeira abordagem é justamente o local onde a Extension Function foi implementada, atualmente deixamos dentro de um Companion Object justamente por utilizarmos uma classe utilitária.

O principal motivo dessa abordagem é justamente por que no Java não tínhamos a capacidade de adicionar novos comportamentos para classes que não são nossas.

Em outras palavras, com a Extension Function as classes utilitárias não são mais necessárias! Portanto, podemos refatorar o nosso código para que o arquivo da classe CurrencyUtil mantenha apenas a Extension Function:

package br.com.alexf.utilimport java.math.BigDecimal
import java.text.DecimalFormat
import java.util.Locale
fun BigDecimal.formatForBrazilianCurrency(): String {
fun BigDecimal.formatForBrazilianCurrency(): String {
val brazilianFormat = DecimalFormat
.getCurrencyInstance(Locale("pt", "br"))
return brazilianFormat.format(this)
}
}

Nome do pacote para manter as Extension Functions

A próxima técnica a ser considerada é o local da Extension Function e o nome do arquivo.

Sendo mais objetivo, nesta etapa é importante pensar que faz muito sentido mantermos as Extension Functions dentro de um pacote que indique a qual classe se destina as extensões.

Portanto, podemos criar um pacote chamado de extension e criar todas as extensões do projeto dentro dele.

Definindo o nome do arquivo

O último ponto importante é o nome do arquivo. Um padrão que costumo usar é manter o nome da classe que está ganhando a extensão junto com o sufixo Extension, no nosso caso, podemos deixar como BigDecimalExtension.

Manter apenas o nome da classe é uma técnica válida, afinal, se está dentro do pacote extension, obviamente é uma extensão.

Com essas modificações o nosso código ficou da seguinte maneira:

Extension Function após última refatoração

Após a refatoração, no código onde utilizamos essa função, basta apenas modificar o import:

import br.com.alexf.extension.formatForBrazilianCurrency

Isso mesmo! Conseguimos importar funções no Kotlin diretamente! Lembre-se que o Kotlin é uma linguagem multiparadigma, ou seja, podemos usar features do OO juntas do funcional 😄

Analise de necessidade da Extension Function

Agora que aprendemos as técnicas visando as boas práticas de código, chegamos ao último ponto importante para fechar o assunto de Extension Function. E começamos com o velho conselho do tio Ben:

“Lembre-se, com grandes poderes vem grandes responsabilidades”

Em outras palavras, com essa quantidade de poder que a Extension Function oferece, é muito importante refletir nos seguintes pontos antes de considerar tal implementação:

  • É um comportamento genérico o suficiente que vou reutilizar em outros pontos do projeto?
  • Preciso delegar essa responsabilidade para uma classe que não é minha?
  • O comportamento que a classe tá ganhando faz sentido para a razão dela existir?

Se você responder todas essas perguntas de maneira positiva, provavelmente a Extension Function faz sentido, caso contrário, muito provavelmente, não vale a pena considerar essa feature como solução do problema.

Apenas para deixar mais claro o quão problemático uma Extension Function mal pensava pode ser.

Quando criamos uma extensão que não faz sentido, os outros devs que verem o nosso codigo não irão entender o motivo da extensão e, consequentemente, vai prejudicar tanto o seu tempo que vai ter que explicar o que foi feito e, talvez, realizar uma refatoração que poderia ser evitada, como também, o tempo dos demais.

Sendo assim, cuidado redobrado com esse tipo de solução! Aplique com muita consciência.

Código fonte do projeto

Caso tenha alguma dúvida ou simplesmente queira consultar o código desenvolvido no artigo, fique à vontade em dar uma olhada no repositório GitHub do projeto.

Para saber mais

Podem existir situações nas quais queremos deixar as Extension Functions dentro de classes, porém, tal abordagem vem com restrições de acesso, como por exemplo, apenas os membros da classe possuem acesso a ela!

Isso pode ser benéfico em casos onde a extensão não faz sentido para outros pontos do sistema, mas ao mesmo tempo, não permite a reutilização…

Portanto, se a sua intenção é criar uma extensão que todo mundo tenha acesso, deixe em um arquivo da mesma maneira como fizemos, inclusive, essa técnica é conhecida como top level.

Conclusão

Neste artigo vimos que o processo de refatoração envolve diversas técnicas, desde a extração de funções, como também, a delegação de responsabilidade enviando funções para classes mais específicas.

Também aprendemos a criar classes utilitárias no Kotlin por meio do Companion Object que permite uma implementação de membros estáticos assim como vemos no Java, então, por fim, fizemos uso de uma das features mais poderosas do Kotlin que é a Extension Function.

E aí, o que você achou desta técnica aqui no Kotlin? Aproveite e deixe o seu comentário 😃

--

--