
Passo 1 de 8
Reconhecer as responsabilidades do catálogo
Use o cadastro de um item para distinguir regras do catálogo, execução do caso de uso, armazenamento e interação com o terminal.
Trilha de aprendizado · Nível 15 · Tutorial 2
Organizar a aplicação em componentes com contratos claros, mantendo as regras do catálogo independentes do terminal e do formato de armazenamento.
Reconhecer as responsabilidades do catálogo
Use o cadastro de um item para distinguir regras do catálogo, execução do caso de uso, armazenamento e interação com o terminal. 2 min
Separar dependências de chamadas
Veja como manter as importações voltadas para modelos e contratos internos, mesmo quando a execução percorre interface, caso de uso e adaptador. 3 min
Manter regras e modelos livres de tecnologia
Delimite o modelo interno do catálogo para que suas regras não dependam de terminal, arquivos ou formatos externos. 3 min
Definir o contrato que o caso de uso precisa
Especifique a fronteira de persistência a partir do que o cadastro realmente precisa observar. 3 min
Implementar o cadastro com dependência injetada
Implemente um caso de uso de cadastro que depende apenas de um contrato de repositório e valide seu comportamento com um adaptador em memória. 4 min
Confinar o formato JSON ao adaptador
Implemente um adaptador JSON que traduza registros externos para modelos internos, preserve o contrato da aplicação e sinalize dados inválidos como falhas de persistência. 4 min
Conectar interface e persistência na composição
Monte os componentes concretos em um único ponto e mantenha a escolha do armazenamento fora das regras do catálogo. 3 min
Validar as fronteiras e a substituição do adaptador
Conclua o recorte do catálogo executando os mesmos cenários com persistência em memória e em JSON, e verifique onde cada dependência pode existir. 4 min

Passo 1 de 8
Use o cadastro de um item para distinguir regras do catálogo, execução do caso de uso, armazenamento e interação com o terminal.
Retomando os critérios de aceitação, considere um recorte autocontido: ao cadastrar um item, o código deve ser único, o título não pode ser vazio e, quando o cadastro for aceito, o item deve poder ser encontrado depois.
Esses resultados não dizem que a regra precisa conhecer terminal, arquivos ou JSON. Eles descrevem o que o catálogo deve garantir.
Exemplo
Regra do catálogo: “Não aceitar dois itens com o mesmo código.”
Decisão externa: guardar os itens em um arquivo JSON e mostrar “Item cadastrado” no terminal.
A primeira continua válida se a interface ou o armazenamento mudarem; as demais podem mudar sem alterar o significado de código único.
Domínio representa Item e protege regras próprias, como título não vazio.
Aplicação executa o caso de uso “cadastrar item”: coordena a validação, a verificação de duplicidade e a inclusão.
Persistência é um adaptador externo: lê e grava dados no formato escolhido.
Interface é outro adaptador externo: recebe valores do terminal e apresenta o resultado. Ela não decide as regras do catálogo.
Compare uma implementação concentrada com uma distribuição que preserva as regras internas quando JSON ou terminal mudam.

À esquerda, vários motivos de mudança se acumulam em uma função. À direita, cada fronteira concentra uma responsabilidade.
Dica
Não crie uma classe para cada função. A separação é útil quando há uma responsabilidade distinta: uma regra do catálogo, a coordenação de uma ação, um formato de armazenamento ou uma forma de interação. Para este recorte, poucos módulos e funções bem nomeados já podem bastar.
Relacione cada ação à fronteira que deve ser sua responsável principal.
Toque em um item e depois no par correspondente.

Passo 2 de 8
Veja como manter as importações voltadas para modelos e contratos internos, mesmo quando a execução percorre interface, caso de uso e adaptador.
No cadastro, a execução pode seguir interface → aplicação → adaptador. Já as importações devem proteger o núcleo: o domínio não importa interface, persistência nem aplicação; a aplicação importa modelos e contratos internos, nunca um adaptador concreto.
Portanto, uma seta de chamada em execução não determina uma seta de importação entre módulos.
Compare os dois diagramas: acima estão as importações entre módulos; abaixo, as chamadas feitas quando um cadastro é executado.

