Pular para o conteúdo

37 · OOP como sistema

10 exercícios: .

bash
python3 exercicios/run_all.py 37

Os exercícios#

#TítuloEnunciado
242Contratos pequenos, e quem depende de qualmodele um armazenamento de documentos em que quem so LE
243O que cada modificador prometemodele uma conta com identidade imutavel, limite de classe,
244De quem e o erro?um estoque com pre-condicao, pos-condicao e invariante, e
245Sobrecarga que diz o que aceitauma formatacao de valores com variantes por tipo e aridade,
246Uma metaclasse que registra, valida e auditatodo modelo de dominio precisa de 'id', ganha nome de tabela
247Um validador generico escrito com reflexaovalide qualquer objeto pelas anotacoes dos campos, sem que o
248Copia, imutabilidade e serializacao que nao confia no dadopersista um pedido com cliente e itens (e um ciclo), recarregue
249Portas, adaptadores e o conteinerum caso de uso de cadastro que nao conhece banco nem e-mail,
250Padroes com mecanismo: comando, estado e especificacaoum editor de pedido com desfazer, um fluxo de aprovacao que
251Nascer, viver sob threads, e morrer limpoum contador de acessos seguro sob concorrencia, um cache que

242 · Contratos pequenos, e quem depende de qual#

Enunciado. modele um armazenamento de documentos em que quem so LE

exercicios/37-oop-sistema/242_contratos_e_segregacao.df
// nao dependa de quem ESCREVE, e confira que um blueprint que promete
// um contrato cumpre a assinatura inteira.

contract Leitura:
    action buscar(id: Integer)
    get total() -> Integer

contract Escrita:
    action salvar(id: Integer, texto: String)

contract Armazem extends Leitura, Escrita:
    action apagar(id: Integer)

blueprint ArmazemMemoria with Armazem:
    private docs := {}

    action buscar(id: Integer):
        yield self.docs[id] ?? void

    action salvar(id: Integer, texto: String):
        self.docs[id] := texto

    action apagar(id: Integer):
        self.docs.remove(id)

    get total():
        yield len(self.docs)

// Quem so exibe depende de Leitura — e nada mais.
action exibir(fonte: Leitura, id: Integer) -> String:
    yield fonte.buscar(id) ?? "(vazio)"

a := spawn ArmazemMemoria()
a.salvar(1, "ata da reunião")
assert exibir(a, 1) is "ata da reunião"
assert exibir(a, 2) is "(vazio)"
assert a.total is 1

// O contrato vale como tipo — inclusive o que ele herda.
action arquivar(destino: Escrita):
    destino.salvar(99, "arquivado")
arquivar(a)
assert a.total is 2

// Uma implementacao que pede argumento a menos e recusada na declaracao.
monitor:
    blueprint Quebrado with Escrita:
        action salvar(id: Integer):
            yield id
    assert no, "devia ter recusado"
handle SignatureMismatchError as e:
    assert "Escrita" in e.message

// E uma que esquece um metodo tambem.
monitor:
    blueprint Esquecido with Leitura:
        total := 0
    assert no, "devia ter recusado"
handle TraitContractError as e:
    assert "buscar" in e.message

// Um contrato nao nasce.
monitor:
    spawn Leitura()
    assert no
handle AbstractInstantiationError:
    out "contrato nao se instancia"

out "242 ok"

O que se pratica#

Declarar contract, estender contratos, adotar com with, usar o contrato como tipo de parâmetro, e ver as duas recusas da declaração.

O que este exercício ensina que não é óbvio#

1. O contrato não tem corpo, e isso é o que o separa do trait. Um trait pode trazer implementação padrão; um contract só declara. A diferença parece pequena até o dia em que um contrato "com um método padrão só" arrasta uma dependência para todo mundo que o implementa.

2. Segregar é depender do menor contrato que serve. exibir recebe uma Leitura, e não um Armazem. Um cache, um arquivo remoto ou um dublê de teste servem ali sem implementar salvar e apagar — é o Princípio da Segregação de Interface escrito no tipo do parâmetro.

3. A assinatura é conferida, e não só o nome. salvar(id) com o nome certo e um argumento a menos quebra todo código escrito contra o contrato. O nome certo é o que faria a quebra passar despercebida até a primeira chamada — por isso a recusa é na declaração (SignatureMismatchError), e o check acusa antes de rodar.

4. Uma propriedade exigida aceita campo. get total() -> Integer é cumprido por um get total() ou por um campo total. Quem lê não sabe a diferença, e não deveria saber.

Para ir além#

  • Acrescente um contract Buscavel extends Leitura com procurar(termo)

