leanmind logo leanmind text logo

Blog

Refactorización Avanzada

Dominar el refactoring productivo para maximizar el retorno de la inversión.

Kotlin ¿Sabías qué?

Por Adrián Ferrera González y Miguel Ángel Pérez García

Kotlin es un lenguaje de programación moderno, eficiente y diseñado para ser sencillo y seguro. Desde su introducción, ha ganado popularidad debido a su interoperabilidad con Java y su capacidad para escribir código conciso y expresivo. A continuación, exploraremos algunas de las características clave de Kotlin que lo hacen destacar, y cómo estas pueden mejorar la experiencia de desarrollo.

Variables: val y var

En Kotlin, existen dos tipos principales de variables: val y var. La diferencia fundamental es que val define variables inmutables, es decir, una vez asignado un valor no se puede cambiar, mientras que var permite la reasignación de valores. Además, Kotlin introduce la palabra clave lateinit, que indica que una variable var se inicializará más tarde, lo que es útil cuando la inicialización inmediata no es posible.

Puedes verificar si una variable lateinit ha sido inicializada utilizando el siguiente código, siempre que la variable no sea de un tipo primitivo:

lateinit var foo: String
    
if(::foo.isInitialized) {
}

Template strings

Los nombres de los métodos pueden utilizar template strings, lo que aporta mucho valor a la hora de definir los tests:

@Test
fun `should do something`() {
    // ...
}

Raw Strings