O adaptador pode receber chamadas da aplicação sem que a aplicação importe sua implementação.
Uma organização local já basta para explicitar as fronteiras. Não é preciso criar uma classe ou diretório para cada função:
dominio/item.py: modelo e regras próprias do item.aplicacao/cadastrar_item.py: caso de uso.aplicacao/portas.py: contrato consumido pela aplicação.adaptadores/repositorio_json.py: detalhe de armazenamento.interface/terminal.py: coleta e apresentação de valores.composicao.py: módulo que conhecerá as implementações concretas.Isso é uma organização de código, não uma decisão de empacotamento ou distribuição.
O caso de uso recebe um objeto compatível com o contrato. Ele não cria nem importa um repositório JSON.
# aplicacao/portas.py
from typing import Protocol
class Catalogo(Protocol):
# As operações necessárias serão definidas no próximo passo.
...
# aplicacao/cadastrar_item.py
from aplicacao.portas import Catalogo
from dominio.item import Item
def cadastrar_item(item: Item, catalogo: Catalogo) -> Item:
# Consulta e inclusão pertencem a este fluxo de aplicação.
# Não importe JsonCatalogo nem crie adaptadores aqui.
return item
# adaptadores/repositorio_json.py
from aplicacao.portas import Catalogo
class CatalogoJson:
# Implementará o comportamento esperado por Catalogo.
...Qual alternativa revela um vazamento arquitetural dentro de aplicacao/cadastrar_item.py?
Como o caso de uso chama o objeto adaptador durante o cadastro, ele precisa importar a implementação concreta desse adaptador.

Passo 3 de 8
Delimite o modelo interno do catálogo para que suas regras não dependam de terminal, arquivos ou formatos externos.
O modelo de domínio representa um item válido do catálogo: código e título. Ele pode verificar uma regra que depende somente desses valores, como “o título não pode estar vazio”.
Ele não deve saber se os valores vieram do terminal, de uma API ou de um teste. Também não deve saber onde o item será salvo. Essas são decisões externas ao modelo.
O domínio recebe valores já separados e produz um modelo válido — ou sinaliza uma falha de negócio. Formatação para pessoas e detalhes técnicos ficam fora dessa fronteira.

O item conhece seus valores e suas regras locais; não conhece a origem nem o destino dos dados.
Dica
Pergunte: “Consigo decidir isso olhando apenas para um item?” Se sim, a regra provavelmente pode ficar no modelo. Se precisa observar outros itens ou um recurso externo, ela não é uma regra local do item.
Arquivo sugerido: dominio.py
from dataclasses import dataclass
class TituloInvalidoError(ValueError):
"""O título não atende a uma regra do catálogo."""
@dataclass(frozen=True)
class ItemCatalogo:
codigo: str
titulo: str
def __post_init__(self) -> None:
if not self.titulo.strip():
raise TituloInvalidoError(
"Um item do catálogo precisa de título."
)
Ao construir ItemCatalogo, um título em branco gera TituloInvalidoError: uma falha compreensível para a aplicação. Quando a construção funciona, o resultado é o próprio ItemCatalogo.
Não coloque aqui print, encerramento do processo, caminho de arquivo, leitura de teclado nem uma mensagem preparada para a tela. Mais adiante, a interface decidirá como apresentar uma falha; o armazenamento decidirá como preservar o item.
Exemplo
ItemCatalogo("A-10", " ") pode ser rejeitado imediatamente: a regra usa somente o título.
Já decidir se o código A-10 é único exige consultar o catálogo existente. Portanto, essa verificação não cabe dentro de ItemCatalogo; ela será responsabilidade da aplicação ao coordenar a operação de cadastro.
Considere a necessidade: cadastrar um item com título não vazio e código único. Justifique onde devem ficar: (1) a validação do título, (2) a consulta necessária para detectar duplicidade e (3) a apresentação de sucesso ou erro à pessoa usuária. Diga também o que o domínio deve devolver ou sinalizar.
Escreva pelo menos 120 caracteres (0/120).
Resumo

