# Tema 2 · Callbacks, listeners, handlers y despacho

Programación Dirigida por Eventos · Grado en Ingeniería Informática · 2026/27

Entregar código para que otro lo invoque: callbacks síncronos y diferidos, listeners, ciclo de vida, tablas de despacho, Observer, pub/sub, MVVM y todo lo que puede salir mal al notificar.

## Callbacks y listeners

_Qué es entregar una función para que otro la llame, las cuatro formas de escribirla, clausuras, callbacks síncronos frente a diferidos y cómo una fuente gestiona su lista de listeners._

### Entregar, no ejecutar · Teoría

En la Unidad 1 decíamos alegremente «el framework me llama». Hoy toca saber **exactamente qué le entregas y cuándo lo ejecuta**.

Un **callback** es una función que entregas a otra pieza (una fuente, una librería, un framework) para que **ella** la invoque más tarde.

<div class="flow">
  <div><b>Tu código</b><small>define la reacción: { pulsaciones++ }</small></div>
  <div><b>1 · registra</b><small>entrega la función como un valor</small></div>
  <div><b>Fuente</b><small>la guarda: listener = f</small></div>
  <div><b>2 · ocurre el suceso</b><small>clic, timeout, dato recibido…</small></div>
  <div><b>3 · invoca</b><small>la fuente llama a f</small></div>
</div>

Entre el paso 1 y el 3 tu código **no espera**: sigue con otra cosa o incluso termina.

### Tres ideas que lo definen

<div class="cards">
  <div class="card"><h4>1 · Se entrega, no se ejecuta</h4><p>Pasas la función como un valor más. Los paréntesis de la llamada los pondrá otro.</p></div>
  <div class="card"><h4>2 · Inversión de control</h4><p>Tú decides QUÉ ocurre; el que guarda el callback decide CUÁNDO (y cuántas veces, quizá ninguna).</p></div>
  <div class="card"><h4>3 · Un contrato de por medio</h4><p>Ambos lados acuerdan la firma: qué parámetros recibe el callback y qué devuelve.</p></div>
</div>

![Dos carriles en el tiempo. En el de tu código: 1, registra con addListener(f) y sigue con otra cosa o incluso termina. En el de la fuente: guarda f (listener = f), 2, ocurre el suceso (clic, timeout) y 3, invoca f(evento). Una flecha naranja sube de la fuente a tu carril: f se ejecuta (pulsaciones++) porque la fuente la llama. Tú decides qué hace f; quien guarda f decide cuándo.](/img/pde/tema-2/2a-inversion-control.svg "Inversión de control: tu código entrega f y sigue; la fuente la invoca cuando ocurre el suceso")

> [!ANALOGIA] «No nos llame, ya le llamaremos»
> Dejas tu teléfono en la consulta del médico. Tú decides qué harás cuando suene (ir corriendo); la consulta decide cuándo llamarte. El número de teléfono es el callback; el acuerdo «te llamaremos a este número para darte cita» es el contrato.

> [!NOTA] Conexión con la U1
> El **receptor** del trío fuente / evento / receptor se entrega a la fuente precisamente en forma de callback: es el pegamento que une las tres piezas.

**Preguntas**

**1.** En un callback, ¿quién decide **cuándo** se ejecuta la reacción?

   a) El código que define la reacción, al pasarla como argumento
   b) El compilador, que reordena las llamadas
   c) Quien guarda el callback (fuente, librería o framework)
   d) El sistema operativo, siempre en otro hilo

**2.** ¿Cómo se llama el principio por el que tú decides qué pasa, pero otro decide cuándo pasa?

### Tu primer callback: descargar() · Ejercicio

Un ejemplo de consola, sin Android ni GUI, para ver las piezas desnudas:

```kotlin
// Quien OFRECE el servicio declara qué función espera
fun descargar(url: String, alTerminar: (String) -> Unit) {
    val contenido = "…datos de $url…"   // trabajo (simulado)
    alTerminar(contenido)                // «ya te llamo yo»
}

// Quien USA el servicio entrega la función; no la ejecuta
fun main() {
    descargar("https://uax.com") { texto ->
        println("Recibido: $texto")
    }
}
```

Fíjate en tres cosas:

1. **El contrato es un tipo función.** `(String) -> Unit` significa «recibe un `String` y no devuelve nada». Cualquier función con esa forma sirve.
2. **La invocación la hace el otro.** `descargar()` decide cuándo llamar a `alTerminar`, con qué dato… y si llega a llamarlo.
3. **Entregamos código, no un resultado.** La lambda `{ texto -> … }` viaja como argumento. `main()` no sabe en qué momento se ejecutará.

> [!TRUCO] Sintaxis de lambda final
> En Kotlin, si el último parámetro es una función, la lambda puede ir **fuera** de los paréntesis. Por eso `descargar("https://uax.com") { … }` equivale a `descargar("https://uax.com", { … })`.

Responde antes de seguir: lee el código como si fueras `descargar()` y no `main()`.

**Preguntas**

**3.** En `descargar("https://uax.com") { texto -> println("Recibido: $texto") }`, la lambda…

   a) Viaja como argumento y la ejecuta `descargar()` cuando decide
   b) Se ejecuta antes de llamar a `descargar()` y se le pasa su resultado
   c) Se lanza automáticamente en otro hilo
   d) No se ejecuta nunca porque no lleva paréntesis

**4.** ¿Qué decide `descargar()` y **no** `main()`? Marca todas.

   a) Cuándo llamar a `alTerminar`
   b) Qué hace la reacción con el texto recibido
   c) Con qué dato lo llama
   d) Si llega a llamarlo o no

**5.** Escribe el tipo función de Kotlin que significa «recibe un `Int` y no devuelve nada».

### Cuatro formas, un mismo contrato · Teoría

El contrato clásico es una **interfaz de un solo método**. Es lo que hay debajo de `View.OnClickListener` en Android:

```kotlin
// El contrato
fun interface OnPulsado {
    fun pulsado(id: Int)
}

// La fuente guarda el callback
class Boton(val id: Int) {
    var listener: OnPulsado? = null
    fun pulsar() {
        listener?.pulsado(id)
    }
}
```

El compilador acepta **cualquier cosa que cumpla el contrato**. Hay cuatro formas de fabricarla:

```kotlin
// A · Clase con nombre
class Registrador : OnPulsado {
    override fun pulsado(id: Int) = println("Botón $id")
}
boton.listener = Registrador()

// B · Clase anónima
boton.listener = object : OnPulsado {
    override fun pulsado(id: Int) { println("Botón $id") }
}

// C · Lambda (conversión SAM)
boton.listener = OnPulsado { id -> println("Botón $id") }

// D · Referencia a función
fun registrar(id: Int) = println("Botón $id")
boton.listener = OnPulsado(::registrar)
```

| Forma | Úsala cuando… | Cuidado con… |
| --- | --- | --- |
| A · Clase con nombre | La reacción tiene estado propio, se reutiliza o se quiere probar aparte. | Mucho código para reacciones de una línea. |
| B · Clase anónima | La interfaz tiene varios métodos o necesitas `this` del propio listener. | Verbosa; captura la clase que la rodea. |
| C · Lambda | Reacción corta y local. La opción por defecto en Kotlin. | Cada lambda es un objeto distinto (importa al desregistrar). |
| D · Referencia `::f` | La función ya existe y su firma encaja con el contrato. | Una referencia ligada (`obj::f`) retiene a `obj`. |

> [!NOTA] ¿Qué es SAM?
> *Single Abstract Method*: una interfaz con un único método abstracto. Marcarla como `fun interface` permite que una lambda se convierta automáticamente en un objeto que la implementa.

**Preguntas**

**6.** ¿Cómo se llama (por sus siglas) la conversión que permite escribir `OnPulsado { id -> … }` con una lambda?

**7.** Reacción de una línea, local, que no vas a reutilizar. ¿Qué forma es la opción por defecto en Kotlin?

   a) A · Clase con nombre
   b) B · Clase anónima
   c) D · Referencia `::f`
   d) C · Lambda

### Elige la forma adecuada · Ejercicio

Las cuatro formas compilan y hacen lo mismo. Lo que cambia es **cuánto cuesta escribirlas, leerlas y mantenerlas**. Decide para cada situación.

**Situación 1.** Tienes un `AnimatorListener` con cuatro métodos (`onAnimationStart`, `onAnimationEnd`, `onAnimationCancel`, `onAnimationRepeat`) y en uno de ellos necesitas referirte al propio listener con `this` para quitarlo.

**Situación 2.** Ya existe en tu clase `fun guardarEnLog(id: Int)`, que hace justo lo que quieres al pulsar.

**Situación 3.** La reacción lleva un contador interno de pulsaciones y un umbral, la usan tres pantallas y quieres probarla con un test unitario sin botones.

**Situación 4.** Te piden revisar este código:

```kotlin
class Pantalla {
    val datos = cargarImagenesEnormes()
    fun alPulsar(id: Int) { /* … */ }
}

val p = Pantalla()
botonGlobal.listener = OnPulsado(p::alPulsar)
```

`botonGlobal` vive toda la aplicación. ¿Qué consecuencia tiene esa referencia ligada?

> [!TRUCO] Pregunta guía
> Antes de elegir, pregúntate: ¿la reacción tiene **estado** o **varios métodos**? ¿Existe **ya** la función? Si la respuesta es «no» a todo, una lambda.

**Preguntas**

**8.** Situación 1 (interfaz de varios métodos y necesitas `this` del listener):

   a) C · Lambda
   b) B · Clase anónima
   c) D · Referencia `::f`
   d) Ninguna sirve

**9.** Situación 2 (la función ya existe y su firma encaja):

   a) D · Referencia `::guardarEnLog`
   b) A · Clase con nombre
   c) B · Clase anónima
   d) C · Lambda que la llama

**10.** Situación 4: ¿qué pasa con `p` y sus imágenes?

   a) Nada: las referencias a función no retienen objetos
   b) Se copian las imágenes dentro del botón
   c) Siguen en memoria mientras `botonGlobal` guarde el listener, porque `p::alPulsar` retiene a `p`
   d) Se liberan en cuanto termina la función que creó `p`

### Clausuras: la lambda recuerda dónde nació · Teoría

Una lambda puede usar variables del sitio donde se escribió, aunque se ejecute mucho después y desde otro sitio:

```kotlin
fun main() {
    var pulsaciones = 0          // local de main()
    val boton = Boton(1)

    boton.listener = OnPulsado { // captura pulsaciones
        pulsaciones++
        println("Van $pulsaciones")
    }

    boton.pulsar(); boton.pulsar()
}
```

`Boton.pulsar()` no tiene ni idea de que existe `pulsaciones`… pero la lambda sí. Eso es una **clausura** (*closure*): **función + entorno**.

<div class="cards">
  <div class="card"><h4>1 · Función + entorno</h4><p>La lambda recuerda las variables del lugar donde nació, no del lugar donde la llaman.</p></div>
  <div class="card"><h4>2 · Captura la variable, no una copia</h4><p>Si la lambda incrementa <code>pulsaciones</code>, el cambio se ve fuera. En Java solo se pueden capturar variables <b>efectivamente finales</b>.</p></div>
  <div class="card"><h4>3 · Vive lo que viva el callback</h4><p>Mientras el botón guarde la lambda, todo lo que esta referencia sigue en memoria.</p></div>
</div>

> [!CUIDADO] Para la Clase 2
> Si lo capturado es **una pantalla entera** y la fuente nunca suelta el callback… ¿quién libera esa pantalla? Guarda esta pregunta: es el problema del *listener olvidado*.

> [!EXAMEN]
> «Una clausura captura la variable, no su valor» y «lo capturado vive lo que viva el callback» son dos frases que conviene saber justificar con un ejemplo.

**Preguntas**

**11.** ¿Qué imprime el programa? Escribe las dos líneas separadas por una coma.

**12.** ¿Qué captura exactamente la lambda?

   a) Una copia del valor 0 en el momento de crearse
   b) Nada: `pulsar()` le pasa el valor como parámetro
   c) La propia variable `pulsaciones`, así que sus cambios se ven fuera
   d) Solo el objeto `boton`

### Delegación: la pantalla firma el contrato · Teoría

Otra forma muy común en Android (y en Java en general): **la propia clase implementa la interfaz del listener** y se entrega a sí misma con `this`.

```kotlin
class PantallaLogin : AppCompatActivity(), View.OnClickListener {

    override fun onCreate(b: Bundle?) {
        super.onCreate(b)
        setContentView(R.layout.login)
        findViewById<Button>(R.id.btnEntrar).setOnClickListener(this)
        findViewById<Button>(R.id.btnSalir).setOnClickListener(this)
    }

    override fun onClick(v: View) {
        when (v.id) {
            R.id.btnEntrar -> entrar()
            R.id.btnSalir  -> finish()
        }
    }
}
```

1. **La pantalla firma el contrato.** Al implementar `OnClickListener`, la Activity se convierte en un callback válido.
2. **Dos fuentes, un mismo delegado.** Cada botón solo sabe *a quién avisar* (`this`). El botón sigue siendo reutilizable: no sabe nada del login.
3. **El delegado reparte.** `when (v.id)` asocia *fuente → reacción*. Es, literalmente, una **tabla de despacho escrita a mano**.

> [!NOTA] Delegar = encargar a otro
> La fuente (el botón) **delega** la decisión de qué hacer en otro objeto (la pantalla). Por eso se habla del patrón *delegación*: el botón hace de botón y la pantalla decide.

> [!CUIDADO] Lo que viene
> Cada botón nuevo añade una rama al `when`. Con tres no pasa nada; con treinta, ese `when` se convierte en un sitio donde todo el mundo toca. En la Clase 2 lo cambiaremos por **datos** (una tabla) en vez de **código**.

**Preguntas**

**13.** El usuario pulsa **Salir**. ¿Qué secuencia ocurre?

   a) El botón llama a `finish()` directamente
   b) El botón llama a `onClick(v)` de la pantalla con `v.id == R.id.btnSalir`, y el `when` elige `finish()`
   c) Android llama a `onCreate` de nuevo y registra otra vez los botones
   d) El botón llama a `entrar()` y luego a `finish()`

**14.** ¿Qué es `when (v.id)` dentro de `onClick`, en el vocabulario de la unidad?

   a) Una tabla de despacho escrita a mano (fuente → reacción)
   b) Un callback diferido
   c) Una clausura
   d) Un listener por botón

### ¿Ahora o después? Síncrono frente a diferido · Teoría

La sintaxis de entregar un callback es **idéntica** en `lista.forEach { … }` y en `boton.setOnClickListener { … }`. Lo que cambia es **qué hace con él quien lo recibe**.

<div class="cards">
  <div class="card"><h4>Síncrono · forEach { … }</h4><p>Se invoca <b>antes de que retorne</b> la función que lo recibe, en la misma pila y el mismo hilo. Ej.: <code>forEach</code>, <code>map</code>, <code>sortedBy</code>, el <code>descargar()</code> de antes.</p></div>
  <div class="card"><h4>Diferido / asíncrono · setOnClickListener { … }</h4><p>La función solo lo <b>guarda</b> y retorna. Lo invoca más tarde el bucle de eventos u otro hilo: 0, 1 o N veces. Ej.: listeners de UI, temporizadores, red, sensores.</p></div>
</div>

![Dos pilas de llamadas sobre una línea de tiempo. Síncrono, lista.forEach: dentro de la barra del llamante está forEach y, dentro de forEach, dos ejecuciones del callback; forEach retorna después; misma pila, mismo hilo. Diferido, boton.setOnClickListener: dentro del llamante, la llamada solo guarda f y retorna enseguida; el llamante termina. Más tarde, en otra pila (el bucle de eventos u otro hilo), se ejecuta el callback tras el clic.](/img/pde/tema-2/2a-sincrono-diferido.svg "Misma sintaxis, distinto momento: el síncrono corre dentro de la llamada; el diferido, en otra pila más tarde")

| ¿Cuándo corre? | Callback síncrono | Callback diferido / asíncrono |
| --- | --- | --- |
| Momento | Antes de que retorne la función que lo recibe. | Después: cuando ocurra el suceso (o nunca). |
| Pila de llamadas | La del llamante: en el depurador ves quién te llamó. | La del bucle de eventos o la de otro hilo. |
| Excepciones | Un `try/catch` alrededor de la llamada las captura. | El `try/catch` del registro no las ve: hay que tratarlas dentro. |
| Hilo | El mismo que hizo la llamada. | Puede ser otro (hilo de UI, del timer, de red). |
| Estado | No cambia entre el registro y la ejecución. | Puede haber cambiado: compruébalo de nuevo al entrar. |
| Vida de lo capturado | Dura lo que la llamada. | Dura mientras la fuente guarde el callback. |

> [!IMPORTANTE] Diferido no significa necesariamente otro hilo
> El **mismo** bucle de eventos puede ejecutarlo más tarde. En JavaScript lo ves claro:

```js
function aplicar(x, cb) { cb(x); }
aplicar(5, console.log);               // síncrono: imprime ya
setTimeout(() => console.log(5), 0);   // diferido: queda programado
```

La primera reacción se ejecuta antes de que `aplicar` retorne; la segunda queda en la cola y la ejecutará el mismo hilo **cuando termine el código actual**, aunque el retardo sea 0.

**Preguntas**

**15.** ¿Qué llamadas invocan su callback **de forma síncrona** (antes de retornar)?

   a) `lista.forEach { … }`
   b) `boton.setOnClickListener { … }`
   c) `lista.sortedBy { … }`
   d) `setTimeout(() => …, 0)`
   e) El `descargar(url) { … }` de antes
   f) `lista.map { … }`

**16.** Envuelves `boton.setOnClickListener { error("fallo") }` en un `try/catch`. Al pulsar el botón, ¿quién captura la excepción?

   a) Tu `try/catch`, porque rodea el código de la lambda
   b) Nadie de tu `try/catch`: cuando se lanza, el registro ya terminó hace rato
   c) El compilador la convierte en un aviso
   d) La propia lambda, automáticamente

**17.** Ordena lo que imprime: `aplicar(5, console.log); setTimeout(() => console.log("T"), 0); console.log("fin");`

   Ordena: 5 · T · fin

### Predice: A, B, C, D con un Timer · Ejercicio

Dos callbacks con la misma pinta. Predice la salida **antes** de ejecutar (en clase: 1 minuto en pareja).

```kotlin
import java.util.Timer
import kotlin.concurrent.schedule

fun main() {
    println("A")
    listOf(1, 2).forEach { println("B$it") }
    Timer().schedule(1000) { println("C") }
    println("D")
}
```

Piénsalo así:

1. ¿Qué llamadas ejecutan el callback **antes de retornar**?
2. ¿Cuáles solo lo **guardan** para más tarde?
3. Cuando por fin se ejecute «C», ¿seguirá `main()` en marcha? ¿En qué hilo correrá?

> [!TRUCO] Pista
> Que dos líneas se parezcan no significa que se ejecuten igual: hay que saber qué hace con el callback **quien lo recibe**.

Cuando tengas tu predicción, compruébala en la línea temporal:

> _Aquí hay un simulador interactivo (`sync-async`): ábrelo en la web._
> [!NOTA] Un detalle de la JVM
> `java.util.Timer` crea su propio hilo (`Timer-0`) y, por defecto, no es *daemon*: el proceso no termina al acabar `main()` mientras ese hilo siga vivo. Por eso «C» llega a imprimirse aunque `main()` ya haya retornado.

> [!EXAMEN]
> Este tipo de ejercicio («¿en qué orden se imprime?») es carne de parcial. La clave siempre es la misma: **¿la función que recibe el callback lo ejecuta o lo guarda?**

**Preguntas**

**18.** Ordena las líneas tal como aparecen en la consola.

   Ordena: A · B1 · B2 · C · D

**19.** ¿En qué hilo se ejecuta `println("C")`?

   a) En `main`, cuando termina
   b) En el hilo de UI
   c) En el hilo del Timer (`Timer-0`)
   d) En ninguno: no llega a ejecutarse

**20.** Cuando se imprime «C», ¿sigue `main()` en marcha?

   a) Sí, `main()` espera bloqueada en `schedule` hasta que pase el segundo
   b) No: `main()` ya retornó; la lambda sigue viva porque el `Timer` la guarda
   c) Sí, porque `println("D")` tarda un segundo
   d) No, y por eso C no se imprime nunca

### Listener frente a handler · Teoría

Dos palabras que se usan casi como sinónimos, pero que responden a preguntas de diseño distintas.

<div class="cards">
  <div class="card"><h4>LISTENER · «el que escucha»</h4><p>Se <b>suscribe a una fuente concreta</b> y es avisado cuando algo ocurre. De 0 a N por fuente; la fuente no sabe quiénes son ni qué hacen. Se registra y se da de baja en ejecución: <code>addXxxListener</code> / <code>removeXxxListener</code>.<br><br>Ejemplos: <code>OnClickListener</code>, <code>SensorEventListener</code>, el consumidor de una cola.</p></div>
  <div class="card"><h4>HANDLER · «el que se hace cargo»</h4><p>Es el <b>responsable de un tipo de evento</b>: el código al que el despacho entrega ese caso. Normalmente uno por tipo; lo elige el despachador según el evento. Puede <b>consumir</b> el evento e impedir que siga propagándose (p. ej. devolver <code>true</code> en <code>onTouch</code>).<br><br>Ejemplos: <code>manejar()</code> de agendaeventos.kt, cada rama de un <code>when</code>, una rutina de interrupción.</p></div>
</div>

La pregunta que te tienes que hacer al diseñar:

> **¿Se suscribe a una fuente (listener) o atiende un tipo de evento (handler)?**

Dicho de otra forma, como lo resumen los obligatorios de la unidad: *suscripción = quién recibe; handler = qué hace al recibir*. El listener expresa un **interés continuado** (puede recibir muchas notificaciones); el handler contiene la reacción que procesa **un evento concreto**.

> [!CUIDADO] Ojo en Android
> `android.os.Handler` es **otra cosa**: es la puerta a la cola de mensajes de un hilo (sirve para mandarle trabajo). No confundas la clase de Android con el concepto general de handler.

**Preguntas**

**21.** Una rutina de interrupción que atiende el evento «byte recibido por la UART» es, en este vocabulario…

   a) Un listener
   b) Un handler
   c) Una fuente
   d) Una clausura

**22.** ¿Qué es `android.os.Handler`?

   a) El nombre que da Android a cualquier listener
   b) La interfaz que implementa `onClick`
   c) Una clase ligada a la cola de mensajes de un hilo
   d) El despachador de eventos táctiles

### Una fuente por dentro: lista, alta y baja · Teoría

¿Cómo está hecha por dentro una fuente que admite varios listeners? Una lista y tres operaciones:

```kotlin
typealias Listener = (String) -> Unit

class Sensor {
    private val listeners = mutableListOf<Listener>()

    fun addListener(l: Listener)    { listeners += l }
    fun removeListener(l: Listener) { listeners -= l }

    fun publicar(dato: String) {
        for (l in listeners.toList()) l(dato)
    }
}
```

![A la izquierda, desde fuera, addListener(l) y removeListener(l) apuntan a la lista; la baja exige la misma referencia. En el centro, el Sensor con su lista privada listeners: enPantalla, enFichero y alarma. A la derecha, publicar(dato) recorre una copia discontinua de esa lista y llama l(dato) a cada uno, uno detrás de otro.](/img/pde/tema-2/2a-fuente-lista.svg "Una fuente por dentro: lista privada, alta y baja desde fuera, y difusión sobre una copia")

<div class="cards">
  <div class="card"><h4>1 · La lista es de la fuente</h4><p>Es <b>privada</b>: desde fuera solo se puede pedir alta o baja. Cuándo se recorre lo decide la fuente.</p></div>
  <div class="card"><h4>2 · Se recorre una copia</h4><p><code>toList()</code> hace una copia. Así un listener puede darse de baja (o añadir otro) dentro de su propio callback sin romper la iteración.</p></div>
  <div class="card"><h4>3 · La baja exige la misma referencia</h4><p>Dos lambdas con el mismo texto son <b>dos objetos</b>. Guarda el listener en una <code>val</code> si piensas quitarlo.</p></div>
</div>

```kotlin
val enPantalla: Listener = { println("UI: $it") }

sensor.addListener(enPantalla)
sensor.removeListener(enPantalla)               // OK
sensor.removeListener { println("UI: $it") }    // no quita nada
```

> [!NOTA] ¿Por qué la copia?
> Si recorres la lista original y, dentro del bucle, un listener se da de baja a sí mismo, estás modificando la colección que iteras. En Kotlin/Java eso suele acabar en `ConcurrentModificationException`. Recorrer una **foto** de la lista evita el problema: los cambios se aplican a la siguiente ronda.

**Preguntas**

**23.** ¿Para qué sirve el `toList()` en `publicar()`?

   a) Para que los listeners se ejecuten en paralelo
   b) Para ordenar los listeners por prioridad
   c) Para que un listener pueda darse de alta o de baja durante la difusión sin romper la iteración
   d) Para convertir el `String` en una lista de caracteres

**24.** ¿Por qué `listeners` es `private`?

   a) Para que solo la fuente decida cuándo se recorre y desde fuera solo se pueda pedir alta o baja
   b) Porque Kotlin no permite listas públicas de funciones
   c) Para que la recolección de basura la libere antes
   d) Para que los listeners no se ejecuten dos veces

### Encuentra el fallo: la baja que no da de baja · Ejercicio

Un compañero escribe esto y jura que ha quitado el listener:

```kotlin
val sensor = Sensor()

sensor.addListener { println("UI: $it") }
sensor.addListener { println("LOG: $it") }

// … más tarde, al cerrar la pantalla
sensor.removeListener { println("UI: $it") }

sensor.publicar("21 ºC")
```

Antes de mirar las preguntas, razona:

- ¿Cuántos objetos función hay en la lista **antes** de `removeListener`?
- ¿Qué objeto le estás pidiendo a la lista que quite?
- ¿Está ese objeto en la lista?

> [!CUIDADO] Igualdad de lambdas
> Dos lambdas con el mismo código fuente son dos instancias distintas. `removeListener` compara por **identidad** del objeto, no por lo que «pone» dentro. Lo mismo pasa en el navegador con `removeEventListener` si le pasas una función flecha nueva.

<details><summary>Ver solución comentada</summary>
<p>La lista tiene dos lambdas. <code>removeListener { … }</code> crea una <b>tercera</b> lambda, que no está en la lista, así que <code>-=</code> no encuentra nada que quitar. Al publicar se imprimen <code>UI: 21 ºC</code> y <code>LOG: 21 ºC</code>. Arreglo: guardar el listener en una <code>val</code> y usar esa misma referencia en el alta y en la baja (o, como veremos en la Clase 2, que el alta devuelva una suscripción cancelable).</p>
</details>

**Preguntas**

**25.** ¿Qué imprime `sensor.publicar("21 ºC")`?

   a) Solo `LOG: 21 ºC`
   b) `UI: 21 ºC` y `LOG: 21 ºC`
   c) Nada: `removeListener` vació la lista
   d) Lanza una excepción porque el listener no existe

**26.** ¿Qué cambios arreglan la baja? Marca todos los válidos.

   a) Guardar la lambda en `val enPantalla` y usar esa misma `val` en `addListener` y `removeListener`
   b) Llamar a `removeListener { … }` dos veces para asegurarse
   c) Hacer que `addListener` devuelva un objeto/función que, al invocarlo, quite exactamente ese listener
   d) Escribir la lambda de la baja exactamente igual, con los mismos espacios

### Varios suscriptores en una misma fuente · Teoría

Una misma fuente, un mismo evento, N reacciones independientes:

<div class="flow">
  <div><b>Sensor</b><small>publicar("21 ºC")</small></div>
  <div><b>1 · enPantalla</b><small>pinta el valor</small></div>
  <div><b>2 · enFichero</b><small>lo guarda en el log</small></div>
  <div><b>3 · alarma</b><small>avisa si supera el umbral</small></div>
</div>

Es una **difusión** (*broadcast*): cada listener recibe su propia llamada con el mismo evento, una detrás de otra. Cuatro reglas:

1. **El orden habitual es el de alta…** pero solo es un contrato si la documentación lo dice. El DOM lo garantiza; Swing, no.
2. **No dependas del orden.** Si B necesita que A haya corrido antes, no son dos listeners: es uno solo, o A debe emitir su propio evento.
3. **Una excepción corta la cadena.** Si el 2 falla, el 3 no se entera. Una fuente robusta envuelve cada llamada en `try/catch`.
4. **Consumir el evento.** Algunos despachos permiten parar la propagación: devolver `true`, `stopPropagation()`…

```kotlin
fun publicar(dato: String) {
    for (l in listeners.toList()) {
        try { l(dato) }
        catch (e: Exception) { println("listener falló: ${e.message}") }
    }
}
```

> [!IMPORTANTE] Contrato de notificación
> Una fuente con varios receptores debe dejar claro: si el fallo de un handler **detiene o no** a los siguientes, si una alta o baja durante la notificación **afecta a la ronda actual**, y qué pasa con una entrega **ya encolada** cuando alguien se da de baja.

**Preguntas**

**27.** Con el `publicar()` original (sin `try/catch`), `enFichero` lanza una excepción. ¿Qué le pasa a `alarma`?

   a) Se ejecuta igualmente, después del error
   b) Se ejecuta antes que `enFichero` para evitar el fallo
   c) No se entera: la excepción corta el bucle de difusión
   d) Se ejecuta dos veces para compensar

**28.** ¿En cuál de estas plataformas está documentado que los listeners se llaman en orden de alta?

   a) Swing
   b) El DOM del navegador
   c) En todas: es una ley general
   d) En ninguna

**29.** El listener B solo funciona si A ya se ha ejecutado. ¿Qué haces?

   a) Registrar A antes que B y confiar en el orden
   b) Fusionarlos en uno solo, o que A emita su propio evento al que se suscriba B
   c) Registrar B dos veces
   d) Añadir un `Thread.sleep` en B

### Repaso de la Clase 1 · Repaso

> [!RESUMEN] Chuleta
> 1. **Callback**: función que entregas para que otro la invoque. Tú decides **qué**; él decide **cuándo** (inversión de control). Hay un contrato: la firma.
> 2. **Cuatro sintaxis, un contrato**: clase con nombre, clase anónima, lambda (SAM) y referencia `::f` cumplen la misma interfaz de un método.
> 3. **Clausura** = función + entorno. Captura la variable, no una copia; lo capturado vive lo que viva el callback.
> 4. **Síncrono ≠ diferido**: los listeners de eventos son diferidos (otra pila, quizá otro hilo, estado que pudo cambiar). Diferido no implica necesariamente otro hilo.
> 5. **Fuente = lista de listeners**: alta, baja (misma referencia) y difusión sobre una copia. El orden no se presupone; una excepción corta la cadena.

| Concepto | Pregunta que responde |
| --- | --- |
| Listener | ¿Quién se suscribe a esta fuente? |
| Handler | ¿Quién se hace cargo de este tipo de evento? |
| Delegación | ¿A qué objeto avisa la fuente para que decida? |

### Para pensar antes de la Clase 2