e veja o check cobrar a implementação.

  • Troque fonte: Leitura por fonte: Armazem em exibir e responda:

que blueprint de teste ficou mais caro de escrever?

243 · O que cada modificador promete#

Enunciado. modele uma conta com identidade imutavel, limite de classe,

exercicios/37-oop-sistema/243_modificadores.df
// estado privado e uma familia fechada de tipos de conta.

abstract sealed blueprint Conta:
    readonly numero := 0
    static steady LIMITE_DIARIO := 5000
    private saldo := 0.0
    protected historico := []

    action setup(numero):
        self.numero := numero

    abstract action tarifa(valor)

    action depositar(valor):
        self.saldo += valor
        self.historico.append(valor)
        yield self.saldo

    final action extrato():
        yield $"{self.numero}: {self.saldo}"

    internal action auditoria():
        yield len(self.historico)

blueprint Corrente extends Conta:
    override action tarifa(valor):
        yield valor * 0.01

final blueprint Poupanca extends Conta:
    override action tarifa(valor):
        yield 0

c := spawn Corrente(101)
c.depositar(200)
assert c.extrato() is "101: 200.0"
assert c.tarifa(100) is 1.0
assert c.auditoria() is 1  // internal: mesmo arquivo
assert Conta.LIMITE_DIARIO is 5000

// readonly: so a construcao escreve
monitor:
    c.numero := 999
    assert no
handle ReadOnlyFieldError:
    out "o numero de uma conta nao muda"

// private: nem leitura, nem chamada de fora
monitor:
    out c.saldo
    assert no
handle TypeError as e:
    assert "private" in e.message

// static steady: constante de classe
monitor:
    Conta.LIMITE_DIARIO := 1
    assert no
handle ConstantReassignmentError:
    out "limite e constante"

// final: ninguem herda de Poupanca
monitor:
    blueprint PoupancaPlus extends Poupanca:
        bonus := 1
    assert no
handle FinalBlueprintError:
    out "poupanca e final"

// final action: a filha nao troca o extrato
monitor:
    blueprint Fraude extends Conta:
        override action tarifa(v):
            yield 0
        action extrato():
            yield "tudo certo"
    assert no
handle FinalOverrideError:
    out "extrato e final"

// override: o nome errado e pego na declaracao
monitor:
    blueprint Digitada extends Conta:
        override action tarifaa(v):
            yield 0
    assert no
handle OverrideTargetError as e:
    assert "tarifa" in e.dica

out "243 ok"

O que se pratica#

readonly, static steady, private, protected, internal, abstract, final (em ação e em blueprint), sealed e override — cada um com a recusa que o torna real.

O que este exercício ensina que não é óbvio#

1. Um modificador que ninguém confere é um comentário. Cada linha do exercício termina num erro com nome próprio (ReadOnlyFieldError, FinalBlueprintError, OverrideTargetError…), e é esse nome que um handle pega. É a diferença entre uma convenção e uma regra.

2. `readonly` é sobre o nascimento, não sobre o arquivo. O setup escreve numero; depois, ninguém escreve — nem um método da própria conta. O check acusa um método comum que tente, antes de rodar.

3. `internal` fala de ARQUIVO; `private` e `protected` falam de LINHAGEM. A auditoria é visível para todo o código deste arquivo, e invisível para quem adota o arquivo. É a visibilidade de um módulo.

4. `abstract sealed` fecha a família. Corrente e Poupanca são as únicas contas possíveis — outra só num .df diferente, que o sealed recusa. final em Poupanca fecha ainda mais: nem aqui ela tem filhas.

5. `override` pega o erro de digitação. Sem ele, tarifaa seria um método novo, a tarifa abstrata continuaria pendente, e a mensagem falaria de método faltando — longe da causa. Com ele, a mensagem diz "did you mean 'tarifa'?".

Para ir além#

  • Rode dataforge check neste arquivo sem os monitor: cada recusa vira

erro antes de rodar.

  • Mova Corrente para outro arquivo e veja o SealedBlueprintError.

244 · De quem e o erro?#

Enunciado. um estoque com pre-condicao, pos-condicao e invariante, e

exercicios/37-oop-sistema/244_design_por_contrato.df
// uma operacao defeituosa que o contrato denuncia.