Passo 4 de 8
Especifique a fronteira de persistência a partir do que o cadastro realmente precisa observar.
O caso de uso de cadastro não precisa saber como os dados são armazenados. Ele só precisa de duas capacidades: procurar um código e incluir um item.
Por isso, o contrato fica próximo da aplicação consumidora. Ele importa apenas o modelo interno Item e define o menor conjunto de operações necessário. Um adaptador externo poderá cumprir esse contrato com memória, arquivo ou outra tecnologia, sem alterar o caso de uso.
A aplicação conhece o contrato; detalhes externos ficam do outro lado da fronteira.

O caso de uso depende da abstração que consome, e não de uma implementação de armazenamento.
Arquivo sugerido: aplicacao/contratos.py. O Protocol descreve o que a aplicação exige, não como alguém deve implementar.
from typing import Protocol
from dominio.item import Item
class FalhaDePersistencia(Exception):
"""Não foi possível executar uma operação exigida pelo contrato."""
class RepositorioDeItens(Protocol):
def buscar_por_codigo(self, codigo: str) -> Item | None:
"""Retorna o item cadastrado ou None se o código estiver ausente.
Não altera o catálogo.
Pode levantar FalhaDePersistencia.
"""
def adicionar(self, item: Item) -> None:
"""Inclui um item cujo código ainda não está cadastrado.
Após retornar, uma busca pelo código do item deve encontrá-lo.
Pode levantar FalhaDePersistencia.
"""Exemplo
Considere buscar_por_codigo("A-10"):
None: a consulta funcionou e não existe item com esse código.FalhaDePersistencia: a consulta não pôde ser concluída de modo confiável.Não transforme uma falha de leitura em None. Isso faria a aplicação interpretar “não consegui consultar” como “o código está livre” e poderia permitir uma duplicidade.
A semântica do contrato completa as assinaturas:
buscar_por_codigo é uma consulta: não altera o estado.adicionar tem efeito observável: depois de retornar, a busca pelo mesmo código encontra o item.adicionar é que o código ainda não esteja cadastrado; o caso de uso verifica isso antes de pedir a inclusão.Esse é um recorte sequencial. A sequência “buscar, depois adicionar” não promete atomicidade nem segurança entre escritores concorrentes. Essas garantias exigiriam requisitos e mecanismos adicionais.
Dica
Não acrescente listar, remover, atualizar ou métodos genéricos como salvar apenas por hábito. O contrato deve expressar o comportamento que este consumidor precisa agora. Compatibilidade é cumprir esses comportamentos, e não somente ter métodos com nomes parecidos.
Associe cada situação ao significado correto no contrato de persistência.
Toque em um item e depois no par correspondente.

Passo 5 de 8
Implemente um caso de uso de cadastro que depende apenas de um contrato de repositório e valide seu comportamento com um adaptador em memória.
O caso de uso coordena o cadastro nesta ordem:
Item válido.Ele recebe o repositório como parâmetro. Portanto, sabe o que precisa fazer, mas não sabe se os dados estão em memória, em JSON ou em outra tecnologia.
A consulta e a inclusão são chamadas por meio do contrato; a implementação concreta permanece fora do caso de uso.