1. Una pantalla se registra como listener del GPS y el usuario la cierra. **¿Quién la da de baja?**
2. Nuestro `when (v.id)` crece con cada botón nuevo. ¿Cómo asociar evento → reacción **con datos** en vez de con código?
3. En C no hay lambdas ni clausuras. ¿Cómo le llega a un callback **«su» contexto**?

**Preguntas**

**30.** Pregunta tipo examen: ¿qué diferencia a un callback síncrono de uno diferido?

   a) El síncrono se escribe con lambda y el diferido con clase anónima
   b) El síncrono se invoca antes de que retorne la función que lo recibe; el diferido se guarda y se invoca después (o nunca)
   c) El diferido siempre se ejecuta en otro hilo
   d) El síncrono solo existe en JavaScript

**31.** ¿Qué necesita `removeListener` para quitar de verdad un listener? (dos o tres palabras)

**32.** Marca las afirmaciones **verdaderas**.

   a) Una referencia ligada `obj::f` retiene a `obj` mientras la fuente la guarde
   b) Un `try/catch` alrededor de `setOnClickListener` captura las excepciones del clic
   c) En Java, una lambda solo puede capturar variables efectivamente finales
   d) Si un listener depende de que otro se ejecute antes, basta con registrarlo después
   e) `when (v.id)` en un `onClick` compartido es una tabla de despacho escrita a mano

### Solucionario

1. c) Quien guarda el callback (fuente, librería o framework)  
   Tú decides **qué** ocurre; quien guarda el callback decide **cuándo** y cuántas veces lo invoca. Eso es la inversión de control.
2. `inversion de control`  
   **Inversión de control**: el flujo ya no lo dirige tu código, sino quien guarda tus callbacks.
3. a) Viaja como argumento y la ejecuta `descargar()` cuando decide  
   La lambda es un **valor**. Quien pone los paréntesis de la llamada es `descargar()`, en la línea `alTerminar(contenido)`.
4. a) Cuándo llamar a `alTerminar`; c) Con qué dato lo llama; d) Si llega a llamarlo o no  
   Qué hacer con el texto lo decide quien escribe la lambda (`main`). Cuándo, con qué dato y si se llama, lo decide `descargar()`.
5. `(Int) -> Unit`  
   `(Int) -> Unit`. `Unit` es el «no devuelve nada» de Kotlin (el `void` de Java o C).
6. `SAM`  
   **Conversión SAM**: una interfaz con un único método abstracto (`fun interface`) se puede implementar con una lambda.
7. d) C · Lambda  
   La **lambda** es la forma corta y local. Ojo solo a una cosa: cada lambda es un objeto distinto, lo que importará al darse de baja.
8. b) B · Clase anónima  
   Con varios métodos no hay conversión SAM, y dentro de un `object : …` el `this` es el propio listener. **Clase anónima**.
9. a) D · Referencia `::guardarEnLog`  
   Si la función ya existe con la firma del contrato, la **referencia** `::f` evita escribir una lambda envoltorio.
10. c) Siguen en memoria mientras `botonGlobal` guarde el listener, porque `p::alPulsar` retiene a `p`  
   Una **referencia ligada** `obj::f` guarda dentro a `obj`. Mientras la fuente la guarde, `p` (y sus imágenes) no se puede liberar. Es el germen de las fugas de la Clase 2.
11. `Van 1, Van 2`  
   `Van 1` y `Van 2`: la lambda incrementa **la misma** variable `pulsaciones` en cada llamada.
12. c) La propia variable `pulsaciones`, así que sus cambios se ven fuera  
   Kotlin captura **la variable**. Por eso el contador avanza. Java, en cambio, solo te deja capturar variables efectivamente finales (que no cambian).
13. b) El botón llama a `onClick(v)` de la pantalla con `v.id == R.id.btnSalir`, y el `when` elige `finish()`  
   El botón invoca el **delegado** (`onClick`) y es el delegado quien despacha según `v.id`.
14. a) Una tabla de despacho escrita a mano (fuente → reacción)  
   Asocia cada fuente con su reacción: una **tabla de despacho** hecha con código. En la Clase 2 la convertiremos en datos.
15. a) `lista.forEach { … }`; c) `lista.sortedBy { … }`; e) El `descargar(url) { … }` de antes; f) `lista.map { … }`  
   `forEach`, `sortedBy`, `map` y nuestro `descargar()` llaman al callback antes de volver. Los listeners de UI y los temporizadores solo lo **guardan**.
16. b) Nadie de tu `try/catch`: cuando se lanza, el registro ya terminó hace rato  
   El callback es **diferido**: se ejecuta en otra pila (la del bucle de eventos). El `try/catch` del registro ya no existe. Hay que tratar las excepciones **dentro** del callback.
17. 1. 5 → 2. fin → 3. T  
   `aplicar` llama al callback antes de retornar (5). `setTimeout` solo programa; se imprime `fin` y, cuando el código actual termina, el bucle ejecuta el callback (T).
18. 1. A → 2. B1 → 3. B2 → 4. D → 5. C  
   `forEach` ejecuta su lambda antes de retornar (B1, B2). `schedule` la guarda y retorna enseguida, así que se imprime D. Un segundo después, el hilo del Timer imprime C.
19. c) En el hilo del Timer (`Timer-0`)  
   El `Timer` tiene su propio hilo. Diferido aquí sí implica otro hilo (aunque no siempre es así).
20. b) No: `main()` ya retornó; la lambda sigue viva porque el `Timer` la guarda  
   `main()` terminó tras imprimir D. La lambda sobrevive porque la guarda el `Timer`: lo capturado vive lo que viva el callback.
21. b) Un handler  
   Se hace cargo de **un tipo de evento** concreto: es un **handler**.
22. c) Una clase ligada a la cola de mensajes de un hilo  
   Comparte nombre, pero es otra cosa: la vía para encolar mensajes o tareas en la cola de un hilo.
23. c) Para que un listener pueda darse de alta o de baja durante la difusión sin romper la iteración  
   Se recorre una **copia estable**: las altas y bajas que ocurran durante la ronda modifican la lista real, no la que se está recorriendo.
24. a) Para que solo la fuente decida cuándo se recorre y desde fuera solo se pueda pedir alta o baja  
   La lista es **de la fuente**. Si fuese pública, cualquiera podría vaciarla, reordenarla o invocar a los listeners cuando quisiera.
25. b) `UI: 21 ºC` y `LOG: 21 ºC`  
   La baja no quitó nada: se le pasó un objeto nuevo. Los dos listeners siguen ahí. Y `-=` sobre una lista no falla si el elemento no está.
26. a) Guardar la lambda en `val enPantalla` y usar esa misma `val` en `addListener` y `removeListener`; c) Hacer que `addListener` devuelva un objeto/función que, al invocarlo, quite exactamente ese listener  
   Solo funciona usar **la misma referencia**, o que la fuente te devuelva una **suscripción cancelable** que ya la recuerda por ti.
27. c) No se entera: la excepción corta el bucle de difusión  
   La excepción sale del bucle `for` y los listeners posteriores no se llaman. Por eso una fuente robusta envuelve **cada** llamada.
28. b) El DOM del navegador  
   El DOM lo garantiza; Swing no. Por eso la regla es: **no presupongas el orden** salvo que la documentación lo prometa.
29. b) Fusionarlos en uno solo, o que A emita su propio evento al que se suscriba B  
   Si hay dependencia, **no son dos listeners independientes**: o son uno, o A publica un evento propio («ya he terminado») al que escucha B.
30. b) El síncrono se invoca antes de que retorne la función que lo recibe; el diferido se guarda y se invoca después (o nunca)  
   La diferencia está en **quién recibe el callback y qué hace con él**: ejecutarlo ya o guardarlo. La sintaxis es idéntica, y diferido no implica otro hilo.
31. `la misma referencia`  
   **La misma referencia** que se usó en el alta. Dos lambdas iguales en texto son objetos distintos.
32. a) Una referencia ligada `obj::f` retiene a `obj` mientras la fuente la guarde; c) En Java, una lambda solo puede capturar variables efectivamente finales; e) `when (v.id)` en un `onClick` compartido es una tabla de despacho escrita a mano  
   El `try/catch` del registro no ve excepciones diferidas, y depender del orden de alta es frágil salvo que la plataforma lo garantice.

## Ciclo de vida y despacho

_Quién da de baja a un listener y por qué olvidarlo fuga memoria; el mismo concepto de referencia a función en seis lenguajes; tablas de despacho en C, Kotlin y JS, y el contexto (void *ctx) que acompaña a cada callback._

### El listener olvidado · Teoría

Repaso exprés de la Clase 1:

<div class="cards">
  <div class="card"><h4>Callback</h4><p>Función que entregas para que otro la invoque. Tú decides QUÉ; quien la guarda decide CUÁNDO. Si es diferido: otra pila, quizá otro hilo.</p></div>
  <div class="card"><h4>Listener</h4><p>Callback suscrito a una FUENTE. De 0 a N por fuente, con alta y baja en ejecución. La baja exige la misma referencia.</p></div>
  <div class="card"><h4>Handler</h4><p>Callback responsable de un TIPO de evento. Lo elige el despacho. Hasta ahora, escrito a mano: <code>when (evento) { … }</code>.</p></div>
</div>

Hoy: **quién es el dueño de cada callback y cuánto debe vivir**, y cómo montar el despacho para que sirva igual en Android, en un servidor o en un ESP32.

### Quien guarda un callback mantiene vivo todo lo que ese callback referencia

<div class="flow">
  <div><b>Gps</b><small>fuente · vive lo que la app</small></div>
  <div><b>listeners</b><small>lista de callbacks</small></div>
  <div><b>{ pos -> pintar(pos) }</b><small>la lambda captura this</small></div>
  <div><b>PantallaMapa</b><small>debería vivir lo que la pantalla</small></div>
</div>

Cada flecha es una **referencia fuerte**: mientras exista la primera caja, existe la última. El recolector de basura no puede liberar nada de lo que todavía es alcanzable desde el GPS.

![Arriba, la cadena de referencias fuertes: Gps (fuente) apunta a listeners (lista), que apunta a la lambda { pos -> pintar(pos) } (captura this), que apunta a PantallaMapa, ya cerrada. Abajo, los tiempos de vida: Gps vive lo que la app; PantallaMapa está abierta hasta que el usuario la cierra y, a partir de ahí, sigue en memoria.](/img/pde/tema-2/2a-listener-olvidado.svg "El listener olvidado: la pantalla cerrada sigue alcanzable desde el GPS")

<div class="cards">
  <div class="card"><h4>Síntoma 1 · Fuga de memoria</h4><p>El usuario cierra la pantalla, pero el GPS sigue apuntando a ella: ni la pantalla ni sus vistas pueden liberarse. Diez aperturas = diez pantallas en memoria.</p></div>
  <div class="card"><h4>Síntoma 2 · Callbacks fantasma</h4><p>La fuente sigue invocando al listener de una pantalla que ya no existe: trabajo inútil, estado incoherente y, a menudo, una excepción.</p></div>
</div>

**Preguntas**

**1.** La pantalla se cerró, pero no se libera. ¿Por qué?

   a) Porque Android nunca libera Activities
   b) Porque el GPS guarda la lambda y la lambda captura `this` (la pantalla): hay una cadena de referencias fuertes desde algo que vive toda la app
   c) Porque la lambda se ejecuta en otro hilo
   d) Porque `pintar()` es una función privada

**2.** ¿Qué síntomas produce un listener olvidado?

   a) Fuga de memoria: pantallas cerradas que siguen ocupando memoria
   b) Error de compilación en el `addListener`
   c) Callbacks fantasma: se invoca código de una pantalla que ya no existe
   d) El GPS deja de emitir posiciones

### Regla nº 1: toda alta tiene su baja · Teoría

Se registra y se desregistra en **métodos simétricos del ciclo de vida**, con **la misma referencia**.

```kotlin
class PantallaMapa : AppCompatActivity() {

    private val alMoverse: Listener = { pos ->
        pintar(pos)                         // captura this
    }

    override fun onStart() {                // alta
        super.onStart()
        Gps.addListener(alMoverse)
    }

    override fun onStop() {                 // baja
        super.onStop()
        Gps.removeListener(alMoverse)
    }
}
```

1. **Una sola referencia.** El listener vive en una propiedad: es el mismo objeto en el alta y en la baja.
2. **Alta cuando empiezo a necesitarlo.** `onStart`: la pantalla se hace visible. Antes no tiene sentido recibir posiciones.
3. **Baja en el método simétrico.** `onStart ↔ onStop`, `onCreate ↔ onDestroy`, `onResume ↔ onPause`. **Nunca cruzados.**

### El mismo patrón en la web

```js
class PantallaMapa {
  alMoverse = (pos) => {          // captura this
    this.pintar(pos);
  };
  montar()    { gps.addListener(this.alMoverse); }     // alta
  desmontar() { gps.removeListener(this.alMoverse); }  // baja
}
```

La función flecha en un campo **fija `this`** y es el mismo objeto en el alta y en la baja. `montar` equivale a `onStart`: `connectedCallback` en un Web Component o el efecto en React. `desmontar` es `disconnectedCallback` o la función de limpieza del efecto. Si se olvida, la misma fuga que en Android.

> [!EXAMEN]
> «Alta y baja en métodos simétricos del ciclo de vida, con la misma referencia» es la respuesta corta a «¿cómo evitas un listener olvidado?».

**Preguntas**

**3.** Si das de alta un listener en `onResume`, ¿en qué método lo das de baja?

**4.** En la versión JS, ¿por qué `alMoverse` es un **campo** con función flecha y no se escribe `gps.addListener((p) => this.pintar(p))`?

   a) Porque las funciones flecha son más rápidas
   b) Porque así fija `this` y es la misma referencia en el alta y en la baja
   c) Porque JavaScript no admite lambdas como argumento
   d) Porque así el listener se ejecuta de forma síncrona

**5.** En un componente React que se suscribe al GPS dentro de un efecto, ¿dónde va la baja?

   a) En la función de limpieza que devuelve el efecto
   b) En el constructor
   c) En ningún sitio: React da de baja solo
   d) En el siguiente renderizado

### Revisión de código: tres pantallas · Ejercicio

Haces de revisor. Tres compañeros han escrito su pantalla de mapa. `Gps` es un `object` que vive toda la app.

```kotlin
// Pantalla 1
class P1 : AppCompatActivity() {
    private val alMoverse: Listener = { pos -> pintar(pos) }
    override fun onCreate(b: Bundle?)  { super.onCreate(b); Gps.addListener(alMoverse) }
    override fun onDestroy()           { super.onDestroy(); Gps.removeListener(alMoverse) }
}

// Pantalla 2
class P2 : AppCompatActivity() {
    private val alMoverse: Listener = { pos -> pintar(pos) }
    override fun onStart() { super.onStart(); Gps.addListener(alMoverse) }
    override fun onPause() { super.onPause(); Gps.removeListener(alMoverse) }
}

// Pantalla 3
class P3 : AppCompatActivity() {
    override fun onStart() { super.onStart(); Gps.addListener { pos -> pintar(pos) } }
    override fun onStop()  { super.onStop();  Gps.removeListener { pos -> pintar(pos) } }
}
```

Para cada una pregúntate: ¿alta y baja son **simétricas**? ¿Usan **la misma referencia**?

> [!TRUCO] Recordatorio del ciclo de vida
> Si se abre un diálogo encima de la pantalla, la Activity puede pasar por `onPause` y luego por `onResume` **sin** pasar por `onStop`/`onStart`.

<details><summary>Ver solución comentada</summary>
<p><b>P1</b> es correcta: <code>onCreate ↔ onDestroy</code> y la misma propiedad. (Recibe posiciones también cuando no es visible, pero no fuga.)</p>
<p><b>P2</b> cruza pares: alta en <code>onStart</code>, baja en <code>onPause</code>. Tras un diálogo (<code>onPause → onResume</code>) se queda dada de baja aunque sigue visible, porque <code>onStart</code> no se vuelve a llamar.</p>
<p><b>P3</b> crea una lambda nueva en la baja: no quita nada. Cada apertura deja una lambda (y su pantalla) colgada del GPS.</p>
</details>

**Preguntas**

**6.** ¿Qué pantallas tienen un fallo en la gestión del listener?

   a) P1
   b) P2
   c) P3

**7.** Con P3, el usuario abre y cierra la pantalla diez veces. ¿Qué hay colgado del GPS?

   a) Nada: cada `removeListener` quita la suya
   b) Una sola lambda, la última
   c) Diez lambdas, cada una reteniendo su pantalla: fuga y callbacks fantasma
   d) Veinte lambdas, porque cada baja añade una

### ¿Quién es el dueño? Cuatro estrategias · Teoría

**Ownership**: quién responde de que el callback deje de existir a tiempo.

<div class="cards">
  <div class="card"><h4>1 · Baja explícita</h4><p>El suscriptor se da de baja en el método simétrico. Simple, pero depende de la disciplina de quien programa.</p></div>
  <div class="card"><h4>2 · Suscripción cancelable</h4><p>El alta devuelve un objeto «suscripción». Quien lo guarda es el dueño; cancelarlo es la baja.</p></div>
  <div class="card"><h4>3 · Referencia débil</h4><p>La fuente guarda <code>WeakReference</code>: no impide liberar al listener. La limpieza llega tarde y sin avisar.</p></div>
  <div class="card"><h4>4 · Ligado al ciclo de vida</h4><p>El alta recibe el ciclo de vida del suscriptor y la fuente da de baja sola (<code>LiveData</code>, <code>DisposableEffect</code>).</p></div>
</div>

### Estrategia 2 en Kotlin

```kotlin
fun interface Suscripcion { fun cancelar() }

class Sensor {
    private val listeners = mutableListOf<Listener>()
    fun addListener(l: Listener): Suscripcion {
        listeners += l
        return Suscripcion { listeners -= l }
    }
}

val s = sensor.addListener { println(it) }
// …
s.cancelar()            // baja sin guardar la lambda
```

La suscripción **recuerda por ti** la referencia exacta. Adiós al problema de la lambda distinta.

### Estrategia 2 en JavaScript (la habitual)

```js
class Sensor {
  #listeners = [];
  addListener(l) {
    this.#listeners.push(l);
    return () => this.removeListener(l);   // el alta devuelve la baja
  }
}
const cancelar = sensor.addListener(console.log);
cancelar();
```

En el DOM existe `AbortController`:

```js
const control = new AbortController();
boton.addEventListener("click", alPulsar, { signal: control.signal });
// …
control.abort();   // da de baja todo lo ligado a esa señal
```

**Preguntas**

**8.** ¿Cuál es la pega de la estrategia 3 (referencia débil)?

   a) Impide liberar al listener
   b) La limpieza llega tarde y sin avisar: el listener puede seguir recibiendo eventos hasta que actúe el recolector
   c) Solo funciona en C
   d) Obliga a guardar la lambda en una `val`

**9.** En la estrategia 2, ¿quién es el dueño del callback?

   a) La fuente
   b) El recolector de basura
   c) Quien guarda el objeto suscripción
   d) El hilo principal

**10.** ¿Qué clase del DOM permite dar de baja de golpe todos los listeners registrados con su `signal`?

### Una idea, seis sintaxis (y el caso base en C) · Teoría

Para despachar eventos solo hace falta poder guardar **«qué función llamar» en una variable**. Todos los lenguajes lo permiten:

| Lenguaje | Mecanismo | Guardar una referencia a la función | ¿Con contexto? |
| --- | --- | --- | --- |
| C | Puntero a función | `void (*cb)(int) = al_pulsar;` | No |
| C++ | `std::function`, lambdas | `std::function<void(int)> cb = [&](int id) { … };` | Sí (captura) |
| C# | Delegados y eventos | `boton.Click += AlPulsar;` | Sí (objeto) |
| Java | Interfaz funcional, `this::m` | `boton.addActionListener(this::alPulsar);` | Sí (objeto) |
| Kotlin | Tipos función, lambdas, `::f` | `val cb: (Int) -> Unit = ::alPulsar` | Sí (captura) |
| Python / JS | Funciones de primera clase | `btn.addEventListener("click", alPulsar)` | Sí (clausura) |

Fíjate en la última columna: **C es el único que entrega solo la dirección del código**. Lo resolveremos con `void *ctx`.

### Punteros a función en C

Si se entiende aquí, se entiende en cualquier lenguaje: es lo que hay debajo de todos los demás.

```c
typedef enum { EV_BOTON, EV_TIMER, EV_UART, EV_COUNT } ev_tipo_t;

typedef struct {
    ev_tipo_t tipo;
    int       dato;
} evento_t;

/* El «tipo función» de C */
typedef void (*handler_t)(const evento_t *ev, void *ctx);

static void al_pulsar(const evento_t *ev, void *ctx) {
    printf("Botón %d\n", ev->dato);
}

handler_t h = al_pulsar;   /* guardar */
h(&ev, NULL);              /* invocar */
```

1. **El evento es un dato**: un tipo (`enum`) más su carga útil. Igual que la `sealed class EventoAgenda` de la U1.
2. **El contrato es un `typedef`**: equivale a `(Evento, Ctx) -> Unit` en Kotlin; fija parámetros y retorno de todo handler.
3. **El nombre es la dirección**: sin paréntesis, `al_pulsar` es un valor que se guarda, se pasa… y se mete en una tabla.

> [!TRUCO] ¿Para qué sirve EV_COUNT?
> Como los `enum` de C empiezan en 0, el último valor vale exactamente «cuántos tipos hay». Sirve para dimensionar tablas y validar índices.

**Preguntas**

**11.** ¿Qué lenguaje de la tabla guarda **solo la dirección del código**, sin contexto?

   a) Kotlin
   b) C#
   c) JavaScript
   d) C

**12.** ¿Qué palabra clave de C se usa para dar nombre al tipo puntero a función `handler_t`?

**13.** ¿Qué hace `handler_t h = al_pulsar;`?

   a) Llama a `al_pulsar` y guarda lo que devuelve
   b) Guarda la dirección de `al_pulsar` en `h`, sin llamarla
   c) Copia el código de la función en `h`
   d) No compila: falta `&`

### Del switch a la dispatch table · Teoría

La asociación *tipo de evento → handler* deja de ser **código** y pasa a ser un **dato**.

**Antes · el código decide**

```c
void despachar(const evento_t *ev) {
    switch (ev->tipo) {
    case EV_BOTON: al_pulsar(ev, NULL);  break;
    case EV_TIMER: al_vencer(ev, NULL);  break;
    case EV_UART:  al_recibir(ev, NULL); break;
    default: break;
    }
}
```

**Después · los datos deciden**

```c
static const handler_t tabla[EV_COUNT] = {
    [EV_BOTON] = al_pulsar,
    [EV_TIMER] = al_vencer,
    [EV_UART]  = al_recibir,
};

void despachar(const evento_t *ev) {
    if (ev->tipo < EV_COUNT && tabla[ev->tipo])
        tabla[ev->tipo](ev, NULL);
}
```

![Llega un evento con tipo = EV_TIMER. Su tipo se usa como índice en tabla[EV_COUNT], que tiene tres filas: EV_BOTON con al_pulsar, EV_TIMER con al_vencer (resaltada) y EV_UART con al_recibir. De la fila elegida se llama al handler al_vencer(ev, NULL). Debajo, la validación previa: índice en rango y entrada no nula.](/img/pde/tema-2/2a-dispatch-table.svg "Dispatch table: el tipo de evento es el índice; la fila elegida dice a quién llamar")

<div class="cards">
  <div class="card"><h4>Coste constante</h4><p>Un acceso por índice, haya 3 eventos o 300.</p></div>
  <div class="card"><h4>Crecer = añadir una fila</h4><p><code>despachar()</code> no se vuelve a tocar.</p></div>
  <div class="card"><h4>Modificable en ejecución</h4><p>Sin <code>const</code>, la tabla admite <code>registrar()</code> y dar de baja.</p></div>
  <div class="card"><h4>Siempre validar</h4><p>Índice en rango y entrada no nula antes de llamar.</p></div>
</div>

> [!NOTA] Inicializadores designados
> `[EV_BOTON] = al_pulsar` es sintaxis de C99: rellena la posición indicada. Las posiciones que no nombres quedan a `NULL` (en una tabla `static`), de ahí la comprobación `tabla[ev->tipo]`.

> [!EXAMEN]
> «Convierte este `switch` en una tabla de despacho y explica qué ganas» es un ejercicio típico. Las cuatro tarjetas de arriba son la respuesta.

**Preguntas**

**14.** ¿Qué ventajas tiene la dispatch table frente al `switch`?

   a) Coste constante: un acceso por índice
   b) Elimina la necesidad de validar el índice
   c) Añadir un evento es añadir una fila; `despachar()` no se toca
   d) El compilador comprueba que todos los tipos tienen handler
   e) Sin `const`, se puede registrar y dar de baja en ejecución

**15.** ¿Para qué sirve `ev->tipo < EV_COUNT && tabla[ev->tipo]`?

   a) Para ordenar los eventos por prioridad
   b) Para no salirse de la tabla y no llamar a un puntero nulo
   c) Para que el despacho sea síncrono
   d) Para liberar el evento después de procesarlo

### Traza la tabla en C · Ejercicio

Con esta tabla (fíjate en qué filas están rellenas):

```c
static void al_pulsar(const evento_t *ev, void *ctx) { printf("Botón %d\n", ev->dato); }
static void al_vencer(const evento_t *ev, void *ctx) { printf("Timer\n"); }

static const handler_t tabla[EV_COUNT] = {
    [EV_BOTON] = al_pulsar,
    [EV_TIMER] = al_vencer,
};

void despachar(const evento_t *ev) {
    if (ev->tipo < EV_COUNT && tabla[ev->tipo])
        tabla[ev->tipo](ev, NULL);
}
```

…se despachan, en este orden, estos eventos:

| # | tipo | dato |
| --- | --- | --- |
| 1 | `EV_BOTON` | 1 |
| 2 | `EV_UART` | 65 |
| 3 | `EV_TIMER` | 0 |
| 4 | `7` (valor corrupto) | 9 |
| 5 | `EV_BOTON` | 2 |

Recorre la tabla a mano: para cada evento, ¿pasa la validación? ¿Qué handler se llama?

> [!TRUCO] Valores del enum
> `EV_BOTON = 0`, `EV_TIMER = 1`, `EV_UART = 2`, `EV_COUNT = 3`.

<details><summary>Ver traza</summary>
<p>1 → <code>al_pulsar</code>: «Botón 1». 2 → índice válido pero <code>tabla[EV_UART]</code> es <code>NULL</code>: no hace nada. 3 → «Timer». 4 → 7 no es menor que 3: se descarta. 5 → «Botón 2».</p>
</details>

**Preguntas**

**16.** ¿Cuántas líneas se imprimen en total?

**17.** Si quitas la comprobación `ev->tipo < EV_COUNT`, ¿qué pasa con el evento 4?

   a) Nada: C ignora los índices fuera de rango
   b) Se llama a `al_pulsar` por defecto
   c) Se lee fuera de la tabla y se salta a una dirección cualquiera: comportamiento indefinido
   d) El compilador lo detecta y no compila

**18.** Quieres atender un nuevo evento `EV_ADC`. Con la tabla, ¿qué tocas?

   a) Añades el valor al `enum` (antes de `EV_COUNT`) y una fila `[EV_ADC] = al_medir`; `despachar()` no cambia
   b) Añades un `case EV_ADC` dentro de `despachar()`
   c) Reescribes `despachar()` con un bucle
   d) Nada: la tabla detecta el evento sola

### La misma tabla en Kotlin y JS, con varios suscriptores · Teoría

Un mapa *tipo de evento → lista de handlers*: aquí se encuentran listener y handler.

```kotlin
enum class Tipo { ANADIR, LISTAR, ELIMINAR, SALIR }
data class Evento(val tipo: Tipo, val dato: String = "")
typealias Handler = (Evento) -> Unit

class Despachador {
    private val tabla = mutableMapOf<Tipo, MutableList<Handler>>()

    fun on(tipo: Tipo, h: Handler) {
        tabla.getOrPut(tipo) { mutableListOf() } += h
    }
    fun despachar(ev: Evento) {
        tabla[ev.tipo]?.toList()?.forEach { it(ev) }
    }
}

d.on(Tipo.ANADIR) { tareas += it.dato }
d.on(Tipo.ANADIR, ::registrarEnLog)        // 2º suscriptor
d.despachar(Evento(Tipo.ANADIR, "Estudiar U2"))
```

1. **La tabla**: clave = tipo de evento; valor = lista de handlers. Es la lista de callbacks de la Clase 1, una por tipo.
2. **Registrar = escribir en la tabla**: `on()` crea la fila si no existe. Lambdas y referencias `::f` conviven.
3. **Despachar = buscar y difundir**: sin `when`. El bucle de eventos **no sabe qué significa** ningún evento: solo entrega.

```js
class Despachador {
  #tabla = new Map();                       // tipo -> [handlers]
  on(tipo, h) {
    if (!this.#tabla.has(tipo)) this.#tabla.set(tipo, []);
    this.#tabla.get(tipo).push(h);
  }
  despachar(ev) {
    const lista = this.#tabla.get(ev.tipo) ?? [];
    [...lista].forEach((h) => h(ev));
  }
}
d.on("ANADIR", (ev) => tareas.push(ev.dato));
d.on("ANADIR", registrarEnLog);
d.despachar({ tipo: "ANADIR", dato: "Estudiar U2" });
```

Es, en pequeño, lo que hacen por dentro **`EventEmitter`** en Node y **`EventTarget`** en el navegador.

> [!CUIDADO] Sin enum, sin red
> En JS la clave es un texto: si escribes `"ANADRI"`, nadie avisa. En Kotlin el `enum` hace que el compilador te pare.

**Preguntas**

**19.** En `despachar()`, ¿para qué sirven `toList()` (Kotlin) y `[...lista]` (JS)?

   a) Para recorrer una copia: un handler puede dar de alta o de baja sin romper la ronda
   b) Para ordenar los handlers
   c) Para convertir el evento en texto
   d) Para ejecutar los handlers en paralelo

**20.** En la versión JS registras `d.on("ANADRI", h)` por error. ¿Qué ocurre al despachar `{ tipo: "ANADIR" }`?

   a) Error de sintaxis al cargar el script
   b) Excepción en `despachar()`
   c) Se llama a `h` igualmente
   d) `h` no se llama nunca y nadie avisa

### Predice la salida del despachador · Ejercicio

Usa el `Despachador` de Kotlin del paso anterior:

```kotlin
val tareas = mutableListOf<String>()

fun registrarEnLog(ev: Evento) = println("log: ${ev.tipo} ${ev.dato}")

fun main() {
    val d = Despachador()
    d.on(Tipo.ANADIR) { tareas += it.dato }
    d.on(Tipo.ANADIR, ::registrarEnLog)
    d.on(Tipo.LISTAR) { println("tareas: $tareas") }

    d.despachar(Evento(Tipo.ANADIR, "Estudiar U2"))
    d.despachar(Evento(Tipo.ELIMINAR, "x"))
    d.despachar(Evento(Tipo.ANADIR, "Repasar C"))
    d.despachar(Evento(Tipo.LISTAR))
}
```

Pistas para la traza:

