wandres.dev
NIVEL DIOS: SÍNTESIS · pensar en máquinas

Modelar cualquier dominio: del requisito ambiguo al statechart revisable

Un requisito redactado en prosa no contiene los estados: contiene verbos, pantallas y sustantivos que los esconden. Esta lección convierte el modelado en un procedimiento repetible de siete movimientos que va del enunciado ambiguo a un statechart que otra persona puede revisar, define la criba que separa un modo de un dato y de un derivado, muestra el recorrido completo sobre un caso real de devolución de pedido, y fija los cinco criterios que permiten declarar un modelo terminado sin autoengañarse.

⏱ 21 min

La habilidad que separa a quien usa máquinas de estado de quien piensa con ellas no es conocer la API ni recordar la semántica de las transiciones que cruzan niveles: es mirar un párrafo escrito por alguien de producto y ver dentro el grafo. Esa habilidad tiene fama de intuición y de talento, y no lo es en absoluto; es un procedimiento que se puede escribir, enseñar y auditar. Lo que ocurre es que casi nadie lo ha escrito, y por eso cada persona lo reinventa mal, empezando por donde no se debe empezar —dibujando cajas— en lugar de por donde el método realmente arranca: por el inventario de lo que puede ocurrirle al sistema sin que nadie toque la pantalla. Este es ese procedimiento, en orden, con sus criterios de parada y con la advertencia de dónde falla.

🎯 Al terminar esta lección sabrás
  • Extraer estados de un requisito en prosa que no los menciona, usando el vacío del enunciado en lugar de sus palabras.
  • Aplicar los siete movimientos del método en su orden, sabiendo qué desbloquea cada uno.
  • Cribar cada candidato con la prueba de tres vías que decide si es un modo, un dato o un derivado.
  • Declarar terminado un modelo con criterios explícitos y entregarlo en estado revisable por otra persona.

El requisito no contiene los estados

Tomemos un enunciado real, del tipo que llega en una tarjeta de trabajo: el cliente puede devolver un pedido dentro de los treinta días siguientes a la entrega, siempre que el artículo no sea perecedero; nosotros revisamos la devolución y, si procede, reembolsamos. Léelo despacio y busca un estado. No hay ninguno. Hay un actor, un objeto, un plazo, una condición y dos verbos. El grafo no está en el texto: está en su negativo, en todo lo que el texto da por resuelto sin decir cómo.

El negativo se interroga con tres preguntas y solo tres. La primera: ¿qué puede estar ocurriendo ahora mismo que impida esto? Aplicada al enunciado produce de golpe media máquina, porque revela que mientras revisamos la devolución el cliente no puede iniciar otra, y que mientras el reembolso está en curso la devolución ya no se puede cancelar. La segunda: ¿qué tiene que ser cierto antes? Produce las precondiciones que casi siempre son estados previos disfrazados: el pedido tiene que estar entregado, y por tanto existe un estado entregado, y por tanto existe todo el ciclo anterior. La tercera: ¿quién más puede actuar, y cuándo? Produce los eventos que no origina el usuario y que son la mitad olvidada de todo modelo: el plazo que vence, la persona que revisa, el pasarela que responde, el almacén que recibe el paquete.

Hay además una trampa lingüística que conviene desactivar antes de empezar. El lenguaje de negocio nominaliza los estados: dice una solicitud pendiente, un pedido en curso, una cuenta suspendida. Esos sustantivos con adjetivo son estados que ya existen en la cabeza de quien escribió el requisito y que aparecen en la conversación pero no en la especificación, porque quien los usa cree que son evidentes. Cazarlos es el trabajo más rentable de la primera media hora: cada adjetivo aplicado a una entidad es un candidato a estado, y preguntar en voz alta qué se puede y qué no se puede hacer con una solicitud pendiente suele descubrir que hay dos situaciones distintas debajo del mismo adjetivo.

💡
Empezar dibujando cajas es empezar por el final

