wandres.dev
CUÁNDO UNA MÁQUINA · y cuándo no

Dónde brillan las máquinas de estado

Una máquina de estado no es una herramienta universal para gobernar estado: es la forma exacta de un tipo concreto de problema, aquel cuyo dominio ya tiene fases discretas y transiciones legales entre ellas. Esta lección mapea el terreno donde la máquina gana sin discusión —wizards, reproductores, autenticación, drag and drop— y aísla el rasgo estructural que comparten: en todos, lo que puedes hacer a continuación depende de dónde estás ahora. Aprender a reconocer esa forma es la mitad del criterio del nivel.

⏱ 16 min

Los niveles anteriores te dieron el instrumento: FSM, statecharts, XState v5, actores, guards y actions. Este nivel te da el juicio para usarlo, y empieza por la pregunta más fácil de contestar: ¿dónde brilla una máquina sin ninguna duda? La respuesta no es un dominio de aplicación —no es “formularios” ni “media”— sino una forma estructural que ciertos problemas tienen y otros no. Cuando esa forma está presente, la máquina prácticamente se escribe sola y el código sin máquina es el que resulta antinatural. Reconocer la forma es la destreza; los cuatro ejemplos canónicos solo sirven para calibrar el ojo.

🎯 Al terminar esta lección sabrás
  • Aislar el rasgo estructural común a todo problema que una máquina modela con naturalidad.
  • Reconocer wizard, reproductor, autenticación y drag and drop como statecharts latentes.
  • Entender por qué “la especificación ya se lee como un diagrama” es la señal positiva más fuerte.
  • Distinguir modelar estados —las posiciones— de modelar transiciones —las jugadas legales—, que es el valor real.

El patrón común: fases discretas y transiciones legales

Los problemas donde una máquina brilla comparten una silueta, y conviene nombrarla antes que los ejemplos. Primero, hay un conjunto pequeño y enumerable de modos mutuamente excluyentes: el sistema está en exactamente uno a la vez, nunca en dos, nunca en ninguno. Segundo —y esto es lo decisivo— existen reglas sobre qué modo puede seguir a cuál. No es que haya estados; es que las aristas entre ellos están restringidas. Desde pausado puedes ir a reproduciendo o a detenido, pero no puedes “cargar” de nuevo sin más; ese “no puedes” es exactamente lo que la máquina codifica y lo que un puñado de booleanos jamás protege.

El valor, entonces, no vive en los estados. Un simple tipo unión 'inactivo' | 'cargando' | 'reproduciendo' ya te da las posiciones. El valor vive en las transiciones: en que “lo que puedes hacer a continuación depende con nitidez de dónde estás ahora”. Cuando esa dependencia es fuerte, tienes un problema con forma de máquina. Cuando no la hay —cuando cualquier campo puede cambiar en cualquier momento con independencia de los demás, como en un formulario de ajustes— tienes un registro, no una máquina, y forzar la máquina ahí solo añade ceremonia.

Conviene nombrar el invariante que lo hace posible: en un problema con forma de máquina el sistema está en exactamente un estado a la vez, ni en cero ni en dos. Esa exclusión mutua no es un detalle de implementación; es lo que permite razonar el flujo como una sucesión y no como una nube de banderas simultáneas que hay que mantener coherentes a mano. Cuando de verdad necesitas más de un estado activo en paralelo —un reproductor que a la vez gestiona la reproducción y la descarga de subtítulos— no abandonas la forma de máquina: subes a un statechart con regiones ortogonales, la generalización jerárquica del nivel anterior. La familia entera, del enum plano al statechart, cubre desde un estado a la vez hasta la jerarquía; lo único que ninguna variante cubre bien es la ausencia total de reglas.

ℹ️
Estados frente a protocolos

Un enum captura estados; una máquina captura un protocolo. La diferencia es la lista de transiciones legales. Si tu problema es “el dato está en uno de estos valores” sin reglas sobre el paso de uno a otro, un enum en un store basta. Si es “desde aquí solo se puede ir allá o allá”, el protocolo es el que manda, y ese protocolo es una máquina te guste o no: la pregunta es solo si lo escribes explícito o lo dejas implícito y disperso en condicionales.

Cuatro dominios donde la máquina gana

Los ejemplos clásicos no son casualidad: los cuatro son protocolos de interacción, secuencias con reglas, y todo protocolo de interacción es un statechart latente.

🧭

Wizard multipaso

Cada paso es un estado; siguiente, atras y enviar son eventos. La validación es un guard que decide si la transición procede. La máquina impide el bug clásico: saltar al paso tres sin haber completado el uno.

▶️

Reproductor multimedia

inactivo, cargando, reproduciendo, pausado, terminado, con buffering y seeking anidados. El dominio prohíbe reproducir lo que aún no cargó; la máquina lo hace irrepresentable en vez de vigilarlo con if.