Se pueden escribir Strings sin procesar (raw strings) utilizando comillas triples ("""), facilitando trabajar con cadenas de texto que incluyan saltos de línea, sin utilizar caracteres de escape.

val text = """
    Hello World
    using
    multiline
"""

Data classes

Existe un tipo de objeto denominado data-class para almacenar datos, que implementa los métodos equals, hashCode y toString, lo que facilita la manipulación y comparación de objetos sin tener que escribir mucho código adicional.:

data class BookingCustomData(
    val reference: String,
    val origin: String,
    val status: String,
    val killSlots: String,
    val noOfPackages: String,
    val cargoType: String,
    val weight: String,
    val contractRef: String
)
    
val booking = BookingCustomData("ABC123", "Spain", "Pending", "General") println(booking) // Output: BookingCustomData(reference=ABC123, origin=Spain, status=Pending, cargoType=General)

Companion Objects en lugar de métodos estáticos

No existen los métodos estáticos, en su lugar existen los denominados companion objects, que funcionan como objetos singleton asociados a la clase, limitados a contener dichos miembros estáticos:

class Irrelevant {
    companion object {
        const val USERNAME1 = "USERNAME1"
        fun addTenTo(val number: Int): Int {
            return number + 10
        }
    }
}
    
/*...*/
    
val aUserName = Irrelevant.USERNAME1 // aUserName = "USERNAME1"
val newNumber = Irrelevant.addTenTo(10) // newNumber = 20

Herencia

Por defecto, las clases están declaradas como final y no se pueden sobrescribir los métodos en clases derivadas. Es por ello, que para permitir la herencia, es a través de la palabra clave open. Se puede limitar la sobrescritura de métodos por clases nietas, aplicando en las clases hijas la palabra final:

open class Parent() {
    open fun foo (): String {
    	return "Hello from Parent"
    }
}
    
class Child: Parent() {
    final override fun foo (): String {
    	return "Hello from Child"
    }
}
    
class GrandChild: Child() {
    override fun foo (): String { // Error: 'foo' in 'Child' is final and cannot be overridden
    	return "Hello from GrandChild"
    }
}

Extensiones en interfaces y objetos

Tanto las interfaces como los companion objects pueden tener propiedades de extensión. Estas funciones extendidas pueden añadirse desde fuera de la clase original, sin necesidad de modificar su código:

// Interface
val <T> List<T>.lastIndex: Int
    get() = size - 1
    
// Companion
fun Irrelevant.Companion.print() {
    println("Irrelevant")
}

Generics

En un objeto basado en templates, Kotlin permite restringir cómo se pueden usar los tipos genéricos en clases o interfaces, utilizando las anotaciones in y out:

interface BaseUseCase<in I, out O> {
    fun execute(input: I): O
}

Sobrecarga de Operadores

De la misma forma, puedes sobrecargar operadores utilizando la palabra reservada operator. Así, se puede por ejemplo, sobrecargar el operador invoke para así definir una llamada directa a la instancia y que sea el método por defecto, y por tanto, la instancia de la clase actúe como una función:

interface BaseUseCase<in I, out O> {
    operator fun invoke(input: I): O
}
    
class MyUseCase: BaseUseCase {
    override fun invoke(input: String): String {
    	return "Say: $input"
    }
}
    
fun main() { 
    val useCase = MyUseCase() 
    println(useCase("Hello World")) 
}

Funciones de Alcance (Scope Functions)

Todos los métodos pueden usar el operador let, also y run, los cuales forman parte de las denominadas scope functions, para encadenar operaciones (programación funcional) y evitar estructuras de control explícitas. Un ejemplo muy bueno es cuando existen propiedades opcionales.
En el siguiente caso, el valor devuelto por findByCode solo se pasará a delete (como el parámetro it), si este no es nulo:

customerRepository
    .findByCode(customers[0].code)
    ?.let {
        customerRepository.delete(it)
    }

Uso de rangos

El concepto de range permite definir rangos de valores de manera simple, tanto numéricos como de caracteres:

for (i in 1..3) { }
for (i in 'a'..'z') { }

Pattern Matching

En Kotlin existe el concepto de Pattern Matching. Es un sustituto del clásico switch, el cual es especialmente útil ya que:

when (x) {
    1 -> print("value is 1")
    2 -> print("value is 2")
    else -> print("value is not 1 or 2")
}
val extractResult = when (id) {
    in 1..2 -> foo()
    3, 4, 5, 6, 30 -> bar()
    in 23..24 -> otherStuff()
    else -> differentStuff()
}

Sealed Classes

Las sealed classes o clases selladas, son una extensión del concepto de clases jerárquicas, que, a diferencia de las clases convencionales, permiten restringir las posibles subclases a un conjunto definido en tiempo de compilación. Esto es útil por ejemplo, en aquellas situaciones donde tienes un conjunto definido de funcionalidades que puedan ser representadas.

Combinando esto con el pattern matching explicado en el punto anterior, tenemos una herramienta poderosa que nos avisará en el momento en el que no hayamos implementado, por ejemplo, una lógica de negocio:

sealed class Subscription {
    data class Monthly(): Subscription()
    data class Yearly(): Subscription()
    data class Quarterly(): Subscription()
}
    
fun processSubscription(subscription: Subscription) {
    when (subscription) {
        is Subscription.Monthly -> processMonthlySubscription()
        is Subscription.Yearly -> processYearlySubscription()
    }  // Error: Missing Quarterly case
}
```kotlin

## TODO para marcar operaciones no implementadas

Existe la palabra reservada `TODO`, que en , es una función que devuelve `Nothing` y lanza una excepción, permitiendo así marcar lugares en el código que aún no están listos, y facilitando compilar y ejecutar la aplicación sin problemas:

```kotlin
fun TODO(): Nothing = throw NotImplementedError()

Contratos y Smart Cast

Tenemos el denominado contract, una característica experimental y avanzada del lenguaje, los cuales aportan información adicional al compilador sobre el flujo del programa y el manejo de tipos. Con esto nos referimos a aquellas partes que la persona que desarrolla el código conoce, pero el compilador es incapaz de inferir.
Por ejemplo: cuando hacemos una comprobación de que algo no es nulo, por contrato el compilador sabe que puede hacer un smart-cast al tipo Not-nullable.

public inline fun CharSequence?.osNullOrEmpty(): Boolean {
    contract {
    	returns(false) implies (this@isNullOrEmpty != null)
    }
}
    
fun example(text: CharSequence?) { 
    if (!text.isNullOrEmpty()) { 
        // Ahora el compilador sabe que `text` no es nulo 
        println(text.length) // Sin error: smart cast a CharSequence no nullable 
    } 
}

Type aliases

Se pueden definir type aliases typealias para proporcionar nombres alternativos o simplificados, para tipos complejos, haciéndolos más legibles y/o adaptables en el código:

class Irrelevant {
    class Foo
}
    
typealias SomethingRandom = Irrelevant.Foo
    
val fooInstance: SomethingRandom = Irrelevant.Foo()

Delegación de propiedades

Existe un sistema de delegación de propiedades, que permite delegar la lógica de acceso y modificación de las propiedades a otras clases o funciones. Tenemos los siguientes delegados:

class User (val map: Map<String, Any?>) {
    val name: String by map
    val age: Int by map
}
fun foo() {
    val irrelevant: Irrelevant by lazy { Irrelevant() }
}
class User {
    var name: String by observable("<no name>") {
        prop, old, new ->
        println("$old -> $new")
    }
}
    
fun main() {
    val user = User()
    user.name = "first"
    user.name = "second"
}
    
// <no name> -> first
// first -> second

Con lo que hemos visto hasta aquí, podemos decir que Kotlin ofrece múltiples herramientas para escribir código eficiente y expresivo.

¿Conoces alguna otra característica que deberíamos destacar?

Publicado el 23/09/2024 por
Adrián image

Adrián Ferrera González

https://adrianferrera.dev

Miguel Ángel image

Miguel Ángel Pérez García

¿Quieres más? te invitamos a suscribirte a nuestro boletín para avisarte cada vez que recopilemos contenido de calidad que compartir.

Si disfrutas leyendo nuestro blog, ¿imaginas lo divertido que sería trabajar con nosotros? ¿te gustaría?

Impulsamos el crecimiento profesional de tu equipo de developers