Estados paralelos en media: la reproducción, el sonido y los subtítulos
Un reproductor no tiene una dimensión sino varias, y son visiblemente independientes: silenciar no pausa, activar subtítulos no interrumpe el búfer, entrar en pantalla completa no altera el volumen. Modelar esas dimensiones como estados exclusivos obliga a multiplicar el catálogo por cada una de ellas; declararlas como regiones ortogonales convierte la multiplicación en suma. Esta lección construye el reproductor completo con cuatro regiones, explica por qué el silenciado necesita memoria propia, y establece el criterio para detectar la región falsa que en realidad estaba acoplada a otra.
El grafo de siete estados de la primera lección describe una sola dimensión del reproductor: la del transporte, la que responde a la pregunta de si el tiempo del medio avanza. Pero un reproductor real tiene al menos tres dimensiones más que conviven con ella sin determinarla. El sonido puede estar activo o silenciado en cualquiera de los siete estados. Los subtítulos pueden estar visibles u ocultos con total independencia del búfer. La presentación puede ser normal, en pantalla completa o en imagen sobre imagen mientras el vídeo hace cualquier cosa. Si insistes en un único estado activo, el catálogo pasa de siete nodos a siete por dos por dos por tres, ciento sesenta y ocho combinaciones que nadie va a escribir ni a mantener. La ortogonalidad no reduce ese espacio, que es real e irreducible; reduce el texto necesario para describirlo, que pasa de ciento sesenta y ocho nodos a catorce repartidos en cuatro regiones.
- Repartir las dimensiones de un reproductor en regiones ortogonales dentro de un nodo paralelo.
- Leer el valor del snapshot como un conjunto de posiciones simultáneas, una por región.
- Modelar el silenciado con memoria del volumen previo sin acoplar regiones entre sí.
- Aplicar el criterio de independencia para descartar regiones que en realidad no lo eran.
Cuatro dimensiones que no se estorban
La estructura declara un nodo raíz paralelo cuyos hijos son las dimensiones. Cada región conserva su propio initial y su propia submáquina, y ninguna menciona el estado de las demás. El transporte de la primera lección entra tal cual como una región más.
import { setup, assign } from 'xstate'
export const reproductor = setup({
types: {
context: {} as { volumen: number; volumenPrevio: number; pista: string | null },
events: {} as
| { type: 'CARGAR' } | { type: 'PUEDE_REPRODUCIR' } | { type: 'REPRODUCIR' }
| { type: 'PAUSAR' } | { type: 'FIN' } | { type: 'SILENCIAR' }
| { type: 'RESTAURAR_SONIDO' } | { type: 'AJUSTAR_VOLUMEN'; valor: number }
| { type: 'MOSTRAR_SUBTITULOS'; pista: string } | { type: 'OCULTAR_SUBTITULOS' }
| { type: 'PANTALLA_COMPLETA' } | { type: 'VENTANA_FLOTANTE' } | { type: 'PRESENTACION_NORMAL' },
},
}).createMachine({
id: 'reproductor',
type: 'parallel',
context: { volumen: 0.8, volumenPrevio: 0.8, pista: null },
states: {
transporte: {
initial: 'inactivo',
states: {
inactivo: { on: { CARGAR: 'cargando' } },
cargando: { on: { PUEDE_REPRODUCIR: 'pausado' } },
pausado: { on: { REPRODUCIR: 'reproduciendo' } },
reproduciendo: { on: { PAUSAR: 'pausado', FIN: 'terminado' } },
terminado: { on: { REPRODUCIR: 'reproduciendo' } },
},
},
sonido: {
initial: 'audible',
states: {
audible: {
on: {
AJUSTAR_VOLUMEN: { actions: assign({ volumen: ({ event }) => event.valor }) },
SILENCIAR: { target: 'silenciado', actions: assign({ volumenPrevio: ({ context }) => context.volumen, volumen: 0 }) },
},
},
silenciado: {
on: {
RESTAURAR_SONIDO: { target: 'audible', actions: assign({ volumen: ({ context }) => context.volumenPrevio }) },
AJUSTAR_VOLUMEN: { target: 'audible', actions: assign({ volumen: ({ event }) => event.valor }) },
},
},
},
},
subtitulos: {
initial: 'ocultos',
states: {
ocultos: {
on: {
MOSTRAR_SUBTITULOS: { target: 'visibles', actions: assign({ pista: ({ event }) => event.pista }) },
},
},
visibles: { on: { OCULTAR_SUBTITULOS: 'ocultos' } },
},
},
presentacion: {
initial: 'normal',
states: {
normal: { on: { PANTALLA_COMPLETA: 'completa', VENTANA_FLOTANTE: 'flotante' } },
completa: { on: { PRESENTACION_NORMAL: 'normal', VENTANA_FLOTANTE: 'flotante' } },
flotante: { on: { PRESENTACION_NORMAL: 'normal', PANTALLA_COMPLETA: 'completa' } },
},
},
},
})
El valor del snapshot deja de ser una rama y pasa a ser un objeto con una clave por región, todas presentes siempre porque ninguna región puede apagarse mientras el nodo paralelo esté activo.
actor.getSnapshot().value
// { transporte: 'reproduciendo', sonido: 'silenciado', subtitulos: 'visibles', presentacion: 'completa' }
stateDiagram-v2
state reproductor {
state transporte {
[*] --> pausado
pausado --> reproduciendo: REPRODUCIR
reproduciendo --> pausado: PAUSAR
}
--
state sonido {
[*] --> audible
audible --> silenciado: SILENCIAR
silenciado --> audible: RESTAURAR_SONIDO
}
}Las regiones de subtítulos y presentación se omiten del diagrama por espacio y no por jerarquía: viven exactamente al mismo nivel que las dos dibujadas, separadas por la misma línea discontinua que en un statechart marca la frontera entre dimensiones ortogonales.
Silenciar no es poner el volumen a cero: es poner el volumen a cero recordando cuál era. Esa memoria no cabe en el nombre del estado porque el volumen es una magnitud continua y no un conjunto finito de posiciones, así que vive en el contexto como volumenPrevio. La división de trabajo es la habitual y conviene enunciarla: el estado responde a si hay sonido o no, que es una pregunta cualitativa con dos respuestas; el contexto responde a cuánto, que es una pregunta cuantitativa con infinitas. Intentar codificar el volumen como estados produce un catálogo absurdo; intentar codificar el silenciado como un número produce la ambigüedad clásica de no distinguir un volumen bajado a mano de un silenciado que debe poder deshacerse.
Difusión y la región que ignora el evento
Un evento no se dirige a una región concreta: se ofrece a todas y cada una decide por su cuenta. PAUSAR solo lo declara transporte, así que las otras tres lo descartan sin error y sin cambio. Esa indiferencia es precisamente la prueba de la ortogonalidad, y también su ventaja operativa: añadir una quinta dimensión —velocidad de reproducción, capítulos, calidad— no obliga a tocar ni una línea de las cuatro existentes.
| Dimensión | ¿Independiente del transporte? | Veredicto |
|---|---|---|
| sonido audible o silenciado | sí, se puede silenciar pausado o reproduciendo | región propia |
| subtítulos visibles u ocultos | sí, el texto se muestra igual con el vídeo detenido | región propia |
| presentación normal, completa o flotante | sí, el tamaño no altera el búfer | región propia |
| buffering | no, solo existe con la reproducción en marcha | hijo de reproduciendo |
| posición actual del reloj | no es cualitativa, es una magnitud | contexto, no estado |
| calidad adaptativa | discutible: depende de si reproduce | región con guardas o anidado |
La cuarta fila es la más instructiva porque parece candidata y no lo es. El buffering solo tiene sentido mientras el transporte intenta avanzar, y esa dependencia lo descalifica como región: pertenece dentro de reproduciendo, como estado hijo, junto a un hermano que represente el avance efectivo. El criterio es mecánico y no admite negociación: una dimensión merece región propia únicamente si su tabla de transiciones se puede escribir entera sin mencionar el estado de ninguna otra región.
El síntoma de un paralelismo mal puesto es una guarda que consulta la posición de una región hermana, o un campo de contexto que una región escribe para que otra lo lea y decida. Eso es comunicación encubierta, y contradice la premisa que justificaba separarlas. Si aparece, hay tres remedios honestos y ninguno consiste en dejarlo así: anidar la dependiente dentro de la que la condiciona, fundir ambas en una sola región si la dependencia es mutua, o mantenerlas separadas y hacer explícito el vínculo con un evento enviado, que al menos deja rastro en el registro y en el inspector.
La interfaz también se factoriza
La ventaja se propaga a la capa de presentación con una fidelidad notable. Cada control de la barra del reproductor se enlaza a una sola región y solo se vuelve a renderizar cuando esa región cambia: el botón de reproducción escucha transporte, el de volumen escucha sonido, el de subtítulos escucha subtitulos. Un selector por región es más barato y, sobre todo, más claro que una condición compuesta que mezcle las cuatro dimensiones.
Un selector por región
Cada control lee la clave que le concierne. Silenciar no provoca ningún trabajo de renderizado en los controles de transporte.
Coste aditivo
Añadir una dimensión añade una región. El texto crece por suma mientras el espacio de configuraciones crece por producto.
Estado cualitativo, contexto cuantitativo
Audible o silenciado es estado; cuánto volumen es contexto. La frontera entre ambos es la frontera entre distinguir y medir.
Prueba de la indiferencia
Si un evento cambia dos regiones a la vez sin que ninguna sepa de la otra, la factorización es correcta.
Declarar un nodo paralelo parece una decisión de estructura y es en realidad una tesis sobre la naturaleza del problema: estás afirmando que estas dimensiones no se determinan entre sí, que la tabla de una se puede escribir sin conocer la otra, y que cualquier combinación de posiciones es alcanzable y coherente. Esa tesis es verificable y puede ser falsa. Cuando es verdadera, todo el sistema cobra una elegancia que se nota en sitios inesperados: los tests se escriben por región, la interfaz se reparte sin condiciones cruzadas, las funcionalidades nuevas aterrizan sin tocar lo anterior. Cuando es falsa, el modelo miente y el castigo llega tarde y disfrazado, en forma de guardas que espían regiones vecinas, campos de contexto que hacen de canal clandestino y errores que solo se manifiestan en combinaciones concretas que nadie probó porque nadie las había nombrado. Y hay una lección más honda escondida aquí, que es sobre la relación entre el espacio real y su descripción. Las ciento sesenta y ocho combinaciones del reproductor existen tanto si las escribes como si no; la complejidad del dominio es una propiedad del dominio y ninguna técnica la elimina. Lo único que puedes elegir es si tu descripción crece a la misma velocidad que el espacio que describe o si crece más despacio, y toda buena abstracción es exactamente eso: un mecanismo de compresión que preserva la fidelidad. La ortogonalidad comprime multiplicando regiones en lugar de estados, y esa compresión solo es fiel si la independencia que declara es cierta. Por eso la pregunta que decide un nodo paralelo no es cuántos estados me ahorro, sino puedo afirmar con honestidad que estas dimensiones se ignoran mutuamente.
- Construye la máquina paralela y comprueba que el valor inicial contiene las cuatro claves antes de enviar ningún evento.
- Silencia durante la reproducción y verifica que el transporte no se entera: la posición de su región queda intacta.
- Ajusta el volumen estando en silencio y decide de forma razonada si ese gesto debe restaurar el sonido o solo actualizar el valor guardado.
- Calcula cuántos nodos harían falta para modelar las cuatro dimensiones sin paralelismo y compáralo con los catorce que escribiste.
- Intenta añadir
bufferingcomo quinta región, documenta por qué falla el criterio de independencia y colócalo donde corresponde. - Introduce a propósito una guarda en la región de subtítulos que lea la posición del transporte, observa el acoplamiento y elige entre anidar, fundir o comunicar con un evento explícito.