Diseñar la jerarquía: cuándo anidar, cuándo aplanar
La jerarquía es una herramienta con coste, y la tentación de quien acaba de dominarla es cobrarse ese coste sin recibir el beneficio. Esta lección fija los tres únicos motivos que justifican un nivel de anidamiento —factorizar transiciones compartidas, dar ámbito a un recurso y expresar una terminación colectiva—, cataloga el olor a máquina sobre-anidada con sus síntomas observables, y ofrece los tres remedios disponibles: aplanar el nivel inútil, extraer el subárbol a una máquina invocada, o convertir la exclusividad forzada en regiones ortogonales. El cierre del nivel es un criterio, no una técnica: cada nivel debe pagar su alquiler.
Las cuatro lecciones anteriores te dieron poder: anidar, comprobar por prefijo, abrir regiones y cruzar niveles con control fino del ciclo de vida. Esta última lección existe para que ese poder no se convierta en daño. La jerarquía tiene un coste real y a menudo invisible: cada nivel añadido alarga los caminos, complica las comprobaciones externas, multiplica las rutas de entrada y salida que hay que razonar y aleja el diagrama de lo que el dominio de verdad distingue. Un nivel que no factoriza nada, que no da ámbito a ningún recurso y que no expresa ninguna terminación colectiva es puro impuesto. La disciplina que cierra este nivel se resume en una frase: cada nivel de anidamiento debe pagar su alquiler, y si no lo paga, se va.
- Justificar un nivel de anidamiento con uno de los tres motivos legítimos.
- Diagnosticar el olor a sobre-anidación a partir de síntomas observables en el código.
- Elegir entre aplanar, extraer a una máquina invocada o convertir en regiones paralelas.
- Situar la frontera entre una submáquina anidada y un actor invocado independiente.
Los tres motivos que pagan el alquiler
Un nivel se justifica si y solo si hace al menos una de estas tres cosas. Ninguna de ellas es estética, y ninguna tiene que ver con que el diagrama quede más ordenado.
Factorizar transiciones
Varios hijos comparten manejadores para los mismos eventos. Declararlos una vez en el padre elimina aristas duplicadas y las hereda cualquier hijo futuro.
Dar ámbito a un recurso
Un invoke, un after o una pareja entry y exit deben vivir exactamente mientras el sistema esté dentro del grupo, sobreviviendo a los movimientos entre hermanos.
Expresar terminación colectiva
El grupo tiene un desenlace propio que se comunica hacia arriba con onDone, y el padre necesita saber que ese subproceso acabó sin conocer sus detalles.
Lo que NO cuenta
Que los nombres compartan prefijo. Que el diagrama quede más bonito. Que el dominio se describa con esas palabras en una reunión. Nada de eso es una razón.
Los tres motivos tienen algo en común que conviene nombrar: los tres son comprobables. Puedes verificar si dos hijos comparten manejadores leyendo sus claves on; puedes verificar si un recurso debe sobrevivir a los movimientos entre hermanos preguntándote qué pasaría si no lo hiciera; puedes verificar si el grupo tiene desenlace propio buscando quién consume su onDone. Ninguno depende de la sensibilidad del autor, y esa es exactamente la propiedad que necesitas en un criterio que va a tener que sobrevivir a discusiones de equipo.
La cuarta tarjeta merece énfasis porque es donde se pierde la mayoría de la gente. Tener cuatro estados que empiezan por la misma palabra no es motivo para anidarlos: es motivo para preguntarse si comparten algo más que la palabra. Si la respuesta es que no comparten ni eventos, ni recursos, ni desenlace, entonces el prefijo era una coincidencia de nomenclatura y el nivel que ibas a crear sería un nodo de paso, un peaje sin carretera debajo.
El olor a máquina sobre-anidada
La sobre-anidación tiene síntomas observables que puedes buscar mecánicamente en tu código, sin necesidad de juicio estético.
| Síntoma | Qué indica | Remedio probable |
|---|---|---|
| Compuesto con un único hijo | agrupación sin nada que factorizar | aplanar |
Nivel sin on, sin entry, sin exit y sin invoke propios |
nodo de paso puro | aplanar |
Casi todo matches usa cuatro o más segmentos |
la profundidad no abstrae nada | aplanar o extraer |
Rutas completas unidas con || en cada vista |
los prefijos no son significativos | reorganizar los niveles |
| Muchos objetivos absolutos con almohadilla | la jerarquía estorba en vez de ayudar | extraer o partir |
Regiones paralelas que se espían el context |
ortogonalidad falsa | fundir o comunicar con eventos |
El primero de la lista es también el más fácil de arreglar y el que más aparece. Un compuesto con un solo hijo no factoriza nada, porque factorizar exige al menos dos elementos entre los que repartir lo común; lo único que hace es alargar en un segmento todos los patrones que lo atraviesan. Ocurre casi siempre por anticipación: el autor previó que ese grupo tendría hermanos algún día y creó la caja antes de tener con quién compartirla.
// ANTES: un nivel que no factoriza nada. Todo patron gana un segmento inutil.
sesion: {
initial: 'activa',
states: {
activa: { initial: 'ocioso', states: { ocioso: {}, trabajando: {} } },
},
}
// DESPUES: el nivel se va y los patrones se acortan sin perder informacion.
sesion: {
initial: 'ocioso',
states: { ocioso: {}, trabajando: {} },
}
El síntoma más diagnóstico es el cuarto. Si los consumidores de tu máquina nunca pueden preguntar por un prefijo, sino que siempre necesitan enumerar hojas, es que tus nodos intermedios no corresponden a ninguna distinción que le importe a nadie. Un buen nivel intermedio es aquel por el que alguien pregunta: si nadie pregunta jamás por él, no está abstrayendo nada y solo alarga los caminos.
No hay un límite formal, pero la experiencia acumulada apunta a que más allá de tres niveles de profundidad la máquina deja de ser legible de un vistazo y el diagrama pierde su virtud principal, que era servir de documentación viva. Cuando llegues al cuarto nivel, la pregunta correcta casi nunca es cómo organizar mejor la anidación, sino si ese subárbol profundo no era en realidad otra máquina distinta que ha crecido dentro de esta por comodidad. La profundidad excesiva suele ser una frontera de responsabilidad que nadie se atrevió a trazar.
Aplanar, extraer o partir
Diagnosticado el problema, hay exactamente tres remedios y conviene saber cuál aplica.
flowchart TD A[el nivel factoriza eventos, recursos o un onDone?] -->|si| B[el nivel se queda: paga su alquiler] A -->|no| C[el subarbol tiene eventos propios y un resultado?] C -->|no| D[aplanar: subir los hijos un nivel] C -->|si| E[las dimensiones son independientes?] E -->|no| F[extraer a una maquina invocada con invoke] E -->|si| G[partir en regiones paralelas] style B fill:#a6e3a1,color:#11111b style D fill:#89b4fa,color:#11111b style F fill:#cba6f7,color:#11111b style G fill:#fab387,color:#11111b
El árbol se recorre por nodo, no por máquina: la pregunta se hace sobre cada nivel intermedio por separado, y es perfectamente normal que en una misma máquina un nivel se quede, otro se aplane y un tercero se extraiga. Auditar así, nodo a nodo, evita la refactorización total que nadie termina nunca.
Aplanar es el remedio por defecto y el más barato: los hijos suben un nivel, sus nombres se hacen algo más largos si hace falta, y las comprobaciones externas se acortan. Extraer es el remedio cuando el subárbol tiene entidad propia, y la frontera para decidirlo es bastante nítida. Un subárbol merece convertirse en máquina invocada si maneja eventos que el padre nunca necesita ver, si tiene un resultado bien definido que puede devolver como salida, y si su ciclo de vida coincide con el de un estado del padre. Si en cambio comparte eventos con sus hermanos y necesita reaccionar a lo que ocurre fuera, debe quedarse anidado, porque extraerlo obligaría a reenviar mensajes a mano y estarías reconstruyendo el burbujeo con peor sintaxis.
// ANTES: subarbol profundo dentro del padre, con eventos que el padre no usa.
pago: {
initial: 'tarjeta',
states: {
tarjeta: {
initial: 'datos',
states: {
datos: { on: { SIGUIENTE: 'tresDominios' } },
tresDominios: { on: { OK: 'listo' } },
listo: { type: 'final' },
},
},
},
}
// DESPUES: el subarbol es una maquina propia con su salida, invocada por el padre.
pago: {
invoke: {
src: 'maquinaTarjeta',
onDone: { target: 'confirmada', actions: 'guardarComprobante' },
onError: 'rechazado',
},
on: { CANCELAR: 'carrito' },
}
El tercer remedio, partir en regiones, aplica a un error de signo contrario: haber impuesto exclusividad donde el dominio no la tenía. Se reconoce por los nombres compuestos que mezclan dos dimensiones en un solo estado, del tipo editandoConCambiosSinGuardar, y por el hecho de que el número de estados crece como producto en vez de como suma cuando aparece un caso nuevo. Ahí no sobra un nivel, sobra la exclusividad, y la cura es declarar dos regiones ortogonales.
La versión de después no es más corta por casualidad. Al extraer, el padre deja de conocer los tres subestados internos del pago con tarjeta y pasa a conocer solo dos hechos: que terminó bien o que falló. Eso es encapsulación real, con una interfaz explícita —la entrada del actor y su salida— en lugar de una jerarquía transparente donde todo el mundo puede apuntar a todo con un identificador absoluto.
Resume la decisión en dos preguntas. ¿Los hermanos del subárbol necesitan reaccionar a los mismos eventos que él? Si sí, anidar, porque el burbujeo es precisamente el mecanismo que te ahorra escribirlos dos veces. ¿El subárbol produce un resultado que el padre consume y nada más? Si sí, extraer, porque un resultado bien definido es la firma de una unidad con interfaz propia. Cuando ambas respuestas son afirmativas a la vez, lo habitual es que el modelo tenga un problema de fronteras más profundo y convenga replantear qué pertenece a quién antes de elegir la sintaxis.
El error de fondo que produce máquinas sobre-anidadas no es técnico sino epistemológico: consiste en usar la estructura del statechart para organizar el pensamiento del autor en lugar de para reflejar la estructura del problema. La jerarquía se convierte entonces en una taxonomía privada, un sistema de carpetas donde el autor agrupa lo que a él le resulta parecido, y el resultado tiene la misma patología que cualquier taxonomía impuesta: es cómoda para quien la creó e ininteligible para todos los demás, porque codifica asociaciones mentales en vez de hechos del dominio. La prueba decisiva es sencilla y despiadada: cada nodo intermedio de tu máquina debería corresponder a una afirmación verdadera sobre el sistema que alguien externo querría comprobar. Si existe un nodo por el que nadie pregunta nunca, ese nodo no está describiendo el mundo, está describiendo cómo ordenaste tú tus ideas mientras escribías. Y aquí aparece la simetría con todo lo que este nivel te ha enseñado, porque los tres motivos legítimos para anidar son justamente los tres modos en que un grupo de estados puede ser real y no imaginario: comparten cómo reaccionan, comparten qué recursos sostienen, o comparten un desenlace. Reaccionar, sostener y terminar juntos son formas verificables de pertenecer al mismo grupo; llamarse parecido no lo es. Esa distinción entre agrupación verificable y agrupación mental es la que separa una máquina que sirve de documentación viva de una que necesita a su autor sentado al lado para poder leerse. Y como toda buena disciplina de diseño, esta se aplica en la dirección conservadora: ante la duda, el nivel se elimina, porque añadirlo mañana cuesta una refactorización local, mientras que quitarlo cuando ya hay veinte comprobaciones externas que dependen de su profundidad cuesta bastante más que eso.
- Toma la máquina más grande que tengas y anota, para cada nodo intermedio, cuál de los tres motivos legítimos justifica su existencia. Marca los que no tengan ninguno.
- Busca compuestos con un único hijo y aplánalos; comprueba que ninguna comprobación externa se rompe salvo por la longitud del patrón.
- Cuenta cuántos segmentos tiene el patrón medio de tus llamadas a
matches. Si la mayoría llega a la hoja, investiga qué nivel intermedio no está abstrayendo nada. - Localiza todos los objetivos absolutos escritos con almohadilla y clasifica cada uno como salto legítimo o como síntoma de que la jerarquía estorba.
- Elige un subárbol candidato y contesta las dos preguntas de la frontera: si tiene resultado propio y eventos que el padre no necesita, extráelo a una máquina invocada y mide cuánto se simplifica el padre.
- Busca una exclusividad forzada —estados con nombres compuestos que mezclan dos dimensiones— y conviértela en dos regiones paralelas; cuenta cuántos nodos desaparecen.