AA3 · Ejercicios de datos y repositorios¶
Como en las unidades anteriores: parte del esqueleto, escribe la solución en un .kt y pega el main para comprobarla. Tu salida debe coincidir con el resultado esperado, obtenido ejecutando la solución modelo. Los marcados con ⭐ son más difíciles.
Ejercicio AA3.1¶
Mapear con validación. Completa toDominio() para convertir el DTO de un usuario, descartando (null) los incompletos o con un email sin @.
data class UsuarioDto(val id: Int?, val nombre: String?, val email: String?, val edad: Int?)
data class Usuario(val id: Int, val nombre: String, val email: String, val mayorDeEdad: Boolean)
// null si falta el id, el nombre (o está en blanco) o el email, o si el email no contiene «@».
// El nombre se devuelve sin espacios sobrantes. Si no hay edad, se considera menor de edad.
fun UsuarioDto.toDominio(): Usuario? {
// ...
}
Para comprobarlo:
fun main() {
val dtos = listOf(
UsuarioDto(1, " Ana ", "ana@ejemplo.com", 20),
UsuarioDto(2, "Luis", "luis.ejemplo.com", 30),
UsuarioDto(3, "", "x@y.com", 40),
UsuarioDto(null, "Marta", "m@ejemplo.com", 15),
UsuarioDto(5, "Pablo", "p@ejemplo.com", null)
)
dtos.mapNotNull { it.toDominio() }.forEach(::println)
}
Resultado esperado:
Usuario(id=1, nombre=Ana, email=ana@ejemplo.com, mayorDeEdad=true)
Usuario(id=5, nombre=Pablo, email=p@ejemplo.com, mayorDeEdad=false)
Solución modelo (bloqueada)
🔒 La solución completa está bloqueada. Esto es solo un ejemplo de cómo empieza:
data class UsuarioDto(val id: Int?, val nombre: String?, val email: String?, val edad: Int?)
data class Usuario(val id: Int, val nombre: String, val email: String, val mayorDeEdad: Boolean)
fun UsuarioDto.toDominio(): Usuario? {
val id = id ?: return null
val nombre = nombre?.trim()?.takeIf { it.isNotEmpty() } ?: return null
// ... (resto de la solución bloqueado)Ejercicio AA3.2¶
intentar sin tragarse la cancelación. Completa la función para que devuelva Ok con el valor, Fallo con la excepción, y deje pasar la CancellationException.
sealed interface Resultado<out T> {
data class Ok<T>(val valor: T) : Resultado<T>
data class Fallo(val error: Throwable) : Resultado<Nothing>
}
// Ejecuta el bloque y lo convierte en Resultado, pero SIN tragarse la cancelación.
suspend fun <T> intentar(bloque: suspend () -> T): Resultado<T> {
// ...
}
Para comprobarlo:
fun main() = runBlocking {
println(runBlocking { intentar { 21 * 2 } })
println(runBlocking { intentar { error("se rompió") } }.let { (it as Resultado.Fallo).error.message })
val trabajo = launch { intentar { delay(1000) } }
delay(50)
trabajo.cancelAndJoin()
println("cancelación respetada: ${trabajo.isCancelled}")
}
Resultado esperado:
Ok(valor=42)
se rompió
cancelación respetada: true
Solución modelo (bloqueada)
🔒 La solución completa está bloqueada. Esto es solo un ejemplo de cómo empieza:
sealed interface Resultado<out T> {
data class Ok<T>(val valor: T) : Resultado<T>
data class Fallo(val error: Throwable) : Resultado<Nothing>
}
suspend fun <T> intentar(bloque: suspend () -> T): Resultado<T> =
// ... (resto de la solución bloqueado)Ejercicio AA3.3¶
Del error al mensaje. Escribe mensajeDe con un when para traducir cada excepción a un mensaje pensado para el usuario.
class HttpFalso(val codigo: Int) : Exception("HTTP $codigo")
// IOException -> "Sin conexión"; HttpFalso 401 -> "Debes iniciar sesión"; HttpFalso 404 -> "No encontrado";
// cualquier otro código HTTP -> "Error del servidor (código X)"; cualquier otra cosa -> "Error inesperado".
fun mensajeDe(e: Throwable): String {
// ...
}
Para comprobarlo:
fun main() {
listOf<Throwable>(IOException("x"), HttpFalso(401), HttpFalso(404), HttpFalso(500), NullPointerException())
.forEach { println(mensajeDe(it)) }
}
Resultado esperado:
Sin conexión
Debes iniciar sesión
No encontrado
Error del servidor (código 500)
Error inesperado
Solución modelo (bloqueada)
🔒 La solución completa está bloqueada. Esto es solo un ejemplo de cómo empieza:
import java.io.IOException
class HttpFalso(val codigo: Int) : Exception("HTTP $codigo")
fun mensajeDe(e: Throwable): String = when (e) {
is IOException -> "Sin conexión"
is HttpFalso -> when (e.codigo) {
// ... (resto de la solución bloqueado)Ejercicio AA3.4¶
Caché con caducidad. Implementa CacheTtl: obtener devuelve el valor solo si han pasado menos de ttlMs desde que se puso; invalidar lo borra.
// Una caché que guarda UN valor y deja de devolverlo cuando pasa el tiempo indicado.
class CacheTtl<T>(private val ttlMs: Long, private val reloj: () -> Long) {
fun obtener(): T? { /* ... */ return null }
fun poner(valor: T) { /* ... */ }
fun invalidar() { /* ... */ }
}
Para comprobarlo:
fun main() {
var ahora = 0L
val cache = CacheTtl<String>(ttlMs = 1000, reloj = { ahora })
println(cache.obtener())
cache.poner("A")
ahora = 999
println(cache.obtener())
ahora = 1000
println(cache.obtener())
cache.poner("B")
cache.invalidar()
println(cache.obtener())
}
Resultado esperado:
null
A
null
null
Solución modelo (bloqueada)
🔒 La solución completa está bloqueada. Esto es solo un ejemplo de cómo empieza:
class CacheTtl<T>(private val ttlMs: Long, private val reloj: () -> Long) {
private var valor: T? = null
private var guardadoEn = 0L
fun obtener(): T? = valor?.takeIf { reloj() - guardadoEn < ttlMs }
fun poner(valor: T) { this.valor = valor; guardadoEn = reloj() }
// ... (resto de la solución bloqueado)Ejercicio AA3.5¶
Reintentar con espera creciente. Escribe reintentar según las indicaciones. Con 5 intentos y una espera inicial de 10 ms, una petición que falla tres veces debe esperar 10, 20 y 40 ms antes de acertar al cuarto intento.
// Ejecuta `bloque` hasta `intentos` veces. Si falla con IOException, espera y reintenta, DUPLICANDO la espera cada vez.
// Imprime "espero X ms" antes de cada espera. En el último intento, si falla, deja pasar la excepción.
suspend fun <T> reintentar(intentos: Int, esperaInicial: Long, bloque: suspend (Int) -> T): T {
// ...
}
Para comprobarlo:
fun main() = runBlocking {
val r = reintentar(5, 10) { n -> if (n < 4) throw IOException("sin red") else "ok en el intento $n" }
println(r)
}
Resultado esperado:
espero 10 ms
espero 20 ms
espero 40 ms
ok en el intento 4
Solución modelo (bloqueada)
🔒 La solución completa está bloqueada. Esto es solo un ejemplo de cómo empieza:
import java.io.IOException
suspend fun <T> reintentar(intentos: Int, esperaInicial: Long, bloque: suspend (Int) -> T): T {
var espera = esperaInicial
repeat(intentos - 1) { n ->
try {
// ... (resto de la solución bloqueado)Ejercicio AA3.6¶
⭐ Cola de pendientes. Implementa enviarTodos: manda los mensajes en orden y, al primer fallo, se detiene sin perder ese mensaje ni los siguientes.
// Una cola de mensajes por enviar. `enviarTodos` los manda EN ORDEN con `enviar` (que devuelve false si falla);
// al primer fallo se detiene y conserva ese mensaje y los siguientes. Devuelve cuántos se enviaron.
class Bandeja {
private val pendientes = ArrayDeque<String>()
fun añadir(m: String) { pendientes.addLast(m) }
val quedan: List<String> get() = pendientes.toList()
suspend fun enviarTodos(enviar: suspend (String) -> Boolean): Int {
// ...
}
}
Para comprobarlo:
fun main() = runBlocking {
val b = Bandeja()
listOf("hola", "¿qué tal?", "adiós", "hasta luego").forEach(b::añadir)
val n1 = b.enviarTodos { it != "adiós" } // «adiós» no se puede enviar
println("enviados $n1, quedan ${b.quedan}")
val n2 = b.enviarTodos { true }
println("enviados $n2, quedan ${b.quedan}")
}
Resultado esperado:
enviados 2, quedan [adiós, hasta luego]
enviados 2, quedan []
Solución modelo (bloqueada)
🔒 La solución completa está bloqueada. Esto es solo un ejemplo de cómo empieza:
class Bandeja {
private val pendientes = ArrayDeque<String>()
fun añadir(m: String) { pendientes.addLast(m) }
val quedan: List<String> get() = pendientes.toList()
suspend fun enviarTodos(enviar: suspend (String) -> Boolean): Int {
// ... (resto de la solución bloqueado)