🔐

Autenticación

anonimo, autenticando, sesion, refrescando, error, con un reto MFA como subestado. El flujo tiene cancelaciones, reintentos y expiraciones: aristas con reglas, justo lo que una máquina expresa sin esfuerzo.

Drag and drop

reposo, arrastrando, sobre destino, soltando, con Escape que cancela desde cualquier punto intermedio. El gesto es una secuencia estricta con un evento de aborto global: un statechart de manual.

Tomemos el reproductor como espécimen y escribámoslo con la API setup de XState v5. Fíjate en que la definición es la lista de reglas del dominio, casi palabra por palabra.

import { setup } from 'xstate'

export const reproductor = setup({
  types: {
    context: {} as { posicion: number },
    events: {} as
      | { type: 'CARGAR' }
      | { type: 'PLAY' }
      | { type: 'PAUSA' }
      | { type: 'FIN' },
  },
}).createMachine({
  id: 'reproductor',
  initial: 'inactivo',
  context: { posicion: 0 },
  states: {
    inactivo:      { on: { CARGAR: 'cargando' } },
    cargando:      { on: { PLAY: 'reproduciendo' } },
    reproduciendo: { on: { PAUSA: 'pausado', FIN: 'terminado' } },
    pausado:       { on: { PLAY: 'reproduciendo' } },
    terminado:     { on: { CARGAR: 'cargando' } },
  },
})

Lo que no está escrito importa tanto como lo que sí: enviar PLAY estando en inactivo no hace nada, porque esa arista no existe. La máquina ignora los eventos ilegales por construcción, sin un solo if. Ese silencio ante lo imposible es precisamente el trabajo que en la versión de booleanos harías a mano, mal y de forma incompleta.

flowchart LR
I[inactivo] -->|CARGAR| C[cargando]
C -->|PLAY| R[reproduciendo]
R -->|PAUSA| P[pausado]
P -->|PLAY| R
R -->|FIN| T[terminado]
T -->|CARGAR| C
style I fill:#89dceb,color:#11111b
style R fill:#a6e3a1,color:#11111b
style P fill:#f9e2af,color:#11111b
style T fill:#f38ba8,color:#11111b

Los otros tres dominios se leen igual, y cada uno ilumina una pieza que en el código de banderas se dispersa. El wizard es esta misma tabla con los estados llamados pasos y un guard de validación en cada arista de avance. La autenticación aporta un matiz instructivo: la expiración de la sesión es una transición que no dispara el usuario sino el tiempo, un evento que llega solo, y escribirla como arista explícita evita el clásico agujero de seguir mostrando datos privados después de que el token caducó. El drag and drop aporta el rasgo contrario: el Escape que cancela desde cualquier punto intermedio es una transición global, una arista que sale de todos los estados de arrastre hacia reposo, escrita una sola vez en lugar de repetida en cada manejador.

// Wizard: los pasos son estados; avanzar es una transicion con guard.
const checkout = setup({
  types: { context: {} as { pagoValido: boolean } },
}).createMachine({
  id: 'checkout',
  initial: 'envio',
  context: { pagoValido: false },
  states: {
    envio: { on: { SIGUIENTE: 'pago' } },
    pago: {
      on: {
        SIGUIENTE: { target: 'revision', guard: ({ context }) => context.pagoValido },
        ATRAS: 'envio',
      },
    },
    revision: { on: { CONFIRMAR: 'hecho', ATRAS: 'pago' } },
    hecho: {},
  },
})

El guard en pago es la transcripción literal de la frase del diseño “no se avanza a revisión sin un pago válido”. Sin máquina, esa regla vive como un if suelto en un manejador de clic, fácil de olvidar en el segundo botón que también avanza; con máquina, la condición está en la única arista que puede avanzar, y no hay segundo botón que se la salte.

La prueba de la servilleta

Hay un test de campo, barato y sorprendentemente fiable, para saber si estás ante un problema con forma de máquina: dibújalo en una servilleta como cajas y flechas y enséñaselo a alguien que no programa. Si el diseñador o el product manager asienten y dicen “sí, y desde ahí también puede pasar esto”, tienes una máquina. Si en cambio te miran raro porque el problema “no va por pasos”, no la tienes.

La razón profunda es que en estos dominios la especificación ya se lee como un diagrama. El documento de requisitos dice, en prosa, “mientras el usuario se está autenticando, el botón de entrar queda deshabilitado y solo se permite cancelar”. Eso es una transición con un guard y un conjunto de eventos habilitados; está ahí, escrito, antes de que toques el editor. La máquina es la transcripción fiel de esa frase. El código lleno de banderas booleanas es la versión con pérdida: pierde justo la información sobre qué es legal y qué no, y esa pérdida reaparece luego como bugs de estados imposibles.

