Extensões em Kotlin. Atavismo perigoso ou uma ferramenta útil?



Kotlin ainda é um idioma jovem, mas já entrou em nossas vidas. Por isso, nem sempre é claro como implementar corretamente esse ou aquele funcional e qual a melhor prática a ser aplicada.

Particularmente difícil é o caso dos recursos da linguagem, que não estão em Java. Um desses obstáculos foi a expansão .

Essa é uma ferramenta conveniente que torna o código mais legível, exigindo quase nada em troca. Mas, ao mesmo tempo, conheço pelo menos uma pessoa que, se ele não considera a expansão como má, é definitivamente cética em relação a elas. Abaixo, gostaria de discutir as características desse mecanismo, que podem causar controvérsia e mal-entendidos.

Extensões para DTO - violação do modelo de objeto de transferência de dados


Por exemplo, há uma classe User

class User(val name: String, val age: Int, val sex: String) 

Bastante um DTO! Além disso, no código em vários locais, é necessária uma verificação para determinar se o usuário é adulto. A opção mais fácil é fazer uma condição em todos os lugares

 if (user.age >= 18) { ... } 

Mas, como podem existir arbitrariamente muitos desses lugares, faz sentido colocar essa verificação no método.
Existem três opções aqui:

  1. Função divertida isAdult (usuário: Usuário) - as classes de utilitário geralmente consistem nessas funções.
  2. Coloque a função isAdult dentro da classe User

     class User(val name: String, val age: Int, val sex: String) { fun isAdult() = age >= 18 } 

  3. Escreva um invólucro para o usuário que conterá funções semelhantes.

Todas as três opções têm tecnicamente direito à vida. Mas o primeiro adiciona inconveniência na forma da necessidade de conhecer todas as funções de utilidade, embora isso, obviamente, não seja um grande problema.
A segunda opção parece violar o padrão Objeto de Transferência de Dados, uma vez que a classe não é apenas getters e setters. Mas quebrar padrões é ruim.

A terceira opção não viola os princípios de OOP ou modelos, mas sempre que você precisar criar um wrapper, se desejar usar funções semelhantes. Esta opção também não é muito parecida. No final, acontece que você ainda precisa fazer sacrifícios.

Na minha opinião, é mais fácil sacrificar um modelo de DTO. Em primeiro lugar, não encontrei uma única explicação de por que funções (exceto getters e setters) não podem ser feitas no DTO. E, em segundo lugar, apenas em termos de significado, é conveniente ter esse código próximo aos dados em que estamos operando.

Mas nem sempre é possível inserir esse código no shek do DTO, pois o desenvolvedor nem sempre tem a capacidade de editar as classes com as quais trabalha. Por exemplo, essas podem ser classes geradas a partir do xsd. Além disso, para alguém, pode ser incomum e desconfortável escrever esse código nas classes Data. Kotlin oferece uma solução para tais situações na forma de funções e campos de extensão:

 fun User.isAdult() = age >= 18 

Este código pode ser usado como se fosse declarado dentro da classe User:

 if(user.isAdult()) {...} 

O resultado é uma solução bastante precisa que, com o mínimo compromisso, satisfaz nossas necessidades. Se falarmos sobre o fato de o modelo DTO ser violado, queremos lembrar que em Java será um método estático regular do formulário:

 public static final boolean isAdult(@NotNull User receiver) 

Como você pode ver, formalmente, mesmo o modelo não é violado. O uso dessa função parece ter sido declarado em Usuário e o Idea oferecerá isso no preenchimento automático. É muito conveniente

As extensões são específicas. Você não pode saber sobre a existência deles e confundir métodos e campos da entidade com extensões


A idéia é que o desenvolvedor veio ao projeto e, em torno do código implementado nas extensões, não está claro qual método é original e qual é o método de extensão.

Isso não é um problema, pois o Idea ajuda o desenvolvedor nesse assunto e destaca essas funções. Embora com justiça, é preciso dizer que a diferença é mais perceptível no tema Darcula. Se você mudar para Light, tudo ficará menos óbvio e a extensão será diferente apenas em fonte itálica.

Abaixo, vemos um exemplo de chamada de dois métodos: isAdult é o método de extensão, isMale é o método usual dentro da classe User. A captura de tela à esquerda é o tema Darcula, à direita é o tema Light usual.