- Para `ANADIR` hay **dos** handlers en la fila: el primero no imprime nada, el segundo sí.
- ¿Hay fila para `ELIMINAR`? Mira qué hace `tabla[ev.tipo]?` cuando la clave no existe.
- `println` de una `MutableList` imprime sus elementos entre corchetes, separados por coma y espacio.

> [!NOTA] Interpolación
> `"log: ${ev.tipo}"` imprime el **nombre** de la constante del `enum` (`ANADIR`), porque `toString()` de un `enum` devuelve su nombre.

**Preguntas**

**21.** Ordena las líneas que imprime el programa.

   Ordena: log: ANADIR Estudiar U2 · log: ANADIR Repasar C · tareas: [Estudiar U2, Repasar C]

**22.** ¿Qué ocurre con `Evento(Tipo.ELIMINAR, "x")`?

   a) Lanza `NullPointerException`
   b) Nada: no hay fila, `tabla[ev.tipo]` es `null` y el `?.` corta la cadena
   c) Se ejecutan los handlers de `ANADIR`
   d) Elimina la última tarea

### Callback table frente a dispatch table · Teoría

Las dos son **tablas de referencias a función**. Cambia **quién elige la entrada y cuándo**.

**Callback table**

```c
typedef struct {
    void (*al_conectar)(void *ctx);
    void (*al_recibir)(const uint8_t *buf, size_t n, void *ctx);
    void (*al_cerrar)(void *ctx);
} red_callbacks_t;

red_iniciar(&mis_callbacks, &mi_ctx);
```

**Dispatch table**

```c
static const handler_t tabla[EV_COUNT] = {
    [EV_BOTON] = al_pulsar,
    [EV_TIMER] = al_vencer,
    [EV_UART]  = al_recibir,
};
tabla[ev->tipo](ev, ctx);
```

| | Callback table | Dispatch table |
| --- | --- | --- |
| Entradas | Con nombre, fijadas al compilar. | Elegidas por un dato en ejecución: tipo de evento, código de operación, comando recibido. |
| Uso | El cliente rellena los huecos que le interesan y entrega el struct entero. | El despachador indexa la tabla con el dato que llega. |
| Es… | Una **interfaz**: vtables de C++, drivers, pilas de red o Bluetooth. | Un **selector**: bucles de eventos, intérpretes, tablas de interrupciones, protocolos. |

> [!ANALOGIA]
> La **callback table** es el formulario de contacto que rellenas para una empresa: «si hay incidencia, llamad aquí; si se envía el paquete, escribid aquí». La **dispatch table** es la centralita: según la extensión marcada (un dato), conecta con una línea u otra.

Los obligatorios de la unidad lo resumen así: la callback table guarda funciones o delegados **junto a su contexto**; la dispatch table asocia **cada tipo de evento con uno o varios handlers**. La sintaxis cambia entre lenguajes; la asociación evento → reacción permanece.

**Preguntas**

**23.** Una librería Bluetooth te pide un `struct` con `al_conectar`, `al_recibir` y `al_desconectar`. Es una…

   a) Dispatch table
   b) Callback table
   c) Cola de eventos
   d) Suscripción cancelable

**24.** Un intérprete lee un byte de código de operación y llama a la función correspondiente en un array. Es una…

   a) Callback table
   b) Clausura
   c) Dispatch table
   d) Referencia débil

### void *ctx: el contexto que acompaña al callback · Teoría

En C no hay clausuras. ¿Cómo sabe un handler **sobre qué LED** actuar? Quien registra entrega **función + puntero opaco**; quien despacha devuelve ese puntero en cada llamada.

```c
typedef struct { handler_t fn; void *ctx; } entrada_t;
static entrada_t tabla[EV_COUNT];

void registrar(ev_tipo_t t, handler_t fn, void *ctx) {
    tabla[t] = (entrada_t){ fn, ctx };
}

typedef struct { int pin; bool encendido; } led_t;

static void conmutar(const evento_t *ev, void *ctx) {
    led_t *led = ctx;                 /* recuperar contexto */
    led->encendido = !led->encendido;
    gpio_set_level(led->pin, led->encendido);
}

static led_t rojo = { .pin = 2 }, verde = { .pin = 3 };
registrar(EV_BOTON, conmutar, &rojo);
registrar(EV_TIMER, conmutar, &verde);
```

1. **La entrada guarda la pareja** función + contexto. El despachador no sabe qué hay tras el `void*`: solo lo devuelve.
2. **El handler lo recupera** convirtiendo el `void*` a su tipo real. Ni variables globales ni dependencia de una GUI.
3. **Un handler, N contextos**: el mismo código sirve al LED rojo y al verde.

### Una clausura es función + contexto

| Kotlin · lo hace el compilador | C · lo haces tú |
| --- | --- |
| `val led = Led(pin = 2)`<br>`boton.onClick { led.conmutar() }` | `static led_t led = { .pin = 2 };`<br>`registrar(EV_BOTON, conmutar, &led);` |

APIs reales donde viaja el contexto: ESP-IDF `esp_event_handler_register(…, fn, arg)`, FreeRTOS `xTaskCreate(fn, …, pvParameters, …)`, POSIX `pthread_create(&hilo, attr, fn, arg)`.

> [!IMPORTANTE] Ownership sin recolector de basura
> El contexto **debe vivir más que el registro**: nunca una variable local de una función que retorna. Con memoria dinámica: **primero dar de baja, después `free()`**. Misma regla que en Android: toda alta tiene su baja.

**Preguntas**

**25.** ¿Qué sabe el despachador sobre lo que hay detrás de `void *ctx`?

   a) Que siempre es un `led_t`
   b) Su tamaño, para copiarlo
   c) Nada: solo lo guarda y lo devuelve en cada llamada
   d) Que es una variable global

**26.** ¿Qué falla en este código? `void configurar(void) { led_t azul = { .pin = 4 }; registrar(EV_UART, conmutar, &azul); }`

   a) Nada, es correcto
   b) `azul` es local: deja de existir al retornar `configurar()` y la tabla se queda con un puntero colgante
   c) Falta un `free(azul)`
   d) `conmutar` no acepta LEDs azules

**27.** Con un contexto reservado con `malloc`, ordena el final de su vida.

   Ordena: Dar de baja el handler en la tabla · Liberar el contexto con free()

### El despacho completo, sin ninguna GUI · Teoría

Las mismas cuatro piezas en una app Android, en un servidor o en un microcontrolador:

<div class="flow">
  <div><b>Fuentes</b><small>botón (ISR), temporizador, UART / red</small></div>
  <div><b>Cola de eventos</b><small>{tipo, dato} · desacopla</small></div>
  <div><b>Bucle de despacho</b><small>ev = sacar() · no sabe qué significa</small></div>
  <div><b>Tabla tipo → (handler, ctx)</b><small>asocia y entrega contexto</small></div>
</div>

Ejemplo de tabla en ejecución:

| Tipo | Handler | Contexto |
| --- | --- | --- |
| `EV_BOTON` | `conmutar` | `&rojo` |
| `EV_TIMER` | `conmutar` | `&verde` |
| `EV_UART` | `al_recibir` | `&buf` |

Es **portable** porque ninguna pieza menciona botones de pantalla ni vistas: solo tipos de evento, referencias a función y un puntero de contexto.

### Diseño de un dispatcher

- `registrar(tipo, callback, contexto)` devuelve una **suscripción identificable**.
- `despachar(evento)` localiza los handlers por tipo y aplica la **política de orden**.
- `desregistrar(suscripción)` retira el interés y **define qué pasa con las entregas pendientes**.

La tabla de despacho **separa la fuente del código de cada reacción**. En C++ moderno, la misma idea cabe en tres líneas:

```cpp
using Handler = std::function<void(int)>;
std::unordered_map<int, Handler> t;
t[1] = [](int x) { log(x); };
t.at(tipo)(dato);
```

Aquí el contexto puede viajar en el evento o en la clausura (la lambda captura lo que necesite).

**Preguntas**

**28.** Ordena los pasos de un despacho sin GUI.

   Ordena: Buscar: el dispatcher consulta la tabla del tipo de evento · Continuar: aplica la política de orden, fallos y bajas · Crear el evento: la fuente comunica tipo, origen y datos · Invocar: entrega el evento y, si procede, un contexto · Seleccionar: recupera uno o varios callbacks registrados

**29.** ¿Por qué este esquema sirve igual en Android, un servidor o un ESP32?

   a) Porque todos usan Kotlin
   b) Porque ninguna pieza depende de la GUI: solo tipos de evento, referencias a función y contexto
   c) Porque todos tienen recolector de basura
   d) Porque el bucle de despacho conoce el significado de cada evento

### Repaso: lista de comprobación de la Unidad 2 · Repaso

Seis preguntas que hacerse **antes de dar por bueno** un diseño dirigido por eventos:

| # | Pregunta | Qué mirar |
| --- | --- | --- |
| 1 | ¿Cuál es el contrato? | Firma del callback: parámetros, retorno y quién lo invoca. |
| 2 | ¿Síncrono o diferido? | En qué hilo y con qué pila corre; dónde se tratan sus excepciones. |
| 3 | ¿Cuántos suscriptores? | Uno o N por fuente; ¿algo depende del orden de invocación? |
| 4 | ¿Quién da de baja? | Dueño del callback, alta y baja simétricas, misma referencia o suscripción. |
| 5 | ¿Cómo se asocia evento → handler? | `when`/`switch`, o una tabla que crece añadiendo filas. |
| 6 | ¿De dónde saca el handler sus datos? | Clausura, objeto ligado o `void *ctx`; y cuánto vive ese contexto. |

> [!RESUMEN] Chuleta
> - Quien guarda un callback mantiene vivo **todo lo que captura** → fugas y callbacks fantasma.
> - **Toda alta tiene su baja**, en métodos simétricos y con la misma referencia.
> - Cuatro estrategias de ownership: baja explícita, suscripción cancelable, referencia débil, ligado al ciclo de vida.
> - Puntero a función (C) ≈ `std::function` ≈ delegado ≈ `::f` ≈ función de primera clase.
> - **Dispatch table**: la asociación tipo → handler como dato. Validar índice y no nulo.
> - **Callback table** = interfaz con huecos con nombre; **dispatch table** = selector por dato.
> - `void *ctx` = el contexto que en Kotlin captura la clausura. Debe vivir más que el registro.

> [!EXAMEN] Autoevaluación
> ¿Qué diferencia hay entre invocación síncrona y diferida? ¿Quién conserva la referencia y quién realiza la baja? ¿Qué ocurre si un handler falla o cambia el registro durante el despacho?

**Preguntas**

**30.** ¿Cuáles de estas son estrategias de ownership vistas en clase?

   a) Baja explícita en el método simétrico
   b) Registrar el listener dos veces por seguridad
   c) Suscripción cancelable devuelta por el alta
   d) Referencia débil en la fuente
   e) Alta ligada al ciclo de vida del suscriptor

**31.** Completa: «una clausura es función + …»

**32.** ¿Qué pareja de métodos del ciclo de vida está **mal** emparejada?

   a) onCreate ↔ onDestroy
   b) onStart ↔ onStop
   c) onResume ↔ onPause
   d) onStart ↔ onDestroy

### Solucionario

1. b) Porque el GPS guarda la lambda y la lambda captura `this` (la pantalla): hay una cadena de referencias fuertes desde algo que vive toda la app  
   Gps → listeners → lambda → PantallaMapa. Mientras el GPS viva, toda la cadena es alcanzable y no se puede liberar.
2. a) Fuga de memoria: pantallas cerradas que siguen ocupando memoria; c) Callbacks fantasma: se invoca código de una pantalla que ya no existe  
   El compilador no avisa de nada: el fallo aparece en ejecución, como **fuga** y como **callbacks fantasma**.
3. `onPause`  
   `onResume ↔ onPause`. Cada alta en su pareja simétrica.
4. b) Porque así fija `this` y es la misma referencia en el alta y en la baja  
   Una flecha escrita dentro de `addListener` sería un objeto nuevo imposible de pasar luego a `removeListener`. El campo da **una sola referencia**.
5. a) En la función de limpieza que devuelve el efecto  
   La función de limpieza del efecto es el «desmontar» simétrico del alta.
6. b) P2; c) P3  
   P2 cruza `onStart`/`onPause`; P3 da de baja una lambda distinta de la registrada.
7. c) Diez lambdas, cada una reteniendo su pantalla: fuga y callbacks fantasma  
   Las bajas nunca encuentran su lambda. Quedan diez lambdas y, con ellas, diez pantallas en memoria recibiendo posiciones.
8. b) La limpieza llega tarde y sin avisar: el listener puede seguir recibiendo eventos hasta que actúe el recolector  
   La referencia débil no retiene, pero **no sabes cuándo** desaparecerá: mientras tanto puede haber callbacks fantasma.
9. c) Quien guarda el objeto suscripción  
   Quien guarda la suscripción (o la función `cancelar`, o el `AbortController`) es el dueño; cancelarla es la baja.
10. `AbortController`  
   `AbortController`: al llamar a `abort()`, todos los listeners registrados con su `signal` se dan de baja.
11. d) C  
   Un puntero a función de C es solo una dirección. El contexto hay que pasarlo aparte (`void *ctx`).
12. `typedef`  
   `typedef void (*handler_t)(const evento_t *ev, void *ctx);` define el contrato de todos los handlers.
13. b) Guarda la dirección de `al_pulsar` en `h`, sin llamarla  
   Sin paréntesis, el nombre de la función es su **dirección**. La llamada llega después con `h(&ev, NULL)`.
14. a) Coste constante: un acceso por índice; c) Añadir un evento es añadir una fila; `despachar()` no se toca; e) Sin `const`, se puede registrar y dar de baja en ejecución  
   Validar sigue siendo obligatorio (índice en rango y entrada no nula), y el compilador no garantiza que todas las filas estén rellenas.
15. b) Para no salirse de la tabla y no llamar a un puntero nulo  
   Un índice fuera de rango leería memoria ajena; una entrada `NULL` haría saltar a la dirección 0. Ambas cosas cuelgan el programa (o algo peor).
16. `3`  
   Tres: «Botón 1», «Timer» y «Botón 2».
17. c) Se lee fuera de la tabla y se salta a una dirección cualquiera: comportamiento indefinido  
   C no comprueba los límites de un array. `tabla[7]` lee memoria ajena y se intenta llamar a lo que haya allí.
18. a) Añades el valor al `enum` (antes de `EV_COUNT`) y una fila `[EV_ADC] = al_medir`; `despachar()` no cambia  
   Crecer = añadir una fila. Esa es la gracia de pasar del código a los datos.
19. a) Para recorrer una copia: un handler puede dar de alta o de baja sin romper la ronda  
   La misma idea que en `Sensor.publicar`: difundir sobre una **foto** estable de la lista.
20. d) `h` no se llama nunca y nadie avisa  
   Son dos filas distintas de la tabla. El fallo es silencioso: por eso conviene usar constantes o tipos.
21. 1. log: ANADIR Estudiar U2 → 2. log: ANADIR Repasar C → 3. tareas: [Estudiar U2, Repasar C]  
   Cada `ANADIR` añade la tarea y la registra en el log; `ELIMINAR` no tiene fila; `LISTAR` imprime la lista acumulada.
22. b) Nada: no hay fila, `tabla[ev.tipo]` es `null` y el `?.` corta la cadena  
   Sin fila no hay nada que difundir. Igual que en el bus de la Clase 3: si nadie escucha, el evento se pierde en silencio.
23. b) Callback table  
   Entradas con nombre, fijadas al compilar, que tú rellenas: una **callback table** (una interfaz).
24. c) Dispatch table  
   La entrada la elige **un dato en ejecución** (el código de operación): **dispatch table**.
25. c) Nada: solo lo guarda y lo devuelve en cada llamada  
   Es un puntero **opaco**: el despachador no lo interpreta. Solo el handler sabe convertirlo a su tipo real.
26. b) `azul` es local: deja de existir al retornar `configurar()` y la tabla se queda con un puntero colgante  
   El contexto debe vivir más que el registro. Usa `static`, una global o memoria dinámica que liberes **después** de la baja.
27. 1. Dar de baja el handler en la tabla → 2. Liberar el contexto con free()  
   Si liberas primero, entre medias un despacho podría usar memoria ya liberada. Primero baja, después `free()`.
28. 1. Crear el evento: la fuente comunica tipo, origen y datos → 2. Buscar: el dispatcher consulta la tabla del tipo de evento → 3. Seleccionar: recupera uno o varios callbacks registrados → 4. Invocar: entrega el evento y, si procede, un contexto → 5. Continuar: aplica la política de orden, fallos y bajas  
   Crear → buscar → seleccionar → invocar → continuar. Ninguno de los pasos necesita una pantalla.
29. b) Porque ninguna pieza depende de la GUI: solo tipos de evento, referencias a función y contexto  
   El bucle **no sabe qué significa** cada evento; la tabla asocia tipo → reacción. Eso lo hace independiente de la plataforma.
30. a) Baja explícita en el método simétrico; c) Suscripción cancelable devuelta por el alta; d) Referencia débil en la fuente; e) Alta ligada al ciclo de vida del suscriptor  
   Las cuatro estrategias: baja explícita, suscripción cancelable, referencia débil y ligado al ciclo de vida.
31. `contexto`  
   Función + **contexto**: lo que en C pasas a mano con `void *ctx`, Kotlin lo hace solo al capturar variables.
32. d) onStart ↔ onDestroy  
   `onStart` se empareja con `onStop`. Cruzar pares deja la suscripción viva (o muerta) cuando no toca.

## Observer, pub/sub y MVVM

_Ponemos nombre a lo que ya habíamos construido: el patrón Observer del GoF con sus invariantes, el bus de publicación-suscripción con topics y la pantalla como un observador más en MVC y MVVM._

### Ya lo habéis escrito: el Observer del GoF · Teoría

Las dos clases anteriores construyeron las piezas; hoy les ponemos **nombre y contrato**.

| Lo que escribiste | Cómo se llama hoy |
| --- | --- |
| Clase 1 · `Sensor`: fuente con lista privada de listeners, alta, baja y difusión sobre una copia. | **Sujeto** (*Subject*) y **observadores**. Le faltaba estado propio y contrato. |
| Clase 2 · `Despachador`: `Map` tipo → lista de handlers; `on()` registra; `despachar()` difunde. | **Bus de eventos**; el tipo era el **topic**. |
| Lo que falta | La pantalla como un suscriptor cualquiera: **MVC / MVVM**. |

> [!NOTA] ¿Qué es el GoF?
> *Gang of Four*, la «Banda de los Cuatro»: Erich Gamma, Richard Helm, Ralph Johnson y John Vlissides, autores de *Design Patterns: Elements of Reusable Object-Oriented Software* (1994), el catálogo clásico de patrones de diseño. Observer es uno de ellos.

### Dos roles, cuatro operaciones

<div class="cards">
  <div class="card"><h4>Sujeto · Subject / Observable</h4><p>Tiene <b>estado</b> (p. ej. <code>temperatura</code>) y una <b>lista</b> de observadores. Operaciones: <code>attach</code> (suscribir), <code>detach</code> (desuscribir) y <code>notify</code> (notificar), que recorre la lista y llama a <code>update()</code> en cada uno.</p></div>
  <div class="card"><h4>Observador · Observer</h4><p>Solo promete un método, <code>update()</code> (aquí <code>actualizar(t)</code>), que <b>él nunca llama</b>: se lo llama el sujeto. Pantalla pinta, Alarma avisa si se supera un umbral, Registro guarda.</p></div>
</div>

### Tres invariantes

1. **Solo el sujeto cambia su estado.** `private set`: nadie de fuera escribe `temperatura`.
2. **Sin cambio, no hay aviso.** `if (nueva == temperatura) return`.
3. **El observador no pregunta.** Le llaman. No hace *polling* ni conoce a los demás.

**Desacoplamiento**: el termómetro solo sabe que le escuchan «observadores». Añadir un cuarto mañana no toca ni una línea de `Termometro`.

> [!EXAMEN]
> Roles, las cuatro operaciones (attach / detach / notify / update) y las tres invariantes: tenlas a mano para definir Observer en dos líneas.

**Preguntas**

**1.** ¿Qué significa GoF?

   a) Generic Observer Framework
   b) Gang of Four: los cuatro autores del libro *Design Patterns* (1994)
   c) Graph of Functions
   d) Google Open Framework

**2.** ¿Cuáles son invariantes del Observer tal como lo hemos definido?

   a) Solo el sujeto cambia su estado
   b) Cada observador consulta periódicamente al sujeto
   c) Sin cambio, no hay aviso
   d) El observador no pregunta: le llaman
   e) Los observadores se conocen entre sí para coordinarse

### El sujeto en Kotlin: Termometro · Teoría

En clase se escribe en vivo en `pde-consola` (`Observer.kt`, paso 1), nombrando cada pieza al teclearla:

```kotlin
interface Observador {                        // update(): lo llama el sujeto
    fun actualizar(temperatura: Double)
}

class Termometro {                            // Subject / Observable
    private val observadores = mutableListOf<Observador>()

    var temperatura = 20.0
        private set                           // solo el sujeto lo cambia

    fun suscribir(o: Observador)   { observadores += o }   // attach
    fun desuscribir(o: Observador) { observadores -= o }   // detach

    fun medir(nueva: Double) {
        if (nueva == temperatura) return      // sin cambio -> sin aviso
        temperatura = nueva
        notificar()
    }

    private fun notificar() {
        for (o in observadores.toList()) o.actualizar(temperatura)
    }
}
```

<div class="cards">
  <div class="card"><h4>1 · El contrato en una interfaz</h4><p><code>Observador</code> solo promete <code>actualizar()</code>. El sujeto no sabe nada más de quien le escucha.</p></div>
  <div class="card"><h4>2 · Estado con private set</h4><p>Se lee desde fuera, se escribe solo desde <code>medir()</code>. Es lo que lo distingue del <code>Sensor</code> de la Clase 1, que no tenía estado.</p></div>
  <div class="card"><h4>3 · Notificar solo si cambia</h4><p>Y sobre una copia de la lista, como en <code>Sensor.publicar</code>. <code>notificar()</code> es privado: nadie fuerza avisos desde fuera.</p></div>
</div>

> [!TRUCO] Lee cada línea con su nombre GoF
> `suscribir` = attach, `desuscribir` = detach, `notificar` = notify, `actualizar` = update. Si en un examen te dan otro código, busca esas cuatro piezas.

**Preguntas**

**3.** ¿Por qué `notificar()` es `private`?

   a) Porque Kotlin no permite funciones públicas que recorran listas
   b) Para que nadie de fuera fuerce avisos: solo el sujeto decide notificar cuando su estado cambia
   c) Para que los observadores no puedan darse de baja
   d) Para que sea más rápido

**4.** ¿Qué dos palabras de Kotlin hacen que `temperatura` se pueda leer desde fuera pero solo escribir desde dentro de la clase?

### Observadores que no se conocen · Ejercicio

Tres observadores que no saben nada unos de otros; el termómetro no conoce el tipo de ninguno:

```kotlin
class Pantalla : Observador {
    override fun actualizar(temperatura: Double) =
        println("Pantalla: %.1f ºC".format(temperatura))
}

class Alarma(private val umbral: Double) : Observador {
    override fun actualizar(temperatura: Double) {
        if (temperatura > umbral) println("ALARMA: $temperatura > $umbral")
    }
}

class Registro : Observador {
    val historial = mutableListOf<Double>()
    override fun actualizar(temperatura: Double) { historial += temperatura }
}

fun main() {
    val termometro = Termometro()
    termometro.suscribir(Pantalla())
    termometro.suscribir(Alarma(umbral = 30.0))
    termometro.suscribir(Registro())

    termometro.medir(22.5)
    termometro.medir(22.5)   // igual: no notifica
    termometro.medir(31.0)   // salta la alarma
}
```

(Detalle: `String.format` usa la configuración regional; en un equipo en español `%.1f` puede escribir `22,5`. La salida de clase usa punto.)

Predice la salida y después juega con el laboratorio: suscribe y da de baja observadores, mide temperaturas repetidas y por encima del umbral.

> _Aquí hay un simulador interactivo (`observer-lab`): ábrelo en la web._
> [!NOTA] Pregunta de clase
> ¿Qué pasaría en un **contador de pantalla** («veces que ha cambiado la temperatura») si el sujeto avisara sin cambio real? Contaría cambios que no existen. La invariante «sin cambio, no hay aviso» protege a los observadores que **cuentan** o **acumulan**.

**Preguntas**

**5.** Ordena las líneas que imprime `main()`.

   Ordena: ALARMA: 31.0 > 30.0 · Pantalla: 22.5 ºC · Pantalla: 31.0 ºC

**6.** ¿Por qué la segunda medida de 22.5 no imprime nada?

   a) Porque `Pantalla` ignora valores repetidos
   b) Porque `medir()` hace `return` si el valor no cambia: sin cambio no hay aviso
   c) Porque la lista de observadores se vacía tras la primera notificación
   d) Porque `Registro` consume el evento

**7.** Al acabar `main()`, ¿qué contiene el `historial` del `Registro`?

   a) [22.5, 22.5, 31.0]
   b) [31.0]
   c) [22.5, 31.0]
   d) [] (vacío)

### El mismo Observer en JavaScript · Teoría

Sin interfaces: el contrato es **«tener un método `actualizar()`»**.

```js
class Termometro {                         // sujeto
  #observadores = [];
  #temperatura = 20.0;                     // privado: solo lo cambia el sujeto

  suscribir(o)   { this.#observadores.push(o); }
  desuscribir(o) { this.#observadores = this.#observadores.filter(x => x !== o); }

  medir(nueva) {
    if (nueva === this.#temperatura) return;   // sin cambio -> sin aviso
    this.#temperatura = nueva;
    this.#notificar();
  }

  #notificar() {
    for (const o of [...this.#observadores]) o.actualizar(this.#temperatura);
  }
}

class Pantalla { actualizar(t) { console.log(`Pantalla: ${t.toFixed(1)} ºC`); } }

class Alarma {
  constructor(umbral) { this.umbral = umbral; }
  actualizar(t) { if (t > this.umbral) console.log(`ALARMA: ${t} > ${this.umbral}`); }
}
```

<div class="cards">
  <div class="card"><h4>1 · Contrato implícito</h4><p>No hay <code>interface</code>: cualquier objeto con <code>actualizar()</code> vale. El error, si lo hay, aparece <b>en ejecución</b>, no al compilar.</p></div>
  <div class="card"><h4>2 · La baja por identidad</h4><p><code>filter(x => x !== o)</code>: otra instancia «igual» no quita nada. Igual que <code>removeEventListener</code>.</p></div>
  <div class="card"><h4>3 · Observadores sin herencia</h4><p><code>Pantalla</code> y <code>Alarma</code> no comparten superclase. <i>Duck typing</i> puro: «si camina como un pato y grazna como un pato…».</p></div>
</div>

> [!NOTA] Campos con #
> `#observadores` y `#temperatura` son campos **privados** de JavaScript: fuera de la clase ni se leen ni se escriben. Cumplen la invariante «solo el sujeto cambia su estado».

**Preguntas**

**8.** Suscribes por error `{ actualisar(t) { … } }` (con «s»). ¿Cuándo te enteras?

   a) Al compilar, porque no implementa la interfaz
   b) Al suscribirlo, `suscribir()` lanza un error
   c) Al notificar: `o.actualizar` no es una función y salta un `TypeError` en ejecución
   d) Nunca, JavaScript lo corrige solo

**9.** `t.suscribir(new Pantalla()); t.desuscribir(new Pantalla());` ¿Cuántos observadores quedan?

   a) 0
   b) 1
   c) 2
   d) Depende del navegador

### Traduce Sensor.kt y elige push o pull · Ejercicio

Dos minutos en voz alta: el `Sensor` de la Clase 1, con vocabulario GoF.

| En `Sensor.kt` (Clase 1) | En el GoF |
| --- | --- |
| `addListener(l)` | attach / suscribir |
| `removeListener(l)` | detach / desuscribir |
| `publicar(dato)` | notify (pero sin estado propio) |
| `val enPantalla: Listener` | un observador cuyo update es la lambda |
| **¿Qué le falta?** | estado propio y «sin cambio, no hay aviso» |

### Un matiz del GoF: push frente a pull

<div class="cards">
  <div class="card"><h4>PUSH</h4><p>El sujeto <b>empuja el dato</b>: <code>actualizar(temperatura)</code>. Simple. Es lo que hemos escrito y lo que hace un listener de Android.</p></div>
  <div class="card"><h4>PULL</h4><p>El sujeto <b>solo avisa</b>: <code>actualizar(sujeto)</code>. El observador pregunta <code>sujeto.temperatura</code>. Cada uno coge lo que necesita cuando el estado es grande.</p></div>
</div>

> [!TRUCO] Regla práctica
> **Push** cuando el dato es pequeño y todos lo quieren; **pull** cuando el estado es grande y cada observador usa una parte distinta.

Decide para cada caso:

- **Caso 1.** Un sensor de temperatura que avisa a una pantalla, un log y una alarma. Todos quieren el mismo `Double`.
- **Caso 2.** Un documento de un editor con texto, estilos, cursor, selección e historial. La barra de estado quiere el número de palabras; la miniatura, el texto; el botón «deshacer», el historial.

**Preguntas**

**10.** Caso 1 (dato pequeño, todos lo quieren):

   a) Pull
   b) Push
   c) Ninguno: hay que usar polling
   d) Da igual, pull siempre es mejor

**11.** Caso 2 (estado grande, cada uno usa una parte distinta):

   a) Push: se envía el documento entero a todos en cada cambio
   b) Pull: se avisa con `actualizar(documento)` y cada observador lee la parte que necesita
   c) Ninguno: cada observador debe guardar su propia copia del documento
   d) Pub/sub con un topic por carácter

**12.** ¿Qué le falta a `Sensor` para ser un sujeto Observer completo?

   a) Estado propio
   b) Una lista de listeners
   c) La regla «sin cambio, no hay aviso»
   d) Operaciones de alta y baja

### Aparece un intermediario: el bus · Teoría

Quien publica **no tiene lista**; quien se suscribe **no le conoce**. Los une un **topic**.

```kotlin
typealias Suscriptor = (Any) -> Unit

object Bus {                                  // uno para toda la aplicación
    private val topics = mutableMapOf<String, MutableList<Suscriptor>>()

    fun suscribir(topic: String, s: Suscriptor): () -> Unit {
        topics.getOrPut(topic) { mutableListOf() } += s
        return { topics[topic]?.remove(s) }  // función de baja (Clase 2)
    }

    fun publicar(topic: String, dato: Any) {
        val lista = topics[topic] ?: return   // nadie escucha: se pierde sin error
        for (s in lista.toList()) s(dato)
    }
}
```

<div class="cards">
  <div class="card"><h4>1 · object: uno por aplicación</h4><p>Es el <code>Despachador</code> de la Clase 2 con el tipo sustituido por un <code>String</code>. En Node: <code>EventEmitter</code>; entre máquinas: Kafka, RabbitMQ, MQTT.</p></div>
  <div class="card"><h4>2 · suscribir devuelve la baja</h4><p>El patrón de la suscripción cancelable. Quien guarda la función es el dueño.</p></div>
  <div class="card"><h4>3 · ?: return</h4><p>Si nadie escucha, el mensaje se pierde en silencio. Decisión de diseño, no bug… y tiene consecuencias.</p></div>