blueprint Estoque:
    itens := {}
    invariant len([q cycle q in self.itens.values() given q smaller 0]) is 0, "quantidade negativa"

    action entrar(produto, n):
        expects n bigger 0, "entrada precisa ser positiva"
        promises self.quantidade(produto) is before(self.quantidade(produto)) + n
        self.itens[produto] := self.quantidade(produto) + n
        yield self.itens[produto]

    action sair(produto, n):
        expects n bigger 0, "saida precisa ser positiva"
        self.itens[produto] := self.quantidade(produto) - n
        yield self.itens[produto]

    action entrar_com_bug(produto, n):
        promises self.quantidade(produto) is before(self.quantidade(produto)) + n, "somou errado"
        self.itens[produto] := self.quantidade(produto) + n - 1

    action quantidade(produto):
        yield self.itens[produto] ?? 0

e := spawn Estoque()
assert e.entrar("caneta", 10) is 10
assert e.sair("caneta", 4) is 6

// pre-condicao: quem CHAMOU errou
monitor:
    e.entrar("caneta", 0)
    assert no
handle PreconditionError as erro:
    assert "positiva" in erro.message

// pos-condicao: a ACAO errou
monitor:
    e.entrar_com_bug("lapis", 5)
    assert no
handle PostconditionError as erro:
    assert "somou errado" in erro.message

// invariante: a operacao deixou o objeto inconsistente
monitor:
    e.sair("caneta", 100)
    assert no
handle InvariantError as erro:
    assert "negativa" in erro.message

// Os tres sao ContractError: um handle so pega a familia inteira.
monitor:
    e.sair("caneta", -1)
handle ContractError as erro:
    out "contrato:", erro.type

out "244 ok"

O que se pratica#

expects, promises com before(…) e outcome, e invariant — e a família ContractError, que pega os três.

O que este exercício ensina que não é óbvio#

1. A cláusula diz de quem é a culpa. Um assert diz que algo deu errado. expects diz que quem chamou errou; promises diz que a ação errou; invariant diz que a operação deixou o objeto inconsistente. Num sistema de duzentos arquivos, é a diferença entre abrir o arquivo certo e abrir três.

2. `promises` roda na saída, mesmo escrito no topo. Ele é tirado do corpo pelo parser e conferido quando a ação devolve. before(expr) guarda o valor da entrada — sem ele, uma pós-condição não conseguiria falar de mudança, só de estado final.

3. A invariante é conferida quando a chamada mais de fora termina. Dentro de um método o objeto passa por estados intermediários — tirar de um lugar e pôr em outro são duas escritas. Cobrar a invariante entre elas recusaria todo método correto.

4. `quantidade` é chamada dentro de `promises` e da invariante. Uma chamada de dentro do próprio objeto não dispara a conferência de novo: ela é passo da operação, não operação.

Para ir além#

  • Remova o expects de sair e veja o sair(…, -1) aparecer como

invariante quebrada — o erro certo, mas agora culpando a operação em vez de quem chamou.

245 · Sobrecarga que diz o que aceita#

Enunciado. uma formatacao de valores com variantes por tipo e aridade,

exercicios/37-oop-sistema/245_sobrecarga.df
// um construtor com duas formas, e as duas recusas da resolucao.

overload action formatar(valor: Integer):
    yield $"{valor}"
overload action formatar(valor: Float):
    yield $"{valor:.2f}"
overload action formatar(valor: Float, casas: Integer):
    yield str(round(valor, casas))
overload action formatar(valor: Boolean):
    yield "sim" given valor otherwise "não"

assert formatar(3) is "3"  // Integer vence Float: e exato
assert formatar(2.5) is "2.50"
assert formatar(2.567, 1) is "2.6"
assert formatar(yes) is "sim"

blueprint Intervalo:
    inicio := 0
    fim := 0

    overload action setup(fim: Integer):
        self.fim := fim
    overload action setup(inicio: Integer, fim: Integer):
        self.inicio := inicio
        self.fim := fim

    action tamanho():
        yield self.fim - self.inicio

assert(spawn Intervalo(10)).tamanho() is 10
assert(spawn Intervalo(3, 10)).tamanho() is 7

// nenhuma variante aceita texto: a mensagem lista as que existem
monitor:
    formatar("dez")
    assert no
handle OverloadResolutionError as e:
    assert "formatar(valor: Integer)" in e.nota

// duas variantes igualmente boas: escolher pela ordem seria arbitrario
overload action juntar(a: Integer, b):
    yield "primeira"
overload action juntar(a, b: Integer):
    yield "segunda"
monitor:
    juntar(1, 2)
    assert no
handle AmbiguousOverloadError:
    out "empate recusado"
assert juntar(1, "x") is "primeira"

out "245 ok"

O que se pratica#

overload action no topo e em construtor, a escolha por aridade e por tipo, e as recusas OverloadResolutionError e AmbiguousOverloadError.

O que este exercício ensina que não é óbvio#

