Na Creditas estamos começando a utilizar Kotlin para modelar nossas aplicações que são Core Business e que exigem padrões de projeto mais complexos como o DDD. Até então estávamos utilizando majoritariamente Ruby para esse fim. Porém, dado a inúmeras insatisfações com a stack Ruby para esse caso de uso em específico, resovemos testar Kotlin e o resultado tem sido muito bom. Talvez no futuro eu faça algum post falando mais detalhes por trás das motivações dessa migração. Neste aqui pretendo focar em algumas simples features da linguagem que inicialmente nos chamaram atenção e nos fizeram avaliar com seriedade essa linguagem que hoje estamos utilizando.
Uma das coisas que tem vindo bem forte no movimento de linguagens funcionais é a imutabilidade. Kotlin, diferente de algumas linguagens como C#, Java, Ruby e Python, tem uma série de facilidades para lidar com imutabilidade de forma simples e sem verbosidade. Um exemplo são os data classes
:
Criando Value Objects/DTOs
data class Endereco(val rua: String, val numero: Int)
Ao usar a keyword data class
você ganha diversas coisas como, por exemplo, um construtor padrão, um lindo método equals()
que compara os valores dos atributos ao invés da referência da instância, além de outros métodos como copy()
, toString()
.
val instancia1 = Endereco("foo", 1)
val instancia2 = Endereco("foo", 1)
instancia1 == instancia2
// true!
São funcionalidades perfeitas para atender implementações de Value Objects do DDD e DTOs.
Criando uma classe
Uma classe comum contendo um atributo nome
imutável (val) e endereco
mutável (var):
class Pessoa(val nome: String, var endereco: Endereco) {
fun tamanhoDoNome() {
println("Olá, meu nome tem ${nome.length} letras!")
}
}
Diferença entre val
e var
Eles na verdade são açucares sintáticos para criação de métodos getters e setters. O val
equivale a uma propriedade com apenas getter, o var
equivale a uma propriedade com getter e setter.
A seguinte classe:
class Animal(var nome: String)
É equivalente a isso em Java:
public final class Animal {
private String nome
public Animal(String nome) {
this.nome = nome;
}
public String getNome() {
return this.nome;
}
public void setNome(string value) {
this.nome = value;
}
}
Muito menos verboso, não é mesmo? Durante a migração de um dos nossos projetos de Ruby para Kotlin conseguimos diminuir quase pela metade a quantidade de linhas de código. Nunca se esqueça: quanto mais boilerplate você escreve, mais brechas para bugs você abre, mais código você tem para testar, mais código você tem para ser lido (por você e pelos seus colegas) e mantido. Complexidade, seja lá de qual forma se manifeste, é custo.
Observação: Ambos val
e var
podem ser utilizados tanto em classes normais class
quanto em classes de dados data class
. As pessoas normalmente entendem o data class
como sendo por si só uma classe imutável, quando na verdade o que garante que seus campos não sejam modificados é o uso de atributos do tipo val
. O data class
por si só apenas facilita a criação do construtor, método equals()
e etc (como citado anteriormente).
Fechado por padrão
Note o final
na tradução para Java no código anterior. Isso mesmo, em Kotlin todas as classes são seladas por padrão (“Design and document for inheritance or else prohibit it”). Caso vocẽ queria estender uma classe você terá que marcá-la como open
. Gosto desse comportamento pois se formos analisar em nossos projetos a esmagadora maioria das classes não possuem herança e não foram projetadas para isso.
Interoperabilidade
Outra coisa legal, em relação a interoperabilidade, é que se você consumir em Java um arquivo .jar
que foi gerado a partir de Kotlin, você vai poder chamar os métodos setters e getters normalmente sem perceber nada de diferente do que está acostumado.
Variáveis locais imutáveis
Não apenas um açucar sintático para criação dos getters, setters e parâmetros de construtor, o val
é uma keyworkd que também pode ser utilizada no escopo de funções para definir “variáveis” imutáveis (que nesse caso não deveriam ser chamadas de variáveis, já que não variam, anyway… é assim que a propria documentação chama):
val animal = Animal("Scooby")
animal = Animal("Marley") //Error: Val cannot be reassigned!
Das linguagens que citei anteriormente C#, Ruby e Python simplesmente não possuem essa feature (possuem apenas para tipos primitivos, através do uso de constantes hardcoded). Java possui através da junção das keyworkds final
e var
, exemplo: final var animal = new Animal("Scooby")
.
Observação 2: Na verdade o val
não garante a imutabilidade de fato. [MIND BLOW] O val
apenas garante que aquela variável vai ser atribuída uma única vez. Ou seja, a instância para qual aquela variável está apontando pode sofrer mudanças internas e isso está além do escopo do val.
Por exemplo, imaginando que a classe Animal
tenha um método que altera estado do objeto, adicionando uma coleira no animal:
val animal = Animal("Scooby")
animal.adicionarColeira(coleira)
//A instância do objeto foi modificada, porém a variável não foi reatribuida, então Sucesso!
Null safety
A proteçao contra nulos é a feature que eu mais gosto em Kotlin e não está presente em nenhuma das linguagens citadas anteriormente.
O sistema de tipos de Kotlin foi desenhado visando eliminar o perigo de exceções de
NullReference
no código, também conhecido como o Erro de um Bilhão de Dólares.
Em Kotlin os tipos nulos e não nulos são tratados como coisas distintas. O default é ser não nulo. Caso você queira que seja nulo você precisa dizer isso explicitamente, e o compilador vai te OBRIGAR a tratar essa variável em todo lugar. Isso acaba por desencorajar o uso de tipos nulos e nos obriga a refletir onde eles realmente são necessários, o que é ótimo.
Por exemplo, se no código anterior alterarmos o atributo nome
para ser nullable, o seguinte código não compila:
class Pessoa(val nome: String?, var endereco: Endereco) {
fun tamanhoDoNome() {
println("Olá, meu nome tem ${nome.length} letras!")
}
}
É preciso fazer o tratamento da possibilidade de null reference no trecho ${nome.lenght}
! Uma das formas de tratar isso seria utilizando o operador ?
, também conhecido como safe call operator, exemplo:${nome?.length}
. Entretanto nem sempre é necessário, pois o compilador é inteligente e consegue identificar se você já tratou a variável em determinado escopo, veja o exemplo abaixo:
if (nome != null) {
// Aqui dentro a variável nome é automaticamente convertida para 'não nula'
// Portanto não é necessário tratar o null reference, o compilador sabe que não vai acontecer!
println("Olá, meu nome tem ${nome.length} letras!")
}
O nome dessa feature é smart cast e não serve apenas para tipos nullables.
Smart Cast
fun funcaoQualquer(x: Any) {
if (x is String) {
// x automaticamente convertido para String nesse escopo!
print(x.length)
}
}
E funciona com o pattern matching do when
também:
when (x) {
is Int -> print(x + 1)
is String -> print(x.length + 1)
is IntArray -> print(x.sum())
}
Lidando com coleções
Esse post mostra ótimos exemplos ao se trabalhar com coleções em Kotlin. Vou reproduzir um bem simples aqui:
class Student(
val name: String,
val surname: String,
val passing: Boolean,
val averageGrade: Double
)
Considerando uma lista que contem vários estudantes (classe definida acima), é possível fazer operações nessa lista utilizando as funções da standard library do Kotlin:
students.filter { it.passing && it.averageGrade > 4.0 }
.sortedBy { it.averageGrade }
.take(10)
.sortedWith(compareBy({ it.surname }, { it.name }))
O código é auto explicativo pra maioria das pessoas que vem de outras linguagens, já que quase todas dão suporte pra esses tipos de operações. C# dá suporte pra isso há 10 anos, Java introduziu as Streams no Java 8 há 4 anos (porém com uma sintaxe muito mais suja do que o exemplo acima), Ruby, Python, Javascript e praticamente todas as linguagens mainstreams de mercado dão suporte, umas de forma mais clean e completa do que outras, mas todas possuem suporte em algum nível.
Monads e outras abstrações funcionais
Kotlin não possui na sua standard library um grande acervo para abstrações e estruturas de dados comuns em linguagens funcionais. Entretanto, é possível utilizar esse tipo de feature com o auxílio de libs de extensão como o Arrow. Esse tipo de lib é bem comum mesmo em linguagens que possuem certo suporte para estruturas de dados funcionais. Scala, por exemplo, apesar de possuir algumas estruturas já implementadas na sua standard library, possui libs fortes que complementam essas funcionalidades como a Scalaz e ScalaCats.
Outras features não mencionadas
Nesse post citei apenas algumas features para não estender tanto, mas tem muito mais coisas interessantes do que mostrei aqui. Aqui vai mais algumas:
- Funções soltas em nível de package (fora de classes)
- Tipos de dados algébricos
- Pattern matching exaustivos com
when
- Ótimo suporte para criação de DSLs poderosas
- Delegated properties
- Extension functions
A documentação oficial da linguagem é muito boa e considero um ótimo começo de estudo pra quem tiver interesse em se aprofundar mais!
comments powered by Disqus