</div>

<div class="flow">
  <div><b>Publicador</b><small>no conoce a nadie</small></div>
  <div><b>Bus</b><small>topic → lista de suscriptores</small></div>
  <div><b>Suscriptores</b><small>no conocen al publicador</small></div>
</div>

> [!ANALOGIA]
> Un tablón de anuncios por secciones: quien cuelga un anuncio en «Pisos» no sabe quién lo leerá, y quien mira «Pisos» no sabe quién lo colgó. Si nadie mira la sección «Bicicletas», ese anuncio no le llega a nadie… y nadie se entera.

> [!NOTA] Topics jerárquicos
> Nombres como `"sensor/temperatura"` o `"usuario/login"` imitan el estilo de MQTT, donde los topics se organizan como rutas.

**Preguntas**

**13.** `Bus.publicar("sensor/humedad", 55)` y nadie se ha suscrito a ese topic. ¿Qué pasa?

   a) Lanza una excepción
   b) Se guarda hasta que llegue un suscriptor
   c) El mensaje se pierde en silencio
   d) Se reenvía a todos los topics

**14.** ¿Qué devuelve `Bus.suscribir(...)`?

   a) El número de suscriptores del topic
   b) Una función que, al llamarla, da de baja a ese suscriptor
   c) El último dato publicado en el topic
   d) Nada (`Unit`)

### Predice la salida del bus · Ejercicio

El mismo bus en JavaScript (un `EventEmitter` escrito a mano):

```js
const Bus = {
  topics: new Map(),
  suscribir(topic, s) {                                   // on()
    if (!this.topics.has(topic)) this.topics.set(topic, []);
    this.topics.get(topic).push(s);
    return () => this.topics.set(topic,                   // off()
      this.topics.get(topic).filter(x => x !== s));
  },
  publicar(topic, dato) {                                 // emit()
    const lista = this.topics.get(topic);
    if (!lista) return;                                   // nadie escucha: silencio
    for (const s of [...lista]) s(dato);
  },
};

const bajaLog = Bus.suscribir("sensor/temperatura", d => console.log(`log: ${d}`));
Bus.suscribir("sensor/temperatura", d => { if (d > 30) console.log(`alarma: ${d}`); });
Bus.suscribir("usuario/login", u => console.log(`bienvenida a ${u}`));

Bus.publicar("sensor/temperatura", 28.0);
Bus.publicar("sensor/humedad", 55);          // nadie suscrito
Bus.publicar("usuario/login", "ana");
bajaLog();
Bus.publicar("sensor/temperatura", 35.0);
```

Traza cada `publicar`: ¿qué topic?, ¿quién está suscrito en ese momento?, ¿qué condición se cumple?

> [!NOTA] 28 y no 28.0
> En JavaScript todos los números son `Number`: `28.0` se imprime como `28`. La versión Kotlin del mismo programa imprimiría `log: 28.0` y `alarma: 35.0`.

Equivalencias: Node `EventEmitter` (`on` / `emit` / `off`), `CustomEvent` en el DOM y, entre procesos, MQTT o Kafka: los mismos tres verbos.

**Preguntas**

**15.** Ordena las líneas que imprime el programa.

   Ordena: alarma: 35 · bienvenida a ana · log: 28

**16.** ¿Por qué no aparece `log: 35`?

   a) Porque 35 supera el umbral y la alarma consume el evento
   b) Porque `bajaLog()` quitó ese suscriptor antes de publicar 35
   c) Porque el bus solo entrega el primer mensaje de cada topic
   d) Porque `sensor/temperatura` se borró al publicar humedad

### Observer frente a pub/sub · Teoría

Se gana **desacoplamiento total**; se pierde **la ayuda del compilador**.

![Dos esquemas lado a lado. Observer: el sujeto Termometro guarda la lista y llama a actualizar(t) en Pantalla, Alarma y Registro, que se suscribieron a él. Publicación-suscripción: el Publicador publica en el Bus, en el topic sensor/temperatura, y el bus entrega a los suscriptores log y alarma; nadie conoce a nadie, solo comparten el topic.](/img/pde/tema-2/2a-observer-vs-pubsub.svg "Observer: el sujeto tiene la lista. Pub/sub: la lista está en el bus y solo se comparte el topic")

| | Observer (GoF) | Publicación-suscripción |
| --- | --- | --- |
| Quién conoce a quién | El observador tiene la referencia del sujeto (se suscribe a él). | Nadie conoce a nadie; comparten un topic. |
| Quién guarda la lista | El sujeto. | El intermediario (bus, broker). |
| Quién puede notificar | Solo el sujeto, al cambiar su estado. | Cualquiera que publique en el topic. |
| Tipo del dato | Fuerte: la interfaz lo fija (`Double`). | Débil: `Any` y un `String` como clave → errores en ejecución. |
| Si nadie escucha | El sujeto lo sabe (lista vacía). | El mensaje se pierde y nadie se entera. |
| Cuándo usarlo | Pocos sujetos, relación clara, misma unidad de código. | Muchos productores/consumidores; módulos, procesos o máquinas distintos. |

### Riesgos heredados de la Clase 2, agravados

- **Reentrancia**: un suscriptor que publica en su propio topic dispara una difusión dentro de otra.
- **Suscriptores lentos**: la difusión es síncrona; uno lento retrasa a todos los que van detrás.
- **Trazabilidad**: «quién escucha qué» ya no se ve siguiendo una referencia; hay que **buscar el string** por todo el proyecto.

> [!CUIDADO] Un topic mal escrito no da error
> `Bus.suscribir("sensor/temperatrua") { … }` compila y se ejecuta sin quejarse… y nunca recibe nada. Dos defensas: **constantes** para los nombres de topic, o **tipos** (`enum` / `sealed class`) como clave, como en el `Despachador` de la Clase 2.

**Preguntas**

**17.** ¿En qué esquema detecta el compilador que mandas un `String` donde se esperaba un `Double`?

   a) En el bus pub/sub, gracias al topic
   b) En el Observer, porque la interfaz fija el tipo de `actualizar()`
   c) En ninguno de los dos
   d) En los dos por igual

**18.** ¿En qué situaciones encaja mejor pub/sub que Observer?

   a) Módulos que no deben conocerse entre sí
   b) Un único sujeto y dos observadores en la misma clase
   c) Productores y consumidores en procesos o máquinas distintas
   d) Muchos productores y muchos consumidores

**19.** ¿Qué evita que un topic mal escrito pase desapercibido?

   a) Definir los nombres de topic como constantes
   b) Usar un `enum` o una `sealed class` como clave del bus
   c) Publicar dos veces cada mensaje
   d) Recorrer una copia de la lista de suscriptores

### La GUI como observador: Observable<T> y el ViewModel · Teoría

En `pde-plantilla` (vistas clásicas, TRAB U1) el `onClick` hace **dos trabajos**:

```kotlin
botonPulsa.setOnClickListener {
    pulsaciones++            // 1. cambia el estado (regla de negocio)
    actualizarInterfaz()     // 2. repinta a mano
}

private fun actualizarInterfaz() {
    textPulsaciones.text = "Pulsaciones: $pulsaciones"
}
```

`pulsaciones++` es lógica que debería poder **probarse sin pantalla**; `actualizarInterfaz()` conoce el `TextView` y crece una línea con cada vista nueva. **Objetivo**: que el estado sea un **sujeto** y cada trozo de pantalla un **observador**. Así el listener del botón solo hace un trabajo: avisar al modelo.

```kotlin
class Observable<T>(valorInicial: T) {     // LiveData en miniatura
    private val observadores = mutableListOf<(T) -> Unit>()

    var valor: T = valorInicial
        set(nuevo) {
            field = nuevo
            for (o in observadores.toList()) o(nuevo)
        }

    fun observar(o: (T) -> Unit) {
        observadores += o
        o(valor)                            // valor actual al suscribirse
    }
}

class ContadorViewModel {                  // estado y reglas; sin pantallas
    val pulsaciones = Observable(0)
    val activo = Observable(true)

    fun onPulsar() { if (activo.valor) pulsaciones.valor++ }
    fun onCambiarActivo(v: Boolean) { activo.valor = v }
}
```

<div class="cards">
  <div class="card"><h4>1 · El setter notifica</h4><p>Cambiar <code>valor</code> es el <code>medir()</code> del termómetro: guarda y avisa. El sujeto ahora es genérico.</p></div>
  <div class="card"><h4>2 · Valor actual al suscribirse</h4><p><code>o(valor)</code> en <code>observar()</code>: una vista que llega tarde se pinta bien. LiveData hace exactamente esto.</p></div>
  <div class="card"><h4>3 · Reglas sin pantalla</h4><p><code>onPulsar()</code> decide si cuenta. Se prueba creando el ViewModel y leyendo <code>pulsaciones.valor</code>.</p></div>
</div>

**Preguntas**

**20.** ¿Cuáles son los dos trabajos que mezcla el `onClick` de `pde-plantilla`?

   a) Registrar el listener y darlo de baja
   b) Cambiar el estado y repintar la interfaz a mano
   c) Leer el sensor y escribir en el log
   d) Crear la Activity y destruirla

**21.** ¿Para qué sirve la línea `o(valor)` dentro de `observar()`?

   a) Para que la vista que se suscribe se pinte enseguida con el valor actual
   b) Para dar de baja al observador
   c) Para evitar notificar si no hay cambio
   d) Para copiar la lista de observadores

### Las vistas se suscriben · Ejercicio

Tres «vistas» sobre el mismo estado, añadidas **sin abrir el ViewModel**. Hoy son `println`; mañana, un `TextView` o un Composable.

```kotlin
fun main() {
    val vm = ContadorViewModel()

    vm.pulsaciones.observar { println("[TextView] Pulsaciones: $it") }
    vm.pulsaciones.observar { if (it >= 3) println("[Toast] ¡tres ya!") }
    vm.activo.observar      { println("[Button] enabled = $it") }

    vm.onPulsar(); vm.onPulsar(); vm.onPulsar()
    vm.onCambiarActivo(false)
    vm.onPulsar()            // no cuenta: la regla vive en el ViewModel
}
```

Traza con cuidado:

1. Cada `observar` llama **inmediatamente** al observador con el valor actual.
2. Cada `pulsaciones.valor++` pasa por el setter y avisa a **los dos** observadores de `pulsaciones`, en orden de alta.
3. ¿Qué hace `onPulsar()` cuando `activo.valor` es `false`?

> [!TRUCO] Fíjate en la primera línea
> «Pulsaciones: 0» aparece **antes** de pulsar nada. Es `o(valor)` al suscribirse: la vista que llega tarde también se pinta bien.

**Preguntas**

**22.** Ordena la salida completa del programa.

   Ordena: [Button] enabled = false · [Button] enabled = true · [TextView] Pulsaciones: 0 · [TextView] Pulsaciones: 1 · [TextView] Pulsaciones: 2 · [TextView] Pulsaciones: 3 · [Toast] ¡tres ya!

**23.** ¿Qué imprime el último `vm.onPulsar()`?

   a) `[TextView] Pulsaciones: 4`
   b) `[Toast] ¡tres ya!` otra vez
   c) Nada: `activo` es `false` y el ViewModel no cuenta la pulsación
   d) `[Button] enabled = false` otra vez

### MVVM en JavaScript y los nombres: MVC y MVVM · Teoría

El mismo mini-MVVM en el navegador. Las vistas son `span.textContent` y `button.disabled`; el ViewModel no cambia:

```js
class Observable {                              // LiveData en miniatura
  #observadores = []; #valor;
  constructor(inicial) { this.#valor = inicial; }
  get valor() { return this.#valor; }
  set valor(nuevo) { this.#valor = nuevo; for (const o of [...this.#observadores]) o(nuevo); }
  observar(o) { this.#observadores.push(o); o(this.#valor); }
}

class ContadorViewModel {                       // nada de DOM aquí
  pulsaciones = new Observable(0);
  activo = new Observable(true);
  onPulsar() { if (this.activo.valor) this.pulsaciones.valor++; }
  onCambiarActivo(v) { this.activo.valor = v; }
}

const vm = new ContadorViewModel();
vm.pulsaciones.observar(n => span.textContent = `Pulsaciones: ${n}`);   // vista 1
vm.pulsaciones.observar(n => { if (n >= 3) mostrarToast("¡tres ya!"); }); // vista 2
vm.activo.observar(a => boton.disabled = !a);                             // vista 3
boton.addEventListener("click", () => vm.onPulsar());                    // el listener solo avisa
```

La vista es **una línea de binding**, sin lógica; el listener del clic **solo avisa** al ViewModel.

![A la izquierda, las vistas: listener del clic, span con Pulsaciones, toast y botón disabled. A la derecha, ContadorViewModel con onPulsar() y dos Observable, pulsaciones y activo. El listener del clic llama a vm.onPulsar(), que hace valor++ en pulsaciones. El span y el toast se suscriben a pulsaciones con observar, y el botón a activo. Todas las flechas van de la vista al ViewModel: al cambiar un valor, Observable llama a las lambdas que cada vista le entregó.](/img/pde/tema-2/2a-mvvm.svg "MVVM: la vista llama y se suscribe; el ViewModel no conoce ninguna vista")

| Pieza de hoy | MVC (Smalltalk, 1979) | MVVM (Android, .NET, Vue) | En Android real |
| --- | --- | --- | --- |
| `ContadorViewModel` + `Observable` | Modelo (sujeto) | ViewModel expone estado observable | `ViewModel` + `LiveData` / `StateFlow` |
| `println` / `TextView` | Vista (observador) | La vista se suscribe: *binding* | `observe(owner) { }` · `collectAsState()` |
| `onPulsar()`, `onCambiarActivo()` | Controlador: recibe la entrada, modifica el modelo | La vista llama métodos del ViewModel | `setOnClickListener { vm.onPulsar() }` |

> [!IMPORTANTE] Lo que no cambia
> En los dos, **el modelo es el sujeto y la vista un observador**. La vista nunca es la dueña del estado.

> [!NOTA] Sesión 0 · Compose
> En Compose no escribíais `actualizarInterfaz()`: la **recomposición** es esto mismo hecho por el framework. `collectAsState()` es `observar()`.

**Preguntas**

**24.** En MVC, ¿qué papel hace la pieza que recibe la entrada del usuario y modifica el modelo?

   a) Vista
   b) Modelo
   c) Controlador
   d) Observable

**25.** En Jetpack Compose, ¿qué equivale a nuestro `observar()`?

   a) `setContentView()`
   b) `collectAsState()`
   c) `findViewById()`
   d) `onCreate()`

### La escalera de la Unidad 2 · Ejercicio

Cinco nombres, **una sola idea**: cada peldaño añade una pieza.

<div class="flow">
  <div><b>1 · Callback</b><small>la fuente recibe una función · Clase 1</small></div>
  <div><b>2 · Listener</b><small>varias funciones, alta y baja · Clase 1, Sensor</small></div>
  <div><b>3 · Observer (GoF)</b><small>sujeto con estado + contrato update() · hoy</small></div>
  <div><b>4 · Pub/sub · bus</b><small>nadie conoce a nadie; un topic en medio · Clase 2 (Despachador) y hoy</small></div>
  <div><b>5 · MVC / MVVM</b><small>el modelo es el sujeto; la pantalla, un observador más · hoy</small></div>
</div>

Idea de la clase: **Sensor era un Observer, Despachador era un bus. Una vista es un observador más.**

### Criterio de elección

| Usa… | Cuando… |
| --- | --- |
| Callback | Una operación puntual. |
| Observer | Estado claro, pocos interesados, mismo código. |
| Bus | Módulos que no deben conocerse o viven en sitios distintos. |
| MVVM | Siempre que haya pantalla. |

Cuanto más subes, **más desacoplo y menos compilador** (menos comprobaciones en tiempo de compilación).

Aplica el criterio a los casos de las preguntas.

**Preguntas**

**26.** Ordena la escalera de la Unidad 2, del peldaño más bajo al más alto.

   Ordena: Callback · Listener · MVC / MVVM · Observer (GoF) · Pub/sub (bus)

**27.** Quieres que te avisen **una vez** cuando termine de descargarse un fichero. ¿Qué peldaño basta?

   a) MVVM
   b) Pub/sub con un broker
   c) Callback
   d) Observer con tres observadores

**28.** El módulo de pagos debe avisar al de correo y al de estadísticas, que lo mantienen equipos distintos y no deben depender de él. ¿Qué eliges?

   a) Un bus pub/sub con topics
   b) Un callback
   c) Un `when` en el módulo de pagos
   d) Un `Observable` dentro de una Activity

### Ampliación: invariantes y riesgos durante el despacho · Teoría

> [!NOTA] Contenido de ampliación (stretch)
> Va más allá de los obligatorios, pero ayuda a responder bien las preguntas de «qué puede salir mal».

### Invariantes de una notificación

<div class="cards">
  <div class="card"><h4>Baja efectiva</h4><p>Una suscripción retirada no recibe entregas futuras, según el contrato.</p></div>
  <div class="card"><h4>Registro íntegro</h4><p>Un handler no corrompe la lista al registrar o retirar otro (de ahí la ronda sobre una copia).</p></div>
  <div class="card"><h4>Datos suficientes</h4><p>El evento transporta lo necesario para procesarlo sin consultar al emisor.</p></div>
</div>

### Riesgos durante el despacho

- **Reentrancia**: un handler provoca otro despacho antes de que acabe el primero.
- **Cambios en notify**: altas y bajas durante la iteración alteran quién recibe el evento.
- **Ciclos**: A avisa a B y B vuelve a avisar a A sin condición de parada.

### Costes de un callback mal gestionado

- **Lentitud**: el despacho síncrono retrasa a todos los receptores posteriores.
- **Referencia obsoleta**: el callback sobrevive a la pantalla, tarea o dispositivo que lo creó.
- **Anidación**: varias dependencias anidadas dispersan errores y cancelación.

### Traza: un observador que toca el sujeto desde dentro

```kotlin
val temp = Observable(20)
temp.observar { if (it > 40) temp.valor = 40 }   // A: «corrige» desde dentro
temp.observar { println("Pantalla: $it") }        // B
temp.valor = 45
println("valor final: ${temp.valor}")
```

Al poner 45, el setter recorre [A, B] con `nuevo = 45`. A, **antes de que B se entere**, pone 40: se abre una **segunda ronda** completa (A con 40, B con 40). Al volver, la primera ronda continúa y entrega 45 a B. Resultado: la pantalla muestra **45** pero el valor real es **40**.

**Preguntas**

**29.** Ordena la salida de la traza.

   Ordena: Pantalla: 20 · Pantalla: 40 · Pantalla: 45 · valor final: 40

**30.** Con difusión sobre una copia, el observador A da de baja a B **durante** una notificación, y B va después de A. ¿Recibe B este evento?

   a) No, nunca más recibe nada, ni este evento
   b) Sí recibe este (está en la copia de la ronda), pero no los siguientes
   c) Recibe este evento dos veces
   d) Se lanza `ConcurrentModificationException`

### Repaso: tres preguntas de parcial y puente a la U3 · Repaso

> [!RESUMEN] Chuleta
> - **Observer (GoF)**: sujeto con estado + lista; attach / detach / notify / update. Invariantes: solo el sujeto cambia su estado, sin cambio no hay aviso, el observador no pregunta.
> - **Push** (dato pequeño, todos lo quieren) frente a **pull** (estado grande, cada uno coge su parte).
> - **Pub/sub**: un bus con topics; nadie conoce a nadie; `suscribir` devuelve la baja; si nadie escucha, se pierde. Más desacoplo, menos compilador.
> - **MVVM**: el ViewModel es el sujeto (estado + reglas, sin pantalla); las vistas observan; el listener del botón solo avisa. `Observable<T>` entrega el valor actual al suscribirse.
> - **Escalera**: callback → listener → Observer → pub/sub → MVC/MVVM.

> [!EXAMEN] Las tres preguntas que pueden caer en el parcial 1
> 1. ¿Qué invariantes rompe un observador que **modifica el estado del sujeto desde dentro de `actualizar()`**? ¿Qué les pasa a los demás observadores?
> 2. **Un topic mal escrito no da error.** Propón dos formas de evitarlo en un bus.
> 3. **Reescribe `MainActivity` con `ContadorViewModel`.** Explica qué línea desaparece de `pde-plantilla` y por qué.

<details><summary>Ideas para responderlas</summary>
<p>1. Rompe «solo el sujeto cambia su estado» y provoca <b>reentrancia</b>: se abre una notificación dentro de otra, los demás reciben avisos anidados y desordenados y pueden quedarse con un valor obsoleto (la traza del paso anterior).</p>
<p>2. Constantes para los nombres de topic; o tipos (<code>enum</code> / <code>sealed class</code>) como clave, como en el <code>Despachador</code>.</p>
<p>3. Desaparece la llamada a <code>actualizarInterfaz()</code> (y la función): el <code>TextView</code> observa <code>vm.pulsaciones</code> y se repinta solo. El listener queda en <code>setOnClickListener { vm.onPulsar() }</code>.</p>
</details>

### Puente a la Unidad 3

**Hoy todo ha sido síncrono**: `publicar()` recorre la lista y vuelve. En la U3 metemos una **cola** entre el que publica y el que escucha: bucle de eventos, orden, prioridades… y el handler lento que congela la pantalla.

**Preguntas**

**31.** Al reescribir `MainActivity` con `ContadorViewModel`, ¿qué desaparece del listener del botón?

   a) `setOnClickListener`
   b) La llamada a `actualizarInterfaz()`: la vista observa el estado y se repinta sola
   c) `vm.onPulsar()`
   d) Nada, queda igual

**32.** Marca las afirmaciones **verdaderas**.

   a) En pub/sub, si nadie está suscrito a un topic, el publicador recibe un error
   b) En Observer, el sujeto es quien guarda la lista de observadores
   c) `Observable<T>` llama al observador con el valor actual en cuanto se suscribe
   d) En MVVM la vista es la dueña del estado
   e) Un observador que modifica el sujeto dentro de `actualizar()` provoca reentrancia

**33.** ¿Qué estructura se colocará en la U3 entre quien publica y quien escucha? (una palabra)

### Solucionario

1. b) Gang of Four: los cuatro autores del libro *Design Patterns* (1994)  
   *Gang of Four*: Gamma, Helm, Johnson y Vlissides.
2. a) Solo el sujeto cambia su estado; c) Sin cambio, no hay aviso; d) El observador no pregunta: le llaman  
   Las otras dos son justo lo contrario: nada de *polling* y los observadores no se conocen.
3. b) Para que nadie de fuera fuerce avisos: solo el sujeto decide notificar cuando su estado cambia  
   Si fuese pública, cualquiera podría provocar avisos sin cambio real y se rompería «sin cambio, no hay aviso».
4. `private set`  
   `private set` en la propiedad: el getter es público, el setter privado. Invariante 1.
5. 1. Pantalla: 22.5 ºC → 2. Pantalla: 31.0 ºC → 3. ALARMA: 31.0 > 30.0  
   22.5 notifica (Pantalla imprime; Alarma no salta; Registro guarda en silencio). El segundo 22.5 no notifica. 31.0 notifica: Pantalla y luego Alarma.
6. b) Porque `medir()` hace `return` si el valor no cambia: sin cambio no hay aviso  
   La comprobación vive en el **sujeto**, no en cada observador: `if (nueva == temperatura) return`.
7. c) [22.5, 31.0]  
   Solo recibe las dos notificaciones reales: 22.5 y 31.0.
8. c) Al notificar: `o.actualizar` no es una función y salta un `TypeError` en ejecución  
   Sin interfaz, nadie comprueba el contrato hasta que se intenta llamar a `actualizar()`.
9. b) 1  
   La baja compara por **identidad** (`!==`). Una segunda `new Pantalla()` es otro objeto: no quita nada.
10. b) Push  
   Un `Double` que todos necesitan: se empuja directamente en `actualizar(t)`.
11. b) Pull: se avisa con `actualizar(documento)` y cada observador lee la parte que necesita  
   Con estado grande y necesidades distintas, cada observador **tira** (*pull*) de lo suyo.
12. a) Estado propio; c) La regla «sin cambio, no hay aviso»  
   Lista, alta y baja ya las tenía. Le faltaban **estado** y **notificar solo si cambia**.
13. c) El mensaje se pierde en silencio  
   `topics[topic] ?: return`: sin lista no hay nada que hacer, y nadie se entera. Es una decisión de diseño.
14. b) Una función que, al llamarla, da de baja a ese suscriptor  
   Suscripción cancelable (estrategia 2 de la Clase 2): quien guarda esa función es el dueño de la suscripción.
15. 1. log: 28 → 2. bienvenida a ana → 3. alarma: 35  
   28 → solo log (la alarma no salta). Humedad → silencio. Login → bienvenida. Tras la baja del log, 35 → solo alarma.
16. b) Porque `bajaLog()` quitó ese suscriptor antes de publicar 35  
   `bajaLog` es la función de baja que devolvió `suscribir`. Al invocarla, el log deja de escuchar.
17. b) En el Observer, porque la interfaz fija el tipo de `actualizar()`  
   El bus usa `Any` y un `String` como clave: los errores aparecen en ejecución. El Observer tiene tipado fuerte.
18. a) Módulos que no deben conocerse entre sí; c) Productores y consumidores en procesos o máquinas distintas; d) Muchos productores y muchos consumidores  
   Pocos sujetos y una relación clara en el mismo código: Observer. El bus brilla cuando hace falta desacoplar mucho.
19. a) Definir los nombres de topic como constantes; b) Usar un `enum` o una `sealed class` como clave del bus  
   Con constantes o tipos, un error de escritura se convierte en error de compilación.
20. b) Cambiar el estado y repintar la interfaz a mano  
   Estado y pantalla pegados: `pulsaciones++` y `actualizarInterfaz()` en el mismo sitio.
21. a) Para que la vista que se suscribe se pinte enseguida con el valor actual  
   Una vista que llega tarde no se queda en blanco esperando el próximo cambio. LiveData se comporta igual.
22. 1. [TextView] Pulsaciones: 0 → 2. [Button] enabled = true → 3. [TextView] Pulsaciones: 1 → 4. [TextView] Pulsaciones: 2 → 5. [TextView] Pulsaciones: 3 → 6. [Toast] ¡tres ya! → 7. [Button] enabled = false  
   Suscribirse pinta el valor actual (0 y true; el Toast calla con 0). Tres pulsaciones: 1, 2, 3 y, tras el 3, el Toast. Desactivar avisa al botón. La última pulsación no cuenta.
23. c) Nada: `activo` es `false` y el ViewModel no cuenta la pulsación  
   La regla («si no está activo, no cuenta») vive en el ViewModel y se puede probar sin pantalla.
24. c) Controlador  
   El **controlador**. En MVVM ese papel lo cubren los métodos del ViewModel (`onPulsar()`), a los que llama la vista.
25. b) `collectAsState()`  
   `collectAsState()` suscribe el Composable al estado; cada cambio provoca una **recomposición**, que es el repintado automático.
26. 1. Callback → 2. Listener → 3. Observer (GoF) → 4. Pub/sub (bus) → 5. MVC / MVVM  
   Callback → listener → Observer → pub/sub → MVC/MVVM. Cada peldaño añade una pieza a la anterior.
27. c) Callback  
   Operación puntual: un callback como el `descargar(url, alTerminar)` de la Clase 1.
28. a) Un bus pub/sub con topics  
   Módulos que no deben conocerse: el intermediario (bus) desacopla al publicador de sus consumidores.
29. 1. Pantalla: 20 → 2. Pantalla: 40 → 3. Pantalla: 45 → 4. valor final: 40  
   La ronda anidada (40) se completa antes de que la exterior entregue 45 a B. B se queda con un valor **obsoleto**: eso es la reentrancia.
30. b) Sí recibe este (está en la copia de la ronda), pero no los siguientes  
   La copia es una foto de la ronda actual. La baja es efectiva para **entregas futuras**, como dice la invariante. Si quieres otra política, el contrato debe decirlo.
31. b) La llamada a `actualizarInterfaz()`: la vista observa el estado y se repinta sola  
   El listener pasa a hacer un solo trabajo: `vm.onPulsar()`. El repintado lo hace el observador del `TextView`.
32. b) En Observer, el sujeto es quien guarda la lista de observadores; c) `Observable<T>` llama al observador con el valor actual en cuanto se suscribe; e) Un observador que modifica el sujeto dentro de `actualizar()` provoca reentrancia  
   En pub/sub el mensaje se pierde **en silencio**, y en MVVM el estado es del ViewModel, nunca de la vista.
33. `cola`  
   Una **cola** de eventos: el despacho deja de ser síncrono y aparecen el bucle de eventos, el orden y las prioridades.

## Cuando notificar sale mal

_La letra pequeña de publicar(): orden, altas y bajas en plena ronda, reentrancia y ciclos, antipatrones de suscripción (duplicados, bajas fallidas, callbacks tardíos, excepciones, punteros colgantes) y los errores típicos de Observer, MVP y MVVM._

### Un publicar() tiene letra pequeña · Teoría

En las clases anteriores de esta unidad construimos callbacks, listeners, un sujeto observable y un bus de mensajes. Todo funcionaba por una razón que no dijimos en voz alta: **los suscriptores se portaban bien**. Recibían el aviso, hacían su trabajo y devolvían el control.

Hoy toca lo contrario. Mientras el sujeto recorre su lista, **cada suscriptor puede cambiar el sistema que le está llamando**.

<div class="flow">
  <div><b>Sensor</b><small>publicar(31 °C)</small></div>
  <div><b>Dispatcher</b><small>recorre la lista</small></div>
  <div><b>Pantalla</b><small>turno 1</small></div>
  <div><b>Sirena</b><small>turno 2</small></div>
  <div><b>Historial</b><small>turno 3</small></div>
</div>

### Qué es una ronda

Una **ronda** empieza al entrar en `publicar()` y termina cuando `publicar()` retorna. Si un suscriptor publica durante su turno, empieza una **ronda anidada** antes de que acabe la primera.

Ahora imagina que la Pantalla, durante su turno…

- …da de baja a la Sirena → *orden y cambios en la lista* (bloque 1).
- …vuelve a llamar a `publicar()` → *reentrancia y ciclos* (bloque 2).
- …lanza una excepción o tarda 3 segundos → *antipatrones* (bloque 3).

### Las cuatro preguntas de la letra pequeña

<div class="cards">
  <div class="card"><h4>1 · Orden</h4><p>¿Se garantiza que se avisa en el orden de alta?</p></div>
  <div class="card"><h4>2 · Altas y bajas en la ronda</h4><p>¿Afectan a la ronda en curso o a la siguiente?</p></div>
  <div class="card"><h4>3 · Reentrancia</h4><p>¿Se puede publicar desde un suscriptor? ¿Se entrega ya o se encola?</p></div>
  <div class="card"><h4>4 · Fallos</h4><p>Si un suscriptor lanza, ¿se sigue avisando a los demás?</p></div>
</div>

> [!IMPORTANTE] La idea de la clase
> Dos implementaciones con **la misma lista y el mismo bucle** pueden responder distinto a estas cuatro preguntas. Esas respuestas **son la API**, aunque no aparezcan en la firma de `publicar()`.