1. Numa linguagem dinâmica, sobrecarga é documentação que se cumpre. Um given typeof(valor) faria o mesmo — e aceitaria qualquer coisa, falhando lá dentro. As variantes declaram as formas, e a chamada fora delas é recusada com a lista das que existem na nota.

2. O tipo exato vence o compatível. Um Integer serve onde se pede Float. Sem essa regra formatar(3) empataria entre as duas primeiras variantes; com ela, vence a de Integer, que é a que quem escreveu quis.

3. Empate é erro, e não sorteio. juntar(1, 2) cabe nas duas variantes com a mesma precisão. Escolher a primeira escrita faria mover uma variante de lugar mudar o resultado de um programa que não a chama. juntar(1, "x") não empata: só a primeira aceita texto em b.

4. Toda variante é marcada. Misturar action f com overload action f é recusado — não há leitura em que as duas convivam.

Para ir além#

  • Acrescente overload action formatar(valor: Integer, casas: Integer)

e descubra qual chamada existente passa a empatar.

246 · Uma metaclasse que registra, valida e audita#

Enunciado. todo modelo de dominio precisa de 'id', ganha nome de tabela

exercicios/37-oop-sistema/246_metaclasses.df
// automaticamente, e cada escrita de campo fica registrada.

adopt Arcane.Reflexo as R

meta blueprint Modelo:
    tabelas := {}
    escritas := []

    action on_forge(molde):
        campos := [c["nome"] cycle c in R.campos(molde)]
        given not("id" in campos):
            trigger $"{R.nome(molde)} precisa declarar 'id'"
        self.tabelas[R.nome(molde)] := R.nome(molde).lower() + "s"

    action on_write(obj, nome, valor):
        self.escritas.append($"{R.nome(obj)}.{nome}")

    action on_missing(obj, nome):
        yield void

blueprint Entidade using Modelo:
    id := 0

blueprint Cliente extends Entidade:
    nome := ""

blueprint Pedido extends Entidade:
    total := 0.0

meta_obj := R.meta_instancia(Pedido)
assert meta_obj.tabelas is {"Entidade": "entidades", "Cliente": "clientes", "Pedido": "pedidos"}

c := spawn Cliente()
c.nome := "Ana"
c.id := 7
assert meta_obj.escritas is ["Cliente.nome", "Cliente.id"]
assert c.telefone is void  // on_missing: campo opcional

// a validacao roda na DECLARACAO
monitor:
    blueprint SemId using Modelo:
        texto := ""
    assert no
handle Error as e:
    assert "precisa declarar 'id'" in e.message

// um gancho com nome errado nunca rodaria — por isso e recusado
monitor:
    meta blueprint Errada:
        action on_forje(m):
            yield void
    assert no
handle MetaclassError as e:
    assert "on_forge" in e.dica

out "246 ok"

O que se pratica#

meta blueprint, using, os ganchos on_forge, on_write e on_missing, a herança da metaclasse e Reflexo.meta_instancia.

O que este exercício ensina que não é óbvio#

1. A metaclasse age sobre a CLASSE, uma vez, e sobre os OBJETOS, sempre. on_forge roda quando Cliente é declarado; on_write roda em cada escrita de cada cliente. É por isso que a validação de id falha na declaração, antes de existir objeto nenhum.

2. Ela é herdada. Cliente e Pedido não escrevem using, e são governados. Uma regra de domínio escrita uma vez vale para a família inteira — e uma filha não consegue escapar dela.

3. A metaclasse tem uma instância só. tabelas e escritas são campos dessa instância, compartilhada por todos os modelos. É o self dos ganchos, e é onde um registro de classes naturalmente mora.

4. Nomes de gancho são fixos, e o errado é recusado. Um on_forje nunca rodaria, e nada avisaria: a metaclasse pareceria ignorada. A recusa vem com a sugestão do nome certo.

5. Um gancho não dispara outro. R.campos(molde) e R.nome(obj) dentro dos ganchos não caem em on_read — sem essa regra, auditar leituras seria recursão infinita.

Para ir além#

  • Acrescente on_spawn devolvendo um objeto já criado para o mesmo id

e transforme o modelo num mapa de identidade.

247 · Um validador generico escrito com reflexao#

Enunciado. valide qualquer objeto pelas anotacoes dos campos, sem que o

exercicios/37-oop-sistema/247_reflexao.df
// validador conheca o tipo — e confirme que a reflexao nao fura 'private'.

adopt Arcane.Reflexo as R

action Obrigatorio(alvo):
    yield void
action Tamanho(minimo, maximo):
    yield lambda alvo => void

