Pular para o conteúdo principal

A porta MazeRepository

A operação "gerar um labirinto" precisa terminar gravando o arquivo de saída. Quem comanda essa operação é o GenerateMaze (página "O caso de uso GenerateMaze"); quem sabe gravar é o FileMazeRepository (página "Gravando o arquivo de saída"). Só que a arquitetura do projeto proíbe o GenerateMaze de importar o FileMazeRepository. Esta página mostra como um consegue usar o outro sem importá-lo.

A regra que impede o import​

O projeto é dividido em camadas, e uma camada só pode importar as que estão mais para dentro (parte 2).

  • O GenerateMaze é um use case: uma operação completa da aplicação, como "gerar e gravar um labirinto". Ele fica na camada use_cases, mais para dentro.
  • O FileMazeRepository fica na camada repositories, a que mexe em arquivos, mais para fora. O detalhe de como gravar (formato, disco, mensagens de erro) pode mudar sem a operação mudar.

Então o GenerateMaze precisa gravar, mas não pode citar a classe que grava.

A analogia: a tomada​

A parte 2 comparou a porta a uma tomada da parede. Quem instala a tomada não sabe qual aparelho vai ser ligado nela; define só o formato do encaixe. Qualquer aparelho com o plugue certo funciona, e trocar de aparelho não exige mexer na parede.

No código é igual:

TomadaCódigo
a parede, que precisa de "algo ligado ali"GenerateMaze, que precisa de "algo que grave"
o formato do encaixea porta MazeRepository: "um método save(path, maze)"
o aparelho com o plugue certoFileMazeRepository, que grava em arquivo
quem liga o aparelho na tomadao a_maze_ing.py, o arquivo principal (parte 14)

O formato do encaixe é definido por quem usa (o use case), não por quem é ligado. Por isso a porta mora junto do use case, em amazeing/use_cases/ports.py.

A porta em Python: typing.Protocol​

class MazeRepository(Protocol):
"""Storage of a generated maze."""

def save(self, path: str, maze: GeneratedMaze) -> None:
"""Store a maze, replacing what the path held before."""

Um Protocol descreve uma forma: "qualquer objeto que tenha um método save(path: str, maze: GeneratedMaze) -> None". Ele não grava nada; não tem código, só a assinatura. O corpo do método é só a docstring, que já é um comando válido e dispensa pass.

-> None quer dizer que o método não devolve nada: ele só faz o trabalho (gravar).

Sem herança​

A diferença para uma classe base comum está em como uma classe passa a "ser" um MazeRepository:

Classe base abstrata (ABC)Protocol
a classe concreta precisa herdar dela: class FileMazeRepository(MazeRepository)basta ter o método com a assinatura certa, sem herdar nada
o repositório importaria use_cases/ports.pyo repositório não importa nada de use_cases

Uma ABC (abstract base class) é uma classe feita só para ser herdada. Com ela, o FileMazeRepository teria de importar ports.py, que mora em use_cases.

Pela direção das setas, isso não seria proibido: repositories fica mais para fora que use_cases, e uma camada de fora pode depender de uma de dentro. Quem proíbe é a tabela de imports permitidos da parte 2: repositories só importa domain, utils e errors. É uma escolha do projeto: o repositório sabe gravar e ler arquivos e não precisa saber que existe um use case.

Com o Protocol, essa regra continua cumprida: vale a forma do objeto, não a árvore de herança. É a ideia do "se anda como pato e grasna como pato, é um pato".

Como as peças se ligam, o arquivo completo da porta e um exemplo do mypy conferindo a forma estão na próxima página.