> [!ANALOGIA] Pasar lista
> Pasar lista parece trivial hasta que un alumno se va a mitad, otro llega tarde o alguien interrumpe con una pregunta. Lo que ocurra entonces no lo decide la lista, sino las normas de la clase. Con `publicar()` pasa lo mismo: las normas son el contrato.

**Preguntas**

**1.** ¿Cuándo termina una **ronda** de notificación?

   a) Cuando el primer suscriptor devuelve el control
   b) Cuando el sujeto cambia de valor
   c) Cuando `publicar()` retorna
   d) Cuando se vacía la lista de suscriptores

**2.** Marca las cuatro preguntas que forman la «letra pequeña» de `publicar()`.

   a) ¿Se garantiza el orden de alta?
   b) ¿Qué tipo devuelve `publicar()`?
   c) ¿Las altas y bajas afectan a la ronda en curso o a la siguiente?
   d) ¿Se puede publicar desde un suscriptor?
   e) ¿Cómo se llama el topic?
   f) Si un suscriptor lanza, ¿se sigue avisando a los demás?

### Caso: el envío gratis que no aparece · Ejercicio

Tienda web. Dos módulos se suscriben al **mismo evento** del bus. `banner.js` se carga antes que `resumen.js`, así que su suscriptor se registra primero. La tienda regala el envío a partir de 50 €.

```js
// banner.js (se carga primero)
bus.suscribir("carrito/anadido", (item) => {
  if (resumen.total >= 50) banner.mostrar("¡Envío gratis!");
});

// resumen.js (se carga después)
bus.suscribir("carrito/anadido", (item) => {
  resumen.total += item.precio;
});

// El carrito lleva 45 €; se añaden unas zapatillas de 10 €
bus.publicar("carrito/anadido", { nombre: "Zapatillas", precio: 10 });
```

Antes de seguir, **predice**: recorre la ronda suscriptor por suscriptor y apunta cuánto vale `resumen.total` en cada momento.

> [!TRUCO] Cómo predecir una ronda
> Escribe en una columna los suscriptores en orden de alta. Para cada uno, anota qué **lee** y qué **escribe**. El fallo suele estar donde alguien lee algo que otro todavía no ha escrito.

**Preguntas**

**3.** ¿Qué ve el usuario después de añadir las zapatillas?

   a) El banner «¡Envío gratis!»
   b) Nada: el total ya es 55 € y no hay banner
   c) El banner aparece dos veces
   d) Un error porque `resumen` todavía no existe

**4.** Si alguien reordena los `<script>` y `resumen.js` pasa a cargarse primero, ¿qué ocurre?

   a) Sigue sin aparecer el banner
   b) El bus lanza una excepción por suscriptores duplicados
   c) Aparece el banner: el fallo desaparece… hasta que otro cambio de orden lo traiga de vuelta
   d) El banner aparece antes de añadir las zapatillas

### Estado derivado en el sujeto · Teoría

El banner no falla por un error de sintaxis, sino por una **dependencia oculta**: necesita un resultado que calcula *otro suscriptor del mismo evento*.

<div class="cards">
  <div class="card"><h4>Lee un estado que aún no existe</h4><p>El banner consulta <code>resumen.total</code> antes de que se actualice.</p></div>
  <div class="card"><h4>El orden lo decide el &lt;script&gt;</h4><p>Si se reordenan los imports, el fallo desaparece… o aparece en otro sitio.</p></div>
  <div class="card"><h4>Nadie lo ha prometido</h4><p>Si la API no garantiza el orden (pregunta 1), depender de él es apostar.</p></div>
</div>

### Solución: el sujeto calcula y luego avisa

```js
// carrito.js
class Carrito {
  #total = 0;
  anadir(item) {
    this.#total += item.precio;            // primero el estado
    bus.publicar("carrito/cambiado",
      { item, total: this.#total });       // después el aviso
  }
}

bus.suscribir("carrito/cambiado", ({ total }) => {
  if (total >= 50) banner.mostrar("¡Envío gratis!");
});
```

El total pasa a ser **estado derivado del sujeto**: el carrito lo calcula antes de avisar y el evento ya lleva el valor correcto. Nadie depende de nadie, y el orden de carga deja de importar.

> [!IMPORTANTE] Regla
> Los suscriptores de un mismo evento deben ser **independientes entre sí**. Si B necesita el resultado de A, entonces A es parte del sujeto, o A publica su propio evento (por ejemplo `carrito/total-cambiado`) y B se suscribe a ese.

> [!NOTA] Invariante «datos suficientes»
> El evento transporta lo necesario para procesarlo **sin consultar al emisor** ni a otros módulos. Si un suscriptor tiene que ir a mirar el estado de otro, ya depende de cuándo lo mire.

> [!EXAMEN]
> «Primero el estado, después el aviso». Un sujeto que avisa antes de terminar de actualizarse, o que deja que los suscriptores calculen lo que él debería saber, genera fallos que dependen del orden.

**Preguntas**

**5.** ¿Cuál es el arreglo que propone el material para el envío gratis?

   a) Garantizar con un comentario que `banner.js` se cargue siempre después de `resumen.js`
   b) Que el banner espere 100 ms con `setTimeout` antes de leer el total
   c) Que el banner se suscriba dos veces al evento
   d) Que el carrito actualice su total y publique `carrito/cambiado` con el total ya calculado

**6.** En la solución, ¿qué campo del evento `carrito/cambiado` lee el banner para decidir? (una palabra)

### Caso: la valoración que nunca se pide · Ejercicio

App de reparto. Un `Pedido` avisa de sus cambios de estado a tres suscriptores: el **mapa** (solo le interesa el pedido hasta que se entrega, así que se da de baja él solo), la **petición de valoración** y el **historial**.

```js
class Pedido {
  #oyentes = [];
  suscribir(f) {
    this.#oyentes.push(f);
    return () => this.#oyentes.splice(this.#oyentes.indexOf(f), 1);
  }
  cambiarEstado(estado) {
    this.#oyentes.forEach((f) => f(estado)); // lista VIVA
  }
}

const pedido = new Pedido();

const bajaMapa = pedido.suscribir((e) => {
  console.log(`Mapa: ${e}`);
  if (e === "entregado") bajaMapa();       // un solo uso
});

pedido.suscribir((e) => {
  if (e === "entregado") console.log("¿Qué tal tu pedido?");
});

pedido.suscribir((e) => console.log(`Historial: ${e}`));

pedido.cambiarEstado("entregado");
```

Dos detalles: `suscribir` devuelve la función de baja (como vimos en la clase 2), y `cambiarEstado` recorre **la propia lista** `#oyentes`, no una copia. `forEach` avanza por índices: 0, 1, 2…

Dibuja el array antes y después del `splice` y predice la salida.

**Preguntas**

**7.** ¿Qué líneas aparecen en la consola?

   a) `Mapa: entregado`
   b) `¿Qué tal tu pedido?`
   c) `Historial: entregado`
   d) Un `TypeError` por modificar el array durante `forEach`

**8.** Justo después de la baja del Mapa, ¿en qué posición queda Valoración y por qué índice sigue el bucle?

   a) Valoración en la posición 1; el bucle va a la 1
   b) Valoración en la posición 2; el bucle vuelve a la 0
   c) Valoración en la posición 0; el bucle pasa a la 1
   d) Valoración desaparece del array junto con el Mapa

### Recorrer una foto de la lista · Teoría

Lo que ha pasado, índice a índice:

| Momento | Posición 0 | Posición 1 | Posición 2 | Bucle |
| --- | --- | --- | --- | --- |
| Antes de la baja | Mapa | Valoración | Historial | i = 0 → Mapa |
| Tras la baja | Valoración | Historial | — | i = 1 → Historial |

Valoración pasa a la posición 0 cuando el bucle ya va por la 1. Se la salta **sin avisar**: no hay excepción, no hay mensaje, simplemente un suscriptor que no se entera. Es el peor tipo de fallo, porque solo lo detecta quien echa de menos la línea.

### Solución: recorrer una foto

```js
cambiarEstado(estado) {
  [...this.#oyentes].forEach((f) => f(estado)); // copia
} // Mapa · ¿Qué tal tu pedido? · Historial
```

La ronda recorre **una copia** de la lista tal y como estaba al empezar. Las altas y bajas modifican la lista real, no la foto, así que el bucle no se descoloca.

![A la izquierda, la lista viva: en el turno i = 0 el Mapa hace splice(0, 1), Valoración baja a la posición 0 e Historial a la 1; el bucle pasa a i = 1, llama a Historial y Valoración se queda sin turno. A la derecha, la foto: la ronda recorre la copia con Mapa, Valoración e Historial en las posiciones 0, 1 y 2 y avisa a los tres, mientras la baja solo cambia la lista real](/img/pde/tema-2/2b-lista-viva-foto.svg "Lista viva frente a foto: con splice los índices se desplazan y el bucle se salta a Valoración; la copia no se mueve")

| Lenguaje | Cómo se hace la foto |
| --- | --- |
| JavaScript | `[...listeners]` |
| Kotlin | `oyentes.toList()` |
| C++ | `auto foto = handlers_;` |

> [!CUIDADO] En Kotlin el fallo no es silencioso
> `for (f in oyentes)` con una baja dentro lanza `ConcurrentModificationException`. **Un fallo que se ve siempre es mejor que un suscriptor que desaparece en silencio.**

> [!NOTA] Invariante «registro íntegro»
> Un handler no debe corromper la lista al registrar o retirar a otro. La foto es la forma más sencilla de garantizarlo.

**Preguntas**

**9.** ¿Qué excepción lanza Kotlin si se da de baja un suscriptor dentro de `for (f in oyentes)`?

**10.** ¿Por qué el material considera preferible el comportamiento de Kotlin al de JavaScript en este caso?

   a) Porque un fallo que se ve siempre es mejor que un suscriptor que desaparece en silencio
   b) Porque Kotlin es más rápido recorriendo listas
   c) Porque en Kotlin la baja no se ejecuta nunca
   d) Porque Kotlin hace la foto automáticamente

### Caso: «Ha llegado a su destino. Gire a la derecha» · Ejercicio

App de navegación en Kotlin. Aprendida la lección anterior, el `Gps` **ya recorre una foto** de su lista (`subs.toList()`), así que nadie rompe el bucle. Hay dos suscriptores, dados de alta en este orden: `controlRuta` y `voz`.

```kotlin
// suscritos en este orden: controlRuta, voz
val controlRuta = gps.suscribir { pos ->
    if (pos == destino) {
        println("Ha llegado a su destino")
        gps.cancelar(voz)                    // se apaga la voz…
    }
}

val voz = gps.suscribir { pos ->
    println(ruta.instruccionPara(pos))       // …pero estaba en la foto
}
```

*(Fragmento tal y como se presenta en clase: lo que importa es el orden de alta y que `controlRuta` cancela a `voz` durante su turno.)*

El coche llega al destino y el `Gps` publica esa posición. `controlRuta` tiene el primer turno y cancela la voz. Para esa posición, la instrucción de ruta sería «En 200 m, gire a la derecha».

Piensa en qué contiene la foto **en el momento en que empezó la ronda**.

**Preguntas**

**11.** ¿Qué se imprime?

   a) Solo «Ha llegado a su destino»
   b) «En 200 m, gire a la derecha» y después «Ha llegado a su destino»
   c) Una `ConcurrentModificationException`
   d) «Ha llegado a su destino» y después «En 200 m, gire a la derecha»

**12.** ¿Por qué aquí no salta `ConcurrentModificationException` aunque se modifique la lista durante la ronda?

   a) Porque el bucle recorre una copia (`toList()`), no la lista que se modifica
   b) Porque `cancelar` no modifica la lista
   c) Porque esa excepción solo existe en Java, no en Kotlin
   d) Porque `voz` todavía no estaba suscrita

### Foto pura o foto + comprobar si sigue activa · Teoría

La foto arregla el bucle, pero un suscriptor dado de baja **durante** la ronda aún recibe el evento en curso. Si eso no es lo que queremos, se añade una comprobación: cada suscripción lleva una bandera `activa`.

```kotlin
class Suscripcion(val f: (Posicion) -> Unit) { var activa = true }

class Gps {
    private val subs = mutableListOf<Suscripcion>()

    fun suscribir(f: (Posicion) -> Unit) =
        Suscripcion(f).also { subs += it }

    fun cancelar(s: Suscripcion) { s.activa = false; subs -= s }

    fun publicar(p: Posicion) {
        for (s in subs.toList())   // foto: nadie rompe el bucle
            if (s.activa) s.f(p)   // una baja cuenta desde ya
    }
}
```

La foto sigue conteniendo a `voz`, pero su suscripción ya está marcada como inactiva y el bucle la salta. Salida: solo «Ha llegado a su destino».

### Dos políticas válidas

|  | Foto pura | Foto + comprobar activa |
| --- | --- | --- |
| Baja durante la ronda | Recibe el evento en curso | No lo recibe |
| Alta durante la ronda | Espera a la siguiente | Espera a la siguiente |
| Quién lo hace así | Node `EventEmitter`, nuestro Bus | DOM `addEventListener` / `removeEventListener` |

> [!IMPORTANTE] Regla
> Ninguna política es «la correcta»; **lo incorrecto es no saber cuál ofrece vuestra API**. En ambas, un alta durante la ronda espera a la siguiente.

> [!NOTA] Invariante «baja efectiva»
> Una suscripción retirada no recibe entregas *futuras* **según el contrato**. La foto pura cumple el invariante si su contrato dice que la ronda en curso no cuenta como futura; la del DOM, si dice que sí.

> [!EXAMEN]
> «¿Quién recibe un evento si un handler se da de baja durante la notificación?» La respuesta correcta empieza por «depende de la política»: explica las dos y di cuál usa la plataforma del enunciado.

**Preguntas**

**13.** En el DOM, un listener que se quita con `removeEventListener` durante la ronda de un evento…

   a) …recibe igualmente el evento en curso
   b) …no recibe el evento en curso
   c) …provoca que se cancele la ronda entera
   d) …lanza una excepción en el siguiente listener

**14.** Durante una ronda, un suscriptor da de alta a otro nuevo. Con cualquiera de las dos políticas, el nuevo…

   a) …recibe el evento en curso al final de la ronda
   b) …recibe el evento en curso inmediatamente
   c) …espera a la siguiente ronda
   d) …no recibe nunca nada hasta que se reinicie el sujeto

**15.** ¿Cómo se llama la bandera de `Suscripcion` que comprueba el bucle antes de llamar?

### Caso: el carrito que muestra 12 cuando hay 10 · Ejercicio

Bloque 2: **reentrancia**. Usamos el `Observable` de la clase 3, sin aviso inicial al suscribirse. Ya recorre una foto. Hay 10 unidades en stock; un observador **limita** la cantidad al stock y otro la **pinta**. El usuario escribe 12.

```kotlin
class Observable<T>(inicial: T) {
    private val observadores = mutableListOf<(T) -> Unit>()
    var valor: T = inicial
        set(nuevo) {
            if (nuevo == field) return
            field = nuevo
            for (o in observadores.toList()) o(nuevo)
        }
    fun observar(o: (T) -> Unit) { observadores += o }
}

fun main() {
    val stock = 10
    val cantidad = Observable(1)
    cantidad.observar { if (it > stock) cantidad.valor = stock } // limitar
    cantidad.observar { println("  [Carrito] cantidad: $it") }   // pintar
    cantidad.valor = 12                                          // el usuario escribe 12
    println("  valor real: ${cantidad.valor}")
}
```

Pista de lectura: `cantidad.valor = stock` no es una simple asignación. Es una llamada al `set`, que tiene **su propio `for`**. ¿Termina ese `for` antes o después de que el primero llegue a `pintar`? ¿Y qué valor lleva cada `for` en su variable `nuevo`?

**Preguntas**

**16.** Ordena las líneas de la salida del programa.

   Ordena: [Carrito] cantidad: 10 · [Carrito] cantidad: 12 · valor real: 10

**17.** ¿Qué invariante se rompe?

   a) Que lo último que pinta la vista coincida con el valor real del modelo
   b) Que el stock nunca baje de cero
   c) Que cada observador se suscriba una sola vez
   d) Que el valor inicial sea 1

### Rondas anidadas y dos arreglos · Teoría

Un observador que cambia el sujeto **en su turno** abre una ronda anidada. La traza:

```text
ronda 1 · valor = 12
  ① limitar(12) → valor = 10
      ronda 2 · valor = 10
        ① limitar(10) · nada
        ② pintar(10) → «cantidad: 10»
  ② pintar(12) → «cantidad: 12»
  ↑ lo último que se pinta es el valor viejo
```

La ronda 1 no se entera de que el valor ha cambiado: sigue repartiendo el 12 que tenía en `nuevo`. Todo ocurre en **un solo hilo**: la reentrancia es una llamada dentro de otra.

### A · La regla vive en el sujeto (preferida)

```kotlin
class CarritoViewModel(private val stock: Int) {
    val cantidad = Observable(1)
    fun onCambiarCantidad(n: Int) {
        cantidad.valor = n.coerceIn(1, stock)
    }
}
// Los observadores solo pintan
// SALIDA A:  [Carrito] cantidad: 10
```

Que el observador no tenga que corregir al sujeto: la validación forma parte del sujeto (`coerceIn` recorta al intervalo 1..stock) y el valor inválido nunca llega a publicarse.

### B · Entrega diferida: las rondas no se anidan

```kotlin
class Observable<T>(inicial: T) {
    private val observadores = mutableListOf<(T) -> Unit>()
    private val pendientes = ArrayDeque<T>()       // cola de avisos
    private var notificando = false
    var valor: T = inicial
        set(nuevo) {
            if (nuevo == field) return
            field = nuevo
            pendientes.addLast(nuevo)
            if (notificando) return                  // reentrada: encola y vuelve
            notificando = true
            try {
                while (pendientes.isNotEmpty()) {
                    val v = pendientes.removeFirst()
                    for (o in observadores.toList()) o(v)
                }
            } finally { notificando = false }
        }
    fun observar(o: (T) -> Unit) { observadores += o }
}
// SALIDA B:  [Carrito] cantidad: 12
//            [Carrito] cantidad: 10   ← valor real
//            valor real: 10
```

Si no se puede evitar que un observador publique, que **la ronda anidada espere**: cada ronda termina antes de que empiece la siguiente. En la Unidad 3 esto tiene nombre: **run-to-completion**. Con B se ve un instante el 12; por eso A va primero.

![A la izquierda, sin cola: dentro de la ronda 1 (nuevo = 12), limitar pone el valor a 10 y abre la ronda 2, que se completa entera y pinta 10; al volver, la ronda 1 pinta 12, el valor viejo, y la pantalla se queda en 12 con el valor real en 10. A la derecha, con cola: limitar deja el 10 en pendientes, la ronda 1 termina pintando 12 un instante y después la ronda 2 pinta 10, que coincide con el valor real](/img/pde/tema-2/2b-rondas-anidadas-cola.svg "El carrito sin cola (rondas anidadas) y con la cola del arreglo B (una ronda tras otra)")

**Preguntas**

**18.** ¿Por qué el arreglo A (regla en el sujeto) se prefiere al B (cola de avisos)?

   a) Porque B no compila en Kotlin
   b) Porque A usa menos memoria al no tener observadores
   c) Porque B anida todavía más rondas
   d) Porque con A el valor inválido nunca se publica; con B la vista llega a mostrar el 12 un instante

**19.** ¿Cómo se llama (en inglés) la propiedad de que cada ronda termine antes de que empiece la siguiente?

**20.** En el arreglo B, cuando `limitar` asigna 10 durante la ronda, ¿qué hace el `set`?

   a) Abre una ronda anidada como antes
   b) Descarta el 10 porque ya se está notificando
   c) Guarda el 10 en `pendientes` y retorna, porque `notificando` es `true`
   d) Lanza una excepción por reentrada

### Caso: el tema oscuro que rebota · Ejercicio

App de móvil con smartwatch. Cuando el usuario cambia el tema, cada módulo lo aplica y, para que «el otro se entere», **avisa de que ya lo tiene** por el mismo topic.

```js
// Módulo Reloj
bus.suscribir("ajustes/tema", (tema) => {
  reloj.aplicarTema(tema);
  bus.publicar("ajustes/tema", tema); // reenvía por el mismo topic
});

// Módulo Móvil
bus.suscribir("ajustes/tema", (tema) => {
  movil.aplicarTema(tema);
  bus.publicar("ajustes/tema", tema);
});

bus.publicar("ajustes/tema", "oscuro");
```

El bus entrega de forma síncrona: `publicar` llama a los suscriptores dentro de la propia llamada, como todos los que hemos construido en la unidad.

Antes de seguir, sigue la pila de llamadas a mano durante tres o cuatro niveles: `publicar` → suscriptor del reloj → `publicar` → … ¿Hay algún punto en el que alguien decida **no** volver a publicar? ¿Comprueba alguien si el tema ya era «oscuro»?

**Preguntas**

**21.** ¿Qué ocurre al ejecutarlo?

   a) Ambos aplican el tema oscuro una vez y termina
   b) Cada uno aplica el tema dos veces y termina
   c) `RangeError: Maximum call stack size exceeded`
   d) El bus ignora la segunda publicación porque el tema no ha cambiado

**22.** Si el bus usara la cola del arreglo B (entrega diferida, sin rondas anidadas), ¿se arreglaría?

   a) Sí: la cola evita las rondas anidadas y con ello el problema
   b) Sí, pero solo si hay un único suscriptor
   c) No, porque la cola descarta los mensajes repetidos
   d) No: cambia el desbordamiento de pila por un bucle infinito de mensajes

### Separar órdenes y hechos · Teoría

Dos módulos que reenvían lo que reciben forman un **ciclo de notificación**. El problema no es la entrega síncrona (por eso la cola no lo arregla), sino que el mismo topic se usa para dos cosas distintas: *pedir* un cambio y *anunciar* que ya ha ocurrido.

![Arriba, Móvil y Reloj publican y reciben por el mismo topic ajustes/tema: cada uno aplica el tema y lo reenvía, así que cada entrega vuelve a publicar y la pila crece hasta el RangeError. Abajo, la orden ajustes/cambiar-tema llega solo al dueño, ajustes, que no publica si no hay cambio; si lo hay, publica el hecho ajustes/tema-cambiado, y Reloj y Móvil solo lo aplican sin reenviarlo](/img/pde/tema-2/2b-ordenes-hechos.svg "Ciclo de notificación con un solo topic frente a órdenes y hechos separados")

<div class="cards">
  <div class="card"><h4>Orden</h4><p><code>ajustes/cambiar-tema</code>: «quiero que el tema sea oscuro». La escucha solo el dueño del dato.</p></div>
  <div class="card"><h4>Hecho</h4><p><code>ajustes/tema-cambiado</code>: «el tema ya es oscuro». Lo publica solo el dueño; los demás reaccionan y nunca lo reenvían.</p></div>
</div>

```js
// Solo el dueño escucha ÓRDENES y publica HECHOS
bus.suscribir("ajustes/cambiar-tema", (tema) => {
  if (tema === ajustes.tema) return;      // sin cambio, sin aviso
  ajustes.tema = tema;
  bus.publicar("ajustes/tema-cambiado", tema);
});

// Los demás reaccionan a hechos y nunca los reenvían
bus.suscribir("ajustes/tema-cambiado", (t) => reloj.aplicarTema(t));
bus.suscribir("ajustes/tema-cambiado", (t) => movil.aplicarTema(t));

bus.publicar("ajustes/cambiar-tema", "oscuro");
```

Tres medidas, cada una suficiente para cortar el ciclo y juntas mucho más robustas:

1. **Un único dueño** del dato `tema`: es el único que lo modifica.
2. **Órdenes y hechos en topics distintos**: quien reacciona a un hecho no publica otro hecho igual.
3. **Sin cambio, sin aviso**: si el valor no cambia, no se publica nada. (El `Observable` de la clase 3 ya lo hacía con `if (nuevo == field) return`.)

> [!ANALOGIA] El eco del grupo de WhatsApp
> Si cada vez que alguien dice «quedamos a las 8» todos responden reenviando «quedamos a las 8», el grupo no para nunca. Lo sensato es que uno *proponga*, el organizador *confirme* una vez, y los demás se limiten a leer la confirmación.

> [!EXAMEN]
> Síntoma: desbordamiento de pila o tráfico infinito. Antipatrón: ciclo de notificación. Remedio: órdenes frente a hechos; sin cambio, sin aviso.

**Preguntas**

**23.** ¿Qué medidas de la solución ayudan a cortar el ciclo?

   a) Usar un topic para órdenes y otro para hechos
   b) Pasar el bus a entrega diferida con una cola
   c) No publicar si el valor no ha cambiado
   d) Que solo el dueño del dato publique el hecho
   e) Que cada módulo reenvíe el hecho a los demás para asegurarse

**24.** ¿A qué topic se suscriben el reloj y el móvil en la solución?

### Caso: pedido cobrado tres veces · Ejercicio

Bloque 3: **antipatrones de suscripción y despacho**. Empezamos por un checkout web con un modal de pago.

```js
function abrirModalPago() {
  modal.hidden = false;
  // alta en CADA apertura
  botonConfirmar.addEventListener("click", () => {
    enviarPedido(carrito);
    modal.hidden = true;
  });
}

// cerrar no da de baja
function cancelarModal() { modal.hidden = true; }
```

El usuario duda: **abre** el modal, **cancela**, lo vuelve a **abrir**, **cancela** otra vez, lo **abre** por tercera vez y por fin pulsa **Confirmar**. Un solo clic. Nada en la interfaz delata el problema: el modal se abre y se cierra con normalidad cada vez.

> [!TRUCO]
> Recuerda de la clase 2 que `addEventListener` ignora un alta duplicada **solo si es exactamente la misma función**. Pregúntate cuántas funciones distintas hay registradas en el botón cuando llega el clic.

**Preguntas**

**25.** ¿Cuántas veces se ejecuta `enviarPedido()` con ese único clic en Confirmar? (un número)

**26.** ¿Por qué `addEventListener` no descarta los listeners repetidos?

   a) Porque solo ignora duplicados de la misma función, y cada apertura crea una lambda nueva
   b) Porque el modal está oculto cuando se registran
   c) Porque `addEventListener` nunca ignora duplicados
   d) Porque `modal.hidden = true` reinicia los listeners

### Alta única y misma referencia · Teoría

En el pedido cobrado tres veces hay dos causas: **una lambda nueva cada vez** y **sin baja simétrica** (abrir y cancelar no son un par alta/baja). Remedio: dar de alta donde se nace, no donde se repite.

```js
// Se registra UNA vez, al cargar la página, con una función con nombre
botonConfirmar.addEventListener("click", confirmar);

function confirmar() {
  enviarPedido(carrito);
  modal.hidden = true;
}

function abrirModalPago() { modal.hidden = false; }
```

La otra opción es mantener el alta en la apertura pero hacer la **baja en `cancelarModal()`**, por ejemplo registrando el listener con `{ signal }` de un `AbortController` y llamando a `abort()` al cerrar.

> [!NOTA] En Android
> El mismo fallo es un `flow.collect { }` dentro de `onResume()` sin cancelarlo: cada vuelta a primer plano añade un colector más.

### El feed que sigue cargando tras salir

Ahora el error inverso: hay baja, pero **no da de baja nada**.

```js
class FeedInfinito {
  montar() {
    window.addEventListener("scroll", this.cargarMas.bind(this));
  }
  desmontar() {
    window.removeEventListener("scroll", this.cargarMas.bind(this));
  }
  cargarMas() { if (cercaDelFinal()) this.pedirPagina(); }
}
```

`bind` crea una función nueva **en cada llamada**. La baja busca otra función, no la encuentra y calla. El usuario sale del feed a su perfil y cada scroll sigue pidiendo páginas; el `FeedInfinito` no se libera nunca.

```js
class FeedInfinito {
  cargarMas = () => {                 // campo: una sola función, fija this
    if (cercaDelFinal()) this.pedirPagina();
  };
  montar()     { window.addEventListener("scroll", this.cargarMas); }
  desmontar()  { window.removeEventListener("scroll", this.cargarMas); }
}
```

> [!IMPORTANTE] Regla
> La **misma referencia** en el alta y en la baja. En Kotlin, `sensor.removeListener { pintar(it) }` tiene el mismo fallo: esa lambda acaba de nacer y nunca estuvo en la lista.

**Preguntas**

**27.** ¿Por qué `removeEventListener("scroll", this.cargarMas.bind(this))` no quita el listener?

   a) Porque `removeEventListener` solo funciona con `window.onscroll`
   b) Porque `this` vale otra cosa dentro de `desmontar()`
   c) Porque el scroll se sigue produciendo mientras se desmonta
   d) Porque `bind` devuelve una función nueva, distinta de la que se registró

**28.** ¿Qué remedios evitan el cobro triple?

   a) Registrar el listener una sola vez al cargar la página
   b) Mantener el alta en la apertura y darlo de baja al cancelar con un `AbortController`
   c) Desactivar el botón durante 300 ms tras cada clic
   d) Registrar la misma lambda flecha escrita dentro de `abrirModalPago()`

**29.** ¿Cuál es el equivalente Android de este antipatrón que cita el material?

   a) Un `flow.collect { }` dentro de `onResume()` sin cancelarlo
   b) Llamar a `setContentView` dos veces
   c) Usar `ViewModel` en lugar de `Presenter`
   d) Registrar el `OnClickListener` en `onCreate`

### Caso: el buscador con resultados de otra búsqueda · Ejercicio

Autocompletado de una tienda. Cada vez que cambia el texto, se espera 300 ms y se piden sugerencias al servidor.

```js
input.addEventListener("input", () => {
  setTimeout(async () => {                         // espera 300 ms, nadie la cancela
    const q = encodeURIComponent(input.value);
    const res = await fetch(`/buscar?q=${q}`);
    pintarSugerencias(await res.json());
  }, 300);
});
```

El usuario escribe «mango» letra a letra: `m`, `ma`, `man`, `mang`, `mango`. Cada tecla acaba lanzando su propia petición. La de «man» resulta ser **lenta**: su respuesta llega *después* que la de «mango».

```text
teclas      m       ma      man       mang      mango
peticiones  fetch   fetch   fetch     fetch     fetch
llegadas    …  «mango» ✓  …  «man» (lenta, la última en llegar)
```

Piensa en dos cosas: cuántos temporizadores se programan, y qué respuesta es la última en llamar a `pintarSugerencias`.

**Preguntas**

**30.** ¿Qué sugerencias ve el usuario al final, con «mango» escrito?

   a) Las de «mango», porque es la última búsqueda que se pidió
   b) Ninguna: las dos respuestas se anulan
   c) Las de «man» («mandarina, manzana…»), porque es la última respuesta en llegar
   d) Las de «m», porque fue la primera

**31.** Si el usuario escribe las cinco teclas, ¿cuántos temporizadores (y por tanto peticiones) se programan como máximo con este código?

**32.** ¿Qué callbacks pendientes habría que cancelar cuando el usuario pulsa una tecla nueva?

   a) El temporizador de la tecla anterior, si aún no ha saltado
   b) La petición `fetch` anterior que sigue en vuelo
   c) El propio listener de `input`
   d) El callback de la tecla actual