blueprint Usuario:
    @Obrigatorio
    @Tamanho(3, 20)
    nome := ""

    @Obrigatorio
    email := ""

    private senha := "segredo"

    action saudar():
        yield "olá, " + self.nome

// O validador so conhece anotacoes, nunca 'Usuario'.
action validar(obj):
    problemas := []
    cycle campo in R.campos(obj):
        given campo["visibilidade"] isnt "public":
            skip
        valor := R.ler(obj, campo["nome"])
        cycle marca in campo["anotacoes"]:
            given marca["nome"] is "Obrigatorio" and valor is "":
                problemas.append($"{campo['nome']} é obrigatório")
            orif marca["nome"] is "Tamanho" and valor isnt "":
                minimo, maximo := marca["args"]
                given len(valor) smaller minimo or len(valor) bigger maximo:
                    problemas.append($"{campo['nome']} fora de {minimo}..{maximo}")
    yield problemas

u := spawn Usuario()
assert validar(u) is ["nome é obrigatório", "email é obrigatório"]
R.escrever(u, "nome", "Al")
R.escrever(u, "email", "al@x.com")
assert validar(u) is ["nome fora de 3..20"]
u.nome := "Alice"
assert validar(u) is []

// invocacao por nome
assert R.invocar(u, "saudar") is "olá, Alice"
assert R.tem(u, "saudar")
assert "senha" in [c["nome"] cycle c in R.campos(Usuario)]  // saber que existe…
monitor:
    R.ler(u, "senha")  // …nao e poder ler
    assert no
handle TypeError as e:
    assert "private" in e.message

// sugestao de nome parecido, como dado
assert R.sugerir(u, "sadar") is ["saudar"]

// o diagrama sai da mesma informacao
diagrama := R.diagrama([Usuario])
assert "-senha" in diagrama and "+saudar()" in diagrama

out "247 ok"

O que se pratica#

Anotações em campo (@Obrigatorio, @Tamanho(3, 20)), Reflexo.campos, ler/escrever/invocar por nome, sugerir e diagrama.

O que este exercício ensina que não é óbvio#

1. Em campo, o decorador é anotação. @Tamanho(3, 20) não embrulha nada — não há valor para embrulhar na declaração. Ele grava nome e argumentos, e quem precisa lê: aqui, o validador.

2. O validador não conhece `Usuario`. Ele pergunta ao tipo o que o tipo declara. É assim que ORM, serializador e formulário são escritos uma vez para todos os modelos — e é por isso que reflexão existe.

3. Saber que existe não é poder ler. R.campos lista senha com a visibilidade ao lado, porque isso é documentação. R.ler(u, "senha") é recusado exatamente como u.senha seria: se a reflexão abrisse o que o autor fechou, private seria só uma sugestão para quem não conhece o módulo — e é justamente quem conhece que precisa ser contido.

4. Reflexão custa. Cada leitura por texto passa pela busca completa de membro, visibilidade e ganchos. Use em framework e ferramenta; no laço quente, escreva u.nome.

Para ir além#

  • Acrescente @Formato("email") e valide com Arcane.Regex.
  • Gere o formulário HTML de Usuario a partir das mesmas anotações.

248 · Copia, imutabilidade e serializacao que nao confia no dado#

Enunciado. persista um pedido com cliente e itens (e um ciclo), recarregue

exercicios/37-oop-sistema/248_objetos_e_serializacao.df
// so os tipos esperados, e passe uma configuracao congelada adiante.

adopt Arcane.Objetos as O

blueprint Cliente(nome):
    pedidos := []
    private cpf := "000"

blueprint Item(produto, preco):
    quantidade := 1

blueprint Pedido(cliente):
    itens := []
    invariant len(self.itens) smaller_eq 50

    action total():
        yield sum([i.preco * i.quantidade cycle i in self.itens])

ana := spawn Cliente("Ana")
p := spawn Pedido(ana)
p.itens.append(spawn Item("caneta", 3.0))
p.itens.append(spawn Item("caderno", 12.5))
ana.pedidos.append(p)  // ciclo: cliente -> pedido -> cliente

// ── serializar ──
dado := O.para_vault(ana)
assert dado["$tipo"] is "Cliente"
assert not("cpf" in dado)  // private nao sai
assert dado["pedidos"][0]["cliente"]["$ref"] is dado["$id"]

texto := O.para_json(ana)
volta := O.de_json(texto, [Cliente, Pedido, Item])
assert volta.pedidos[0].total() is 15.5
assert volta.pedidos[0].cliente is volta  // o ciclo voltou como ciclo

// ── o dado nao escolhe o tipo ──
monitor:
    O.de_vault({"$tipo": "Administrador", "poderes": "todos"}, [Cliente, Pedido, Item])
    assert no