O caso de uso chama o contrato. O adaptador concreto realiza o armazenamento.
Dica
Criar RepositorioEmMemoria() dentro de cadastrar() acoplaria o caso de uso à implementação. Receber repositorio por parâmetro torna a dependência visível e substituível.
No seu computador, crie a pasta catalogo com um arquivo vazio __init__.py e os três arquivos abaixo. Depois, crie uma pasta tests. O exemplo é autocontido e não depende de arquivos de aulas anteriores.
from dataclasses import dataclass
class TituloInvalido(ValueError):
pass
class CodigoDuplicado(ValueError):
pass
@dataclass(frozen=True)
class Item:
codigo: str
titulo: str
def __post_init__(self) -> None:
if not self.titulo.strip():
raise TituloInvalido("O título não pode ser vazio.")from typing import Protocol
from catalogo.dominio import Item
class RepositorioDeItens(Protocol):
def buscar_por_codigo(self, codigo: str) -> Item | None:
"""Retorna o item encontrado ou None quando o código está ausente."""
def adicionar(self, item: Item) -> None:
"""Inclui um item cujo código ainda não está cadastrado."""from catalogo.contratos import RepositorioDeItens
from catalogo.dominio import CodigoDuplicado, Item
def cadastrar(
codigo: str,
titulo: str,
repositorio: RepositorioDeItens,
) -> Item:
item = Item(codigo=codigo, titulo=titulo)
if repositorio.buscar_por_codigo(codigo) is not None:
raise CodigoDuplicado(f"O código {codigo!r} já está cadastrado.")
repositorio.adicionar(item)
return itemEste adaptador satisfaz o comportamento necessário sem herdar do Protocol. Ele guarda itens em um dicionário e expõe quantidade_de_gravacoes para tornar observável que uma rejeição não provocou inclusão.
from catalogo.dominio import Item
class RepositorioEmMemoria:
def __init__(self) -> None:
self._itens: dict[str, Item] = {}
self.quantidade_de_gravacoes = 0
def buscar_por_codigo(self, codigo: str) -> Item | None:
return self._itens.get(codigo)
def adicionar(self, item: Item) -> None:
self._itens[item.codigo] = item
self.quantidade_de_gravacoes += 1import pytest
from catalogo.cadastro import cadastrar
from catalogo.dominio import CodigoDuplicado, TituloInvalido
from catalogo.memoria import RepositorioEmMemoria
def test_cadastra_item_e_o_torna_consultavel() -> None:
repositorio = RepositorioEmMemoria()
resultado = cadastrar("A1", "Python avançado", repositorio)
assert resultado.codigo == "A1"
assert repositorio.buscar_por_codigo("A1") == resultado
assert repositorio.quantidade_de_gravacoes == 1
def test_titulo_invalido_nao_grava() -> None:
repositorio = RepositorioEmMemoria()
with pytest.raises(TituloInvalido):
cadastrar("A1", " ", repositorio)
assert repositorio.buscar_por_codigo("A1") is None
assert repositorio.quantidade_de_gravacoes == 0
def test_codigo_duplicado_nao_grava_novamente() -> None:
repositorio = RepositorioEmMemoria()
cadastrar("A1", "Primeiro título", repositorio)
with pytest.raises(CodigoDuplicado):
cadastrar("A1", "Outro título", repositorio)
assert repositorio.buscar_por_codigo("A1").titulo == "Primeiro título"
assert repositorio.quantidade_de_gravacoes == 1Exemplo
Na raiz que contém as pastas catalogo e tests, execute:
python -m pytest -qO resultado esperado é 3 passed. Nos dois cenários de rejeição, a contagem de gravações permanece em 0 ou não aumenta além da gravação inicial.
Execute os testes no seu computador. Quais foram os resultados? Cite a evidência de que o título inválido e a tentativa de código duplicado não causaram uma nova gravação.
Escreva pelo menos 80 caracteres (0/80).

Passo 6 de 8
Implemente um adaptador JSON que traduza registros externos para modelos internos, preserve o contrato da aplicação e sinalize dados inválidos como falhas de persistência.
O repositório JSON é dono do caminho do arquivo, da leitura, da gravação e dos nomes presentes no registro armazenado. O caso de uso continua trabalhando apenas com Item e com o contrato de repositório.
Assim, {"codigo": "PY-01", "titulo": "Python"} é um detalhe do adaptador. O modelo interno não recebe dicionários, caminhos nem decide como um arquivo será escrito.
O adaptador converte o formato externo ao entrar e ao sair da aplicação.