La tentación universal es abrir el editor de diagramas y colocar las cuatro cajas obvias. El problema es que las cuatro cajas obvias son las del camino feliz, y a partir de ahí el modelo crece por acumulación defensiva: cada caso raro que aparece se cuelga como puede de la estructura que ya está dibujada. Los modelos hechos así se reconocen a simple vista porque tienen un estado con seis flechas de salida y una nube de estados terminales inconexos. El orden correcto es el inverso: primero el inventario de eventos, incluidos los del mundo; después los modos; y solo al final la forma del dibujo, que a esas alturas se impone sola.

El método en siete movimientos

Los movimientos van en este orden porque cada uno produce el material que el siguiente necesita. Saltarse el segundo es la causa del noventa por ciento de los modelos incompletos.

flowchart TD
A[1 separar entidades de adjetivos que esconden modos] --> B[2 enumerar eventos del usuario y del mundo]
B --> C[3 rellenar la matriz de modo por evento sin celdas vacias]
C --> D[4 renombrar en lenguaje de dominio no de implementacion]
D --> E[5 factorizar lo repetido hacia un estado padre]
E --> F[6 separar en regiones lo que es realmente independiente]
F --> G[7 elegir los estados finales y que entrega cada uno]
G --> H{cabe en una pantalla y lo entiende producto}
H -->|no| C
H -->|si| I[modelo revisable]
style I fill:#a6e3a1,color:#11111b
style B fill:#f9e2af,color:#11111b

El movimiento uno separa entidades de modos. En el enunciado hay un pedido, una devolución y un reembolso: tres entidades, y por tanto potencialmente tres máquinas, no una. Confundir entidades es el error estructural más caro y se comete al principio; la señal de alarma es un estado cuyo nombre contiene una conjunción, del tipo pedido entregado y devolución en revisión, que es siempre un producto cartesiano de dos máquinas que deberían estar separadas.

El movimiento dos enumera eventos, y aquí está la mitad del valor del método. Escribe dos columnas: lo que puede hacer el usuario y lo que puede hacer el mundo. La segunda columna es la que nadie escribe y la que produce los fallos raros de producción: el plazo de treinta días que vence, la respuesta del proveedor de pagos que llega tarde, el operador que revisa desde el panel interno, el paquete que se recibe físicamente, la sesión que caduca, el mismo cliente actuando desde otra pestaña. Un modelo cuya columna del mundo está vacía no es un modelo: es una descripción de la interfaz.

El movimiento tres rellena la matriz de modo por evento con la regla que ya conoces: ninguna celda en blanco y ninguna marcada como no aplicable. Las opciones legales son exactamente cuatro, y elegir explícitamente entre ellas es donde el modelado se gana el sueldo.

➡️

Transicionar

El evento es legal aquí y mueve el sistema. Es la única opción que dibuja una flecha, y por eso es la que se sobreusa: no todo evento válido debe cambiar de estado.

🔇

Ignorar en silencio

El evento llega y no significa nada en esta situación. Es la respuesta correcta a la segunda pulsación del botón de pagar, y evita el doble cobro sin una sola línea de defensa.

🚫

Rechazar con mensaje

El evento no es legal y el usuario debe enterarse. Distinto de ignorar: aquí hay una acción de salida hacia la interfaz aunque el estado no cambie.

📥

Encolar para después

El evento importa pero ahora no se puede atender. Exige un sitio donde guardarlo y una decisión sobre cuándo se drena; es la opción más cara y la que más se elige a la ligera.

La distinción entre ignorar y rechazar parece cosmética y es de producto: ignorar afirma que la acción no tenía sentido, rechazar afirma que el usuario se equivocó. Un sistema que ignora cuando debía rechazar deja a la gente pulsando un botón que aparentemente no hace nada; uno que rechaza cuando debía ignorar acusa al usuario de un error que no cometió.

El movimiento cuatro renombra. Un estado llamado esperandoRespuestaDelServicioDeReembolsos es correcto y es un fracaso: describe la implementación, envejece con ella y expulsa a producto de la conversación. reembolso_en_curso dice lo mismo en el idioma del dominio y sobrevive a tres cambios de proveedor.

Los movimientos cinco y seis dan forma. Si la misma celda se repite en muchas filas, esa transición pertenece a un padre: es jerarquía. Si dos grupos de modos avanzan sin mirarse —el estado de la revisión y el estado de la comunicación con el cliente—, son regiones ortogonales y forzarlos a un solo eje produce la explosión combinatoria que Harel vino a resolver.