handle UnsafeDeserializationError:
    out "tipo nao autorizado: recusado"

// ── invariante conferida na chegada ──
itens_demais := [{"$tipo":"Item", "produto":"x", "preco":1} cycle i in range(60)]
monitor:
    O.de_vault({"$tipo": "Pedido", "itens": itens_demais}, [Pedido, Item])
    assert no
handle InvariantError:
    out "dado que viola a regra: recusado na chegada"

// ── copia e igualdade ──
copia := O.clonar_fundo(p)
copia.itens[0].quantidade := 10
assert p.itens[0].quantidade is 1
assert not O.igual(copia, p)
assert O.igual(O.clonar_fundo(p), p)

// ── congelar ──
blueprint Config:
    porta := 8080
    hosts := ["a"]
cfg := O.congelar(spawn Config())
monitor:
    cfg.porta := 80
    assert no
handle FrozenObjectError:
    out "config congelada"
editavel := O.clonar(cfg)
editavel.porta := 80
assert editavel.porta is 80 and cfg.porta is 8080

out "248 ok"

O que se pratica#

Objetos.para_vault/para_json com ciclo, de_vault/de_json com lista de tipos, clonar e clonar_fundo, igual e congelar.

O que este exercício ensina que não é óbvio#

1. O dado não escolhe o tipo. de_vault exige a lista do que pode construir. Um JSON com "$tipo": "Administrador" é recusado com UnsafeDeserializationError. Em linguagens que deixaram o dado escolher, foi assim que desserialização virou execução de código alheio.

2. Ciclo não é recursão infinita. O pedido aponta para o cliente, que aponta para o pedido. A segunda vez que um objeto aparece ele vira {"$ref": n}, e na volta a referência é religada: volta.pedidos[0].cliente is volta.

3. A reconstrução confere a invariante. O setup não roda — ele pede argumentos que o dado não tem —, mas a regra do tipo vale: um pedido com 60 itens é recusado na chegada, e não três telas depois.

4. `private` não sai. Serializar é publicar. {"privados": yes} inclui os campos fechados para persistência própria, e a escolha fica explícita.

5. Congelar é para sempre; a cópia não. clonar de um objeto congelado devolve um objeto editável — é o jeito de mexer numa configuração que outra thread pode estar lendo.

Para ir além#

  • Declare static versao_do_esquema := 2 em Pedido e escreva uma

metaclasse com on_deserialize que migra o dado da versão 1.

249 · Portas, adaptadores e o conteiner#

Enunciado. um caso de uso de cadastro que nao conhece banco nem e-mail,

exercicios/37-oop-sistema/249_injecao_hexagonal.df
// montado por um conteiner — e trocado por dubles no teste.

adopt Arcane.Injecao as DI

// ── portas (o dominio) ──
contract Usuarios:
    action existe(email: String) -> Boolean
    action gravar(email: String)

contract Avisos:
    action enviar(para: String, texto: String)

blueprint Cadastro(usuarios: Usuarios, avisos: Avisos):
    action registrar(email: String):
        expects "@" in email, "e-mail invalido"
        given self.usuarios.existe(email):
            yield "ja existe"
        self.usuarios.gravar(email)
        self.avisos.enviar(email, "bem-vindo")
        yield "criado"

// ── adaptadores (a borda) ──
blueprint UsuariosMemoria with Usuarios:
    emails := []
    action existe(email: String) -> Boolean:
        yield email in self.emails
    action gravar(email: String):
        self.emails.append(email)

blueprint AvisosFalsos with Avisos:
    enviados := []
    action enviar(para: String, _texto: String):
        self.enviados.append(para)

// ── composicao: um lugar so ──
c := DI.conteiner()
c.unico(Usuarios, UsuariosMemoria)
c.unico(Avisos, AvisosFalsos)
c.transitorio(Cadastro)

cadastro := c.resolver(Cadastro)
assert cadastro.registrar("ana@x.com") is "criado"
assert cadastro.registrar("ana@x.com") is "ja existe"
assert c.resolver(Avisos).enviados is ["ana@x.com"]
assert c.conferir() is []

// ── o grafo que o conteiner monta ──
assert c.grafo()["Cadastro"] is ["Usuarios", "Avisos"]

// ── escopo: um por pedido ──
blueprint Transacao:
    aberta := yes
    action fechar():
        self.aberta := no

c.por_escopo(Transacao)
pedido := c.escopo("pedido 1")
t := pedido.resolver(Transacao)
assert t is pedido.resolver(Transacao)
pedido.fechar()
assert not t.aberta

