Skip to main content

Métodos de extensión

⏱ Dedicación recomendada: 0 minutos
Esto considera el contenido visible y relevante, e ignora texto colapsado o marcado como opcional.


r8vnhill/object-oriented-programming-kt

Be lazy...

Puedes ejecutar el siguiente comando para crear el módulo

./gradlew setupExtensionsModule

Mientras se crean los archivos necesarios, puedes leer el código para saber qué está pasando.

convention-plugins/src/main/kotlin/extensions.gradle.kts
import tasks.ModuleSetupTask

tasks.register<ModuleSetupTask>("setupExtensionsModule") {
description = "Creates the base module and files for the Data-Driven Testing project"
moduleName.set("extensions")
doLast {
createFiles(
packageName = "utils",
main to "StringExtensions.kt",
test to "StringExtensionsTest.kt",
main to "ListExtensions.kt",
test to "ListExtensionsTest.kt",
)
createFiles(
packageName = "connection",
main to "Host.kt",
test to "HostTest.kt",
)
createFiles(
packageName = "greet",
main to "Greeter.kt",
test to "GreeterTest.kt",
)
}
}

Preocúpate de que el plugin extensions esté aplicado en el archivo build.gradle.kts de tu proyecto.

./gradlew setupExtensionsModule

Preocúpate de que el nuevo módulo esté incluido en el archivo settings.gradle.kts.

Los métodos de extensión (o funciones de extensión) son una característica poderosa que te permite añadir nuevas funciones a clases existentes sin modificar su código fuente o utilizar herencia. Esto es especialmente útil cuando quieres extender clases de librerías o APIs que no puedes cambiar directamente.

Método de extensión


Un método de extensión es una función que añade comportamiento adicional a una clase ya existente. Aunque parece que estás añadiendo un nuevo método a la clase, en realidad estás definiendo una función que opera sobre una instancia de esa clase. La sintaxis te permite llamar a esta función como si fuera un método miembro de la clase.

Sintaxis de métodos de extensión en Kotlin

La sintaxis básica para definir una función de extensión es la siguiente:

fun ClassName.methodName(parameters): ReturnType {
// Implementación de la función
}

Donde:

  • ClassName es la clase que estás extendiendo.
  • methodName es el nombre de la nueva función.
  • parameters son los parámetros que la función acepta.
  • ReturnType es el tipo de dato que la función devuelve.

Caso de estudio: Creando un matcher como función de extensión

En la lección sobre matchers personalizados, aprendimos a crear nuestros propios matchers para hacer las pruebas más legibles, permitiendo expresiones como 2 should beEven(). Pero, ¿cómo podemos encadenar varios matchers personalizados, al igual que los predefinidos de Kotest?

Vamos a definir cómo queremos que se vea la sintaxis de estos matchers personalizados dentro de un test:

checkAll(
Arb.int(0..1000).map { it * 2 }
) { evenNumber ->
evenNumber
.shouldBeEven()
.shouldNotBeNegative()
}
checkAll(
Arb.negativeInt().filter { it % 2 != 0 }
) { oddOrNegativeNumber ->
shouldThrow<AssertionError> {
oddOrNegativeNumber
.shouldBeEven()
.shouldNotBeNegative()
}
}

Para hacer esto posible, necesitamos que nuestras funciones de matcher devuelvan el mismo valor de tipo Int y permitan encadenar llamadas. Esto implica definir las funciones con el tipo Int.() -> Int.

Definición de los matchers personalizados

A continuación se muestra cómo podemos crear nuestras funciones de extensión para permitir el encadenamiento de matchers:

fun Int.shouldBeEven(): Int {
this should beEven()
return this
}

fun Int.shouldNotBeNegative(): Int {
this shouldNot beNegative()
return this
}
¿Qué acabamos de hacer?

Hemos definido dos funciones de extensión para Int que permiten encadenar matchers personalizados:

  • shouldBeEven(): Verifica si un número es par.
  • shouldNotBeNegative(): Verifica si un número no es negativo.

Ambas funciones devuelven el valor original para permitir el encadenamiento de matchers.

Estas funciones permiten escribir pruebas más limpias y legibles al encadenar múltiples condiciones. Al devolver el valor original, podemos aplicar múltiples validaciones sin necesidad de romper la fluidez del código de prueba.

Ejercicio

Implementa las funciones de extensión shouldBeNegative: Int.() -> Int y shouldNotBeNegative: Int.() -> Int para verificar si un número es negativo o no.

Solución
fun Int.shouldBeNegative(): Int {
this should beNegative()
return this
}

fun Int.shouldNotBeNegative(): Int {
this shouldNot beNegative()
return this
}

Caso de estudio: Añadiendo una función a List<T>

Los métodos de extensión pueden ser genéricos, lo que te permite trabajar con clases genéricas.

Supongamos que queremos añadir una función second() a las listas, que devuelve el segundo elemento o lanza una excepción si la lista tiene menos de dos elementos.

Especificación BDD

"A list" - {
"when getting the second element" - {
"should return the last element if the list has 2 elements" {}
("should return the element at index 1 if the list has more than " +
"2 elements") {}
"should throw an exception if the list has less than 2 elements" {}
}
}
fun <T> List<T>.second(): T {
if (size < 2) throw NoSuchElementException("La lista tiene menos de dos elementos")
return this[1]
}
listOf(10, 20, 30).second() shouldBe 20

shouldThrow<NoSuchElementException> {
emptyList<Int>().second()
}
¿Qué acabamos de hacer?
  • [1] Definimos una función de extensión second() para List<T>.
  • [2] Verificamos si la lista tiene al menos dos elementos.
  • [3] Devolvemos el segundo elemento usando this[1].

