Pular para o conteúdo

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.

dataforge
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
traitcontract
corpo de métodopode ter (implementação padrão)recusado
estende outronãocontract A extends B, C
confere aridade de quem implementanãosim — SignatureMismatchError
propriedade exigidanãoget total() -> Integer
vale como tipo de parâmetrosimsim, 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áusulaOndeQuando rodaErroQuem errou
expects cond, msgcorpo de açãoonde estiverPreconditionErrorquem chamou
promises cond, msgtopo do corpona saída, com outcomePostconditionErrora ação
invariant cond, msgcorpo de blueprintdepois de construir e de cada método públicoInvariantErrora operação
dataforge
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.message

A 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.

dataforge
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"