// ── o que ele recusa ──
monitor:
    DI.conteiner().resolver(Cadastro)
    assert no
handle DependencyResolutionError as e:
    assert "Usuarios" in e.message

out "249 ok"

O que se pratica#

contract como porta, blueprint with como adaptador, Arcane.Injecao com unico, transitorio, por_escopo, conferir e grafo.

O que este exercício ensina que não é óbvio#

1. O caso de uso não adota nada de fora. Cadastro conhece dois contratos e zero implementações. Trocar SQLite por PostgreSQL, ou SMTP por uma fila, não muda uma linha dele — é a arquitetura hexagonal, e é o Princípio da Inversão de Dependência escrito em tipos.

2. O contêiner lê o que já estava escrito. usuarios: Usuarios no cabeçalho é a dependência declarada; não há anotação @Inject nova. O contêiner resolve pelo tipo.

3. A composição mora num lugar só. As quatro linhas de registro são o único ponto do programa que sabe quais adaptadores existem. No teste, são essas quatro linhas que mudam — e só elas.

4. `conferir()` no teste pega o registro que falta. Um contêiner que só descobre a dependência sem registro quando a rota é chamada descobre em produção.

5. Por escopo é por pedido. Uma transação por pedido HTTP, fechada no fim. Pedir uma dependência por_escopo ao contêiner raiz é recusado: ali ela viraria um único calado, e duas requisições dividiriam a transação.

Para ir além#

  • Registre Usuarios como por_escopo e Cadastro como unico, e leia

a mensagem sobre dependência cativa.

250 · Padroes com mecanismo: comando, estado e especificacao#

Enunciado. um editor de pedido com desfazer, um fluxo de aprovacao que

exercicios/37-oop-sistema/250_padroes.df
// recusa transicao invalida, e regras de negocio combinaveis.

adopt Arcane.Padroes as P

// ── Command: desfazer e refazer ──
blueprint Carrinho:
    itens := []

blueprint Adicionar(carrinho, item):
    action executar():
        self.carrinho.itens.append(self.item)
    action desfazer():
        self.carrinho.itens.pop()

carrinho := spawn Carrinho()
historico := P.comandos()
historico.executar(spawn Adicionar(carrinho, "caneta"))
historico.executar(spawn Adicionar(carrinho, "caderno"))
historico.desfazer()
assert carrinho.itens is ["caneta"]
historico.refazer()
assert carrinho.itens is ["caneta", "caderno"]

// ── State: transicoes com guarda ──
fluxo := P.maquina("rascunho", {
        "enviar": {"de":["rascunho"], "para": "em_analise",
            "guarda": lambda ctx => len(ctx.itens) bigger 0},
        "aprovar": {"de":["em_analise"], "para": "aprovado"},
        "recusar": {"de":["em_analise"], "para": "rascunho"},
    })
assert not fluxo.pode("aprovar")
fluxo.ir("enviar", carrinho)
fluxo.ir("recusar")
fluxo.ir("enviar", carrinho)
fluxo.ir("aprovar")
assert fluxo.historico is ["rascunho", "em_analise", "rascunho", "em_analise", "aprovado"]
monitor:
    fluxo.ir("enviar", carrinho)
    assert no
handle StateError:
    out "aprovado nao volta para analise"

// ── Specification + Repository ──
record Produto:
    id: Integer
    preco: Float
    estoque: Integer

repo := P.repositorio()
repo.salvar(Produto(1, 5.0, 10))
repo.salvar(Produto(2, 80.0, 0))
repo.salvar(Produto(3, 120.0, 3))

caro := P.especificacao(lambda p => p.preco bigger 50, "caro")
disponivel := P.especificacao(lambda p => p.estoque bigger 0, "disponivel")
vitrine := repo.filtrar(caro.e(disponivel))
assert [p.id cycle p in vitrine] is [3]
assert [p.id cycle p in repo.filtrar(caro.nao().ou(disponivel.nao()))] is [1, 2]

// ── Observer com prioridade ──
auditoria := []
eventos := P.observavel()
eventos.assinar(lambda d => auditoria.append("log"), 0)
eventos.assinar(lambda d => auditoria.append("seguranca"), 10)
eventos.notificar("pedido aprovado")
assert auditoria is ["seguranca", "log"]

out "250 ok"

O que se pratica#

Padroes.comandos, Padroes.maquina com guarda, Padroes.especificacao combinada, Padroes.repositorio e Padroes.observavel com prioridade.

O que este exercício ensina que não é óbvio#

