Skip to main content

Tareas como clases

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


r8vnhill/echo-app-kt

A veces necesitamos definir tareas más complejas que requieren lógica más avanzada o necesitamos reutilizar tareas en diferentes contextos. En estos casos, es posible definir tareas como clases en Gradle, lo que permite encapsular la lógica y reutilizarla fácilmente en diferentes partes del proyecto.

Estas tareas pueden definirse en archivos *.gradle.kts, como se hace habitualmente con las tareas regulares, o en archivos .kt, como cualquier otra clase en Kotlin. En este ejemplo, utilizaremos el enfoque de definir las tareas en un archivo .kt separado, manteniendo una clara separación de responsabilidades. De esta manera, el archivo *.gradle.kts se limita a registrar y configurar las tareas, mientras que la lógica específica de cada tarea se maneja de forma independiente en archivos .kt.

Esta separación no solo mejora la organización del proyecto, sino que también facilita el mantenimiento del código, al mantener los archivos de configuración más limpios y centrados exclusivamente en la configuración, delegando la lógica de las tareas a clases de Kotlin, lo que permite aplicar buenas prácticas de programación orientada a objetos y reutilización de código.

Single Responsibility Principle

Recordemos que el Principio de Responsabilidad Única (SRP) establece que un componente (ya sea una clase, función, o módulo) debe tener una sola razón para cambiar. Al definir las tareas como clases independientes, seguimos este principio, ya que cada clase se encarga exclusivamente de la lógica de una tarea específica. Esto no solo mejora la claridad y mantenibilidad del código, sino que también facilita su evolución, ya que los cambios futuros solo afectarán a la clase encargada de esa tarea en particular.

Caso de estudio: Fibonacci revisited

Hasta ahora, hemos creado una tarea que imprime los primeros 10 números de la secuencia de Fibonacci. Supongamos que ahora queremos extender esta tarea para calcular la secuencia hasta un número específico proporcionado por lx usuarix. Para hacer esto, definiremos la tarea como una clase en un archivo .kt.

Primero, creamos un paquete llamado tasks en el directorio src/main/kotlin del módulo convention-plugins. Dentro de este paquete, crearemos un archivo llamado FibonacciTask.kt con el siguiente contenido:

abstract class FibonacciTask : DefaultTask() {
@get:Input
abstract val number: Property<Int>

@TaskAction
fun calculateFibonacci() {
val n = number.get()
if (n < 0) {
throw StopExecutionException("The number must be greater than or equal to 0")
}
var first = 0
var second = 1
repeat(n) {
print("$first ")
second += first
first = second - first
}
println("\nThe $n-th Fibonacci number is: $first")
}
}
¿Qué acabamos de hacer?
  • [1-20]: Definimos una clase que extiende DefaultTask. Lo más común es que las tareas en Gradle hereden de DefaultTask. Notamos que la clase es abstracta, lo que indica que ciertos parámetros de la tarea, como el input, deben ser definidos antes de usarla.
  • [2]: Anotamos la propiedad number con @get:Input para indicar que es un input de la tarea. Esto permite que Gradle detecte cambios y decida si es necesario ejecutar la tarea nuevamente.
  • [3]: Utilizamos un Property<Int> para almacenar el número hasta el cual queremos calcular la secuencia de Fibonacci. Property es como una caja que contiene un valor mutable que Gradle puede monitorear.
  • [5-19]: Definimos el método calculateFibonacci y lo anotamos con @TaskAction. Este método contiene la lógica principal de la tarea. Gradle lo ejecuta automáticamente cuando la tarea se llama, ejecutándose entre los bloques doFirst y doLast.
  • [7]: Utilizamos number.get() para obtener el valor del número proporcionado por lx usuarix.
  • [8-10]: Validamos que el número sea mayor o igual a 0. Si no lo es, lanzamos una excepción StopExecutionException para detener la ejecución de la tarea. Gradle manejará esta excepción y mostrará un mensaje de error.

