Um dos maiores pontos fortes do Kotlin também pode ser sua fraqueza. Quando falamos sobre classes de dados hoje, e da mesma forma sobre registros Java, tendemos a focar, pelo menos muitas pessoas que conheço, na eliminação completa do código clichê. O código padrão tem sido algo que incomoda os desenvolvedores há muito tempo. Ele existe não apenas em Java, mas em outras linguagens também. Se pensarmos em criar objetos de acesso a dados de qualquer forma, realmente não importa se fazemos isso em Kotlin, Java ou qualquer outra linguagem.
Frequentemente repetimos o mesmo processo: criação de repositório, serviço e controlador. Muito do que fazemos ainda é clichê. Embora o código padrão possa ter feito parte da decisão de criar classes de dados ou registros Java, um paradigma mais importante da engenharia de software foi o verdadeiro ponto focal da criação desses tipos de estruturas de dados.
Tudo se resume à imutabilidade ou, em qualquer caso, à possibilidade dela. Vamos voltar no tempo e voltar a 1995, onde começamos em algum momento a criar estruturas de dados onde especificaríamos coisas diferentes. Definiríamos os campos, os getters, os setters, as propriedades, os acessadores, os atributos e os parâmetros de forma a podermos passar nossos argumentos para os métodos, as funções ou os construtores. Nesta parte teremos alguma programação divertida usando os personagens da série de meados dos anos 80: The Golden Girls. Dito isso, vamos ver como seria uma aula daqui a uns 15 anos:
public class GoldenGirlsJava { public String goldenGirl1; private final String goldenGirl2; private final String goldenGirl3; private final String goldenGirl4; public GoldenGirlsJava() { this.goldenGirl1 = "Dorothy Zbornak"; this.goldenGirl2 = "Rose Nylund"; this.goldenGirl3 = "Blanche Devereaux"; this.goldenGirl4 = "Sophia Petrillo"; } public GoldenGirlsJava( String goldenGirl1, String goldenGirl2, String goldenGirl3, String goldenGirl4 ) { this.goldenGirl1 = goldenGirl1; this.goldenGirl2 = goldenGirl2; this.goldenGirl3 = goldenGirl3; this.goldenGirl4 = goldenGirl4; } public String getGoldenGirl1() { return goldenGirl1; } public void setGoldenGirl1(String goldenGirl1) { this.goldenGirl1 = goldenGirl1; } public String getGoldenGirl2() { return goldenGirl2; } public String getGoldenGirl3() { return goldenGirl3; } public String getGoldenGirl4() { return goldenGirl4; } @Override public String toString() { return "GoldenGirlsJava{" + "goldenGirl1='" + goldenGirl1 + '\'' + ", goldenGirl2='" + goldenGirl2 + '\'' + ", goldenGirl3='" + goldenGirl3 + '\'' + ", goldenGirl4='" + goldenGirl4 + '\'' + '}'; } }
Era uma quantidade enorme de código para digitar apenas para criar dois construtores. Embora o código hash e os iguais não fossem necessariamente obrigatórios em todas as ocasiões, quase sempre tivemos que implementar um construtor sem argumentos ao lado de um construtor mais generalista. A vantagem de criar todo esse código padrão é que sabíamos muito claramente como tudo ficaria após a compilação para bytecode.
No entanto, e em qualquer caso, o código padrão sempre foi uma questão controversa para muitos desenvolvedores e, então, em 2009, o lombok apareceu e revolucionou a forma como criamos estruturas de dados. Ele introduziu o conceito de usar um processador de anotações e interpretar anotações específicas que dariam as qualidades que precisávamos para nossas classes e, portanto, uma classe anotada em lombok ficaria assim:
@Getter @Setter @AllArgsConstructor @ToString public class GoldenGirlsLombok { public String goldenGirl1; private final String goldenGirl2; private final String goldenGirl3; private final String goldenGirl4; public GoldenGirlsLombok() { this.goldenGirl1 = "Dorothy Zbornak"; this.goldenGirl2 = "Rose Nylund"; this.goldenGirl3 = "Blanche Devereaux"; this.goldenGirl4 = "Sophia Petrillo"; } }
E por um tempo, as pessoas ficaram muito felizes com Lombok! Finalmente, o peso de ter que criar todo aquele código padronizado desapareceu. Mas essa sensação perdurou por cerca de 7 anos, quando chegou um novo player e desta vez foi algo que a indústria de desenvolvimento de software não esperava. Chamava-se Kotlin e em 2016 estreou com a introdução imediata de classes de dados. Nossa implementação das Golden Girls em Kotlin ficaria assim:
data class GoldenGirls( var goldenGirl1: String = "Dorothy Zbornak", private val goldenGirl2: String = "Rose Nylund", private val goldenGirl3: String = "Blanche Devereaux", private val goldenGirl4: String = "Sophia Petrillo" )
Embora Kotlin lentamente tenha começado a reunir fãs, Java, por outro lado, percebeu que algo mais estava no mercado e alguns desenvolvimentos desse lado começaram a ganhar força, como o projeto Loom, que estava em construção, mas também foi deixado em segundo plano por um tempo. É por isso que, com o lançamento do Java 14 em 20202, Java introduziu registros java, e as estruturas de dados em Java agora seriam assim:
public record GoldenGirlsRecord( String goldenGirl1, String goldenGirl2, String goldenGirl3, String goldenGirl4 ) { public GoldenGirlsRecord() { this( "Dorothy Zbornak", "Rose Nylund", "Blanche Devereaux", "Sophia Petrillo" ); } }
E até hoje a simplificação e a redução do código parecem continuar. No entanto, com a redução do código padrão, os conceitos de campos, getters, setters, propriedades, acessadores, atributos e parâmetros tornaram-se muito menos visuais e menos fáceis de mapear em nossas mentes. Quer queiramos ou não, esses conceitos ainda são como a JVM funciona e organiza o código com o bytecode. Mas a simplificação excessiva do código é, na verdade, tornar o código mais fácil de ler e, no caso de classes de dados e registros Java, a ideia também é criar estruturas de dados que são imutáveis ou parcialmente imutáveis.
Os registros Java são verdadeiramente imutáveis, no sentido de que todos os valores que contêm ou referências que contêm não podem ser modificados. As classes de dados Kotlin também podem ser verdadeiramente imutáveis pelos mesmos motivos, mas não precisam, o que de certa forma dá permissão aos desenvolvedores para criar códigos complicados e, o pior de tudo, mutáveis. para trabalhar com estruturas como o Spring Framework, que dependem fortemente de anotações. Aqui está um exemplo:
@Entity @Table(name = "shelf_case") data class Case( @Id @GeneratedValue(strategy = GenerationType.SEQUENCE) val id: Long?, var designation: String?, var weight: Long? ) { constructor() : this(0L, null, null) override fun toString(): String { return super.toString() } }
Este exemplo funciona bem. Para os olhos desavisados, não há muita coisa especial acontecendo aqui. Isso funciona em Kotlin, da mesma forma que funcionaria em Java. Mas agora vamos dar uma olhada em um exemplo semelhante, mas desta vez com anotações de Jackson:
data class AccountNumbersPassiveDto( @NotNull val accountNumberLong: Long?, val accountNumberNullable: Long?, @DecimalMax(value = "10") @DecimalMin(value = "5") val accountNumber: BigDecimal, val accountNumberEven: Int, val accountNumberOdd: Int, @Positive val accountNumberPositive: Int, @Negative val accountNumberNegative: Int, @DecimalMax(value = "5", groups = [LowProfile::class]) @DecimalMax(value = "10", groups = [MiddleProfile::class]) @DecimalMax(value = "15", groups = [HighProfile::class]) @DecimalMax(value = "20") val accountNumberMaxList:Int )
Este exemplo irá falhar e a razão para isso é que em Kotlin não há como dizer ao compilador onde exatamente queremos que nossa anotação seja aplicada. No exemplo anterior com a anotação @Entity
, que é uma anotação de persistência jakarta, as anotações são aplicadas corretamente aos campos. Se descompilarmos esse código, encontraremos isto:
public final class Case { @Id @GeneratedValue( strategy = GenerationType.SEQUENCE ) @Nullable private final Long id; @Nullable private String designation; @Nullable private Long weight;
Mas para este último, que é um exemplo de validação de Jacarta, encontramos o seguinte:
public final class AccountNumbersPassiveDto { @Nullable private final Long accountNumberLong; @Nullable private final Long accountNumberNullable; @NotNull private final BigDecimal accountNumber; private final int accountNumberEven; private final int accountNumberOdd;
Isso significa que AccountNumbersDto
não foi afetado por essas anotações em seus campos. Tivemos sorte no primeiro exemplo. Na verdade, também pode ter falhado. Para especificar para onde nossa anotação precisa ir, Kotlin nos dá a possibilidade de usar alvos de site de uso como prefixo de qualquer anotação. O requisito é, obviamente, que as condições sejam cumpridas. Nesse caso, uma maneira de corrigir isso é prefixar todas as anotações com @field
assim:
data class AccountNumbersPassiveDto( @field:NotNull val accountNumberLong: Long?, val accountNumberNullable: Long?, @field:DecimalMax(value = "10") @field:DecimalMin(value = "5") val accountNumber: BigDecimal, val accountNumberEven: Int, val accountNumberOdd: Int, @field:Positive val accountNumberPositive: Int, @field:Negative val accountNumberNegative: Int, @field:DecimalMax(value = "5", groups = [LowProfile::class]) @field:DecimalMax(value = "10", groups = [MiddleProfile::class]) @field:DecimalMax(value = "15", groups = [HighProfile::class]) @field:DecimalMax(value = "20") val accountNumberMaxList:Int )
Se agora tentarmos descompilar o bytecode resultante usando IntelliJ, descobriremos que isso resulta neste código em Java:
public final class AccountNumbersPassiveDto { @NotNull @Nullable private final Long accountNumberLong; @Nullable private final Long accountNumberNullable; @DecimalMax("10") @DecimalMin("5") @org.jetbrains.annotations.NotNull
Isso significa que as anotações foram aplicadas ao campo e que nosso código funciona como deveria.
Você provavelmente pode imaginar neste ponto que as pessoas que trabalham em Java há muito tempo não terão problemas em se adaptar ao Kotlin porque todos nós sabemos o que são campos, parâmetros, propriedades, etc. O que observamos hoje, e também testemunho isso, é que parece que a simplificação do código tem potencial para sair pela culatra. Houve algumas ocasiões em que tomei consciência do tempo gasto em projetos tentando descobrir por que, em alguns projetos ou módulos, as anotações simplesmente não parecem funcionar. E tudo parece se resumir a não fazer uso dos alvos do local de uso.
Minha teoria sobre o que é mais provável é que as novas gerações de desenvolvedores provavelmente olharão para coisas que a minha geração não achou complicadas e aprenderam por instinto, como material realmente complicado. Esse é o caso dos alvos por local de uso. Costumavam ser coisas que podíamos visualizar facilmente. Porém, nos tempos atuais, a confusão gerada com isso parece estar atrasando o desenvolvimento de projetos dentro do prazo em algumas ocasiões. Eu não seria capaz de generalizar, é claro, mas há potencial para o bem e para o mal com registros java e classes de dados. É difícil dizer como as classes de dados e os registros moldarão nosso futuro, mas uma coisa que está clara é que, devido a essa problemática, outros frameworks estão apostando em se tornarem completamente livres de anotações, como é o caso do Ktor.
Eu criei documentação sobre isso no Scribed: Fields-in-Java-and-Kotlin-and-What-to-Expect e também no slide-share: fields-in-java-and-kotlin-and-what-to-expect .
Você também pode encontrar os exemplos que dei no GitHub. O exemplo das garotas de ouro pode ser encontrado aqui: jeorg-kotlin-test-drives e os exemplos de anotações de persistência e validação de Jacarta podem ser encontrados aqui: https://github.com/jesperancinha/jeorg-spring-master-test-drives .
Por fim também criei um vídeo sobre isso no YouTube que você pode conferir aqui:
Também publicadoaqui .