CQRS
O lado que decide e o lado que responde, separados: quando isso simplifica, e quando é peso morto.
CQRS separa comando (muda o estado, e pode ser recusado) de consulta (lê, e nunca muda). No lado do comando, o modelo existe para proteger invariantes; no da consulta, para responder rápido. Tentar que um modelo só faça as duas coisas é de onde vem a tabela com quarenta colunas e três índices que brigam entre si.
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()
// ── lado da consulta: um vault pronto para a tela ──
saldos := {}
action ao_gravar(r):
given r["nome"] is "ContaAberta":
saldos[r["fluxo"]] := 0
orif r["nome"] is "Depositado":
saldos[r["fluxo"]] += r["dados"]["valor"]
orif r["nome"] is "Sacado":
saldos[r["fluxo"]] -= r["dados"]["valor"]
armazem.assinar(ao_gravar)
// ── lado do comando: decide, e grava ──
action comandar(conta, comando):
historia := armazem.ler(conta)
armazem.anexar(conta, decidir(D.reconstituir(historia, aplicar), comando), len(historia))
comandar("a", {"tipo": "abrir", "titular": "Ana"})
comandar("a", {"tipo": "depositar", "valor": 90})
comandar("b", {"tipo": "abrir", "titular": "Bia"})
assert saldos is {"a": 90, "b": 0} // a leitura não reconstitui nada| Vale a pena quando | É peso morto quando |
|---|---|
| as leituras são muito mais numerosas e diferentes das escritas | a tela mostra exatamente o que se grava |
| o modelo de escrita tem invariantes que a tela não precisa ver | é um CRUD |
| a leitura pode estar um pouco atrasada | a leitura precisa ver a escrita no mesmo instante |