Uso de run y Extensiones de Receptores

En Kotlin, la función run te permite ejecutar un bloque de código en el contexto de un objeto. Esto es especialmente útil cuando deseas ejecutar métodos de extensión definidos dentro de una clase de manera concisa y legible.

Ejemplo completo usando run con funciones de extensión internas

A continuación veremos cómo definir una función de extensión dentro de una clase y luego utilizar run desde fuera para ejecutarla.

class Greeter(val greeting: String) {

// Función de extensión interna
fun String.withGreeting(): String = "$greeting, $this!"
}

Para utilizar esta función desde fuera de la clase con run:

val greeter = Greeter("Hello")

val message = greeter.run { "Alice".withGreeting() }

println(message) // Imprime: Hello, Alice!
¿Qué acabamos de hacer?
  • La función de extensión withGreeting() se define dentro de la clase Greeter y tiene acceso a sus propiedades, incluso aunque esté extendiendo la clase String.
  • Al usar greeter.run { ... }, establecemos greeter como el receptor implícito, permitiendo invocar métodos y extensiones definidas en Greeter de forma directa y legible.
  • Esto hace que la función run sea particularmente útil para ejecutar métodos de extensión que dependen del contexto interno de una clase.

Propiedades de Extensión

Al igual que los métodos de extensión, las propiedades de extensión te permiten añadir nuevas propiedades a una clase existente sin modificar su código fuente o utilizar herencia. Aunque parece que estás añadiendo una nueva propiedad a la clase, en realidad estás creando una función que se comporta como si fuera una propiedad.

Definición de Propiedad de Extensión

Una propiedad de extensión sigue la misma sintaxis que una propiedad normal, pero debe definirse como una función getter, ya que no es posible almacenar datos adicionales en la instancia de la clase que estás extendiendo.

Sintaxis de una Propiedad de Extensión

val ClassName.propertyName: PropertyType
get() = // Implementación que devuelve el valor de la propiedad
set(value) {
// Implementación opcional para propiedades de escritura
}

Donde:

  • ClassName es la clase que estás extendiendo.
  • propertyName es el nombre de la nueva propiedad.
  • PropertyType es el tipo de la propiedad.
  • get() es la función que devuelve el valor de la propiedad.
  • set(value) es la función opcional que establece el valor de la propiedad.

Ejemplo: Propiedad de Extensión para String

Supongamos que queremos añadir una propiedad de extensión a la clase String para obtener la primera palabra de una cadena.

val String.firstWord: String
get() = this.split(" ").first()

val sentence = "Kotlin is fun"
println(sentence.firstWord) // Output: Kotlin

Beneficios y limitaciones de los métodos de extensión

Beneficios

  • Mayor legibilidad: Permiten invocar métodos como si fueran parte de la clase original, haciendo el código más expresivo y fácil de entender.
  • Extensibilidad: Facilitan la extensión de clases provenientes de librerías externas o APIs, incluso si no tienes acceso a su código fuente.
  • Flexibilidad: Evitan la necesidad de utilizar herencia excesiva para añadir funcionalidad, manteniendo jerarquías más simples y claras.
  • Separación de responsabilidades: Ayudan a separar claramente nuevas funcionalidades de la implementación original, promoviendo un diseño modular.
  • Facilidad para pruebas unitarias: Facilitan la creación de métodos específicos para pruebas, incrementando la legibilidad y la fluidez de los tests.

Limitaciones

  • Acceso limitado: Las funciones de extensión no pueden acceder a miembros privados o protegidos de la clase que extienden, salvo si están definidas dentro de la misma clase.
  • Riesgo de sobreuso: Pueden incentivar la creación excesiva de métodos, generando dificultad para identificar claramente dónde está implementada una funcionalidad específica.
  • Conflictos potenciales: Dos extensiones con la misma firma pueden causar conflictos, haciendo el código difícil de mantener si no se gestionan adecuadamente los espacios de nombres (imports).
  • No modifican realmente la clase original: Aunque parecen métodos propios de la clase, son funciones externas que podrían causar confusión si no están claramente documentadas.

Aquí tienes la última sección completa con su ejemplo y conclusiones claras para cerrar la lección sobre métodos de extensión:

Conclusiones

Los métodos y propiedades de extensión son herramientas esenciales en Kotlin para añadir funcionalidad adicional a clases existentes de forma clara, concisa y segura. Permiten mantener un diseño modular, limpio y fácil de mantener, evitando el uso innecesario de herencia y promoviendo la separación de responsabilidades.

Sin embargo, como toda herramienta poderosa, requieren un uso cuidadoso y equilibrado para evitar problemas como conflictos en los nombres o confusiones sobre el origen real del método o propiedad.

Puntos clave

  • Extensiones simples y efectivas: Permiten mejorar la usabilidad y expresividad del código sin modificar la implementación original de la clase.
  • Modularidad y legibilidad: Fomentan un diseño modular y un código fácil de mantener y leer.
  • Facilitan la integración con bibliotecas externas: Puedes extender fácilmente clases externas y adaptarlas a tus necesidades sin utilizar herencia.
  • Precaución con conflictos y uso excesivo: Es importante utilizar los métodos y propiedades de extensión con moderación, asegurando claridad y evitando conflictos en los espacios de nombres.

En definitiva, dominar los métodos y propiedades de extensión te permite aprovechar al máximo la flexibilidad y potencia del lenguaje Kotlin, facilitando la creación de código limpio, expresivo y fácil de mantener.

Bibliografías Recomendadas