Contratos
contract, invariant, expects e promises: interfaces que não carregam código, e design por contrato que diz de quem é o erro.
Há dois sentidos de contrato em orientação a objetos, e o DataForge tem os dois: o contrato de interface (o que um tipo promete oferecer) e o design por contrato (o que uma operação exige e garante).
contract: só a assinatura#
Um contract declara métodos e propriedades sem corpo. Um corpo é recusado na leitura: implementação padrão é o papel do trait, e deixar o contrato carregar código apagaria a única diferença entre os dois.
contract Leitura<T>:
action buscar(id: Integer) -> T
contract Escrita<T>:
action salvar(item: T)
contract Repositorio<T> extends Leitura, Escrita:
get total() -> Integer
blueprint RepoMemoria with Repositorio:
itens := {}
action buscar(id: Integer):
yield self.itens[id] ?? void
action salvar(item):
self.itens[len(self.itens) + 1] := item
get total():
yield len(self.itens)
// quem só lê depende só de Leitura — o Princípio da Segregação de Interface
action primeiro(fonte: Leitura):
yield fonte.buscar(1)
r := spawn RepoMemoria()
r.salvar("caneta")
assert primeiro(r) is "caneta"
assert r.total is 1| trait | contract | |
|---|---|---|
| corpo de método | pode ter (implementação padrão) | recusado |
| estende outro | não | contract A extends B, C |
| confere aridade de quem implementa | não | sim — SignatureMismatchError |
| propriedade exigida | não | get total() -> Integer |
| vale como tipo de parâmetro | sim | sim, inclusive o que ele herda |
Na declaração, o blueprint concreto que adota um contrato precisa ter cada método, aceitar todos os argumentos que o contrato passa (parâmetros a mais precisam de padrão) e ter cada propriedade exigida — como get ou como campo.
expects, promises, invariant#
As três cláusulas respondem a mesma pergunta — de quem é o erro? — e é isso que as separa de um assert:
| Cláusula | Onde | Quando roda | Erro | Quem errou |
|---|---|---|---|---|
expects cond, msg | corpo de ação | onde estiver | PreconditionError | quem chamou |
promises cond, msg | topo do corpo | na saída, com outcome | PostconditionError | a ação |
invariant cond, msg | corpo de blueprint | depois de construir e de cada método público | InvariantError | a operação |
blueprint Conta:
saldo := 0
invariant self.saldo bigger_eq 0, "saldo nunca fica negativo"
action depositar(valor):
expects valor bigger 0, "depósito precisa ser positivo"
promises self.saldo is before(self.saldo) + valor
self.saldo += valor
yield self.saldo
action sacar(valor):
expects valor bigger 0, "saque precisa ser positivo"
promises outcome is self.saldo
self.saldo -= valor
yield self.saldo
c := spawn Conta()
c.depositar(100)
assert c.sacar(30) is 70
monitor:
c.depositar(-5)
assert no
handle PreconditionError as e:
assert "positivo" in e.message
monitor:
c.sacar(500)
assert no
handle InvariantError as e:
assert "negativo" in e.messageA invariante é conferida quando a chamada mais de fora termina. Dentro de um método o objeto pode passar por estados intermediários — transferir entre dois campos exige duas escritas — e cobrar ali recusaria todo método correto. Um método private não dispara a conferência: ele é passo de outro.
Liskov, conferido#
O Princípio da Substituição diz que a filha serve onde a mãe servia. A parte que dá para provar sem rodar, o check prova: uma sobrescrita que aceita menos argumentos que a mãe quebra todo código escrito para a mãe, e vira aviso substituicao-quebrada. Contra um contrato, é erro.
blueprint Exportador:
action exportar(dados, formato := "csv"):
yield formato
blueprint ExportadorJson extends Exportador:
action exportar(dados, formato := "json"): // aceita o mesmo: ok
yield formato
assert (spawn ExportadorJson()).exportar([]) is "json"