Na leitura: registro JSON → adaptador → Item válido. Na gravação: Item → adaptador → registro JSON. A aplicação não conhece os nomes dos campos nem o arquivo.
Atenção
Neste exemplo, somente um arquivo inicialmente ausente significa catálogo vazio. JSON malformado, uma estrutura que não seja lista, registros incompletos ou valores que não formem um Item válido são falhas de persistência. Não transforme esses casos em uma lista vazia: isso esconderia dados danificados.
O código abaixo pressupõe os tipos já criados nos passos anteriores: Item valida código e título; RepositorioCatalogo é o Protocol consumido pela aplicação; e FalhaPersistencia representa uma falha prevista nessa fronteira. O adaptador encadeia a exceção técnica com from exc, preservando sua causa para diagnóstico.
from __future__ import annotations
import json
from pathlib import Path
from catalogo.aplicacao.portas import FalhaPersistencia
from catalogo.dominio.item import Item
class RepositorioJson:
def __init__(self, caminho: Path) -> None:
self._caminho = caminho
def buscar_por_codigo(self, codigo: str) -> Item | None:
for item in self._carregar_itens():
if item.codigo == codigo:
return item
return None
def adicionar(self, item: Item) -> None:
itens = self._carregar_itens()
registros = [
{"codigo": existente.codigo, "titulo": existente.titulo}
for existente in itens
]
registros.append({"codigo": item.codigo, "titulo": item.titulo})
try:
conteudo = json.dumps(registros, ensure_ascii=False, indent=2)
self._caminho.write_text(conteudo, encoding="utf-8")
except OSError as exc:
raise FalhaPersistencia("não foi possível gravar o catálogo") from exc
def _carregar_itens(self) -> list[Item]:
try:
conteudo = self._caminho.read_text(encoding="utf-8")
except FileNotFoundError:
return []
except OSError as exc:
raise FalhaPersistencia("não foi possível ler o catálogo") from exc
try:
registros = json.loads(conteudo)
if not isinstance(registros, list):
raise TypeError("a raiz do JSON deve ser uma lista")
return [
Item(codigo=registro["codigo"], titulo=registro["titulo"])
for registro in registros
if isinstance(registro, dict)
]
except (json.JSONDecodeError, KeyError, TypeError, ValueError) as exc:
raise FalhaPersistencia("dados do catálogo são inválidos") from excUse o mesmo caso de uso de cadastro do passo anterior, sem alterá-lo. A primeira instância grava usando o contrato; uma segunda instância, criada com o mesmo caminho, deve recuperar o item. Isso diferencia o JSON do adaptador em memória, que é útil em testes isolados, mas perde seus dados quando a instância deixa de existir.
import pytest
from catalogo.adaptadores.repositorio_json import RepositorioJson
from catalogo.aplicacao.portas import FalhaPersistencia
from catalogo.dominio.item import Item
def test_arquivo_ausente_representa_catalogo_vazio(tmp_path) -> None:
repositorio = RepositorioJson(tmp_path / "catalogo.json")
assert repositorio.buscar_por_codigo("PY-01") is None
def test_item_gravado_e_recuperado_por_nova_instancia(tmp_path) -> None:
caminho = tmp_path / "catalogo.json"
primeira_instancia = RepositorioJson(caminho)
primeira_instancia.adicionar(Item(codigo="PY-01", titulo="Python avançado"))
segunda_instancia = RepositorioJson(caminho)
assert segunda_instancia.buscar_por_codigo("PY-01") == Item(
codigo="PY-01",
titulo="Python avançado",
)
def test_json_invalido_e_falha_de_persistencia(tmp_path) -> None:
caminho = tmp_path / "catalogo.json"
caminho.write_text("{ nao e json }", encoding="utf-8")
repositorio = RepositorioJson(caminho)
with pytest.raises(FalhaPersistencia) as erro:
repositorio.buscar_por_codigo("PY-01")
assert isinstance(erro.value.__cause__, Exception)Dica
No teste de cadastro completo, injete RepositorioJson(tmp_path / "catalogo.json") no mesmo caso de uso usado com o repositório em memória. Sucesso, título inválido e código duplicado devem manter o mesmo resultado de negócio. Apenas a composição escolhe qual adaptador entregar ao caso de uso.
No seu computador, execute os testes. Relate duas evidências: o que aconteceu ao reabrir o repositório JSON e o que aconteceu diante de conteúdo inválido. Inclua por que o caso de uso não precisou ser modificado.
Escreva pelo menos 80 caracteres (0/80).