El movimiento siete cierra. Cada estado final responde a dos preguntas: qué significa que esto terminó así, y qué entrega al que lo invocó. Un modelo sin finales explícitos es un modelo que nadie puede componer.

La criba: modo, dato o derivado

El error de modelado más frecuente no es olvidar un estado: es ascender a estado algo que era un dato, o guardar como dato algo que era un modo. La criba es una prueba de tres vías y se aplica a cada candidato por separado.

Primera vía, la decisiva: ¿cambia el conjunto de acciones legales? Si estando en esa situación el usuario puede hacer cosas distintas de las que puede hacer fuera de ella, es un modo. Si el usuario puede hacer exactamente lo mismo y solo cambia lo que ve, es un dato. Segunda vía: ¿lo puedo calcular a partir de otra cosa que ya tengo? Si sí, es un derivado y guardarlo es abrir la puerta a que las dos copias diverjan. Tercera vía, la de la cardinalidad: ¿el conjunto de valores posibles es cerrado, pequeño y enumerable? Los modos son pocos y tienen nombre; lo que tiene mil valores posibles es contexto por definición.

Candidato Vía 1: acciones Vía 2: calculable Veredicto
La devolución está en revisión sí, no se puede cancelar no modo
El cliente es premium no, solo cambia el plazo no, viene de la cuenta dato en el contexto
Quedan doce días de plazo no por sí mismo sí, desde la fecha de entrega derivado, no guardar
El plazo ha vencido sí, ya no se puede devolver sí | no, depende del reloj modo, con evento del mundo
Hay tres artículos devueltos no no dato en el contexto

La cuarta fila merece atención porque es el caso que más discusiones genera y donde la criba parece contradecirse. El vencimiento del plazo es calculable a partir de la fecha, luego parecería un derivado; pero cambia radicalmente lo que el usuario puede hacer, luego es un modo. La resolución es la que el modelado formal exige siempre: si un cambio de valor debe producir un cambio de comportamiento sin que nadie actúe, hace falta un evento que lo anuncie. El reloj es parte del mundo, y el paso del tiempo se modela como evento, no como una guarda que se evalúa cuando alguien pasa por ahí. Un modelo que decide si el plazo venció solo en el momento en que el usuario pulsa un botón está delegando en la casualidad la corrección de una regla de negocio.

⚠️
El síntoma de haber cribado mal

Si tu máquina tiene más de una docena de estados y ninguna región paralela, casi con seguridad ascendiste datos a estados y estás describiendo el producto cartesiano a mano; los nombres largos con conjunciones lo delatan. Si tu máquina tiene tres estados y un contexto de quince campos con guardas complejas sobre ellos, hiciste lo contrario: escondiste el comportamiento en los datos y el grafo ya no dice la verdad sobre lo que el sistema hace. Ambas patologías se diagnostican contando, y ambas se curan volviendo al movimiento tres con la criba en la mano.

Del enunciado a la máquina revisable

Aplicado el método al enunciado inicial, el resultado cabe en una pantalla y se lee en voz alta sin traducir.

const devolucion = setup({
  types: {} as {
    context: { pedidoId: string; motivo: string | null; intentosReembolso: number }
    events:
      | { type: 'SOLICITAR'; motivo: string }
      | { type: 'CANCELAR' }
      | { type: 'APROBAR' }
      | { type: 'RECHAZAR'; causa: string }
      | { type: 'PAQUETE_RECIBIDO' }
      | { type: 'PLAZO_VENCIDO' }
  },
}).createMachine({
  id: 'devolucion',
  initial: 'elegible',
  context: { pedidoId: '', motivo: null, intentosReembolso: 0 },
  states: {
    elegible: {
      on: {
        SOLICITAR: { target: 'solicitada', actions: 'guardarMotivo' },
        PLAZO_VENCIDO: 'fuera_de_plazo',
      },
    },
    solicitada: {
      on: { CANCELAR: 'cancelada', PAQUETE_RECIBIDO: 'en_revision' },
    },
    en_revision: {
      on: { APROBAR: 'reembolsando', RECHAZAR: 'rechazada' },
    },
    reembolsando: {
      invoke: { src: 'reembolsar', onDone: 'reembolsada', onError: 'reembolso_fallido' },
    },
    reembolsada: { type: 'final' },
    rechazada: { type: 'final' },
    cancelada: { type: 'final' },
    fuera_de_plazo: { type: 'final' },
    reembolso_fallido: { on: { APROBAR: 'reembolsando' } },
  },
  output: ({ context }) => ({ pedidoId: context.pedidoId }),
})

