Kotlin logo

Kotlin

A concise multiplatform language developed by JetBrains

Avaliação do Kotlin em projetos reais

Read this post in other languages:

Postagem convidada de Urs Peter, Engenheiro de Software Sênior e Instrutor de Kotlin certificado pela JetBrains. Para leitores que preferirem uma maneira mais estruturada de adquirir conhecimentos sobre Kotlin, Urs também dirige o Programa de Qualificação em Kotlin da Xebia Academy.

Esta é a segunda postagem da série O guia definitivo para a adoção bem-sucedida do Kotlin em um ambiente dominado pelo Java, que acompanha o processo de adoção do Kotlin por equipes reais, desde a curiosidade de um só desenvolvedor até uma transformação geral da empresa.

Leia a primeira parte: Como iniciar a adoção do Kotlin por desenvolvedores em Java


O estágio de avaliação: o Kotlin como mais que uma brincadeira

Depois que você sentir segurança sobre o Kotlin em testes, será a hora de uma avaliação mais substancial. Há duas abordagens principais:

  1. Desenvolver um novo microsserviço ou aplicativo em Kotlin
  2. Ampliar ou converter um aplicativo em Java já existente

1. Desenvolver um novo microsserviço ou aplicativo em Kotlin

Começar do zero com um novo aplicativo ou microsserviço oferece a experiência completa do Kotlin, sem as limitações de um código antigo. Esta abordagem costuma fornecer a melhor experiência de aprendizado e demonstra mais claramente os pontos fortes do Kotlin.

Dica de profissional: Obtenha a ajuda de um especialista durante este estágio. Embora desenvolvedores tenham uma confiança natural em suas habilidades, evitar erros iniciais, na forma de Kotlin ao estilo do Java e de uma falta de bibliotecas para Kotlin, pode evitar meses de dívida técnica.

É assim que você pode evitar armadilhas comuns ao usar Kotlin a partir de um ambiente de Java:

Armadilha: escolher um framework diferente do que você usa em Java.

Dica: Continue com o seu framework atual

O mais provável é que você estivesse usando o Spring Boot com Java; então, use-o com o Kotlin, também. O Spring Boot tem suporte de primeira ao Kotlin, de modo que não há nenhum benefício adicional em usar algo diferente. Além disso, você seria forçado a aprender não apenas uma nova linguagem, mas também um novo framework, o que só traria mais complexidade, sem oferecer nenhuma vantagem.

Importante: o Spring interfere com o princípio de “herança proposital” do Kotlin, o que exige que você marque explicitamente as classes como abertas para poder estendê-las.

Para evitar ter que adicionar a palavra-chave “open” a todas as classes relacionadas ao Spring (como @Configuration e outras), use o seguinte plug-in de build: https://kotlinlang.org/docs/all-open-plugin.html#spring-support. Se você criar um projeto do Spring com a conhecida ferramenta on-line “initializr”, do Spring, esse plug-in de build já estará configurado para você.

Armadilha: escrever em Kotlin ao estilo do Java e usando APIs comuns do Java, em vez da biblioteca-padrão do Kotlin: 

Esta lista pode ser muito longa; então, vamos nos concentrar nas armadilhas mais comuns:

Armadilha 1: usar o Java Stream, em vez das Kotlin Collections

Dica: Sempre use as Kotlin Collections.

As Kotlin Collections têm total interoperabilidade com as Java Collections, mas vêm equipadas com funções de alta ordem, simples, diretas e ricas em recursos, que tornam o Java Stream obsoleto. 

Veja abaixo um exemplo que visa selecionar os 3 produtos mais vendidos por faturamento (preço × número vendido), agrupados por categoria:

Java

record Product(String name, String category, double price, int sold){}