Detrás de la servilleta hay una razón epistemológica. Los cuatro dominios comparten que su especificación precede al código y ya viene escrita en el lenguaje de estados y eventos: el diseñador dibujó pantallas unidas por flechas, el product manager describió pasos, el equipo de seguridad enumeró los estados de una sesión. La máquina no inventa esa estructura, la importa desde donde ya vivía, y por eso un statechart bien hecho suele poder revisarlo alguien que no programa: es el mismo artefacto que ya tenía en la cabeza, ahora ejecutable. El caso opuesto es la señal de alarma: cuando tienes que inventar los estados desde cero porque nadie había pensado el problema así, sospecha que estás imponiendo una forma que el dominio no pedía.

💡
El requisito ya es el statechart

Antes de decidir la herramienta, subraya en la especificación cada frase del tipo “mientras X, solo se puede Y” o “desde X únicamente se llega a Z”. Si encuentras tres o más, no estás decidiendo si usar una máquina: estás decidiendo si escribir la que el dominio ya definió por ti o dejarla implícita. La máquina no añade complejidad al problema; hace visible la que ya tenía.

Conviene un matiz para no caer en el triunfalismo. Que un problema tenga forma de máquina no obliga a traer XState: obliga a modelar el protocolo con honestidad, y eso a veces se hace con veinte líneas a mano (lección 4). Lo que estos cuatro dominios garantizan es que el modelo de máquina es el correcto; la elección de librería frente a implementación manual es un segundo eje, y el coste de equivocarse en él lo veremos en la lección siguiente. Aquí basta con haber calibrado el ojo: protocolo de interacción, transiciones con reglas, especificación que se dibuja. Esa es la forma.

El contraejemplo que afila el criterio

El criterio se entiende mejor por su negación. Piensa en un formulario de preferencias: nombre, idioma, tema claro u oscuro, notificaciones activadas o no. Cuatro campos que cambian con independencia, en cualquier orden, sin que el valor de uno restrinja los valores legales de otro. No hay modos mutuamente excluyentes ni transiciones con reglas: hay un registro cuyos campos se editan a voluntad. Modelarlo como máquina obligaría a un estado por cada combinación —la explosión 2^n que la máquina justamente evita en otros contextos— o a un único estado con todo delegado en el context, que es admitir que no había máquina. El formulario es un store, no un statechart.

La moraleja es precisa: la forma de máquina no la determina la cantidad de estado ni su importancia, sino la estructura de las transiciones. Datos que cambian con independencia unos de otros son un registro por muchos que sean; datos cuyo próximo valor legal depende del actual son una máquina aunque sean solo tres. Confundir ambos produce los dos errores gemelos del nivel: encerrar en un statechart lo que era un formulario, y dejar en banderas sueltas lo que era un protocolo. Toda la destreza del criterio cabe en afinar esta única distinción —independencia frente a dependencia entre transiciones— antes de mirar ninguna librería.

La máquina no es potente: es honesta

La tentación es pensar que la máquina brilla porque es una herramienta poderosa que domina casos difíciles. La verdad es más humilde y más profunda: la máquina brilla porque es la representación más honesta de un dominio que tiene prohibiciones. Su virtud no es hacer más, sino negarse a representar lo que el dominio no permite. Donde existen jugadas ilegales —reproducir sin cargar, avanzar sin validar, refrescar sin sesión— la máquina es simplemente la forma más barata de volver esas jugadas irrepresentables en lugar de vigilarlas con condicionales que siempre olvidan un caso. Por eso los cuatro ejemplos canónicos no son un catálogo arbitrario: son todos protocolos de interacción, y un protocolo es, por definición, un conjunto de secuencias permitidas dentro de un conjunto mayor de secuencias imaginables. Modelar ese protocolo con una máquina no impone una estructura ajena al problema; extrae la estructura que el problema ya tenía y que el código de banderas escondía. Cuando interiorizas esto dejas de preguntar “¿debería usar una máquina aquí?” y empiezas a preguntar “¿este problema ya es una máquina y yo estaba fingiendo que no?”. La mayoría de las veces que la respuesta duele, es que sí.

⚔️ Calibra el ojo con un flujo real
  1. Toma un flujo de tu app —un checkout, un onboarding, un modal de subida de archivo— y dibuja en papel sus estados como cajas.
  2. Traza solo las transiciones legales. Marca en rojo las flechas que tu código actual permite pero que el dominio prohíbe: cada una es un bug latente.
  3. Busca en la especificación o en el diseño tres frases del tipo “mientras X, solo Y”. Comprueba que cada una corresponde a un estado con un conjunto restringido de eventos.
  4. Escribe la máquina con setup de XState v5 para uno de los cuatro dominios del texto y verifica que enviar un evento ilegal no produce ningún cambio de estado.
  5. Enséñale tu servilleta a alguien que no programe. Si entiende el flujo y te corrige una transición, acabas de comprobar que el problema tenía forma de máquina.