### Cancelar lo pendiente: debounce y abort · Teoría

Un callback diferido que nadie cancela llega cuando ya no tiene sentido. Cada callback diferido tiene un dueño que debe poder **anularlo antes de que se ejecute**.

```js
let temporizador;
let control;

input.addEventListener("input", () => {
  clearTimeout(temporizador);                  // 1 anula el callback pendiente
  temporizador = setTimeout(async () => {
    control?.abort();                          // 2 anula la petición anterior
    control = new AbortController();
    try {
      const q = encodeURIComponent(input.value);
      const res = await fetch(`/buscar?q=${q}`, { signal: control.signal });
      pintarSugerencias(await res.json());
    } catch (e) {
      if (e.name !== "AbortError") throw e;    // 3 cancelar no es un error
    }
  }, 300);
});
```

<div class="cards">
  <div class="card"><h4>1 · Debounce</h4><p>Solo sobrevive el temporizador de la última tecla.</p></div>
  <div class="card"><h4>2 · Una petición en vuelo</h4><p>La anterior se aborta y su callback no llega a ejecutarse.</p></div>
  <div class="card"><h4>3 · AbortError se filtra</h4><p>Cancelar no es un fallo; el resto de errores se propaga.</p></div>
</div>

Con el arreglo, si el usuario escribe «mango» sin pausas largas, los temporizadores de `m`, `ma`, `man` y `mang` se anulan: 300 ms después de la última tecla sale **una** petición y se pintan los resultados de «mango».

> [!ANALOGIA] El ascensor
> El ascensor no cierra la puerta 3 segundos después de que entre el *primero*, sino 3 segundos después de que entre el *último*: cada persona nueva reinicia la cuenta. Eso es un debounce.

> [!IMPORTANTE] Regla
> **Un callback diferido es una suscripción de un solo uso: también tiene baja.** `clearTimeout`, `abort()` o `cancel()` son esa baja.

> [!EXAMEN]
> Pregunta típica: «¿qué dos callbacks pendientes hay que cancelar, y en qué momento?». El temporizador anterior, al llegar una tecla nueva; la petición anterior, al lanzar la nueva.

**Preguntas**

**33.** Ordena lo que ocurre en el código corregido desde que llega una tecla hasta que se pinta.

   Ordena: Al saltar, se aborta la petición anterior con `control?.abort()` · Se crea un `AbortController` nuevo · Se lanza `fetch` con `{ signal: control.signal }` · Se llama a `pintarSugerencias` con la respuesta · Se programa un nuevo `setTimeout` de 300 ms · `clearTimeout` anula el temporizador pendiente

**34.** ¿Por qué el `catch` filtra `AbortError` y relanza el resto?

   a) Porque `AbortError` indica que el servidor ha caído
   b) Porque `fetch` nunca lanza otros errores
   c) Para que el temporizador se reprograme solo
   d) Porque cancelar una petición a propósito no es un fallo, pero un error de red real sí debe propagarse

### Caso: la sirena que no suena · Ejercicio

Centralita domótica en C++17 sobre una Raspberry Pi. Ya recorre una foto. Tres suscriptores ante un evento de humo: una notificación push, la sirena y la apertura de puertas.

```cpp
enum class Tipo { Humo, Puerta };
struct Evento { Tipo tipo; std::string zona; };
using Handler = std::function<void(const Evento&)>;

class Centralita {
    std::vector<Handler> handlers_;
public:
    void suscribir(Handler h) { handlers_.push_back(std::move(h)); }
    void publicar(const Evento& ev) {
        auto foto = handlers_;
        for (auto& h : foto) h(ev);
    }
};

centralita.suscribir([](const Evento& ev) { enviarPush(ev); }); // sin red: lanza
centralita.suscribir([](const Evento&) { activarSirena(); });
centralita.suscribir([](const Evento&) { abrirPuertas(); });

centralita.publicar({Tipo::Humo, "cocina"});
```

Se ha ido el wifi. Hay humo en la cocina y el detector llama a `publicar`. `enviarPush` no puede conectar y lanza una excepción.

Sigue la excepción: ¿quién la captura?

**Preguntas**

**35.** ¿Qué pasa en la casa?

   a) No llega el push, pero suenan la sirena y se abren las puertas
   b) Ni sirena ni puertas: la excepción corta la ronda y sube hasta el código del detector
   c) Suena la sirena, pero las puertas no se abren
   d) La centralita reintenta el push hasta que vuelve la red y luego sigue

**36.** Si fueran tres listeners del DOM registrados con `addEventListener` y el primero lanzara, ¿qué haría el navegador?

   a) Informa de la excepción y sigue con el siguiente listener
   b) Detiene la ronda igual que el bucle de C++
   c) Elimina automáticamente el listener que ha fallado
   d) Repite el evento desde el principio

### Aislar a cada suscriptor · Teoría

Una excepción en un suscriptor interrumpe la ronda y **los que van detrás nunca se enteran**. Con humo en la cocina, que falle el push no debería impedir que suene la sirena.

```cpp
void publicar(const Evento& ev) {
    auto foto = handlers_;
    for (auto& h : foto) {
        try {
            h(ev);
        } catch (const std::exception& e) {
            registrarError(ev, e.what());       // se informa
        } catch (...) {
            registrarError(ev, "desconocida");
        }
    }
}
```

Cada suscriptor queda **aislado**: si lanza, se registra el error y el bucle sigue con el siguiente. Fíjate en que no se traga en silencio: `registrarError` deja constancia. Aislar también es decidir **quién se entera** del fallo.

![Arriba, sin aislar: el detector llama a publicar, el Push lanza porque no hay red y la excepción sube por publicar sin que nadie la capture; la Sirena y las Puertas no se enteran. Abajo, con try/catch por suscriptor: el error del Push queda registrado y la ronda sigue, así que la Sirena suena y las Puertas se abren](/img/pde/tema-2/2b-excepcion-aislar.svg "La sirena que no suena: excepción sin aislar frente a try/catch por suscriptor")

### Cada plataforma decide

| Plataforma | Si un listener lanza… |
| --- | --- |
| DOM (`addEventListener`) | Informa de la excepción y sigue con el siguiente |
| Node `EventEmitter` | Se detiene |
| Bucle propio en Kotlin o C++ | Se detiene (salvo que lo aisléis como arriba) |

> [!IMPORTANTE]
> Hay que saber cuál es el caso de vuestra plataforma. Es la pregunta 4 de la letra pequeña: «Si un suscriptor lanza, ¿se sigue avisando a los demás?».

### El primo de la excepción: el handler lento

El despacho es síncrono: si un suscriptor **tarda** 3 segundos (por ejemplo, hace una petición de red en su turno), retrasa a todos los que van detrás y, si está en el hilo de la interfaz, la congela. El remedio es **delegar y retornar**: el handler encarga el trabajo largo y devuelve el control enseguida. Lo estudiaremos a fondo en la Unidad 3.

> [!EXAMEN]
> Síntoma: «tras un fallo, el resto no reacciona». Antipatrón: excepción sin aislar. Remedio: `try/catch` por suscriptor, registrando el error.

**Preguntas**

**37.** ¿En qué casos se **detiene** la ronda si un suscriptor lanza (sin aislar)?

   a) Listeners del DOM con `addEventListener`
   b) Node `EventEmitter`
   c) Un `for` escrito a mano en Kotlin
   d) Un `for` escrito a mano en C++

**38.** ¿Dónde se coloca el `try/catch` para aislar a cada suscriptor?

   a) Alrededor de toda la llamada a `publicar` en el detector
   b) Dentro de `suscribir`
   c) En el constructor de `Centralita`
   d) Dentro del `for`, alrededor de cada `h(ev)`

### Caso: el termostato que se reinicia sin motivo · Ejercicio

Termostato con pantalla (ESP32, Arduino, C++). Al entrar en el menú de ajustes se crea una pantalla que se suscribe al sensor; al pulsar «Atrás» se destruye.

```cpp
class SensorTemp {
    std::vector<std::function<void(float)>> oyentes_;
public:
    void suscribir(std::function<void(float)> f) { oyentes_.push_back(std::move(f)); }
    void publicar(float t) { for (auto& f : oyentes_) f(t); }
};

class PantallaAjustes {
    float ultima_ = 0;
public:
    explicit PantallaAjustes(SensorTemp& s) {
        s.suscribir([this](float t) { ultima_ = t; pintar(); }); // captura this
    }
    void pintar();
};

void menuAjustes(SensorTemp& sensor) {
    auto* p = new PantallaAjustes(sensor);
    esperarBotonAtras();
    delete p;                                   // la lambda sigue en oyentes_
}

// 5 s después: sensor.publicar(22.5) → ¿?
```

En Kotlin o JavaScript ya conocemos este olvido (clase 2): el recolector de basura mantiene viva la pantalla, con su fuga de memoria y sus callbacks fantasma. Pero en C++ **no hay recolector**: `delete p` libera la memoria aunque alguien siga apuntando a ella.

**Preguntas**

**39.** ¿Qué ocurre cuando el sensor publica 22,5 °C cinco segundos después?

   a) Nada: la lambda detecta que la pantalla ya no existe
   b) El compilador lo impide: no deja capturar `this` en una lambda
   c) La pantalla se vuelve a crear automáticamente
   d) La lambda escribe en memoria liberada a través de un puntero colgante: basura en el display, un reinicio… o nada durante semanas

**40.** ¿Qué mecanismo de C++ se ejecuta **siempre** cuando un objeto deja de existir?

### RAII: la suscripción se da de baja sola · Teoría

Sin recolector de basura, el listener olvidado no provoca una fuga: provoca un **puntero colgante**. La solución en C++ es **RAII**: si la suscripción es un *miembro* del suscriptor, muere con él, y su destructor hace la baja.

```cpp
class SensorTemp {
    std::map<int, std::function<void(float)>> oyentes_;
    int siguiente_ = 0;
public:
    class Suscripcion {                                   // el objeto «baja»
        SensorTemp* sensor_; int id_;
    public:
        Suscripcion(SensorTemp* s, int id) : sensor_(s), id_(id) {}
        ~Suscripcion() { sensor_->oyentes_.erase(id_); }  // baja
        Suscripcion(const Suscripcion&) = delete;
        Suscripcion& operator=(const Suscripcion&) = delete;
    };

    std::unique_ptr<Suscripcion> suscribir(std::function<void(float)> f) {
        int id = siguiente_++;
        oyentes_[id] = std::move(f);
        return std::make_unique<Suscripcion>(this, id);
    }

    void publicar(float t) {
        auto foto = oyentes_;
        for (auto& [id, f] : foto) f(t);
    }
};

class PantallaAjustes {
    std::unique_ptr<SensorTemp::Suscripcion> sub_;   // miembro
public:
    explicit PantallaAjustes(SensorTemp& s)
        : sub_(s.suscribir([this](float t) { pintar(t); })) {}
    void pintar(float t);
}; // delete pantalla → ~sub_ → baja automática
```

<div class="cards">
  <div class="card"><h4>1 · La suscripción es un objeto</h4><p>Es la suscripción cancelable de la clase 2, ahora con forma de clase.</p></div>
  <div class="card"><h4>2 · Destructor = baja</h4><p>Nadie tiene que acordarse: la llama el compilador.</p></div>
  <div class="card"><h4>3 · Mismo tiempo de vida</h4><p>Miembro de la pantalla y no copiable: muere con ella.</p></div>
</div>

> [!NOTA] El mismo patrón en otras plataformas
> `DisposableEffect` en Compose, `AbortController` en el DOM o Qt al destruir un `QObject`. **La baja la hace el ciclo de vida, no la disciplina.**

| Sin baja… | Consecuencia |
| --- | --- |
| Kotlin / JavaScript | El recolector mantiene viva la pantalla: fuga de memoria y callbacks fantasma |
| C++ | La pantalla ya no existe: basura en el display, reinicio… o nada durante semanas |

**Preguntas**

**41.** ¿Por qué `Suscripcion` borra su constructor de copia y su asignación por copia?

   a) Para que no haya dos objetos cuyo destructor intente dar de baja la misma suscripción
   b) Para que ocupe menos memoria en el ESP32
   c) Porque `std::map` no admite objetos copiables
   d) Para que la lambda pueda capturar `this`

**42.** ¿Qué mecanismos cita el material como el mismo patrón (la baja la hace el ciclo de vida)?

   a) `DisposableEffect` en Compose
   b) `AbortController` en el DOM
   c) Qt al destruir un `QObject`
   d) `setTimeout` en JavaScript
   e) `println` en Kotlin

### Ficha de diagnóstico: del síntoma al antipatrón · Ejercicio

En producción nunca te dicen «hay una reentrancia en la línea 12». Te dicen «la pantalla pone 12 y hay 10». La **ficha de diagnóstico** es un mapa que va del **síntoma que se observa** al **antipatrón** que lo causa y a su **remedio**. La usaremos tal cual en la clase 5.

Intenta responder las preguntas sin abrir la ficha; después compruébala completa.

<details><summary>Ver la ficha completa (11 filas)</summary>
<table>
<thead><tr><th>#</th><th>Síntoma que se observa</th><th>Antipatrón</th><th>Remedio</th></tr></thead>
<tbody>
<tr><td>1</td><td>El resultado depende del orden de carga de los módulos</td><td>Suscriptores que dependen unos de otros</td><td>Estado derivado en el sujeto</td></tr>
<tr><td>2</td><td>Un suscriptor nunca se entera, sin ningún error</td><td>Baja durante el recorrido de la lista viva</td><td>Foto de la lista</td></tr>
<tr><td>3</td><td>Un suscriptor dado de baja reacciona una última vez</td><td>Foto sin comprobar si sigue activo</td><td>Bandera <code>activa</code></td></tr>
<tr><td>4</td><td>La vista muestra un valor distinto del modelo</td><td>Reentrancia: un observador corrige al sujeto</td><td>Regla en el sujeto / cola</td></tr>
<tr><td>5</td><td>Desbordamiento de pila o tráfico infinito</td><td>Ciclo de notificación</td><td>Órdenes vs hechos; sin cambio, sin aviso</td></tr>
<tr><td>6</td><td>La acción se ejecuta 2, 3… N veces</td><td>Alta en un método que se repite</td><td>Alta única o baja simétrica</td></tr>
<tr><td>7</td><td>Pantalla cerrada que reacciona; memoria que crece</td><td>Baja con otra referencia</td><td>Misma referencia / suscripción</td></tr>
<tr><td>8</td><td>Aparecen resultados de una búsqueda anterior</td><td>Callback diferido sin cancelar</td><td><code>clearTimeout</code>, <code>abort</code>, <code>cancel</code></td></tr>
<tr><td>9</td><td>Tras un fallo, el resto no reacciona</td><td>Excepción sin aislar</td><td><code>try/catch</code> por suscriptor</td></tr>
<tr><td>10</td><td>Reinicios aleatorios en C/C++</td><td>Puntero colgante en el callback</td><td>RAII: suscripción como miembro</td></tr>
<tr><td>11</td><td>La interfaz se congela mientras un handler trabaja</td><td>Handler lento en la ronda</td><td>Delegar y retornar (U3 / U6)</td></tr>
</tbody>
</table>
</details>

> [!EXAMEN]
> Un ejercicio de diagnóstico típico da el síntoma (o la salida de un programa) y pide tres cosas: el antipatrón, el remedio y la pregunta del contrato que nadie se hizo.

**Preguntas**

**43.** Síntoma: «cada vez que vuelvo a la pantalla de chat, los mensajes nuevos suenan una vez más: 1, 2, 3 pitidos…». ¿Antipatrón?

   a) Ciclo de notificación
   b) Callback diferido sin cancelar
   c) Alta en un método que se repite (sin baja simétrica)
   d) Puntero colgante en el callback

**44.** Síntoma: «el suscriptor de estadísticas a veces no registra nada, y no hay ningún error en el log». Justo antes, otro suscriptor se da de baja durante su turno. ¿Remedio?

   a) `try/catch` por suscriptor
   b) Recorrer una foto de la lista
   c) Separar órdenes y hechos
   d) Debounce con `clearTimeout`

**45.** Síntoma: «al recibir un mensaje, la interfaz se queda congelada un par de segundos». Un suscriptor sube una copia al servidor en su turno. ¿Remedio?

   a) Delegar el trabajo lento y retornar enseguida
   b) Recorrer una foto de la lista
   c) Usar RAII para la suscripción
   d) Publicar el mensaje dos veces

### GoF Observer, MVP y MVVM: quién conoce a quién · Teoría

Bloque 4. Los tres modelos son **la misma idea colocada en sitios distintos**. Para compararlos, sigue la flecha que llega a la vista: indica quién conoce a quién.

<div class="cards">
  <div class="card"><h4>GoF Observer</h4><p>El sujeto conoce una lista de «observadores», no sus tipos. Les llama con <code>update(t)</code>.</p></div>
  <div class="card"><h4>MVP</h4><p>El Presenter conoce la vista (a través de una interfaz) y le <b>ordena</b> qué pintar: <code>mostrar(n)</code>.</p></div>
  <div class="card"><h4>MVVM</h4><p>El ViewModel <b>no</b> conoce la vista: la vista se <b>suscribe</b> a su estado observable.</p></div>
</div>

![Tres filas. GoF Observer: el sujeto, que conoce una lista, llama a update(t) en N observadores de cualquier tipo. MVP: el Presenter guarda la vista y le llama con mostrar(n); la vista, con la interfaz ContadorView, le avisa con onPulsar(). MVVM: la vista avisa al ViewModel con onPulsar() y observa su estado con una suscripción; el ViewModel no conoce la vista y ninguna flecha llega a ella](/img/pde/tema-2/2b-quien-conoce-a-quien.svg "Quién conoce a quién: sigue la flecha que llega a la vista")

### El mismo contador en los dos

```kotlin
// MVP · el Presenter ordena pintar
interface ContadorView {                       // contrato de la vista
    fun mostrarPulsaciones(n: Int)
    fun habilitarBoton(activo: Boolean)
}

class ContadorPresenter(private var vista: ContadorView?) {
    private var pulsaciones = 0
    private var activo = true
    fun onPulsar() {
        if (!activo) return
        pulsaciones++
        vista?.mostrarPulsaciones(pulsaciones)  // ordena
    }
    fun onCambiarActivo(v: Boolean) {
        activo = v; vista?.habilitarBoton(v)
    }
}
// Activity : ContadorView
// boton.setOnClickListener { presenter.onPulsar() }
// override fun mostrarPulsaciones(n: Int) { texto.text = "…$n" }
```

```kotlin
// MVVM · la vista observa el estado (clase 3)
class ContadorViewModel {                      // no conoce ninguna vista
    val pulsaciones = Observable(0)
    val activo = Observable(true)
    fun onPulsar() {
        if (activo.valor) pulsaciones.valor++  // solo estado
    }
    fun onCambiarActivo(v: Boolean) { activo.valor = v }
}
// Activity
// boton.setOnClickListener { vm.onPulsar() }
// vm.pulsaciones.observar { texto.text = "Pulsaciones: $it" }
// vm.activo.observar { boton.isEnabled = it }
```

> [!IMPORTANTE] Fíjate
> El listener del botón es **idéntico** en los dos: solo avisa. Lo que cambia es la vuelta: el Presenter hace **una llamada** a la vista; el ViewModel cambia **un estado** que la vista observa. De esa diferencia salen los errores típicos de cada uno.

**Preguntas**

**46.** ¿En qué modelo el componente de lógica **no** tiene ninguna referencia a la vista?

   a) MVP
   b) MVVM
   c) En los dos
   d) En ninguno

**47.** ¿Qué parte del código es igual en la versión MVP y en la MVVM?

   a) El listener del botón: solo avisa a la lógica
   b) Cómo llega el nuevo valor a la pantalla
   c) La interfaz `ContadorView`
   d) El uso de `Observable`

### Comparativa y regla de MVVM · Teoría

Qué aporta cada modelo y dónde suele fallar:

|  | GoF Observer | MVP | MVVM |
| --- | --- | --- | --- |
| Quién conoce a quién | El sujeto, una lista de observadores | Presenter ↔ Vista (por interfaz) | La vista conoce el ViewModel; el ViewModel no conoce la vista |
| Cómo llega el cambio | `notify()` → `update()` en cada uno | Llamada directa `vista.mostrar…()` | La vista observa el estado |
| Número de vistas | N | Normalmente 1 | N |
| Cómo se prueba | Sujeto + observador falso | Presenter + vista falsa | ViewModel solo; se lee el estado |
| Rotación / ciclo de vida | No lo contempla | A mano: adjuntar / soltar | Lo resuelve el framework (ViewModel, LiveData, `repeatOnLifecycle`) |
| Error típico | Observador que modifica al sujeto (reentrancia) | Presenter que llama a una vista muerta | Evento puntual tratado como estado, o vista guardada en el ViewModel |
| Dónde lo veis | `addListener`, `EventTarget`, `Sensor` | Android clásico, WinForms | Android Jetpack, WPF, Vue, SwiftUI |

> [!IMPORTANTE] Regla
> En MVVM **la flecha nunca apunta del ViewModel a la vista**. Si un ViewModel guarda una `Activity`, una `View` o un `Context` de pantalla, se ha convertido en un MVP mal hecho, con fuga de memoria incluida.

> [!ANALOGIA] El tablón y el encargado
> En MVP, el Presenter es un encargado que llama por teléfono a cada empleado para decirle qué hacer: si el empleado se ha ido, la llamada se pierde (o peor). En MVVM, el ViewModel escribe en un tablón y cada empleado que llega lo lee: no necesita saber quién está.

> [!EXAMEN]
> Una pregunta clásica: «¿qué diferencia hay entre MVP y MVVM?». La respuesta corta: en MVP el Presenter **llama** a la vista; en MVVM la vista **se suscribe** al estado del ViewModel, que no la conoce. De ahí salen la prueba, el número de vistas y los errores típicos.

**Preguntas**

**48.** ¿Cómo se prueba la lógica en MVVM según la tabla?

   a) Con una vista falsa que registra las llamadas
   b) Arrancando la Activity en un emulador
   c) Con un observador falso y un sujeto real
   d) Con el ViewModel solo, leyendo su estado

**49.** ¿Cuáles de estas cosas **no** debería guardar nunca un ViewModel?

   a) Una referencia a la `Activity`
   b) Un `StateFlow` con el estado de la pantalla
   c) Una `View` concreta, como un `TextView`
   d) Un `Context` de pantalla
   e) Un repositorio de datos

### Caso MVP: el perfil que llega a una pantalla muerta · Ejercicio

Android con MVP. El Presenter recibe la vista en el constructor y la guarda.

```kotlin
class PerfilPresenter(
    private val vista: PerfilView,      // referencia fija
    private val repo: Repositorio
) {
    fun cargar() {
        repo.cargarPerfil { perfil ->    // 3 s de red
            vista.mostrarPerfil(perfil)  // ¿sigue viva?
        }
    }
}
```

Escena: el usuario abre su perfil, se llama a `cargar()` y la petición tarda 3 segundos. **A mitad de la espera gira el móvil.** En Android, girar destruye la `Activity` y crea una nueva (lo vimos al hablar del ciclo de vida).

Piensa en dos cosas:

- Guardar la vista en el Presenter, ¿a qué equivale en el vocabulario de la unidad? ¿Hay su pareja?
- Cuando llega la respuesta, ¿a qué `Activity` apunta `vista`?

**Preguntas**

**50.** ¿Qué ve el usuario cuando llega la respuesta?

   a) El perfil en la nueva Activity, sin problemas
   b) Un error de compilación por usar `vista` en una lambda
   c) La nueva Activity vacía: el perfil se pinta en la Activity muerta, que además no se puede liberar (fuga)
   d) El perfil aparece dos veces, una en cada Activity

**51.** En el lenguaje de la unidad, guardar la vista en el Presenter es…

   a) …un alta implícita sin su baja
   b) …una baja anticipada
   c) …un callback síncrono
   d) …una foto de la lista

### Adjuntar y soltar la vista · Teoría

El Presenter guarda la vista, que es **un alta implícita**. Sin baja, el callback de red la encuentra muerta. La solución es dar al Presenter un alta y una baja explícitas, ligadas al ciclo de vida de la Activity:

```kotlin
class PerfilPresenter(private val repo: Repositorio) {
    private var vista: PerfilView? = null
    private var ultimo: Perfil? = null

    fun adjuntar(v: PerfilView) {
        vista = v
        ultimo?.let { v.mostrarPerfil(it) }   // repinta al volver
    }

    fun soltar() { vista = null }

    fun cargar() = repo.cargarPerfil { p ->
        ultimo = p
        vista?.mostrarPerfil(p)
    }
}
// Activity: onStart { adjuntar(this) }  onStop { soltar() }
```

Tres cambios:

1. **`vista` es anulable**: puede no haber nadie escuchando, y `vista?.` no llama si es `null`.
2. **Adjuntar y soltar** son el alta y la baja, colocadas en `onStart` y `onStop`.
3. **Estado guardado (`ultimo`)**: si la respuesta llega cuando no hay vista, se guarda; al adjuntar la nueva, se repinta con el último valor.

Para que el arreglo funcione tras un giro, el Presenter tiene que sobrevivir a la recreación de la Activity (si se crea uno nuevo con cada Activity, se pierde `ultimo`).

> [!NOTA] MVP acaba reinventando MVVM
> Un MVP bien hecho acaba reinventando la mitad de MVVM: **estado guardado**, **alta y baja ligadas al ciclo de vida** y **repintado al suscribirse**. Por eso Android pasó a MVVM, donde el framework ya da todo eso hecho.

**Preguntas**

**52.** ¿En qué métodos del ciclo de vida coloca el material `adjuntar` y `soltar`?

   a) `onCreate` y `onDestroy`
   b) `onResume` y `onCreate`
   c) En el constructor del Presenter
   d) `onStart` y `onStop`

**53.** ¿Para qué sirve `ultimo` en el Presenter corregido?

   a) Para no volver a llamar a la red si el perfil ya se cargó una vez
   b) Para saber cuál fue la última vista adjuntada
   c) Para cancelar la petición en curso
   d) Para repintar la nueva vista con el último perfil cuando se adjunta

### Caso MVVM: el Toast que vuelve al girar · Ejercicio

Ahora MVVM. Al confirmar un pedido, el ViewModel quiere que la pantalla muestre «Pedido enviado ✔».

```kotlin
class PedidoViewModel : ViewModel() {
    val mensaje = MutableLiveData<String>()   // un aviso como estado
    fun onConfirmar() {
        // … enviar el pedido
        mensaje.value = "Pedido enviado ✔"
    }
}

// PedidoActivity.onCreate
vm.mensaje.observe(this) { texto ->
    Toast.makeText(this, texto, Toast.LENGTH_SHORT).show()
}
```

Recuerda dos cosas de la clase 3:

- `LiveData` **guarda el último valor**. Justo lo que queríamos para `pulsaciones`: al suscribirse, la vista recibe el valor actual, como `o(valor)` en `observar()`.
- El `ViewModel` **sobrevive** al giro; la Activity no. La nueva Activity hace un `observe()` nuevo, es decir, **una suscripción nueva**.

Escena: el usuario confirma, ve el Toast y después gira el móvil. Y lo vuelve a girar.

**Preguntas**

**54.** ¿Qué ocurre cada vez que el usuario gira el móvil?

   a) Nada: el Toast solo se muestra al confirmar
   b) El Toast «Pedido enviado ✔» vuelve a aparecer
   c) La app falla porque el ViewModel apunta a la Activity vieja
   d) El pedido se envía otra vez

**55.** Si en lugar de un Toast el observador hiciera `navegar(PantallaConfirmacion)`, ¿qué notaría el usuario al girar o al volver con «Atrás»?

   a) Que la app lo vuelve a llevar a la confirmación cada vez que la pantalla del pedido se suscribe de nuevo
   b) Nada, la navegación solo ocurre una vez
   c) Que el botón «Atrás» deja de funcionar por completo
   d) Que la confirmación aparece vacía

### Estado frente a evento, con Flow · Teoría

Repintar con el valor actual al suscribirse es **correcto para un estado e incorrecto para un aviso**. Lo que es un estado se repinta siempre; lo que es un aviso se consume una sola vez.

```kotlin
data class PedidoUi(val enviando: Boolean = false, val enviado: Boolean = false)

class PedidoViewModel(private val repo: Repositorio) : ViewModel() {
    private val _estado = MutableStateFlow(PedidoUi())          // ESTADO
    val estado: StateFlow<PedidoUi> = _estado

    private val _avisos = Channel<String>(Channel.BUFFERED)     // EVENTO
    val avisos = _avisos.receiveAsFlow()

    fun onConfirmar() = viewModelScope.launch {
        _estado.update { it.copy(enviando = true) }
        repo.enviar()
        _estado.update { it.copy(enviando = false, enviado = true) }
        _avisos.send("Pedido enviado ✔")
    }
}

// PedidoActivity.onCreate
lifecycleScope.launch {
    repeatOnLifecycle(Lifecycle.State.STARTED) {   // alta/baja solas
        launch { vm.estado.collect { pintar(it) } }  // se repinta al girar
        launch { vm.avisos.collect { toast(it) } }   // se muestra una vez
    }
}
```

- **`StateFlow`** guarda el valor actual y lo entrega a cada suscriptor nuevo: estado.
- **`Channel` + `receiveAsFlow()`**: cada aviso lo consume un único colector una sola vez; al girar no se repite.
- **`repeatOnLifecycle(STARTED)`** hace el alta al llegar a STARTED y la baja al salir: el adjuntar/soltar del MVP, gratis.

### El modelo de la Unidad 2 en Android

| Unidad 2 | Android |
| --- | --- |
| `observar()` / `suscribir()` | `collect { }` / `observe()` |
| Valor actual al suscribirse | `StateFlow`, `LiveData` |
| Aviso que no se repite | `Channel` + `receiveAsFlow()` |
| Toda alta tiene su baja | `repeatOnLifecycle(STARTED)` |
| `OnClickListener` que solo avisa | `vm.onConfirmar()` |

> [!IMPORTANTE] Regla
> Antes de exponer algo desde un ViewModel, pregúntate: **¿es algo que ES o algo que HA PASADO?** Lo primero es estado; lo segundo, evento. «El pedido está enviado» es estado; «el pedido se acaba de enviar» es evento.

**Preguntas**

**56.** ¿Cuáles de estos son **estado** (algo que ES) y deberían repintarse al girar?

   a) El número de pulsaciones del contador
   b) «Mostrar el Toast: pedido enviado»
   c) Que el pedido está enviado (`enviado = true`)
   d) «Navegar a la pantalla de confirmación»
   e) Que el botón está deshabilitado mientras se envía

**57.** ¿Qué tipo de Kotlin usa el ViewModel corregido para los avisos de un solo uso (antes de convertirlo con `receiveAsFlow`)?

### Caso integrador: encuentra los cuatro fallos · Ejercicio

Pantalla de seguimiento de una app de reparto. Compila, funciona en la demo… y falla en producción.