List products = List.of(
           new Product("Lollipop", "sweets", 1.2, 321),
           new Product("Broccoli", "vegetable", 1.8, 5);

Map<String, List> top3RevenueByCategory =
       products.stream()
          .collect(Collectors.groupingBy(
                Product::category,
                Collectors.collectingAndThen(
                    Collectors.toList(),
                    list -> list.stream()
                              .sorted(Comparator.comparingDouble(
                                  (Product p) -> p.price() * p.sold())
                                   .reversed())
                                   .limit(3)
                                   .toList()
                       		)
          )
);

Kotlin

val top3RevenueByCategory: Map<String, List> =
   products.groupBy { it.category }
       .mapValues { (_, list) ->
           list.sortedByDescending { it.price * it.sold }.take(3)
       }

A interoperabilidade do Kotlin com o Java permite que você trabalhe com classes e records do Java como se fossem nativos do Kotlin, embora você também possa usar uma classe (de dados) do Kotlin como alternativa.

Armadilha 2: continuar usando tipos “Optional” no Java.

Dica: Adote os tipos “Nullable”

Uma das principais razões pelas quais desenvolvedores em Java mudam para o Kotlin é o suporte incorporado a valores nulos, que dá adeus às NullPointerExceptions. Assim, tente usar apenas tipos “Nullable”. Não use mais tipos “Optional”. Você ainda tem tipos “Optional” nas suas interfaces? É assim que você se livra deles facilmente, convertendo-os em tipos “Nullable”:

Kotlin

//Let’s assume this repository is hard to change, because it’s a library you depend on
class OrderRepository {
      //it returns Optional, but we want nullable types
      fun getOrderBy(id: Long): Optional = …
}

//Simply add an extension method and apply the orElse(null) trick
fun OrderRepository.getOrderByOrNull(id: Long): Order? = 
                                    getOrderBy(id).orElse(null)

//Now enjoy the safety and ease of use of nullable types:

//Past:
 val g = repository.getOrderBy(12).flatMap { product ->
     product.goody.map { it.name }
}.orElse("No goody found")

//Future:
 val g = repository.getOrderByOrNull(12)?.goody?.name ?: "No goody found"

Armadilha 3: continuar usando wrappers estáticos.

Dica: Adote métodos de extensão

Os métodos de extensão oferecem muitas vantagens:

  • Eles tornam o seu código muito mais fluente e legível que os wrappers.
  • A complementação de código é capaz de encontrá-los, o que não acontece com os wrappers.
  • Como os métodos de extensão precisam ser importados, eles permitem que você use os recursos adicionados de forma seletiva, em um trecho específico do seu aplicativo.

Java

//Very common approach in Java to add additional helper methods
public class DateUtils {
      public static final DateTimeFormatter DEFAULT_DATE_TIME_FORMATTER = 
           DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");

      public String formatted(LocalDateTime dateTime, 
		              DateTimeFormatter formatter) {
         return dateTime.format(formatter);
      }

      public String formatted(LocalDateTime dateTime) {
         return formatted(dateTime, DEFAULT_DATE_TIME_FORMATTER);
      }
}

//Usage
 formatted(LocalDateTime.now());

Kotlin

val DEFAULT_DATE_TIME_FORMATTER: DateTimeFormatter = 
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")

//Use an extension method, including a default argument, which omits the need for an overloaded method. 
fun LocalDateTime.asString(
   formatter: DateTimeFormatter = DEFAULT_DATE_TIME_FORMATTER): String = 
      this.format(formatter)

//Usage
LocalDateTime.now().formatted()

Tenha em mente que Kotlin oferece métodos e variáveis de nível superior. Isso implica que é possível simplesmente declarar, por exemplo, DEFAULT_DATE_TIME_FORMATTER como sendo de nível superior, sem precisar associá-lo a um objeto, como acontece no Java.

Armadilha 4: usar as APIs do Java (de forma desajeitada)

Dica: Use o elegante equivalente do Kotlin. 

A biblioteca-padrão do Kotlin usa métodos de extensão, que tornam as bibliotecas do Java muito mais amigáveis, apesar de a implementação subjacente ainda ser em Java. Quase todas as principais bibliotecas e frameworks de terceiros, como o Spring, fizeram o mesmo.

Exemplo de biblioteca-padrão:

Java

String text;
try (
       var reader = new BufferedReader(
                  new InputStreamReader(new FileInputStream("out.txt"), 
            StandardCharsets.UTF_8))) {
   text = reader
            .lines()
            .collect(Collectors.joining(System.lineSeparator()));
}
System.out.println("Downloaded text: " +  text + "n");

Kotlin

//Kotlin has enhanced the Java standard library with many powerful extension methods, like on java.io.*, which makes input stream processing a snap due to its fluent nature, fully supported by code completion

val text = FileInputStream("path").use {
             it.bufferedReader().readText()
           }
println("Downloaded text: $textn");

Exemplo no Spring:
Java

final var books =  RestClient.create()
       .get()
       .uri("http://.../api/books")
       .retrieve()
       .body( new ParameterizedTypeReference<List>(){}); // ⇦ inconvenient ParameterizedTypeReference

Kotlin

import org.springframework.web.client.body
val books = RestClient.create()
   .get()
   .uri("http://.../api/books")
   .retrieve()
   .body<List>() //⇦ Kotlin offers an extension that only requires the type without the need for a ParameterizedTypeReference

Armadilha 5: usar um arquivo separado para cada classe pública

Dica: Combine classes públicas relacionadas em um só arquivo. 

Isso permite ter uma boa compreensão de como um (sub)domínio está estruturado, sem ter que navegar por dúzias de arquivos.

Java

Kotlin

//For domain classes consider data classes - see why below
data class User(val email: String,
            //Use nullable types for safety and expressiveness
           val avatarUrl: URL? = null, 
           var isEmailVerified: Boolean)

data class Account(val user:User,
              val address: Address,
              val mfaEnabled:Boolean,
              val createdAt: Instant)

data class Address(val street: String,
              val city: String,
              val postalCode: String)

Armadilha 6: adotar o paradigma mutável de programação

DIca: adote a imutabilidade — o padrão no Kotlin

A tendência é clara em muitas linguagens de programação, incluindo o Java: a imutabilidade está ganhando da mutabilidade. 

O motivo é simples: a imutabilidade evita efeitos colaterais indesejados, tornando o código mais seguro, previsível e fácil de raciocinar. Ela também simplifica a concorrência, pois dados imutáveis podem ser compartilhados livremente entre threads, sem risco de condições de corrida.

É por isso que a maioria das linguagens modernas, incluindo o Kotlin, ou enfatizam a imutabilidade como padrão, ou a encorajam fortemente. No Kotlin, a imutabilidade é o padrão, embora a mutabilidade continue sendo uma opção, quando realmente necessária.

Aqui está um guia rápido do power pack de imutabilidade do Kotlin:

1. Use val, em vez de var

Prefira usar val, em vez de var. Se você usar var quando val poderia ter sido usado, o IntelliJ IDEA lhe enviará uma notificação. 

2. Use classes de dados (imutáveis) com copy(...)

Em classes relacionadas a domínios, use classes data com val. As classes data do Kotlin costumam ser comparadas aos records do Java. Embora haja alguma sobreposição, as classes data oferecem um recurso matador: copy(...), cuja ausência torna muito trabalhoso transformar record, o que muitas vezes é necessário na lógica de negócios:

Java

//only immutable state
public record Person(String name, int age) {
   //Lack of default parameters requires overloaded constructor
   public Person(String name) { 
       this(name, 0);
   }
   //+ due to lack of String interpolation
  public String sayHi() {
       return "Hello, my name is " + name + " and I am " + age + " years old.";
   }
}

//Usage
final var jack = new Person("Jack", 42);
jack: Person[name=Jack, age=5]

//The issue is here: transforming a record requires manually copying the identical state to the new instance ☹️
final var fred = new Person("Fred", jack.name);

Kotlin

//also supports mutable state (var)
data class Person(val name: String,
                  val age: Int = 0) {
  //string interpolation
  fun sayHi() = "Hi, my name is $name and I am $age years old."
}
val jack = Person("Jack", 42)
jack: Person(name=Jack, age=42)

//Kotlin offers the copy method, which, due to the ‘named argument’ feature, allows you to only adjust the state you want to change 😃
val fred = jack.copy(name = "Fred")
fred: Person(name=Fred, age=42)

Então, use classes “data” quando relacionadas a domínios, sempre que possível. Sua natureza imutável garante uma experiência segura, concisa e tranquila ao trabalhar com o núcleo do seu aplicativo.     

Dica: prefira coleções imutáveis, em vez de mutáveis

Coleções imutáveis têm vantagens claras quanto à segurança das threads, podem ser passadas adiante com segurança e são mais fáceis de raciocinar. Embora Java ofereça alguns recursos de imutabilidade para coleções, é perigoso usar esses recursos, porque podem facilmente causar exceções em tempo de execução:

Java

List.of(1,2,3).add(4); ❌unsafe 😬! .add(...) compiles, but throws UnsupportedOperationException

Kotlin

//The default collections in Kotlin are immutable (read-only)
listOf(1,2,3).add(4);  //✅safe: does not compile

val l0 = listOf(1,2,3) 
val l1 = l0 + 4 //✅safe: it will return a new list containing the added element
l1 shouldBe listOf(1,2,3,4) //✅

O mesmo se aplica ao uso de Collections.unmodifiableList(...), que não só é inseguro, mas também requer alocação adicional:

Java

class PersonRepo {
   private final List cache = new ArrayList();
   // Java – must clone or wrap every call
   public List getItems() {
       return Collections.unmodifiableList(cache);   //⚠️extra alloc
   }
}

//Usage
personRepo.getItems().add(joe) ❌unsafe 😬! .add(...) can be called but throws UnsupportedOperationException

Kotlin

class PersonRepo {

//The need to type ‘mutable’ for mutable collections is intentional: Kotlin wants you to use immutable ones by default. But sometimes you need them:

   private val cache: MutableList = mutableListOf()

   fun items(): List = cache //✅safe: though the underlying collection is mutable, by returning it as its superclass List, it only exposes the read-only interface

}

//Usage
personRepo.items().add(joe) //✅safe:😬! Does not compile

Quando se trata de concorrência, deve-se preferir usar estruturas de dados imutáveis, incluindo coleções. No Java, é preciso um esforço adicional com coleções especiais, que oferecem uma API diferente ou limitada, como CopyOnWriteArrayList. Por outro lado, no Kotlin, List, que é somente para leitura, funciona para quase todos os casos de uso. 

Se você precisar de coleções mutáveis e seguras para threads, o Kotlin oferece coleções persistentes (persistentListOf(...), persistentMapOf(...)), que compartilham todas a mesma interface poderosa.

Java

ConcurrentHashMap persons = new ConcurrentHashMap();
persons.put("Alice", 23);
persons.put("Bob",   21);

//not fluent and data copying going on
Map incPersons = new HashMap(persons.size());
persons.forEach((k, v) -> incPersons.put(k, v + 1));

//wordy and data copying going on
persons
   .entrySet()
   .stream()
   .forEach(entry -> 
      entry.setValue(entry.getValue() + 1));

Kotlin

persistentMapOf("Alice" to 23, "Bob" to 21)
         .mapValues { (key, value) -> value + 1 } //✅same rich API like any other Kotlin Map type and not data copying going on

Armadilha 7: continuar usando builders (ou pior ainda, tentar usar o Lombok) 

Dica: Use argumentos nomeados.

Builders são muito comuns no Java. Embora sejam convenientes, adicionam mais código, são inseguros e aumentam a complexidade. Eles não têm utilidade no Kotlin, pois um único recurso da linguagem os torna obsoletos: os argumentos nomeados. 

Java

public record Person(String name, int age) {

   // Builder for Person
   public static class Builder {
       private String name;
       private int age;

       public Builder() {}

       public Builder name(String name) {
           this.name = name;
           return this;
       }

       public Builder age(int age) {
           this.age = age;
           return this;
       }

       public Person build() {
           return new Person(name, age);
       }
   }
}

//Usage
new JPerson.Builder().name("Jack").age(36).build(); //compiles and succeeds at runtime

new JPerson.Builder().age(36).build(); //❌unsafe 😬: compiles but fails at runtime.

Kotlin

data class Person(val name: String, val age: Int = 0)

//Usage - no builder, only named arguments.
Person(name = "Jack") //✅safe: if it compiles, it always succeeds at runtime
Person(name = "Jack", age = 36) //✅

2. Ampliar ou converter um aplicativo em Java já existente

Se você não tiver a opção de desenvolver do zero uma aplicação para experimentar o Kotlin, a solução é adicionar novos recursos ou módulos inteiros em Kotlin a uma base de código já existente em Java. Graças à interoperabilidade transparente entre o Kotlin e o Java, você pode escrever código Kotlin que parece ser Java para ser chamado a partir de código Java. Esta abordagem permite:

  • Uma migração gradual, sem uma reprogramação dramática
  • Testes do Kotlin no mundo real, no seu contexto específico
  • Aumentar a confiança da equipe através de código de produção em Kotlin

Em vez de começar com alguma coisa qualquer, considere estas abordagens diferentes:

De fora para dentro:

Comece pela parte das “folhas” do seu aplicativo — por exemplo, controlador, tarefa em lote, etc. — e depois vá avançando para o domínio central. Isso lhe dará estas vantagens: 

  • Isolamento em tempo de compilação: as classes das “folhas” raramente têm outras coisas dependendo delas; então, você pode mudá-las para Kotlin e ainda fazer build com o resto do sistema inalterado.
  • Menos edições em cascata. Uma interface de usuário ou um controlador convertido pode chamar código existente no domínio do Java quase sem alterações, graças à interoperabilidade transparente.
  • Solicitações de pull menores, revisões mais fáceis. Você pode migrar arquivo a arquivo ou recurso a recurso.

De dentro para fora:

Começar pelo núcleo e depois prosseguir para as camadas externas costuma ser uma abordagem mais arriscada, pois isso elimina as vantagens da abordagem de fora para dentro, mencionadas acima. Porém, é uma abordagem viável nos seguintes casos:

  • Núcleo muito pequeno ou autocontido. Se a sua camada de domínio consistir em apenas algumas classes simples de Java (POJOs) e serviços, pode ser barato convertê-la logo no início e isso pode liberar imediatamente construtos idiomáticos (classes de dados e de valores, hierarquias seladas).
  • A arquitetura já está para ser redesenhada, mesmo. Se você estiver planejando refatorar invariantes ou introduzir padrões de projeto orientado por domínios (DDD), tais como objetos de valor ou agregados, durante a migração, às vezes pode ser mais limpo reprojetar primeiro o domínio em Kotlin.
  • Contratos de segurança estrita contra valores nulos. Colocar o Kotlin no núcleo transforma o domínio em uma “fortaleza segura contra valores nulos”. As camadas externas em Java ainda podem enviar valores nulos, mas os limites se tornam explícitos e mais fáceis de vigiar.

Módulo a módulo

  • Se a sua arquitetura estiver organizada por recursos, em vez de em camadas, e os módulos tiverem um tamanho administrável, então é uma boa estratégia converter os módulos um a um.

Recursos de linguagem para converter Java em Kotlin

O Kotlin oferece uma variedade de recursos — principalmente anotações — que permitem que o seu código em Kotlin se comporte como Java nativo. Isso é especialmente valioso em ambientes híbridos, nos quais Kotlin e Java coexistam na mesma base de código.
Kotlin

class Person @JvmOverloads constructor(val name: String,
                          var age: Int = 0) {
  companion object {

  @JvmStatic
  @Throws(InvalidNameException::class)
  fun newBorn(name: String): Person = if (name.isEmpty()) 
       throw InvalidNameException("name not set")
     else Person(name, 0)

   @JvmField
   val LOG = LoggerFactory.getLogger(KPerson.javaClass)
  }
}

Java

//thanks to @JvmOverloads an additional constructor is created, propagating Kotlin’s default arguments to Java
var john =  new Person("John");

//Kotlin automatically generates getters (val) and setters (var) for Java
john.setAge(23);
var name = ken.getName();

//@JvmStatic and @JvmField all accessing (companion) object fields and methods as statics in Java

//Without @JvmStatic it would be: Person.Companion.newBorn(...)
var ken =  Person.newBorn("Ken"); 

//Without @JvmField it would be: Person.Companion.LOG
Person.LOG.info("Hello World, Ken ;-)");

//@Throws(...) will put the checked Exception in the method signature 
try {
  Person ken =  Person.newBorn("Ken");
} catch (InvalidNameException e) {
  //…
}

Kotlin

@file:JvmName("Persons")
package org.abc

@JvmName("prettyPrint")

fun Person.pretty() =
      Person.LOG.info("$name is $age old")

Java

//@JvmName for files and methods makes accessing static fields look like Java: without it would be: PersonKt.pretty(...)
Persons.prettyPrint(ken)

O conversor de Java para Kotlin do IntelliJ IDEA

O IntelliJ IDEA oferece um conversor de Java para Kotlin; então, teoricamente, essa ferramenta pode fazer isso para você. Porém, o código resultante está longe de ser perfeito; assim, use-o apenas como ponto de partida. A partir daí, converta-o em uma representação mais apropriada para o Kotlin. Discutiremos mais este assunto na postagem final desta série do blog: Fatores de sucesso para a adoção do Kotlin em larga escala.

Muito provavelmente, usar o Java como ponto de partida fará você escrever em Kotlin com o estilo do Java, o que lhe trará alguns benefícios, mas não liberará o poderoso potencial do Kotlin. Por isso, escrever um novo aplicativo é a abordagem que prefiro. 

Em seguida nesta série

Esta parte da nossa série de postagens no blog, O guia definitivo para a adoção bem-sucedida do Kotlin em um ambiente dominado pelo Java, demonstrou como experimentos com o Kotlin podem evoluir para código de produção. Nossa próxima postagem se concentrará no lado humano da adoção: convencer os seus colegas. Ela explicará como apresentar argumentos claros e orientados pelo código, como guiar novos desenvolvedores e criar uma comunidade de Kotlin pequena, mas duradoura dentro da sua equipe.

Artigo original em inglês por:

Urs Peter

Urs é um calejado engenheiro de software, arquiteto de soluções, palestrante e instrutor, com mais de 20 anos de experiência no desenvolvimento de sistemas resilientes, escaláveis e críticos, a maioria envolvendo Kotlin e Scala.

Além de seu trabalho como consultor, ele também é um instrutor apaixonado e autor de uma grande variedade de treinamentos, desde cursos das linguagens Kotlin e Scala até treinamentos em arquiteturas como microsserviços e arquiteturas dirigidas por eventos.

Sendo uma pessoa por natureza voltada para outras pessoas, ele adora compartilhar conhecimentos, inspirar e ser inspirado por colegas em encontros e congressos. Urs é um instrutor de Kotlin certificado pela JetBrains.

Alyona Chernyaeva

Alyona Chernyaeva

image description

Discover more