Fonte de eventos
O estado não é guardado: é derivado dos fatos. O armazém, as duas funções puras e o que isso compra.
Num modelo comum, a tabela guarda o estado de agora: o saldo é 70. Numa fonte de eventos, guarda-se o que aconteceu — aberta, depositou 100, sacou 30 — e o saldo é uma conta feita sobre isso. A pergunta "qual era o saldo em março?" passa a ter resposta, e "por que o saldo é 70?" também.
O modelo inteiro cabe em duas funções puras:
| Função | Recebe | Devolve | Pode recusar? |
|---|---|---|---|
decidir | o estado e um comando | os eventos que ele produz | sim — é aqui que mora a regra |
aplicar | o estado e um evento | o estado seguinte | nunca — o fato já aconteceu |
dataforge
adopt Arcane.Dominio as D
// evoluir: o estado depois de um fato. Pura, e nunca recusa nada.
aplicar := {
"ContaAberta": lambda s, d: {"titular": d["titular"], "saldo": 0, "aberta": yes},
"Depositado": lambda s, d: {...s, "saldo": s["saldo"] + d["valor"]},
"Sacado": lambda s, d: {...s, "saldo": s["saldo"] - d["valor"]},
"ContaEncerrada": lambda s, d: {...s, "aberta": no}
}
// decidir: os fatos que um comando produz — ou o motivo da recusa.
action decidir(estado, comando):
tipo := comando["tipo"]
given tipo is "abrir":
yield [{"nome": "ContaAberta", "dados": {"titular": comando["titular"]}}]
given not (estado["aberta"] ?? no):
trigger "a conta não está aberta"
given tipo is "depositar":
yield [{"nome": "Depositado", "dados": {"valor": comando["valor"]}}]
given tipo is "sacar":
given comando["valor"] bigger estado["saldo"]:
trigger $"saldo {estado["saldo"]} não cobre {comando["valor"]}"
yield [{"nome": "Sacado", "dados": {"valor": comando["valor"]}}]
trigger $"comando desconhecido: {tipo}"
armazem := D.armazem()
action executar(conta, comando):
historia := armazem.ler(conta)
estado := D.reconstituir(historia, aplicar)
novos := decidir(estado, comando)
armazem.anexar(conta, novos, len(historia))
yield D.reconstituir(historia + novos, aplicar)
executar("conta-1", {"tipo": "abrir", "titular": "Ana"})
executar("conta-1", {"tipo": "depositar", "valor": 100})
final := executar("conta-1", {"tipo": "sacar", "valor": 30})
assert final["saldo"] is 70
// a história inteira continua ali
assert [e["nome"] cycle e in armazem.ler("conta-1")] is ["ContaAberta", "Depositado", "Sacado"]
// e o passado tem resposta: o saldo depois do segundo fato
assert D.reconstituir(armazem.ler("conta-1")[0:2], aplicar)["saldo"] is 100Por que `aplicar` nunca recusa#
Um evento é um fato no passado. Se aplicar pudesse recusar Sacado, reconstituir uma conta antiga falharia no dia em que a regra de saque mudasse — e o histórico, que é a fonte da verdade, deixaria de ser legível. A regra vive em decidir, que só olha o presente.
| Compra | Custa |
|---|---|
| auditoria completa, de graça | reconstituir a cada comando (ou um instantâneo) |
| responder sobre o passado | evento nunca muda: um erro vira um evento de correção |
| novos modelos de leitura a partir do histórico | projeções a manter |
| depurar reproduzindo a sequência exata | um armazém que nunca apaga |