Passo 7 de 8
Monte os componentes concretos em um único ponto e mantenha a escolha do armazenamento fora das regras do catálogo.
Depois que código e título já foram separados, a interface só encaminha esses valores ao caso de uso e apresenta o item retornado. Ela não abre arquivos, não conhece JSON e não decide se um código é único.
Para manter essa fronteira explícita, receba tanto o caso de uso quanto o repositório por parâmetro. Assim, a interface não precisa importar nenhum adaptador concreto.
Uma interface local e fina, sem detalhes de armazenamento.
from collections.abc import Callable
from catalogo.dominio.item import Item
def cadastrar_e_mostrar(
cadastrar: Callable[[object, str, str], Item],
repositorio: object,
codigo: str,
titulo: str,
) -> None:
item = cadastrar(repositorio, codigo, titulo)
print(f"Item cadastrado: {item.codigo} — {item.titulo}")
Dica
print pertence à interface, pois é apresentação. A validação do título e a verificação de duplicidade continuam no modelo e no caso de uso, respectivamente.
A raiz de composição é o módulo que importa classes concretas, constrói objetos e os conecta. Ela pode conhecer RepositorioJson; o caso de uso não pode.
Neste exemplo local, os valores e o caminho são explícitos. Ainda não há parser de argumentos, configuração externa nem instalação do pacote.
Observe que as importações concretas ficam concentradas na composição, enquanto a execução percorre os componentes já conectados.

A composição constrói e injeta. Depois, a interface chama o caso de uso, que usa o repositório recebido.
A composição escolhe JSON e injeta o objeto construído na interface.
from pathlib import Path
from catalogo.adaptadores.repositorio_json import RepositorioJson
from catalogo.aplicacao.cadastrar_item import cadastrar_item
from catalogo.interface_terminal import cadastrar_e_mostrar
def executar_demonstracao() -> None:
arquivo = Path("catalogo.json")
repositorio = RepositorioJson(arquivo)
cadastrar_e_mostrar(
cadastrar_item,
repositorio,
codigo="LIV-001",
titulo="Arquitetura Python",
)
if __name__ == "__main__":
executar_demonstracao()
Para uma demonstração ou teste, a composição pode construir RepositorioMemoria no lugar de RepositorioJson. A interface, o caso de uso e o domínio permanecem iguais porque dependem do comportamento contratado, não da tecnologia escolhida.
Troque somente a criação e a importação na raiz de composição.
from catalogo.adaptadores.repositorio_memoria import RepositorioMemoria
def executar_demonstracao_em_memoria() -> None:
repositorio = RepositorioMemoria()
cadastrar_e_mostrar(
cadastrar_item,
repositorio,
codigo="LIV-001",
titulo="Arquitetura Python",
)
Atenção
Evite condicionais como if usar_json no caso de uso ou no domínio. Também evite criar o repositório dentro de cadastrar_item. Esses locais passariam a conhecer uma decisão externa que a composição já resolve.
Coloque as ações na ordem correta, desde a montagem até a persistência do item.
Para mudar de persistência JSON para memória, qual componente deve ser alterado?

Passo 8 de 8
Conclua o recorte do catálogo executando os mesmos cenários com persistência em memória e em JSON, e verifique onde cada dependência pode existir.
A validação final não procura provar que memória e JSON funcionam internamente do mesmo jeito. Ela verifica o que o caso de uso observa: cadastro válido retorna o item, título vazio é rejeitado e código repetido é rejeitado.
Execute esses cenários contra os dois adaptadores. Depois, acrescente uma evidência exclusiva do JSON: crie outro adaptador apontando para o mesmo arquivo e consulte o item cadastrado. A memória é um substituto de teste volátil; ela não precisa sobreviver à recriação.
A composição conhece as implementações concretas; as camadas internas conhecem apenas modelos e contratos.

