O value object MazeConfig
O programa começa recebendo o caminho de um arquivo de configuração. Dali ele precisa tirar valores confiáveis: uma largura que é um número entre 1 e 100, uma entrada que está dentro da grade, e assim por diante. Se algo estiver errado, ele precisa parar com uma mensagem que diga exatamente o quê. Esta parte constrói essa leitura; esta página mostra as duas etapas dela e o objeto que guarda o resultado.
Lembrando o arquivo
O arquivo de configuração (parte 1) tem uma
opção por linha, no formato CHAVE=valor, e linhas começando com # são comentários:
WIDTH=20
HEIGHT=15
ENTRY=0,0
EXIT=19,14
OUTPUT_FILE=maze.txt
PERFECT=False
As seis chaves acima são obrigatórias pelo enunciado. SEED, a semente do sorteio (mesmo
número, mesmo labirinto), é opcional e foi acrescentada pelo projeto.
Duas etapas, duas classes
- O
FileConfigRepository(amazeing/repositories/config_repository.py) cuida do arquivo: abrir, ler, pular comentários, separar chave e valor. Ele devolve um dicionário de textos, como{"WIDTH": "20", "ENTRY": "0,0", ...}. É um repository, o papel das classes que mexem em arquivo; nenhuma classe fora da camadarepositoriesabre arquivos (parte 2). A outra classe dessa camada, oFileMazeRepository, grava o arquivo de saída (parte 12). - O
MazeConfig(amazeing/domain/value_objects/maze_config.py) cuida das regras: o que é um valor válido. Ele não sabe que existe arquivo; recebe o dicionário e devolve os valores já convertidos, ou um erro.
Separar as duas coisas deixa cada uma simples: as regras podem ser usadas e experimentadas com
um dicionário escrito à mão, sem criar arquivo nenhum. O MazeConfig vem primeiro nesta parte
porque o repositório o usa.
Os campos
MANDATORY_KEYS = ("WIDTH", "HEIGHT", "ENTRY", "EXIT", "OUTPUT_FILE", "PERFECT")
@dataclass(frozen=True)
class MazeConfig:
width: int
height: int
entry: tuple[int, int]
exit: tuple[int, int]
output_file: str
perfect: bool
seed: int | None
Um campo por chave do arquivo, já no tipo certo: entry é a tupla (0, 0), não o texto
"0,0"; perfect é o booleano False, não o texto "False".
int | Nonequer dizer "um inteiro ouNone".seedéNonequando o arquivo não temSEED, e o gerador entendeNonecomo "sorteio diferente a cada execução".MANDATORY_KEYSlista as seis chaves obrigatórias na mesma ordem da tabela de opções do enunciado (seção 2 de O enunciado em linguagem simples):WIDTH,HEIGHT,ENTRY,EXIT,OUTPUT_FILE,PERFECT. É também a ordem em que elas aparecem nas mensagens de erro.SEEDfica de fora porque é opcional.
@dataclass é um decorador da biblioteca padrão que escreve sozinho, a partir dos campos
anotados, o __init__ (o construtor), o __repr__ (o texto que o print mostra) e o __eq__
(a comparação com ==). Ele já apareceu na Cell (parte 4).
frozen=True ("congelado") proíbe mudar um campo depois que o objeto existe: a linha
config.width = 0 levanta (lança) a exceção FrozenInstanceError. O
Experimente da página 6 mostra isso funcionando,
depois que o arquivo estiver completo.
Por que um value object
Um value object é um objeto que é só um valor, como o número 20 ou o texto "abc": não muda
depois de criado, e dois com o mesmo conteúdo são iguais. A configuração é um valor nesse
sentido: ela descreve uma execução do programa e não tem motivo para mudar no meio dela.
Ser imutável é o que torna a validação confiável. Uma vez criado e validado, um MazeConfig
continua válido até o fim do programa, porque nenhum código consegue trocar width para 0
depois. Quem recebe um MazeConfig pode usar os valores sem conferir nada de novo.
A fábrica create
A validação não fica no __init__, e sim num método separado, create:
@classmethod
def create(cls, raw: Mapping[str, str]) -> "MazeConfig":
@classmethodfaz o método ser chamado pela classe, não por um objeto:MazeConfig.create(...). O primeiro parâmetro,cls, recebe a própria classeMazeConfig. Um método assim, que constrói e devolve um objeto novo, se chama fábrica.-> "MazeConfig"anota o tipo de retorno. O nome vem entre aspas porque, no meio da definição da classe, o nomeMazeConfigainda não existe para o Python.rawé umMapping[str, str]: qualquer coisa que se leia como um dicionário de textos para textos. O repositório passa umdict; ocreatesó usaraw[chave]echave in raw. A anotação éMapping, e nãodict, porqueMappingsó oferece leitura: omypyacusaria qualquer linha docreateque tentasse alterar o dicionário recebido.
Por que não validar no __init__? O __init__ que o @dataclass escreve recebe os valores
já convertidos (width=20). O create recebe textos ("20"), converte, confere todas
as regras e só no fim chama cls(...), o __init__. Se uma regra falha, ele levanta
ConfigError (a exceção de configuração da
parte 10) e o objeto nunca chega a existir.
O resultado: não existe MazeConfig inválido.
O gerador também valida
O MazeGenerator, a classe do mazegen que gera o labirinto
(parte 9), já confere várias destas regras quando
é criado. O MazeConfig confere de novo para a mensagem citar a chave do arquivo, que é o
que o usuário escreveu. A página 5 mostra a diferença com
um exemplo.
As próximas páginas constroem o create: converter cada texto
(página 2) e conferir as regras numa ordem fixa
(páginas 3 e 4). O arquivo completo e os exemplos para rodar
estão na página 6.