1. Metade do catálogo já é a linguagem. Template Method é abstract blueprint; Strategy é um contrato com duas implementações; Decorator é mark @…. O módulo só traz os padrões que pedem estado — histórico de comandos, estado atual de uma máquina — que ninguém deveria reescrever.

2. A máquina recusa em vez de ignorar. fluxo.ir("enviar") num pedido aprovado é StateError, com a lista de onde o evento é permitido. Uma máquina que ignora evento inválido esconde exatamente o bug que ela existia para impedir.

3. A especificação é uma regra de negócio com nome. caro.e(disponivel) lê como a frase do negócio, e cada peça se testa sozinha. O repositório recebe a regra, e não um given espalhado por quem lista produtos.

4. Prioridade decide a ordem; o registro não. O ouvinte de segurança foi assinado depois e roda antes. Um observador que depende da ordem de assinatura quebra no dia em que alguém move um adopt.

Para ir além#

  • Faça o ouvinte de segurança devolver "parar" para um pedido suspeito,

e veja o de log não receber o evento.

251 · Nascer, viver sob threads, e morrer limpo#

Enunciado. um contador de acessos seguro sob concorrencia, um cache que

exercicios/37-oop-sistema/251_ciclo_de_vida_e_concorrencia.df
// nao segura os objetos, e um recurso que se libera sozinho.

adopt Arcane.Memoria as Mem

// ── exclusive: o monitor do objeto ──
blueprint Contador:
    acessos := 0
    exclusive action registrar():
        atual := self.acessos
        self.acessos := atual + 1
    exclusive action registrar_dois():
        self.registrar()  // reentrante: nao espera por si
        self.registrar()

contador := spawn Contador()
parallel:
    thread:
        cycle i from 1 to 300:
            contador.registrar()
    thread:
        cycle i from 1 to 150:
            contador.registrar_dois()
    thread:
        cycle i from 1 to 300:
            contador.registrar()
assert contador.acessos is 900

// ── lazy: calcular uma vez por objeto ──
blueprint Relatorio(linhas):
    calculos := 0
    lazy get total():
        self.calculos += 1
        yield sum(self.linhas)

r := spawn Relatorio([1, 2, 3, 4])
assert r.total is 10 and r.total is 10
assert r.calculos is 1

// ── teardown: o recurso se libera quando o ultimo nome solta ──
liberados := []
blueprint Conexao(nome):
    action teardown():
        liberados.append(self.nome)

a := spawn Conexao("principal")
_outra := a
a := void
assert liberados is []  // '_outra' ainda segura
_outra := void
assert liberados is ["principal"]

// ── referencia fraca: o cache que nao vaza ──
cache := Mem.mapa_fraco()
sessao := spawn Conexao("sessao")
cache.definir(sessao, "dados caros de calcular")
assert cache.tamanho is 1
sessao := void
assert cache.tamanho is 0
assert "sessao" in liberados

// ── quantos existem ──
_lote := [spawn Conexao($"c{i}") cycle i in range(5)]
assert Mem.vivos(Conexao) is 5
_lote := void
assert Mem.vivos(Conexao) is 0

out "251 ok"

O que se pratica#

exclusive action reentrante sob parallel, lazy get, teardown, Memoria.mapa_fraco e Memoria.vivos.

O que este exercício ensina que não é óbvio#

1. Sem `exclusive`, o contador perde acessos, calado. Ler, somar e escrever são três passos, e duas threads intercaladas somam o mesmo valor — a armadilha 14 do CLAUDE.md mediu 40.425 de 80.000. exclusive põe uma trava por objeto: dois contadores diferentes não esperam um pelo outro.

2. A trava é reentrante. registrar_dois chama registrar, que também é exclusivo. Com uma trava simples a thread esperaria por si mesma para sempre.

3. `teardown` roda na hora, e não "algum dia". O DataForge herda do CPython a contagem de referências: o objeto morre quando o último nome o solta. Enquanto _outra segurar a conexão, ela vive — é o que o assert do meio confere.

4. O cache com referência forte é o vazamento mais comum que existe. mapa_fraco guarda o dado enquanto a sessão existir, sem ser a razão de ela existir. Soltar a sessão apaga a entrada.

5. Recurso importante fecha com `with` ou `defer`. teardown depende de o último nome soltar o objeto; um objeto preso num ciclo espera o coletor. Para arquivo e conexão, o ponto de fechamento conhecido é melhor.

Para ir além#

  • Tire o exclusive de registrar e rode algumas vezes: o total muda.
  • Rode dataforge check sem o exclusive e leia o aviso

escrita-concorrente.


Rode um isolado com dataforge run exercicios/37-oop-sistema/242_contratos_e_segregacao.df.