Registro de la tarea en un archivo *.gradle.kts

Para usar nuestra tarea, debemos registrarla en algún archivo *.gradle.kts. Aquí te mostramos cómo hacerlo en el archivo playground.gradle.kts:

convention-plugins/src/main/kotlin/playground.gradle.kts
import tasks.FibonacciTask
// ...
tasks.register<FibonacciTask>("fib_10") {
group = "playground"
description = "Calculates the 10th Fibonacci number"
number = 10
doFirst {
println("Calculating the 10th Fibonacci number...")
}
doLast {
println("Calculation complete.")
}
}

tasks.register<FibonacciTask>("fib_20") {
group = "playground"
description = "Calculates the 20th Fibonacci number"
number = 20
doFirst {
println("Calculating the 20th Fibonacci number...")
}
doLast {
println("Calculation complete.")
}
}
¿Qué acabamos de hacer?
  • [3, 5]: Registramos las tareas fib_10 y fib_20 utilizando tasks.register<FibonacciTask>("fib_10") y tasks.register<FibonacciTask>("fib_20"), respectivamente. Esto crea instancias de FibonacciTask con la propiedad number configurada en 10 y 20, respectivamente.
  • [7-12, 19-24]: Los bloques doFirst y doLast definen acciones adicionales que se ejecutan antes y después de la lógica principal de la tarea.

Ejecución de las tareas

Podemos ejecutar las tareas desde la línea de comandos de Gradle:

./gradlew fib_10
./gradlew fib_20

Esto imprimirá en la consola los primeros 10 o 20 números de la secuencia de Fibonacci, dependiendo de la tarea que se ejecute.

Ejercicio: Ejecutar fib_10

¿Qué imprime ejecutar la tarea fib_10? ¿Por qué?

Solución

Ejecutar la tarea fib_10 imprimirá:

> Task :fib_10
Calculating the 10th Fibonacci number...
0 1 1 2 3 5 8 13 21 34
The 10-th Fibonacci number is: 55
Calculation complete.

Esto se debe a que doFirst imprime un mensaje antes de calcular la secuencia de Fibonacci, y doLast imprime un mensaje al finalizar el cálculo.

Ejemplo: Procesar texto

Implementaremos una tarea para procesar archivos de texto. Tendremos archivos de entrada y salida. El texto debe ser tomado de un archivo de texto (input), transformado en mayúsculas, y ser guardado en otro archivo de texto (output).

abstract class UppercaseTask : DefaultTask() {
@get:InputFile
abstract var inputFile: File

@get:OutputFile
abstract var outputFile: File

@TaskAction
fun processText() {
val processedText = inputFile.readText()
.uppercase(Locale.getDefault())
outputFile.writeText(processedText)
}
}
¿Qué acabamos de hacer?
  • [2-3, 5-6]: Anotamos las propiedades inputFile y outputFile con @get:InputFile y @get:OutputFile, respectivamente. Esto indica que inputFile es un archivo de entrada y outputFile es un archivo de salida.

Ahora, registramos la tarea en un archivo *.gradle.kts:

convention-plugins/src/main/kotlin/playground.gradle.kts
import tasks.UppercaseTask

// ...

tasks.register<UppercaseTask>("processText") {
group = "playground"
description = "Process text from input file and write to output file in uppercase"
inputFile = file("input.txt")
outputFile = file("output.txt")
doFirst {
println("Processing text...")
}
doLast {
println("Processing complete.")
}
}

Finalmente, ejecutamos la tarea desde la línea de comandos de Gradle:

# Crear un archivo de texto con contenido
"Este es el contenido del archivo" |
Out-File -FilePath input.txt
.\gradlew processText # Ejecutar la tarea
Get-Content output.txt # Mostrar el contenido del archivo de salida

Con esto deberíamos ver el contenido del archivo de entrada en mayúsculas en el archivo de salida.

> Task :processText
Processing text...
Processing complete.
# ...
ESTE ES EL CONTENIDO DEL ARCHIVO
Ejercicio: Verificación de números pares e impares