As coisas um pouco piores são com os campos. Se, por exemplo, decidirmos implementar o isAdult como um campo de extensão, apenas poderemos diferenciá-lo de um campo regular por tipo de fonte. Neste exemplo, nome é um campo regular. Um campo de extensão produz apenas fonte em itálico.



O ambiente de desenvolvimento do Idea ajuda a determinar qual método é uma extensão e qual é o original ao preencher automaticamente. Isso é conveniente.



A situação é semelhante com os campos.



"Para usuário em <raiz>" significa que é uma extensão.

Além disso, o próprio fato de o Idea "vincular" uma extensão a uma entidade extensível ajuda muito no desenvolvimento, pois métodos e campos de extensão são propostos para o preenchimento automático.

As extensões estão espalhadas por todo o projeto, formando uma lata de lixo


Não temos esse problema em projetos, pois não colocamos extensões arbitrariamente e usamos códigos com extensões públicas em arquivos ou pacotes separados.

Por exemplo, a função isAdult do exemplo acima pode aparecer no arquivo de usuário do pacote de extensões. Se o pacote não for suficiente e você apenas desejar não confundir onde está a classe e onde está o arquivo de função, você pode nomear, por exemplo, _User.kt. O mesmo fizeram os desenvolvedores do JetBrains para coleções. Ou, se a consciência proibir o início do arquivo com um sublinhado, você poderá ligar para user.kt. De fato, não há diferença de qual maneira usar, o principal é que há uniformidade a que toda a equipe adere.

Os criadores do idioma, ao desenvolver métodos de extensão para coleções, os colocaram no arquivo _Collections.kt .

Geralmente, é uma questão de organizar o código, não um problema de extensões. As funções estáticas em Java, e não apenas as estáticas, podem ser espalhadas não menos aleatoriamente que as extensões.

Não ignore as funções de extensão durante o teste da unidade


Na minha opinião, não há necessidade de molhar funções de extensões, assim como não há necessidade de molhar métodos estáticos. Na função de expansão, você deve colocar a lógica de trabalhar com dados existentes. Por exemplo, no caso da função isAdult da classe User, tudo o que você precisa está em isAdult. Você não precisa se molhar.

Considere um exemplo um pouco mais complexo. Há um certo componente que serve para obter usuários de um sistema externo - UserComponent. O método para obter usuários é chamado getUsers. Suponha que houvesse a necessidade de obter todos os usuários ativos e decidisse adicionar a lógica de filtragem na forma de uma função - uma extensão. Como resultado, obtivemos a função:

 fun UserComponent.getActiveUsers(): List<Users> = this.getUsers().filter{it.status == “Active”} 

Pode parecer que aqui está - uma situação em que você precisa de uma simulação para expansão. Mas se você se lembrar de que getActiveUsers é apenas um método estático, verifica-se que a simulação não é necessária. Dip deve ser os métodos e funções chamados na extensão e nada mais.

É possível que a função de extensão se sobreponha à função de mesmo nome localizada dentro da classe estendida


Vamos considerar esse caso usando o exemplo do primeiro parágrafo. Suponha que exista uma extensão de função isAdult, que verifique se o usuário é adulto:

 fun User.isAdult() = age >= 18 

Depois disso, implementamos a função com o mesmo nome dentro de User:

 class User(val name: String, val age: Int, val sex: String){ fun isAdult() = age >= 21 } 

Quando user.isAdult () é chamado, uma função da classe será chamada, apesar do fato de haver uma extensão com o mesmo nome e uma função adequada. Esse caso pode ser confuso, pois os usuários que não estão cientes da função declarada dentro da classe aguardam a conclusão da função de extensão. Esta é uma situação desagradável que pode ter consequências extremamente graves. Nesse caso, não estamos falando sobre o possível inconveniente de uma revisão ou violação de modelo, mas sobre o comportamento potencialmente incorreto do código.

A situação descrita acima mostra que, ao usar as funções de extensão, podem surgir problemas reais.

Para evitá-las, não se esqueça de cobrir o máximo possível as funções de expansão com testes de unidade. Na pior das hipóteses, se os testes falharem, haverá duas funções que funcionam da mesma maneira. Um é uma extensão e o outro está na própria classe. Se os testes falharem, chamará a atenção para o fato de que uma função se sobrepõe a outra.

A extensão está ligada a uma classe, não a um objeto, e isso pode causar confusão


Por exemplo, considere a classe Usuário do primeiro parágrafo. Vamos abrir e criar seu sucessor Student:

 class Student(name: String, age: Int, sex: String): User(name, age, sex) 