```kotlin
class SeguimientoActivity : AppCompatActivity() {
    override fun onResume() {
        super.onResume()
        Pedidos.suscribir { estado ->
            textoEstado.text = estado
            if (estado == "entregado") {
                Pedidos.publicar("pendiente-valoracion")
                Pedidos.desuscribir { }
            }
            Analitica.enviar(estado) // petición HTTP; lanza si no hay red
        }
    }
}
```

Pistas: hay **cuatro** fallos, uno de cada familia:

- uno de **ciclo de vida**,
- uno de **referencia**,
- uno de **reentrancia**,
- uno de **aislamiento o lentitud**.

Para cada fallo: línea, síntoma que vería el usuario y remedio. Contesta la pregunta y después abre la solución.

<details><summary>Ver la solución</summary>
<table>
<thead><tr><th>#</th><th>Fallo</th><th>Síntoma</th><th>Regla</th></tr></thead>
<tbody>
<tr><td>1</td><td>Alta en <code>onResume()</code> sin baja</td><td>Cada vuelta a primer plano añade un suscriptor: N repintados, fuga</td><td>Alta y baja simétricas</td></tr>
<tr><td>2</td><td>Publicar en el mismo sujeto en su ronda</td><td>Los demás reciben <code>pendiente-valoracion</code> antes que <code>entregado</code></td><td>Reentrancia</td></tr>
<tr><td>3</td><td><code>desuscribir { }</code> con lambda nueva</td><td>No da de baja nada</td><td>Misma referencia</td></tr>
<tr><td>4</td><td>Red en el handler, hilo principal, sin aislar</td><td>UI congelada; sin red, la excepción corta la ronda</td><td>Aislar y delegar</td></tr>
</tbody>
</table>
<pre><code>class SeguimientoActivity : AppCompatActivity() {
    private var baja: (() -&gt; Unit)? = null
    override fun onStart() {
        super.onStart()
        baja = Pedidos.suscribir { estado -&gt;      // 1 alta con baja
            textoEstado.text = estado
            if (estado == "entregado")
                mostrarValoracion()               // 2 sin republicar
        }
    }
    override fun onStop() {
        baja?.invoke(); baja = null               // 3 baja simétrica
        super.onStop()
    }
}
// 4 Analítica: fuera de la pantalla y en segundo plano
Pedidos.suscribir { estado -&gt;
    appScope.launch(Dispatchers.IO) {
        runCatching { Analitica.enviar(estado) }
    }
}</code></pre>
</details>

**Preguntas**

**58.** Marca los **cuatro** fallos reales de `SeguimientoActivity`.

   a) Suscribirse en `onResume()` sin dar de baja en ningún sitio
   b) Asignar `textoEstado.text` dentro del suscriptor
   c) Publicar `pendiente-valoracion` desde dentro de la ronda de `Pedidos`
   d) `desuscribir { }` con una lambda recién creada
   e) Comparar `estado` con `==` en lugar de `equals`
   f) Hacer una petición HTTP que puede lanzar dentro del handler, en el hilo principal

**59.** En la versión corregida, ¿qué consiguen `appScope.launch(Dispatchers.IO)` y `runCatching` alrededor de `Analitica.enviar`?

   a) Que la analítica se envíe antes que el repintado
   b) Que la analítica solo se envíe cuando la pantalla está visible
   c) Que la petición se repita hasta tener red
   d) Delegar el trabajo lento fuera del hilo principal y aislar su excepción para que no corte la ronda

### Repaso: lista de comprobación ampliada y puente a la U3 · Repaso

Antes de dar por buena cualquier suscripción, pasa esta lista. Las seis primeras preguntas vienen de la clase 2; las cuatro nuevas son el **contrato de entrega** que hemos visto hoy.

| # | Pregunta | Qué buscas |
| --- | --- | --- |
| 1 | ¿Cuál es el contrato del callback? | Qué recibe, qué devuelve, cuándo se llama |
| 2 | ¿Síncrono o diferido? | ¿Corre dentro de la llamada o más tarde (y en qué hilo)? |
| 3 | ¿Cuántos suscriptores? | Uno o N |
| 4 | ¿Quién da de baja? | Con la misma referencia que el alta |
| 5 | ¿Cómo se asocia evento → handler? | Lista, mapa, topic… |
| 6 | ¿De dónde saca el handler sus datos? | Del evento, no del emisor |
| 7 | ¿Qué pasa con las altas y bajas durante una ronda? | Foto pura o foto + activa |
| 8 | ¿Puede un suscriptor publicar? | ¿Se entrega ya o se encola? ¿Puede formar un ciclo? |
| 9 | ¿Qué pasa si un suscriptor falla o tarda? | ¿Se aísla? ¿Quién se entera? |
| 10 | ¿Es un estado o un evento? | ¿Debe recibirlo quien llega tarde? |

> [!RESUMEN]
> - Las respuestas a orden, altas/bajas, reentrancia y fallos **son la API**, aunque no estén en la firma.
> - Recorre siempre **una foto**; decide si la baja cuenta desde ya (bandera `activa`).
> - Un observador no corrige al sujeto: **regla en el sujeto**, o cola si no hay más remedio.
> - **Órdenes y hechos** en topics distintos; sin cambio, sin aviso.
> - Toda alta tiene su baja, **con la misma referencia**; un callback diferido también.
> - **Aísla** a cada suscriptor; en C++, la baja en un **destructor** (RAII).
> - MVP **ordena** a la vista; MVVM **expone estado** y la vista se suscribe. Estado ≠ evento.

### Puente a la Unidad 3

La cola `pendientes` + `while` del arreglo B es **un bucle de eventos en miniatura**: productor → cola → dispatcher → handler, y cada ronda termina antes de la siguiente (*run-to-completion*). En la Unidad 3 se construye completo, con prioridades, y se estudia su peor enemigo: **el handler lento** que congela la pantalla.

**Preguntas**

**60.** Un suscriptor del `Observable` corrige el valor durante su turno y la vista acaba mostrando un valor viejo. ¿Qué pregunta de la lista nadie se hizo?

   a) 3 · ¿Cuántos suscriptores?
   b) 10 · ¿Es un estado o un evento?
   c) 8 · ¿Puede un suscriptor publicar? ¿Se entrega ya o se encola?
   d) 5 · ¿Cómo se asocia evento → handler?

**61.** Completa la regla: «Un callback diferido es una suscripción de un solo uso: también tiene …»

**62.** ¿Qué elemento de esta clase es «un bucle de eventos en miniatura» que se desarrollará en la Unidad 3?

   a) La foto `toList()`
   b) El `try/catch` por suscriptor
   c) La interfaz `ContadorView` del MVP
   d) La cola `pendientes` con su `while` (arreglo B)

### Solucionario

1. c) Cuando `publicar()` retorna  
   La ronda empieza al entrar en `publicar()` y termina cuando `publicar()` retorna. Si un suscriptor publica durante su turno, se abre una **ronda anidada** dentro de la primera.
2. a) ¿Se garantiza el orden de alta?; c) ¿Las altas y bajas afectan a la ronda en curso o a la siguiente?; d) ¿Se puede publicar desde un suscriptor?; f) Si un suscriptor lanza, ¿se sigue avisando a los demás?  
   Orden, altas/bajas durante la ronda, reentrancia y fallos. Ninguna aparece en la firma, pero todas forman parte del contrato de la API.
3. b) Nada: el total ya es 55 € y no hay banner  
   El banner va primero y lee `resumen.total` cuando aún vale 45 €. Después el resumen lo sube a 55 €, pero el banner ya ha decidido. No hay ningún error: simplemente no sale nada.
4. c) Aparece el banner: el fallo desaparece… hasta que otro cambio de orden lo traiga de vuelta  
   El resultado depende del orden de carga de los módulos. Reordenar «arregla» este caso, pero el fallo puede reaparecer en otro sitio: la dependencia sigue escondida.
5. d) Que el carrito actualice su total y publique `carrito/cambiado` con el total ya calculado  
   El estado derivado (el total) vive en el sujeto. El evento ya lleva el dato correcto, así que los suscriptores no dependen unos de otros ni del orden de carga. Las otras opciones solo esconden la dependencia.
6. `total`  
   El banner desestructura `{ total }` del propio evento: no consulta a `resumen` ni a nadie más.
7. a) `Mapa: entregado`; c) `Historial: entregado`  
   Salen `Mapa: entregado` e `Historial: entregado`. Nadie pide la valoración y **no hay ningún error**: JavaScript no protesta si modificas el array mientras lo recorres.
8. c) Valoración en la posición 0; el bucle pasa a la 1  
   Al quitar el elemento 0, Valoración baja a la posición 0 e Historial a la 1. El bucle ya había procesado el índice 0, así que pasa al 1 y llama a Historial: Valoración se queda sin turno.
9. `ConcurrentModificationException`  
   `ConcurrentModificationException`: el iterador detecta que la lista ha cambiado mientras se recorría. No es concurrencia de hilos: basta con un solo hilo que modifica la lista dentro del bucle.
10. a) Porque un fallo que se ve siempre es mejor que un suscriptor que desaparece en silencio  
   La excepción te obliga a arreglarlo el primer día. En JavaScript el programa sigue funcionando y la valoración simplemente no se pide: puede pasar meses sin que nadie lo note.
11. d) «Ha llegado a su destino» y después «En 200 m, gire a la derecha»  
   La foto se hizo al empezar la ronda y `voz` estaba en ella. Aunque se cancele durante el turno de `controlRuta`, el bucle llega a su posición en la foto y la llama: el conductor oye una instrucción después de haber llegado.
12. a) Porque el bucle recorre una copia (`toList()`), no la lista que se modifica  
   La foto protege al iterador: la lista real cambia, pero el bucle recorre otra. El precio es que la foto puede contener a alguien que ya se ha dado de baja.
13. b) …no recibe el evento en curso  
   El DOM se comporta como «foto + comprobar activa»: la baja cuenta desde ya. Node `EventEmitter` es foto pura: el retirado recibe el evento en curso.
14. c) …espera a la siguiente ronda  
   La foto se hizo antes del alta, así que el nuevo suscriptor no está en ella: su primer aviso será el de la siguiente ronda.
15. `activa`  
   `activa`: `cancelar` la pone a `false` y el bucle salta las suscripciones inactivas aunque sigan en la foto.
16. 1. [Carrito] cantidad: 10 → 2. [Carrito] cantidad: 12 → 3. valor real: 10  
   Ronda 1 (valor 12): `limitar(12)` pone el valor a 10 y abre la ronda 2, que pinta «10». Al volver, la ronda 1 sigue con su `nuevo = 12` y pinta «12». El valor real es 10, pero lo último que se ha pintado es 12.
17. a) Que lo último que pinta la vista coincida con el valor real del modelo  
   La vista se queda con «12» cuando el modelo vale 10. El síntoma típico de la reentrancia: la vista muestra un valor distinto del modelo.
18. d) Porque con A el valor inválido nunca se publica; con B la vista llega a mostrar el 12 un instante  
   B evita que las rondas se aniden y deja la vista con el valor correcto al final, pero el 12 llega a pintarse. A elimina el problema de raíz: el sujeto valida y los observadores solo pintan.
19. `run-to-completion`  
   **Run-to-completion**: cada evento se procesa entero antes del siguiente. Es la base del bucle de eventos de la Unidad 3.
20. c) Guarda el 10 en `pendientes` y retorna, porque `notificando` es `true`  
   La reentrada solo encola. El `while` de la ronda que ya está en marcha recogerá el 10 cuando termine de repartir el 12.
21. c) `RangeError: Maximum call stack size exceeded`  
   Es un **ciclo de notificación**: cada publicación abre una ronda anidada que vuelve a publicar. Nadie corta la cadena y la pila de llamadas se desborda.
22. d) No: cambia el desbordamiento de pila por un bucle infinito de mensajes  
   La cola impide que la pila crezca, pero cada entrega vuelve a encolar otro aviso: la cola nunca se vacía. Si los módulos hablaran por MQTT entre dos dispositivos, sería tráfico sin fin y batería agotada.
23. a) Usar un topic para órdenes y otro para hechos; c) No publicar si el valor no ha cambiado; d) Que solo el dueño del dato publique el hecho  
   Separar órdenes y hechos, un único dueño que publica el hecho y «sin cambio, sin aviso». La cola solo convierte el desbordamiento en un bucle infinito de mensajes.
24. `ajustes/tema-cambiado`  
   `ajustes/tema-cambiado`: es un hecho. Reaccionan aplicando el tema y no publican nada.
25. `3`  
   Tres. Cada apertura añade un listener nuevo y cancelar no quita ninguno. Al pulsar Confirmar, los tres se ejecutan: tres pedidos, tres cobros.
26. a) Porque solo ignora duplicados de la misma función, y cada apertura crea una lambda nueva  
   Cada vez que se ejecuta `abrirModalPago()` nace una función flecha nueva. Para el navegador son funciones distintas aunque tengan el mismo texto.
27. d) Porque `bind` devuelve una función nueva, distinta de la que se registró  
   Cada `bind` crea una función distinta. La baja busca por referencia, no encuentra la suya y no da ningún error. Por eso la solución guarda una sola función en un campo.
28. a) Registrar el listener una sola vez al cargar la página; b) Mantener el alta en la apertura y darlo de baja al cancelar con un `AbortController`  
   Alta única, o alta y baja simétricas. Desactivar el botón no quita los listeners acumulados, y una flecha escrita dentro de la función sigue naciendo nueva en cada apertura.
29. a) Un `flow.collect { }` dentro de `onResume()` sin cancelarlo  
   `onResume()` se ejecuta cada vez que la pantalla vuelve a primer plano: cada vez se añade un colector más si nadie cancela el anterior.
30. c) Las de «man» («mandarina, manzana…»), porque es la última respuesta en llegar  
   Las dos peticiones siguen vivas. La de «mango» pinta primero y la lenta de «man» pinta encima: **carrera de respuestas**.
31. `5`  
   Cinco. Cada tecla programa su propio temporizador y nadie cancela los anteriores: **retrasar no es agrupar**.
32. a) El temporizador de la tecla anterior, si aún no ha saltado; b) La petición `fetch` anterior que sigue en vuelo  
   Hay que anular el temporizador pendiente (para no lanzar búsquedas inútiles) y la petición anterior en vuelo (para que su respuesta no llegue a pintar). El listener de `input` sí debe seguir vivo.
33. 1. `clearTimeout` anula el temporizador pendiente → 2. Se programa un nuevo `setTimeout` de 300 ms → 3. Al saltar, se aborta la petición anterior con `control?.abort()` → 4. Se crea un `AbortController` nuevo → 5. Se lanza `fetch` con `{ signal: control.signal }` → 6. Se llama a `pintarSugerencias` con la respuesta  
   El debounce actúa en el listener (paso 1-2) y el abort dentro del temporizador (pasos 3-5). Si la petición se aborta, su `await` lanza `AbortError` y nunca llega a pintar.
34. d) Porque cancelar una petición a propósito no es un fallo, pero un error de red real sí debe propagarse  
   Abortar es la baja de la petición: es el comportamiento esperado. Tragarse todos los errores escondería fallos reales.
35. b) Ni sirena ni puertas: la excepción corta la ronda y sube hasta el código del detector  
   La excepción sale de `h(ev)`, interrumpe el `for` y sale por `publicar`. Los suscriptores que iban detrás nunca se enteran.
36. a) Informa de la excepción y sigue con el siguiente listener  
   El DOM informa de la excepción (en la consola) y continúa con el siguiente listener. Node `EventEmitter` y un bucle escrito a mano en Kotlin o C++ se detienen.
37. b) Node `EventEmitter`; c) Un `for` escrito a mano en Kotlin; d) Un `for` escrito a mano en C++  
   Solo el DOM sigue con el siguiente listener. Node, Kotlin y C++ propagan la excepción y cortan la ronda.
38. d) Dentro del `for`, alrededor de cada `h(ev)`  
   Un `try` alrededor de toda la ronda capturaría la excepción, pero el bucle ya se habría cortado. Tiene que envolver cada llamada para que el `for` continúe.
39. d) La lambda escribe en memoria liberada a través de un puntero colgante: basura en el display, un reinicio… o nada durante semanas  
   `[this]` no tiene dueño: la lambda guarda un puntero a un objeto que ya no existe. Es comportamiento indefinido: un fallo intermitente que depende de quién ocupe esa memoria (en un ESP32 puede acabar en un reinicio tipo *Guru Meditation*).
40. `destructor`  
   El **destructor**. Si la baja vive en un destructor, nadie tiene que acordarse de hacerla: la llama el compilador.
41. a) Para que no haya dos objetos cuyo destructor intente dar de baja la misma suscripción  
   Una suscripción tiene un único dueño. Si se copiara, la primera copia en morir daría de baja al oyente mientras la otra cree que sigue suscrito. Con `unique_ptr` y sin copia, la vida de la suscripción es exactamente la de la pantalla.
42. a) `DisposableEffect` en Compose; b) `AbortController` en el DOM; c) Qt al destruir un `QObject`  
   Los tres atan la baja al fin de vida de algo (un composable, una señal de aborto, un objeto Qt). `setTimeout` es justo lo contrario: un callback diferido que hay que cancelar a mano.
43. c) Alta en un método que se repite (sin baja simétrica)  
   Fila 6: alguien se suscribe en un método que se ejecuta muchas veces (como `onResume` o abrir un modal) y nunca da de baja. Remedio: alta única o baja simétrica.
44. b) Recorrer una foto de la lista  
   Fila 2: baja durante el recorrido de la lista viva. Los índices se desplazan y alguien se queda sin turno. Remedio: recorrer una foto.
45. a) Delegar el trabajo lento y retornar enseguida  
   Fila 11: handler lento en la ronda. El despacho es síncrono y bloquea a todos. El remedio, delegar y retornar, se desarrolla en la Unidad 3 (y la Unidad 6).
46. b) MVVM  
   El Presenter guarda `vista: ContadorView?` y le llama. El ViewModel solo expone `Observable`s; es la vista la que se suscribe.
47. a) El listener del botón: solo avisa a la lógica  
   `setOnClickListener { …onPulsar() }` es igual en los dos: la vista avisa. La diferencia está en el camino de vuelta.
48. d) Con el ViewModel solo, leyendo su estado  
   Como el ViewModel no conoce la vista, basta con llamar a sus métodos y comprobar el estado. En MVP hace falta una vista falsa porque el Presenter le llama.
49. a) Una referencia a la `Activity`; c) Una `View` concreta, como un `TextView`; d) Un `Context` de pantalla  
   `Activity`, `View` y `Context` de pantalla son la vista: guardarlos convierte el ViewModel en un MVP mal hecho y además provoca fugas, porque el ViewModel sobrevive a la rotación. El estado y el repositorio sí le pertenecen.
50. c) La nueva Activity vacía: el perfil se pinta en la Activity muerta, que además no se puede liberar (fuga)  
   `mostrarPerfil` se llama sobre la Activity destruida: pinta en una pantalla que nadie ve, la mantiene viva en memoria y la nueva se queda vacía.
51. a) …un alta implícita sin su baja  
   El Presenter «se suscribe» a la vista al guardarla: es un alta implícita. Sin una baja (soltar la vista), el callback de red la encuentra muerta.
52. d) `onStart` y `onStop`  
   `onStart { adjuntar(this) }` y `onStop { soltar() }`: un par simétrico. Mientras la pantalla no es visible, el Presenter no le habla.
53. d) Para repintar la nueva vista con el último perfil cuando se adjunta  
   Si el perfil llega con la vista soltada, se guarda en `ultimo`; `adjuntar` lo repinta. Es exactamente el «valor actual al suscribirse» de un observable.
54. b) El Toast «Pedido enviado ✔» vuelve a aparecer  
   Cada Activity nueva hace `observe()` y `LiveData` le entrega el último valor guardado: el Toast se repite en cada giro. El pedido no se reenvía (eso lo hace `onConfirmar`), pero el aviso sí.
55. a) Que la app lo vuelve a llevar a la confirmación cada vez que la pantalla del pedido se suscribe de nuevo  
   Al volver a la pantalla del pedido (girar, o regresar con «Atrás»), su `observe()` recibe otra vez el valor guardado y vuelve a navegar: el usuario queda atrapado. El mensaje no es algo que *es*, sino algo que *ha pasado*.
56. a) El número de pulsaciones del contador; c) Que el pedido está enviado (`enviado = true`); e) Que el botón está deshabilitado mientras se envía  
   Pulsaciones, `enviado` y botón deshabilitado describen cómo está la pantalla: estado. El Toast y la navegación son cosas que han pasado: eventos que se consumen una vez.
57. `Channel`  
   `Channel<String>(Channel.BUFFERED)`, expuesto como flujo con `receiveAsFlow()`. Para el estado se usa `MutableStateFlow`.
58. a) Suscribirse en `onResume()` sin dar de baja en ningún sitio; c) Publicar `pendiente-valoracion` desde dentro de la ronda de `Pedidos`; d) `desuscribir { }` con una lambda recién creada; f) Hacer una petición HTTP que puede lanzar dentro del handler, en el hilo principal  
   1) alta sin baja en un método que se repite; 2) reentrancia al publicar en el mismo sujeto; 3) baja con otra referencia; 4) red en el handler: congela la UI y, sin red, la excepción corta la ronda. Pintar es correcto y en Kotlin `==` compara contenido.
59. d) Delegar el trabajo lento fuera del hilo principal y aislar su excepción para que no corte la ronda  
   Delegar y retornar (el handler no bloquea) y aislar (un fallo de red no afecta a nadie más). Además se saca de la Activity: la analítica no depende de que la pantalla exista.
60. c) 8 · ¿Puede un suscriptor publicar? ¿Se entrega ya o se encola?  
   Es reentrancia: un suscriptor que publica (asigna un valor nuevo) en su turno abre una ronda anidada. La pregunta 8 obliga a decidir si eso se permite y si se entrega ya o se encola.
61. `baja`  
   También tiene **baja**: `clearTimeout`, `abort()`, `cancel()`. Quien da de alta guarda lo que necesita para dar de baja.
62. d) La cola `pendientes` con su `while` (arreglo B)  
   Productor → cola → dispatcher → handler, con run-to-completion. La Unidad 3 lo completa con prioridades y estudia el handler lento.

## Predice, ejecuta, diagnostica

_Cinco escenas con la central de pedidos: predice la salida línea a línea, compárala con la real y diagnostica el fallo con la ficha y la lista de diez preguntas._

### La central de pedidos y el método · Teoría

Hoy no hay ningún concepto nuevo: **los fallos los encontráis vosotros**. Todas las escenas usan la misma central de pedidos, que es el `Sensor` de la clase 1 con otro nombre.

```kotlin
// CentralPedidos.kt
class CentralPedidos {
    private val oyentes = mutableListOf<OyentePedido>()

    fun suscribir(o: OyentePedido) { oyentes += o }       // alta
    fun desuscribir(o: OyentePedido) { oyentes -= o }     // baja
    fun cuantos(): Int = oyentes.size

    // Una ronda: avisa a todos los suscriptores, uno detrás de otro.
    fun publicar(estado: String) {
        for (o in oyentes.toList()) {
            o(estado)
        }
    }
}
```

`OyentePedido` es un tipo función: recibe el estado del pedido (un `String`) y no devuelve nada. Mira bien la línea del `for`: hoy vuelve a salir.

### El método

<div class="flow">
  <div><b>1 · Predecir</b><small>En papel, la salida completa, línea a línea. Antes de ejecutar.</small></div>
  <div><b>2 · Ejecutar</b><small>El archivo de la escena, tal como está. ¿Qué línea no esperabais?</small></div>
  <div><b>3 · Diagnosticar</b><small>Síntoma, fila de la ficha, remedio y pregunta de la lista que nadie se hizo.</small></div>
</div>

> [!IMPORTANTE] El código no se toca
> Si la pantalla no dice lo que habías escrito, **has encontrado el fallo que se busca**. Por eso la predicción tiene que ir antes: si ejecutas primero, cualquier salida te parecerá razonable.

> [!ANALOGIA] El médico
> Un buen diagnóstico no empieza por la radiografía, sino por la hipótesis: «si es esto, debería ver aquello». La salida es tu radiografía; tu predicción, la hipótesis.

En esta sala cada escena tiene dos pasos: primero predices (con el código o el escenario delante) y después, en el paso siguiente, ves la salida real y haces el diagnóstico.

**Preguntas**

**1.** ¿`publicar` recorre la lista viva o una foto?

   a) La lista viva: `oyentes` se recorre directamente
   b) Una foto: `toList()` crea una copia antes de recorrerla
   c) Depende de si hay altas durante la ronda
   d) Ninguna: Kotlin recorre las listas en paralelo

**2.** Ordena los pasos del método de la clase.

   Ordena: Anotar síntoma, fila de la ficha, remedio y pregunta de la lista · Buscar la primera línea que no coincide · Ejecutar el archivo tal como está · Escribir en papel la salida esperada, línea a línea

### Las dos herramientas: ficha y lista · Teoría

Para diagnosticar usaremos las dos herramientas de la clase anterior. Te las dejamos numeradas, porque cada escena se resolverá citando **una fila** y **una pregunta**.

### Ficha de diagnóstico

| Fila | Síntoma que se observa | Antipatrón |
| --- | --- | --- |
| 1 | El resultado depende del orden de carga | Suscriptores que dependen unos de otros |
| 2 | Un suscriptor nunca se entera, sin error | Baja durante el recorrido de la lista viva |
| 3 | Un suscriptor dado de baja reacciona una última vez | Foto sin comprobar si sigue activo |
| 4 | La vista muestra un valor distinto del modelo | Reentrancia: un suscriptor publica en su turno |
| 5 | Desbordamiento de pila o tráfico infinito | Ciclo de notificación |
| 6 | La acción se ejecuta 2, 3… N veces | Alta en un método que se repite |
| 7 | Pantalla cerrada que reacciona; memoria que crece | Baja con otra referencia |
| 8 | Aparecen resultados de una búsqueda anterior | Callback diferido sin cancelar |
| 9 | Tras un fallo, el resto no reacciona | Excepción sin aislar |
| 10 | Reinicios aleatorios en C/C++ | Puntero colgante en el callback |
| 11 | La interfaz se congela mientras un handler trabaja | Handler lento en la ronda |

### Lista de diez preguntas

| # | Pregunta |
| --- | --- |
| 1 | ¿Cuál es el contrato del callback? |
| 2 | ¿Síncrono o diferido? |
| 3 | ¿Cuántos suscriptores? |
| 4 | ¿Quién da de baja? ¿Con la misma referencia que el alta? |
| 5 | ¿Cómo se asocia evento → handler? |
| 6 | ¿De dónde saca el handler sus datos? |
| 7 | ¿Qué pasa con las altas y bajas durante una ronda? |
| 8 | ¿Puede un suscriptor publicar? ¿Se entrega ya o se encola? |
| 9 | ¿Qué pasa si un suscriptor falla o tarda? ¿Quién se entera? |
| 10 | ¿Es un estado o un evento? |

> [!TRUCO]
> A veces el síntoma está en una fila y la causa en otra: «la acción se ejecuta tres veces» (fila 6) puede deberse a una baja que no da de baja nada (fila 7).

**Preguntas**

**3.** ¿Qué número de fila de la ficha corresponde a la **reentrancia**?

**4.** Un suscriptor lanza una excepción y los que van detrás no reaccionan. ¿Qué pregunta de la lista nadie se hizo?

   a) 9 · ¿Qué pasa si un suscriptor falla o tarda?
   b) 2 · ¿Síncrono o diferido?
   c) 6 · ¿De dónde saca el handler sus datos?
   d) 10 · ¿Es un estado o un evento?

### Escena 1 · ¿Qué imprime? · Ejercicio

La escena 1 se hace entre todos y sirve de modelo. Tres minutos, sin ejecutar.

```kotlin
// Escena1Valoracion.kt
fun main() {
    val central = CentralPedidos()
    val historial = mutableListOf<String>()

    // Suscriptor 1: la app del cliente
    central.suscribir { estado ->
        println("  App: $estado")
        if (estado == "entregado") central.publicar("pendiente de valoracion")
    }

    // Suscriptor 2: el historial del pedido
    central.suscribir { estado ->
        println("  Historial: $estado")
        historial += estado
    }

    println("Escena 1")
    central.publicar("en camino")
    central.publicar("entregado")
    println("Historial guardado: $historial")
}
```

Tres cosas en las que fijarse:

1. **Suscriptor 1, la app**: pinta el estado. Mira lo que hace cuando recibe `entregado`.
2. **Suscriptor 2, el historial**: pinta y guarda cada estado, en el orden en que le llega.
3. **Dos avisos seguidos**: `en camino` y `entregado`. ¿Qué queda guardado?

> [!TRUCO] Cómo apuntar las rondas
> Escribe cada `publicar` como un bloque sangrado. Si dentro de un turno se llama a `publicar`, abre un bloque **más sangrado** dentro del actual y termínalo entero antes de volver al de fuera. La salida tiene **ocho líneas**.

**Preguntas**

**5.** Ordena las ocho líneas de la salida de `Escena1Valoracion.kt`.

   Ordena: App: en camino · App: entregado · App: pendiente de valoracion · Escena 1 · Historial guardado: [en camino, pendiente de valoracion, entregado] · Historial: en camino · Historial: entregado · Historial: pendiente de valoracion

**6.** ¿Cuántas rondas se abren en total y cuántas de ellas son anidadas?

   a) 2 rondas, ninguna anidada
   b) 3 rondas, 1 anidada
   c) 3 rondas, ninguna anidada
   d) 4 rondas, 2 anidadas

### Escena 1 · Diagnóstico y dos arreglos · Ejercicio

Lo que sale: una ronda dentro de otra, y **el historial recibe los avisos al revés**.

```text
RONDA 1 · publicar("entregado")
  1 La app recibe entregado
      RONDA 2 · publicar("pendiente de valoracion")
        2 La app recibe pendiente de valoracion
        3 El historial recibe pendiente de valoracion
  4 El historial recibe entregado      ← estado viejo
```

La ronda 1 sigue donde estaba: su último aviso es un estado viejo. Aquí hay **un solo hilo**: la reentrancia es una llamada dentro de otra, no un problema de concurrencia.

### Diagnóstico

| Síntoma | Fila de la ficha | Pregunta de la lista |
| --- | --- | --- |
| El historial guarda los estados en un orden que no ocurrió | 4 · Reentrancia: un suscriptor publica en el mismo sujeto durante su turno | 8 · ¿Puede un suscriptor publicar? ¿Se entrega ya o se encola? |

### Arreglo A · la app no republica

```kotlin
if (estado == "entregado")
    println("  App: que tal tu pedido?")
// Historial guardado: [en camino, entregado]
```

### Arreglo B · una palabra: la central entrega con cola

```kotlin
val central = CentralConCola()
```

`CentralConCola` tiene una cola `pendientes` y una bandera `notificando`, como el `Observable` con entrega diferida de la clase 4: si alguien publica durante una ronda, el aviso **espera en la cola** a que termine. Con B no se toca a ningún suscriptor. Guarda esa imagen para el final.