Vamos a implementar una tarea personalizada que procese un archivo de texto con una lista de números (uno por línea). La tarea verificará si cada número es par o impar y luego escribirá los resultados en un archivo de salida, indicando si el número es "par" o "impar", o si el contenido no es un número válido.

Ejecución de las tareas

Crea un archivo input.txt con una lista de números como el siguiente ejemplo:

input.txt
12
7
cinco
22
9

Puedes crear este archivo de la siguiente manera:

@"
12
7
cinco
22
9
"@ | Out-File -FilePath input.txt

Ejecuta la tarea desde la línea de comandos:

./gradlew processNumbers

El archivo output.txt debería generar un resultado similar al siguiente:

output.txt
12: par
7: impar
cinco: no es un número válido
22: par
9: impar
Ver hints
  • Puedes utilizar la función readLines: File.() -> List<String> para leer el archivo línea por línea.
  • Para convertir las líneas en números, usa toInt: String.() -> Int, manejando la excepción NumberFormatException para los casos donde el contenido no sea un número válido.
Solución
convention-plugins/src/main/kotlin/tasks/ParityTask.kt
abstract class ParityTask : DefaultTask() {
@get:InputFile
abstract var inputFile: File

@get:OutputFile
abstract var outputFile: File

@TaskAction
fun processNumbers() {
val lines = inputFile.readLines()
val results = mutableListOf<String>() // Lista para almacenar los resultados
for (line in lines) {
try {
val num = line.toInt() // Convertir el valor leído a entero
val result = if (num % 2 == 0) "$num: par" else "$num: impar"
results.add(result)
} catch (e: NumberFormatException) {
results.add("$line: no es un número válido")
}
}
outputFile.writeText(results.joinToString("\n"))
}
}
Solución mejorada
convention-plugins/src/main/kotlin/tasks/ParityTask.kt
abstract class ParityTask : DefaultTask() {
@get:InputFile
abstract var inputFile: File

@get:OutputFile
abstract var outputFile: File

@TaskAction
fun processNumbers() = inputFile.readLines().map { line ->
try {
val num = line.toInt()
"$num: ${if (num % 2 == 0) "par" else "impar"}"
} catch (e: NumberFormatException) {
"$line: no es un número válido"
}
}.let { results -> outputFile.writeText(results.joinToString("\n")) }
}

¿Qué aprendimos?

En esta lección, aprendimos cómo definir tareas como clases en Gradle utilizando Kotlin, lo que nos permite encapsular lógica compleja en clases reutilizables y aplicar principios de programación orientada a objetos.

Puntos clave

  1. Separación de responsabilidades: Definir tareas como clases permite delegar la lógica específica a clases independientes, manteniendo los archivos de configuración de Gradle más limpios y enfocados en la configuración de las tareas. Esto también sigue el Principio de Responsabilidad Única (SRP), mejorando la mantenibilidad y facilitando futuras modificaciones.
  2. Reutilización de código: Al encapsular la lógica en clases de Kotlin, podemos reutilizar estas tareas en diferentes contextos del proyecto, mejorando la eficiencia y reduciendo la duplicación de código.
  3. Gradle Tasks en Kotlin: Vimos cómo definir tareas en Kotlin utilizando DefaultTask, con propiedades anotadas como @Input, @InputFile, y @OutputFile, lo que facilita la gestión de entradas y salidas en nuestras tareas.
  4. Tareas parametrizadas: Aprendimos a crear tareas parametrizadas, como la tarea de Fibonacci, donde el número hasta el cual se calcula la secuencia es un parámetro configurable. Esto muestra cómo hacer que nuestras tareas sean más flexibles y personalizables según las necesidades del proyecto.

Al definir tareas como clases, conseguimos un código más modular, limpio y reutilizable, aprovechando las capacidades avanzadas de Gradle y Kotlin.

Bibliografías Recomendadas