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.
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:
- Código esencial
- Código completo
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")
}
}
- Implementación
- Implementación (mejorada)
package tasks
import org.gradle.api.DefaultTask
import org.gradle.api.provider.Property
import org.gradle.api.tasks.Input
import org.gradle.api.tasks.StopExecutionException
import org.gradle.api.tasks.TaskAction
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")
}
}
package tasks
import org.gradle.api.DefaultTask
import org.gradle.api.provider.Property
import org.gradle.api.tasks.Input
import org.gradle.api.tasks.StopExecutionException
import org.gradle.api.tasks.TaskAction
abstract class FibonacciTask : DefaultTask() {
@get:Input
abstract val number: Property<Int>
@TaskAction
fun calculateFibonacci() {
val n = number.get().takeIf { it >= 0 }
?: throw StopExecutionException("The number must be greater than or equal to 0")
var (first, second) = 0 to 1
repeat(n) {
print("$first ")
second += first.also { first = second }
}
println("\nThe $n-th Fibonacci number is: $first")
}
}
- [15-16]: En lugar de usar un
if
para validar el número, utilizamostakeIf
para verificar si el número es mayor o igual a 0. Si la condición no se cumple, lanzamos una excepción. Esto simplifica la validación y hace que el código sea más conciso. - [17]: Utilizamos la asignación múltiple (
var (first, second) = 0 to 1
) para inicializar las variablesfirst
ysecond
en una sola línea. - [20]: Utilizamos el método
also
para actualizar las variablesfirst
ysecond
en una sola línea dentro del buclerepeat
. Esto hace que el código sea más expresivo y eficiente.
Las funciones de alcance (scope functions
) en Kotlin, como let
, run
, with
, apply
y also
, son funciones que permiten realizar operaciones en un objeto dentro de un contexto específico. En este caso, also
se utiliza para realizar una operación adicional en un objeto y devolver el mismo objeto, lo que permite encadenar operaciones de manera más concisa.
- [1-20]: Definimos una clase que extiende
DefaultTask
. Lo más común es que las tareas en Gradle hereden deDefaultTask
. 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 bloquesdoFirst
ydoLast
. - [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
:
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.")
}
}
- [3, 5]: Registramos las tareas
fib_10
yfib_20
utilizandotasks.register<FibonacciTask>("fib_10")
ytasks.register<FibonacciTask>("fib_20")
, respectivamente. Esto crea instancias deFibonacciTask
con la propiedadnumber
configurada en 10 y 20, respectivamente. - [7-12, 19-24]: Los bloques
doFirst
ydoLast
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).
- Código esencial
- Código completo
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)
}
}
package tasks
import org.gradle.api.DefaultTask
import org.gradle.api.tasks.InputFile
import org.gradle.api.tasks.OutputFile
import org.gradle.api.tasks.TaskAction
import java.io.File
import java.util.*
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)
}
}
- [2-3, 5-6]: Anotamos las propiedades
inputFile
youtputFile
con@get:InputFile
y@get:OutputFile
, respectivamente. Esto indica queinputFile
es un archivo de entrada youtputFile
es un archivo de salida.
Ahora, registramos la tarea en un archivo *.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:
- Windows
- Windows (corto)
- Linux/Mac
# 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
# Crear un archivo de texto con contenido
"Este es el contenido del archivo" > input.txt
.\gradlew processText # Ejecutar la tarea
gc output.txt # Mostrar el contenido del archivo de salida
# Crear un archivo de texto con contenido
echo "Este es el contenido del archivo" > input.txt
./gradlew processText # Ejecutar la tarea
cat 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:
12
7
cinco
22
9
Puedes crear este archivo de la siguiente manera:
- Powershell
- Bash
@"
12
7
cinco
22
9
"@ | Out-File -FilePath input.txt
cat <<EOF > input.txt
12
7
cinco
22
9
EOF
Ejecuta la tarea desde la línea de comandos:
./gradlew processNumbers
El archivo output.txt
debería generar un resultado similar al siguiente:
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ónNumberFormatException
para los casos donde el contenido no sea un número válido.
Solución
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
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
- 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.
- 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.
- 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. - 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
- 📚 "Build Script Essentials". (2014). Muschko, Benjamin and Dockter, Hans, en Gradle in Action, (pp. 75–104.) Manning.
- 🌐 "Writing Tasks." Accedido: 10 de septiembre de 2024. [En línea]. Disponible en: https://docs.gradle.org/current/userguide/writing_tasks.html