![Arriba, CentralPedidos: en la ronda 1 de entregado, la app recibe entregado y publica, y la ronda 2 de pendiente de valoracion se abre dentro: la reciben la app y el historial; al volver, el historial recibe entregado, un estado viejo, y guarda en camino, pendiente de valoracion, entregado. Abajo, CentralConCola: el aviso de la app va a la cola, la ronda 1 termina entregando entregado al historial y después la ronda 2 entrega pendiente de valoracion; el historial guarda en camino, entregado, pendiente de valoracion, el orden real](/img/pde/tema-2/2b-escena1-rondas.svg "Escena 1: la ronda anidada desordena el historial; con cola, cada ronda termina antes de la siguiente")

**Preguntas**

**7.** El historial dice que el pedido quedó pendiente de valoración **antes** de entregarse. ¿Quién ha mentido?

   a) La app, porque imprime los estados mal
   b) El historial, que guarda la lista en orden inverso
   c) Nadie a propósito: la central entrega la ronda 2 dentro de la ronda 1 y el historial guarda en el orden en que le llega
   d) Kotlin, porque ejecuta los suscriptores en varios hilos

**8.** Con el arreglo B (`CentralConCola`), ordena lo que acaba guardado en el historial.

   Ordena: en camino · entregado · pendiente de valoracion

### Escena 2 · El timbre · Ejercicio

Desde aquí, en parejas, unos cinco minutos por escena. La central es la del principio, **sin los arreglos** de la escena 1.

**Escenario.** El cliente tiene activado el modo «no molestar». Cuando el repartidor llega a la puerta, el suscriptor de «no molestar» da de baja al timbre. Hay tres suscriptores, en este orden: no molestar, timbre e historial. **¿Suena el timbre?**

En clase solo se proyectan la salida y el arreglo. Este es un código **en versión simplificada**, coherente con esa salida:

```kotlin
// Escena2Timbre.kt (versión simplificada)
fun main() {
    val central = CentralPedidos()
    val timbre: OyentePedido = { estado ->
        if (estado == "en la puerta") println("  Timbre: DING DONG")
    }
    val noMolestar: OyentePedido = { estado ->
        if (estado == "en la puerta") {
            println("  No molestar: se da de baja el timbre")
            central.desuscribir(timbre)
        }
    }
    central.suscribir(noMolestar)
    central.suscribir(timbre)
    central.suscribir { estado -> println("  Historial: $estado") }

    println("Escena 2")
    central.publicar("en la puerta")
    println("Suscriptores: ${central.cuantos()}")
    central.publicar("entregado")
}
```

Recuerda lo que respondiste en el primer paso: `publicar` recorre `oyentes.toList()`.

**Preguntas**

**9.** ¿Aparece la línea `Timbre: DING DONG`?

   a) No: la baja se hace antes de que le llegue el turno al timbre
   b) Sí: el timbre estaba en la foto de la ronda y la central no comprueba si sigue suscrito
   c) No: salta `ConcurrentModificationException`
   d) Sí, y además suena otra vez con `entregado`

**10.** ¿Qué número imprime la línea `Suscriptores: …`?

### Escena 2 · El timbre suena después de la baja · Ejercicio

Salida real:

```text
Escena 2
  No molestar: se da de baja el timbre
  Timbre: DING DONG
  Historial: en la puerta
Suscriptores: 2
  Historial: entregado
```

El cliente pidió no ser molestado y el timbre suena igualmente. En la segunda ronda (`entregado`) el timbre ya no está, porque la foto nueva se hace sin él.

| Síntoma | Fila | Pregunta |
| --- | --- | --- |
| Un suscriptor dado de baja reacciona una última vez | 3 · Foto sin comprobar si el suscriptor sigue activo | 7 · ¿Qué pasa con las altas y bajas durante una ronda? |

### Arreglo en `CentralPedidos.kt`: foto, y comprobar que sigue suscrito

```kotlin
for (o in oyentes.toList()) {
    if (o in oyentes) o(estado)
}
// con el arreglo, la línea «Timbre: DING DONG» desaparece
```

La foto protege el bucle; la comprobación `o in oyentes` hace que **una baja cuente desde ya**. Es la misma idea que la bandera `activa` del GPS de la clase 4, sin necesidad de una clase `Suscripcion`.

> [!IMPORTANTE]
> Ninguna política es «la correcta». Node entrega el aviso en curso; el DOM no. **Lo incorrecto es no saber cuál ofrece vuestra central.** Aquí el «no molestar» necesita la segunda.

**Preguntas**

**11.** ¿Qué comprueba `if (o in oyentes)` dentro del bucle?

   a) Que el suscriptor de la foto siga en la lista real en el momento de su turno
   b) Que `o` no sea `null`
   c) Que la lista no se haya vaciado
   d) Que `o` no se haya suscrito dos veces

**12.** Sin el arreglo, ¿a qué plataforma se parece la `CentralPedidos` original en esta pregunta del contrato?

   a) Al DOM
   b) A Node `EventEmitter`
   c) A ninguna: es un fallo de Kotlin
   d) A los dos por igual

### Escena 3 · La pantalla · Ejercicio

**Escenario.** El usuario entra en la pantalla de seguimiento, sale, vuelve a entrar, vuelve a salir y entra por tercera vez. Estando dentro, el pedido pasa a «en camino». **¿Cuántas veces se pinta el aviso?**

Versión simplificada, coherente con la salida que se vio en clase:

```kotlin
// Escena3Pantalla.kt (versión simplificada)
class PantallaSeguimiento(private val central: CentralPedidos) {
    private fun pintar(estado: String) = println("  Pantalla: $estado")

    fun alVolver() { central.suscribir { estado -> pintar(estado) } }
    fun alSalir()  { central.desuscribir { estado -> pintar(estado) } }
}

fun main() {
    val central = CentralPedidos()
    val pantalla = PantallaSeguimiento(central)
    println("Escena 3")
    pantalla.alVolver(); pantalla.alSalir()   // 1.ª visita
    pantalla.alVolver(); pantalla.alSalir()   // 2.ª visita
    pantalla.alVolver()                       // 3.ª visita: sigue dentro
    central.publicar("en camino")
    println("Suscriptores: ${central.cuantos()}")
}
```

Hay alta y hay baja, en métodos simétricos. A primera vista está bien. Fíjate en **qué objeto** se pasa a `suscribir` y **qué objeto** se pasa a `desuscribir`.

**Preguntas**

**13.** ¿Cuántas líneas `Pantalla: en camino` se imprimen?

**14.** ¿Da algún error `desuscribir` cuando la lambda no está en la lista?

   a) Sí, `NoSuchElementException`
   b) Sí, `ConcurrentModificationException`
   c) No: `oyentes -= o` simplemente no quita nada y sigue
   d) No compila: no se puede pasar una lambda a `desuscribir`

### Escena 3 · Un aviso, tres repintados · Ejercicio

Salida real:

```text
Escena 3
  Pantalla: en camino
  Pantalla: en camino
  Pantalla: en camino
Suscriptores: 3
```

| Síntoma (fila 6) | Causa (fila 7) | Pregunta |
| --- | --- | --- |
| La acción se ejecuta N veces | Baja con otra referencia | 4 · ¿Quién da de baja? ¿Con la misma referencia que el alta? |

Aquí el síntoma está en una fila y la causa en otra. **Dos lambdas con el mismo texto son dos objetos.** La baja buscó un objeto que nunca estuvo en la lista, y no dijo nada. En una app real, además del triple repintado, cada pantalla «cerrada» sigue viva en memoria a través de su lambda.

### Arreglo: una sola referencia, guardada

```kotlin
private val oyente: OyentePedido = { estado -> pintar(estado) }  // propiedad nueva

central.suscribir(oyente)       // en alVolver
central.desuscribir(oyente)     // en alSalir

// con el arreglo: una línea «Pantalla: en camino» y «Suscriptores: 1»
```

La lambda se crea **una vez**, al construir la pantalla, y se guarda en una propiedad. El alta y la baja usan la misma referencia.

> [!NOTA] Es el mismo fallo del feed
> En la clase 4 lo vimos en JavaScript con `bind`: `removeEventListener("scroll", this.cargarMas.bind(this))` tampoco quitaba nada. Cambia el lenguaje, no el fallo.

**Preguntas**

**15.** ¿Qué imprime la escena 3 con el arreglo?

   a) Ninguna línea de pantalla y `Suscriptores: 0`
   b) Tres líneas y `Suscriptores: 1`
   c) Una línea y `Suscriptores: 3`
   d) Una línea `Pantalla: en camino` y `Suscriptores: 1`

**16.** Completa: «el alta y la baja deben usar la misma …»

### Escena 4 · La cocina · Ejercicio

**Escenario.** Restaurante con reparto. Cuando un pedido pasa a `pagado`, tres suscriptores reaccionan en este orden: la app del cliente, la **analítica** y la **cocina**. Hoy la analítica no tiene red y lanza una excepción. **¿Se entera la cocina?**

Versión simplificada, coherente con la salida que se vio en clase:

```kotlin
// Escena4Cocina.kt (versión simplificada)
fun main() {
    val central = CentralPedidos()
    central.suscribir { estado -> println("  App: $estado") }
    central.suscribir { estado ->                         // analítica: hoy sin red
        throw IllegalStateException("analitica sin red ($estado)")
    }
    central.suscribir { estado ->
        if (estado == "pagado") println("  Cocina: a preparar el pedido")
    }

    println("Escena 4")
    central.publicar("pagado")
    println("Fin de la escena 4")
}
```

Recuerda cómo es el bucle de `publicar`: un `for` sobre la foto que llama a `o(estado)`. ¿Hay algún `try` en algún sitio?

> [!TRUCO]
> Cuando una predicción incluye una excepción, escribe también **hasta dónde sube**: qué funciones atraviesa y si alguien la captura. Eso decide qué líneas ya no se imprimen.

**Preguntas**

**17.** ¿Qué se imprime después de `App: pagado`?

   a) `Cocina: a preparar el pedido` y `Fin de la escena 4`
   b) Solo `Fin de la escena 4`
   c) La traza de la excepción: ni la cocina ni el «Fin» llegan a imprimirse
   d) `Cocina: a preparar el pedido` y después la traza de la excepción

**18.** ¿De quién es la responsabilidad de que el fallo de la analítica no afecte a la cocina?

   a) De la cocina, que debería suscribirse la primera
   b) De `main`, que debería envolver `publicar` en un `try`
   c) De la central, que decide qué pasa cuando un suscriptor falla
   d) De nadie: es inevitable

### Escena 4 · Falla la analítica y lo paga la cocina · Ejercicio

Salida real (el texto rojo):

```text
Escena 4
  App: pagado
Exception in thread "main" java.lang.IllegalStateException: analitica sin red (pagado)
    at Escena4CocinaKt.main$lambda$1(Escena4Cocina.kt:11)
    at CentralPedidos.publicar(CentralPedidos.kt:18)
    at Escena4CocinaKt.main(Escena4Cocina.kt:19)
    at Escena4CocinaKt.main(Escena4Cocina.kt)
```

Lee la traza de abajo arriba: `main` llama a `publicar` (línea 19), `publicar` llama a la lambda de la analítica (línea 18 de la central) y la lambda lanza. La línea de `CentralPedidos.publicar` es la que delata dónde **debería** haberse detenido la excepción.

| Síntoma | Fila | Pregunta |
| --- | --- | --- |
| Falla la analítica y la cocina no se entera | 9 · Excepción sin aislar | 9 · ¿Qué pasa si un suscriptor falla o tarda? |

### Arreglo en `CentralPedidos.kt`: aislar a cada suscriptor

```kotlin
try {
    if (o in oyentes) o(estado)
} catch (e: Exception) {
    println("  [Central] un suscriptor ha fallado: ${e.message}")
}
```

(El `if (o in oyentes)` es el arreglo de la escena 2, que se conserva.) Salida con el arreglo:

```text
Escena 4
  App: pagado
  [Central] un suscriptor ha fallado: analitica sin red (pagado)
  Cocina: a preparar el pedido
Fin de la escena 4
```

> [!CUIDADO] Aislar también es decidir quién se entera
> Con el arreglo, ¿quién sabe que la analítica ha fallado? **Solo quien lea ese mensaje.** Ni el que publicó ni la propia analítica lo saben. Por eso en la clase 4 la centralita no se limitaba a capturar: llamaba a `registrarError`.

**Preguntas**

**19.** ¿Qué línea de la traza indica por dónde atravesó la excepción la central sin detenerla?

   a) `at CentralPedidos.publicar(CentralPedidos.kt:18)`
   b) `at Escena4CocinaKt.main(Escena4Cocina.kt)`
   c) `Exception in thread "main"`
   d) `at Escena4CocinaKt.main$lambda$1(Escena4Cocina.kt:11)`

**20.** Con el arreglo, ¿quién se entera de que la analítica ha fallado?

   a) Quien lea la salida de la consola (el mensaje de la central)
   b) El código que llamó a `publicar`
   c) La propia analítica, que reintentará
   d) La cocina

### Escena 5 · ¿En qué orden salen las líneas? · Ejercicio

Esta escena la ejecuta el profesor; portátiles cerrados. Cuatro minutos.

```kotlin
// Escena5Buscador.kt
fun main() {
    val reloj = Timer()
    var enPantalla = "(nada)"

    // Cada vez que el usuario cambia el texto se lanza una búsqueda
    fun alEscribir(texto: String) {
        println("Buscador: el usuario ha escrito '$texto'")
        buscarEnTienda(reloj, texto) { resultado ->
            enPantalla = resultado
            println("  Pantalla (hilo ${Thread.currentThread().name}): $resultado")
        }
    }

    println("Escena 5")
    alEscribir("man")
    alEscribir("mango")
    println("main (hilo ${Thread.currentThread().name}): sigue sin esperar")

    Thread.sleep(1000)  // deja tiempo a que lleguen las respuestas
    reloj.cancel()
    println("El usuario ha escrito 'mango' y ve: $enPantalla")
}
```

`buscarEnTienda` **no devuelve el resultado: devuelve la tarea programada** (`TimerTask`). El resultado llega más tarde, por el callback. «man» tarda **600 ms** en contestar; «mango», **200 ms**. Una versión simplificada de esa función, coherente con la escena:

```kotlin
fun buscarEnTienda(reloj: Timer, texto: String, alLlegar: (String) -> Unit): TimerTask {
    val tarea = object : TimerTask() {
        override fun run() = alLlegar("resultados de '$texto'")
    }
    reloj.schedule(tarea, if (texto == "man") 600L else 200L)
    return tarea
}
```

**Preguntas**

**21.** Ordena las líneas de la salida de `Escena5Buscador.kt`.

   Ordena: Buscador: el usuario ha escrito 'man' · Buscador: el usuario ha escrito 'mango' · El usuario ha escrito 'mango' y ve: resultados de 'man' · Escena 5 · Pantalla (hilo Timer-0): resultados de 'man' · Pantalla (hilo Timer-0): resultados de 'mango' · main (hilo main): sigue sin esperar

**22.** ¿En qué hilo se ejecuta el callback que pinta los resultados?

   a) En `main`, cuando termina `Thread.sleep`
   b) En un hilo nuevo por cada búsqueda
   c) En el hilo del `Timer` (`Timer-0`)
   d) En el mismo hilo que llamó a `alEscribir`

### Escena 5 · Llega tarde la búsqueda vieja · Ejercicio

Lo que ve el usuario: **ha escrito «mango» y en la pantalla están los resultados de «man»**.

<div class="cards">
  <div class="card"><h4>Diferido (clase 1)</h4><p><code>main</code> sigue sin esperar. El callback corre después, y en otro hilo: <code>Timer-0</code>.</p></div>
  <div class="card"><h4>Gana la última en llegar</h4><p>No la última que se pidió. Las dos búsquedas siguen vivas.</p></div>
  <div class="card"><h4>Fila 8 · preguntas 2 y 4</h4><p>Callback diferido sin cancelar. ¿Síncrono o diferido? ¿Quién da de baja?</p></div>
</div>

¿Qué hace `alEscribir` con lo que devuelve `buscarEnTienda`? **Lo tira.** Sin la tarea en la mano no hay nada que cancelar.

### Arreglo: guardar la tarea y cancelar la anterior

```kotlin
var pendiente: TimerTask? = null

fun alEscribir(texto: String) {
    println("Buscador: el usuario ha escrito '$texto'")
    pendiente?.cancel()
    pendiente = buscarEnTienda(reloj, texto) { resultado ->
        // … igual que antes
    }
}
```

Salida con el arreglo:

```text
Escena 5
Buscador: el usuario ha escrito 'man'
Buscador: el usuario ha escrito 'mango'
main (hilo main): sigue sin esperar
  Pantalla (hilo Timer-0): resultados de 'mango'
El usuario ha escrito 'mango' y ve: resultados de 'mango'
```

![Dos líneas temporales de 0 a 1000 ms. Sin cancelar: la búsqueda de 'mango' contesta a los 200 ms y pinta 'mango'; la de 'man' contesta a los 600 ms y pinta 'man' encima, así que enPantalla pasa de (nada) a 'mango' y después a 'man'. Con pendiente?.cancel(): la búsqueda de 'man' se cancela al escribir 'mango', solo pinta 'mango' a los 200 ms y enPantalla se queda en 'mango'](/img/pde/tema-2/2b-carrera-buscador.svg "Carrera de respuestas: gana la última en llegar, salvo que se cancele la tarea anterior")

> [!IMPORTANTE] Regla
> **Un callback diferido es una suscripción de un solo uso: también tiene alta y también tiene baja.** Quien da de alta guarda lo que necesita para dar de baja.

> [!NOTA] Compáralo con la escena 3
> Allí la baja fallaba por no guardar la lambda. Aquí, por no guardar la tarea. Es el mismo error con distinta ropa.

**Preguntas**

**23.** ¿Qué método de `TimerTask` se llama para anular la búsqueda anterior?

**24.** ¿Qué tienen en común el fallo de la escena 3 y el de la escena 5?

   a) Los dos son reentrancia
   b) Los dos lanzan una excepción silenciosa
   c) Los dos se arreglan con una foto de la lista
   d) En los dos, quien da de alta no guarda lo que necesita para dar de baja (la lambda / la tarea)

### Repaso: cinco fallos, cinco preguntas · Repaso

| Escena | Lo que se ve | Fila | Remedio | Pregunta |
| --- | --- | --- | --- | --- |
| 1 · La valoración | El historial guarda un orden que no ocurrió | 4 | No republicar, o entregar con cola | 8 |
| 2 · El timbre | Suena después de darlo de baja | 3 | Foto, y comprobar que sigue suscrito | 7 |
| 3 · La pantalla | Un aviso, tres repintados | 6 y 7 | La misma referencia en alta y baja | 4 |
| 4 · La cocina | Falla la analítica y la cocina no se entera | 9 | `try/catch` por suscriptor | 9 |
| 5 · El buscador | Llegan resultados de una búsqueda anterior | 8 | Guardar la tarea y cancelarla | 2 y 4 |

> [!IMPORTANTE]
> **Cuatro de los cinco programas terminan sin ningún error.** Solo se ven si alguien predice la salida antes de ejecutar.

### Cinco preguntas para el parcial

Intenta contestarlas en voz alta (treinta segundos cada una) antes de abrir la respuesta.

<details><summary>1 · Un suscriptor dado de baja durante una ronda recibe el aviso en curso. ¿Es un fallo?</summary>
<p>No necesariamente: es la política de <b>foto pura</b> (la de Node <code>EventEmitter</code>). Es un fallo si el contrato promete lo contrario (como el DOM) o si nadie sabe qué política ofrece la central, como el «no molestar» de la escena 2.</p>
</details>

<details><summary>2 · ¿Por qué desuscribir { estado -&gt; pintar(estado) } no da de baja nada y tampoco da error?</summary>
<p>Porque esa lambda acaba de nacer: es un objeto distinto del que se dio de alta, aunque tenga el mismo texto. Quitar de la lista algo que no está no es un error; simplemente no quita nada.</p>
</details>

<details><summary>3 · En la escena 1, ¿qué recibe el historial en último lugar y por qué es un problema?</summary>
<p>Recibe <code>entregado</code>, un estado ya viejo: la ronda anidada de <code>pendiente de valoracion</code> se entregó antes. Quien lea el último estado del historial se equivoca.</p>
</details>

<details><summary>4 · Con el try/catch por suscriptor, ¿quién se entera de que la analítica ha fallado?</summary>
<p>Solo quien lea el mensaje que imprime la central. Ni el publicador ni la analítica lo saben: aislar también es decidir quién se entera.</p>
</details>

<details><summary>5 · ¿En qué se parecen cancelar un TimerTask y dar de baja un listener?</summary>
<p>Son lo mismo: un callback diferido es una suscripción de un solo uso. En ambos casos hay que guardar la referencia (tarea o lambda) en el alta para poder dar de baja.</p>
</details>

**Preguntas**

**25.** ¿Cuál de las cinco escenas es la única que termina con una excepción visible?

   a) Escena 1 · La valoración
   b) Escena 3 · La pantalla
   c) Escena 4 · La cocina
   d) Escena 5 · El buscador

**26.** Según la tabla, ¿qué fila de la ficha y qué preguntas de la lista corresponden a la escena del buscador?

   a) Fila 4 · pregunta 8
   b) Fila 9 · pregunta 9
   c) Fila 3 · pregunta 7
   d) Fila 8 · preguntas 2 y 4

**27.** Completa la regla común a las escenas 3 y 5: «Quien da de alta guarda lo que necesita para dar de …»

### Repaso final: lo que queda para la Unidad 3 · Repaso

> [!RESUMEN] Lo que te llevas de esta sala
> - **Predice antes de ejecutar**: cuatro de los cinco fallos no dan ningún error.
> - **Escena 1**: un suscriptor que publica en su turno abre una ronda anidada (reentrancia).
> - **Escena 2**: la foto protege el bucle, pero la baja solo cuenta desde ya si se comprueba (`o in oyentes`).
> - **Escena 3**: dos lambdas con el mismo texto son dos objetos; la baja necesita la misma referencia.
> - **Escena 4**: sin `try/catch` por suscriptor, el fallo de uno lo pagan los demás; y aislar es decidir quién se entera.
> - **Escena 5**: un callback diferido también tiene baja: guarda la `TimerTask` y cancélala.

### Lo que queda para la Unidad 3

Recuerda el arreglo B de la escena 1: cambiar una palabra para usar `CentralConCola`. Este es su `publicar`:

```kotlin
// CentralPedidos.kt · class CentralConCola
fun publicar(estado: String) {
    pendientes.addLast(estado)
    if (notificando) return
    notificando = true
    try {
        while (pendientes.isNotEmpty()) {
            val siguiente = pendientes.removeFirst()
            for (o in oyentes.toList()) {
                o(siguiente)
            }
        }
    } finally {
        notificando = false
    }
}
```

<div class="cards">
  <div class="card"><h4>1 · La cola</h4><p><code>pendientes</code> guarda los avisos que llegan durante una ronda.</p></div>
  <div class="card"><h4>2 · Quién la vacía</h4><p>Quien publicó primero: se queda hasta entregarlo todo.</p></div>
  <div class="card"><h4>3 · El bucle</h4><p>El <code>while</code> entrega los avisos uno a uno. Cada ronda termina antes de la siguiente.</p></div>
</div>

> [!NOTA] Para la Unidad 3
> **¿Y si quien publica dejara el aviso y se fuera?** Alguien tendría que vaciar la cola por él: ese alguien es el bucle de eventos.

**Preguntas**

**28.** En `CentralConCola`, ¿quién vacía la cola `pendientes`?

   a) Un hilo aparte que la revisa cada cierto tiempo
   b) El último suscriptor de cada ronda
   c) Quien publicó primero: se queda en el `while` hasta entregarlo todo
   d) Cada llamada a `publicar`, incluidas las reentrantes

**29.** Ordena lo que hace `CentralConCola.publicar` en la primera llamada (la que no es reentrante).

   Ordena: Añade el aviso al final de `pendientes` · Comprueba `notificando` y, como es `false`, sigue · Con la cola vacía, pone `notificando = false` en el `finally` · Lo entrega a la foto de suscriptores · Pone `notificando = true` · Saca el primer aviso de la cola

### Solucionario

1. b) Una foto: `toList()` crea una copia antes de recorrerla  
   `oyentes.toList()` devuelve una lista nueva con los mismos elementos. Las altas y bajas durante la ronda no rompen el bucle… pero tampoco afectan a quién recibe el aviso en curso. Tenlo en mente para la escena 2.
2. 1. Escribir en papel la salida esperada, línea a línea → 2. Ejecutar el archivo tal como está → 3. Buscar la primera línea que no coincide → 4. Anotar síntoma, fila de la ficha, remedio y pregunta de la lista  
   Predecir → ejecutar → diagnosticar. La línea que no coincide con la predicción es el síntoma.
3. `4`  
   Fila 4: la vista (o el historial) muestra algo distinto de lo que pasó porque un suscriptor publica durante su turno.
4. a) 9 · ¿Qué pasa si un suscriptor falla o tarda?  
   La pregunta 9, que se corresponde con la fila 9 de la ficha: excepción sin aislar.
5. 1. Escena 1 → 2. App: en camino → 3. Historial: en camino → 4. App: entregado → 5. App: pendiente de valoracion → 6. Historial: pendiente de valoracion → 7. Historial: entregado → 8. Historial guardado: [en camino, pendiente de valoracion, entregado]  
   La ronda de `entregado` llama primero a la app, que abre una ronda anidada con `pendiente de valoracion`: la app y el historial la reciben completa. Solo después la ronda de fuera continúa y el historial recibe `entregado`, ya viejo.
6. b) 3 rondas, 1 anidada  
   `en camino`, `entregado` y `pendiente de valoracion`; esta última empieza dentro de la ronda de `entregado`, antes de que termine.
7. c) Nadie a propósito: la central entrega la ronda 2 dentro de la ronda 1 y el historial guarda en el orden en que le llega  
   El historial es fiel a lo que recibe; el problema es el orden de entrega cuando un suscriptor publica en su turno (reentrancia). Es una pregunta del contrato de la central, no de un suscriptor.
8. 1. en camino → 2. entregado → 3. pendiente de valoracion  
   `Historial guardado: [en camino, entregado, pendiente de valoracion]`. La app sigue publicando, pero el aviso se entrega cuando acaba la ronda en curso: el orden vuelve a ser el real.
9. b) Sí: el timbre estaba en la foto de la ronda y la central no comprueba si sigue suscrito  
   El timbre está en la foto que se hizo al empezar la ronda. La baja lo quita de `oyentes`, pero la foto no cambia: el timbre suena después de haberlo dado de baja.
10. `2`  
   2: no molestar e historial. La baja del timbre sí se ha hecho en la lista real; lo que no ha hecho es impedir el aviso en curso.
11. a) Que el suscriptor de la foto siga en la lista real en el momento de su turno  
   La foto decide quién *podría* recibir; la lista real, quién *sigue* suscrito. Si alguien se dio de baja antes de su turno, no se le llama.
12. b) A Node `EventEmitter`  
   Foto pura: el suscriptor dado de baja durante la ronda recibe el aviso en curso, como en Node. Con el arreglo se comporta como el DOM.
13. `3`  
   Tres. Cada `alVolver` añade una lambda nueva; cada `alSalir` intenta quitar otra lambda, también nueva, que nunca estuvo en la lista.
14. c) No: `oyentes -= o` simplemente no quita nada y sigue  
   Quitar de una lista un elemento que no está no es un error en Kotlin: no hace nada. Por eso este fallo es silencioso.
15. d) Una línea `Pantalla: en camino` y `Suscriptores: 1`  
   Las dos primeras visitas dan de alta y de baja la misma referencia; en la tercera el usuario sigue dentro, así que queda exactamente un suscriptor.
16. `referencia`  
   La misma **referencia**: el objeto que se dio de alta es el que hay que dar de baja.
17. c) La traza de la excepción: ni la cocina ni el «Fin» llegan a imprimirse  
   La excepción de la analítica corta la ronda: la cocina va detrás y no recibe el aviso. Como nadie la captura, también corta `main` y el programa termina con la traza.
18. c) De la central, que decide qué pasa cuando un suscriptor falla  
   Es la pregunta 4 de la letra pequeña: «si un suscriptor lanza, ¿se sigue avisando a los demás?». La respuesta la da el bucle de la central. Reordenar suscriptores o capturar en `main` no hace que la cocina se entere.
19. a) `at CentralPedidos.publicar(CentralPedidos.kt:18)`  
   La línea 18 de la central es la llamada `o(estado)` dentro del `for`: ahí es donde debe ir el `try/catch` por suscriptor.
20. a) Quien lea la salida de la consola (el mensaje de la central)  
   Solo quien lea el mensaje. `publicar` retorna con normalidad, la analítica no recibe ninguna señal y la cocina ni lo sabe ni le importa. Aislar sin informar bien es esconder el fallo.
21. 1. Escena 5 → 2. Buscador: el usuario ha escrito 'man' → 3. Buscador: el usuario ha escrito 'mango' → 4. main (hilo main): sigue sin esperar → 5. Pantalla (hilo Timer-0): resultados de 'mango' → 6. Pantalla (hilo Timer-0): resultados de 'man' → 7. El usuario ha escrito 'mango' y ve: resultados de 'man'  
   Las tres primeras líneas de `main` salen de inmediato (callback diferido). A los 200 ms llega «mango» y a los 600 ms «man», que sobrescribe `enPantalla`. Al final de `main` el usuario ve los resultados de «man».
22. c) En el hilo del `Timer` (`Timer-0`)  
   Un `java.util.Timer` ejecuta sus tareas en su propio hilo de fondo, que aquí se llama `Timer-0`. Es un callback diferido, como en la clase 1: corre después y en otro hilo.
23. `cancel`  
   `cancel()`: si la tarea aún no se ha ejecutado, ya no se ejecutará. Es la baja del callback diferido.
24. d) En los dos, quien da de alta no guarda lo que necesita para dar de baja (la lambda / la tarea)  
   Sin la referencia (lambda o `TimerTask`) no hay forma de dar de baja. La regla común: quien da de alta guarda lo que necesita para dar de baja.
25. c) Escena 4 · La cocina  
   Solo la escena 4 termina con una traza. Las otras cuatro acaban «bien»: sus fallos solo los detecta quien predijo la salida antes de ejecutar.
26. d) Fila 8 · preguntas 2 y 4  
   Fila 8 (callback diferido sin cancelar) y preguntas 2 (¿síncrono o diferido?) y 4 (¿quién da de baja?).
27. `baja`  
   Para dar de **baja**: la lambda en la escena 3, la `TimerTask` en la escena 5.
28. c) Quien publicó primero: se queda en el `while` hasta entregarlo todo  
   Las llamadas reentrantes solo encolan y retornan. La primera es la que recorre el `while`. La pregunta para la U3 es qué pasa si ni siquiera ella se queda: dejar el aviso e irse lleva al bucle de eventos.
29. 1. Añade el aviso al final de `pendientes` → 2. Comprueba `notificando` y, como es `false`, sigue → 3. Pone `notificando = true` → 4. Saca el primer aviso de la cola → 5. Lo entrega a la foto de suscriptores → 6. Con la cola vacía, pone `notificando = false` en el `finally`  
   Encolar → comprobar → marcar → sacar y entregar (en bucle mientras queden avisos) → desmarcar en el `finally`, que se ejecuta aunque un suscriptor lance.