Trocar Memória por JSON muda a composição, não o domínio nem o caso de uso.
Dica
A sequência consultar e depois incluir é adequada para este exemplo sequencial. Ela não cria, por si só, uma operação atômica nem coordena dois escritores concorrentes.
Em uma pasta local, crie o pacote catalogo e copie os módulos abaixo com estes nomes. O código contém domínio, contrato, adaptadores, interface fina e composição; não depende de parser de argumentos, configuração externa nem projeto pré-criado.
Crie cada seção em seu respectivo arquivo.
# catalogo/dominio.py
from dataclasses import dataclass
class ItemInvalido(ValueError):
pass
@dataclass(frozen=True, slots=True)
class Item:
codigo: str
titulo: str
def __post_init__(self) -> None:
if not self.titulo.strip():
raise ItemInvalido("o título não pode ser vazio")
# catalogo/aplicacao.py
from typing import Protocol
from catalogo.dominio import Item
class FalhaDePersistencia(RuntimeError):
pass
class CodigoDuplicado(ValueError):
pass
class RepositorioDeItens(Protocol):
def buscar_por_codigo(self, codigo: str) -> Item | None: ...
def adicionar(self, item: Item) -> None: ...
class CadastrarItem:
def __init__(self, repositorio: RepositorioDeItens) -> None:
self._repositorio = repositorio
def executar(self, codigo: str, titulo: str) -> Item:
item = Item(codigo=codigo, titulo=titulo)
if self._repositorio.buscar_por_codigo(item.codigo) is not None:
raise CodigoDuplicado(f"código já cadastrado: {item.codigo}")
self._repositorio.adicionar(item)
return item
# catalogo/adaptadores.py
import json
from pathlib import Path
from catalogo.aplicacao import FalhaDePersistencia
from catalogo.dominio import Item
class RepositorioEmMemoria:
def __init__(self) -> None:
self._itens: dict[str, Item] = {}
self.gravacoes = 0
def buscar_por_codigo(self, codigo: str) -> Item | None:
return self._itens.get(codigo)
def adicionar(self, item: Item) -> None:
self._itens[item.codigo] = item
self.gravacoes += 1
class RepositorioJson:
def __init__(self, caminho: Path) -> None:
self._caminho = caminho
def _carregar(self) -> list[Item]:
if not self._caminho.exists():
return []
try:
bruto = json.loads(self._caminho.read_text(encoding="utf-8"))
if not isinstance(bruto, list):
raise ValueError("a raiz JSON deve ser uma lista")
return [Item(registro["codigo"], registro["titulo"]) for registro in bruto]
except (OSError, json.JSONDecodeError, KeyError, TypeError, ValueError) as erro:
raise FalhaDePersistencia("não foi possível carregar o catálogo") from erro
def _salvar(self, itens: list[Item]) -> None:
registros = [{"codigo": item.codigo, "titulo": item.titulo} for item in itens]
try:
self._caminho.write_text(
json.dumps(registros, ensure_ascii=False, indent=2),
encoding="utf-8",
)
except OSError as erro:
raise FalhaDePersistencia("não foi possível gravar o catálogo") from erro
def buscar_por_codigo(self, codigo: str) -> Item | None:
return next((item for item in self._carregar() if item.codigo == codigo), None)
def adicionar(self, item: Item) -> None:
itens = self._carregar()
itens.append(item)
self._salvar(itens)
# catalogo/interface.py
from catalogo.aplicacao import CadastrarItem
def cadastrar_valores(cadastro: CadastrarItem, codigo: str, titulo: str) -> str:
item = cadastro.executar(codigo, titulo)
return f"Item cadastrado: {item.codigo} — {item.titulo}"
# catalogo/composicao.py
from pathlib import Path
from catalogo.adaptadores import RepositorioEmMemoria, RepositorioJson
from catalogo.aplicacao import CadastrarItem
def montar_com_memoria() -> CadastrarItem:
return CadastrarItem(RepositorioEmMemoria())
def montar_com_json(caminho: Path) -> CadastrarItem:
return CadastrarItem(RepositorioJson(caminho))Salve como test_catalogo.py ao lado da pasta catalogo e execute pytest -q nessa pasta.
import pytest
from catalogo.adaptadores import RepositorioEmMemoria, RepositorioJson
from catalogo.aplicacao import CadastrarItem, CodigoDuplicado
from catalogo.dominio import ItemInvalido
@pytest.fixture(params=["memoria", "json"])
def repositorio(request, tmp_path):
if request.param == "memoria":
return RepositorioEmMemoria()
return RepositorioJson(tmp_path / "catalogo.json")
def test_cadastra_item_em_qualquer_adaptador(repositorio):
cadastro = CadastrarItem(repositorio)
resultado = cadastro.executar("A1", "Python avançado")
assert resultado.codigo == "A1"
assert repositorio.buscar_por_codigo("A1") == resultado
def test_titulo_vazio_e_rejeitado_em_qualquer_adaptador(repositorio):
cadastro = CadastrarItem(repositorio)
with pytest.raises(ItemInvalido):
cadastro.executar("A1", " ")
assert repositorio.buscar_por_codigo("A1") is None
def test_codigo_duplicado_e_rejeitado_em_qualquer_adaptador(repositorio):
cadastro = CadastrarItem(repositorio)
cadastro.executar("A1", "Python avançado")
with pytest.raises(CodigoDuplicado):
cadastro.executar("A1", "Outro título")
assert repositorio.buscar_por_codigo("A1").titulo == "Python avançado"
def test_rejeicoes_de_negocio_nao_gravam_na_memoria():
repositorio = RepositorioEmMemoria()
cadastro = CadastrarItem(repositorio)
with pytest.raises(ItemInvalido):
cadastro.executar("A1", "")
assert repositorio.gravacoes == 0
cadastro.executar("A1", "Python avançado")
with pytest.raises(CodigoDuplicado):
cadastro.executar("A1", "Outro título")
assert repositorio.gravacoes == 1
def test_json_persiste_apos_recriar_o_adaptador(tmp_path):
caminho = tmp_path / "catalogo.json"
CadastrarItem(RepositorioJson(caminho)).executar("A1", "Python avançado")
repositorio_recriado = RepositorioJson(caminho)
assert repositorio_recriado.buscar_por_codigo("A1") == ItemInesperadoNaoUsar
# Substitua apenas a última asserção acima por esta versão correta:
# assert repositorio_recriado.buscar_por_codigo("A1").titulo == "Python avançado"Atenção
O último teste contém deliberadamente uma referência inválida (ItemInesperadoNaoUsar). Antes de executar, substitua as duas últimas linhas indicadas pelo comentário. Isso torna explícita a evidência de durabilidade: um novo RepositorioJson lê o mesmo arquivo e recupera o título salvo.
Após corrigir e executar o teste, relate: quais resultados de negócio foram iguais nos dois adaptadores, qual evidência ocorreu apenas no JSON e em que módulo está a escolha entre memória e JSON.
Escreva pelo menos 120 caracteres (0/120).
Resumo
Use esta revisão ao examinar seu código local.
dominio.py não importa interface, caminhos, JSON ou adaptadores.aplicacao.py declara o Protocol consumido e orquestra o cadastro sem criar repositórios concretos.adaptadores.py contém o caminho e o mapeamento entre registros JSON e Item; uma falha de leitura ou formato vira FalhaDePersistencia com a causa preservada.interface.py recebe valores e apresenta o resultado, mas não carrega arquivos nem aplica a regra de unicidade.composicao.py é o único módulo que importa os adaptadores concretos e decide qual será injetado.Justifique por que as regras do catálogo não dependem do terminal nem do JSON. Cite uma evidência nas importações, uma nos testes e um limite que essa separação não resolve.
Escreva pelo menos 160 caracteres (0/160).
Parabéns! Você concluiu: Separar domínio, persistência e interface
Milhares de cursos online em vídeo, ebooks e áudiobooks.
Para testar seus conhecimentos no decorrer dos cursos online
Gerado diretamente na galeria de fotos do seu celular e enviado ao seu e-mail
Baixe nosso aplicativo pelo QR Code ou pelos links abaixo:.
+ de 10 milhões
de alunos
Certificado grátis e
válido em todo o Brasil
60 mil exercícios
gratuitos
4,8/5 classificação
nas lojas de apps
Cursos gratuitos em
vídeo, ebooks e audiobooks