Definimos a função de extensão para o Aluno, que também determinará se o aluno é adulto ou não. Somente para o aluno alteramos a condição:

 fun Student.isAdult() = this.age >= 16 

E agora escrevemos o seguinte código:

 val user: User = Student("", 17, "M") 

O que retornará user.isAdult ())?
Parece que um objeto do tipo Student e a função devem retornar true. Mas não é tão simples. As extensões são anexadas à classe, não ao objeto, e o resultado será falso.

Não há nada de estranho nisso, se lembrarmos que extensões são métodos estáticos e uma entidade extensível é o primeiro parâmetro nesse método. Esse é outro ponto a ser lembrado ao usar esse mecanismo. Caso contrário, você pode obter um efeito desagradável e inesperado.

Em vez de saída


Esses pontos controversos não parecem perigosos, se você se lembrar do que dizemos extensão - queremos dizer um método estático. Além disso, cobrir essa funcionalidade com testes de unidade ajudará a minimizar possíveis confusões associadas à natureza estática das extensões.

Na minha opinião, as extensões são uma ferramenta poderosa e conveniente que pode melhorar a qualidade e a legibilidade do código, exigindo quase nada em troca. É por isso que eu os amo:

  • As extensões permitem escrever lógica específica para o contexto de uma classe extensível. Graças a isso, os campos e métodos de extensão são lidos como se estivessem sempre presentes na entidade estendida, o que, por sua vez, melhora o entendimento de nível superior do código. Em Java, infelizmente, isso não pode ser feito. Além disso, as extensões têm os mesmos modificadores de acesso que as funções regulares. Isso permite que você escreva códigos semelhantes com o escopo realmente necessário para uma função específica.
  • É conveniente usar as funções de extensão para mapeamento, que você precisa ver bastante ao resolver tarefas diárias. Por exemplo, no projeto, existe uma classe UserFromExternalSystem, que é usada ao chamar um sistema externo, e seria ótimo colocar o mapeamento na função de extensão, esquecê-lo e usá-lo como se estivesse originalmente em Usuário.

     callExternalSystem(user.getUserFromExternalSystem()) 

    Obviamente, o mesmo pode ser feito pelo método usual, mas essa opção é menos legível:

     callExternalSystem(getUserFromExternalSystem(user)) 

    ou essa opção:

     val externalUser = getUserFromExternalSystem(user) callExternalSystem(externalUser) 

    De fato, nenhuma mágica acontece, mas graças a essas ninharias é mais agradável trabalhar com o código.
  • Suporte a ideia e conclusão automática. Diferentemente dos métodos das classes de utilitário, as extensões são bem suportadas pelo ambiente de desenvolvimento. Com o preenchimento automático, as extensões são oferecidas pelo ambiente como funções e campos "nativos". Isso permite um bom aumento na produtividade do desenvolvedor.
  • A favor das extensões está o fato de uma grande parte das bibliotecas Kotlin ser escrita como extensões. Muitos métodos convenientes e favoritos para trabalhar com coleções são extensões (filtro, mapa e assim por diante). Você pode verificar isso examinando o arquivo _Collections.kt .

As vantagens das extensões cobrem possíveis desvantagens. Obviamente, existe um grande risco de uso indevido desse mecanismo e a tentação de colocar todo o código em extensões. Mas aqui a questão é mais sobre a organização do código e o uso competente da ferramenta. Quando usadas corretamente, as extensões se tornarão um verdadeiro amigo e ajudante ao escrever códigos bem lidos e mantidos.

Abaixo estão os links para os materiais que foram usados ​​para preparar este artigo:

  1. proandroiddev.com/kotlin-extension-functions-more-than-sugar-1f04ca7189ff - a partir daqui, consideramos interessantes o fato de que, usando extensões, trabalhamos mais de perto com o contexto.
  2. www.nikialeksey.com/2017/11/14/kotlin-is-bad.html - aqui o autor se opõe a extensões e fornece um exemplo interessante, discutido em um dos pontos acima.
  3. medium.com/@elizarov/i-do-not-see-much-reason-to-mock-extension-functions-7f24d88a188a - A opinião de Roman Elizarov sobre o umedecimento dos métodos de extensão.

Gostaria também de agradecer aos colegas que ajudaram com casos e pensamentos interessantes sobre esse material.

Source: https://habr.com/ru/post/pt485760/


All Articles