Obsérvese lo que el método impuso y que nadie habría escrito de memoria. PLAZO_VENCIDO es un evento y no una guarda, porque el tiempo es del mundo. PAQUETE_RECIBIDO existe porque la columna del mundo lo produjo, aunque el enunciado no lo mencionaba: no se revisa lo que no ha llegado. Y reembolso_fallido no es un final, porque un reembolso que falla no cierra nada; es una situación desde la que se puede volver a intentar, y modelarlo como final habría dejado dinero de clientes atrapado en un estado sin salida.

Un modelo está terminado cuando pasa cinco criterios, y no antes. Uno, cabe en una pantalla sin desplazamiento; si no cabe, hay dos entidades mezcladas. Dos, una persona de producto lee los nombres de los estados y no pregunta qué significa ninguno. Tres, no queda ninguna celda de la matriz sin decisión escrita. Cuatro, todo estado no final tiene al menos una salida y todo estado es alcanzable desde el inicial. Cinco, ningún nombre menciona una tecnología, un componente ni un verbo de infraestructura. Un modelo que pasa los cinco es revisable, y revisable significa exactamente esto: otra persona puede encontrarle un error sin preguntarte nada.

Modelar no es describir un dominio: es obligar al dominio a decidirse

Existe la creencia cómoda de que el modelado es una actividad de transcripción, en la que alguien que entiende el negocio explica cómo funcionan las cosas y alguien que entiende la notación lo dibuja. Esa creencia es falsa y es la razón por la que tantos modelos salen mal hechos por gente competente. La verdad incómoda es que el dominio, tal como existe antes del modelo, está lleno de zonas donde nadie ha decidido nada, y lo está por una razón económica perfectamente racional: decidir cuesta, y mientras la ambigüedad no se topa con una máquina que exige una respuesta, sale gratis mantenerla. Un requisito en prosa puede convivir indefinidamente con la pregunta de qué ocurre si el plazo vence mientras la revisión está en curso, porque la prosa no tiene celdas vacías; solo tiene silencios, y los silencios no se ven. Una matriz de modo por evento sí las tiene, y por eso el modelado es violento en el buen sentido: convierte silencios en huecos con coordenadas, delante de gente que puede rellenarlos. De ahí se sigue algo que cambia cómo trabajas: cuando modelando encuentras una situación que nadie ha previsto, no has descubierto un fallo del requisito, has hecho el trabajo. La medida del valor de una sesión de modelado no es la elegancia del grafo resultante, sino el número de decisiones de producto que se tomaron durante ella y que, sin el grafo, se habrían tomado meses después, en una revisión de código, por la persona con menos contexto de la organización y sin que nadie se enterara de que se estaba decidiendo algo. Modelar bien es, en el fondo, una técnica para adelantar decisiones hasta el momento en que aún son baratas y aún las toma quien debe.

⚔️ Modela un requisito ambiguo de tu backlog
  1. Elige un requisito escrito en prosa y subraya cada sustantivo con adjetivo. Cada uno es un estado que alguien ya tenía en la cabeza y no escribió.
  2. Escribe las dos columnas de eventos y comprométete a que la del mundo tenga al menos cuatro entradas. Si no las encuentras, no has terminado de buscarlas.
  3. Rellena la matriz completa y anota cuántas celdas eran decisiones de producto que nadie había tomado. Ese número es el rendimiento del ejercicio.
  4. Pasa cada campo candidato por la criba de tres vías y clasifícalo como modo, dato o derivado antes de escribir una línea de código.
  5. Comprueba si algún estado tiene un nombre con conjunción. Si lo tiene, separa las dos entidades en dos máquinas y vuelve al movimiento tres.
  6. Somete el resultado a los cinco criterios de terminación y entrégalo a alguien que no participó, con una sola instrucción: encuéntrame un caso que no esté.