Visão do projeto e entregáveis
Neste capítulo, você vai construir um projeto completo (de ponta a ponta) que integra múltiplas classes, herança usada com parcimônia, composição, propriedades, métodos especiais (dunder methods) e organização em pacotes. O objetivo é sair com um pequeno sistema utilizável, com casos de uso claros (criar/editar/listar), validações e tratamento de erros, exemplos de execução e um conjunto mínimo de testes/checagens para garantir comportamento.
Domínio escolhido: Biblioteca (empréstimos de itens)
Você implementará um sistema de biblioteca que gerencia usuários, itens (livros e revistas) e empréstimos. A escolha é proposital: permite herança (tipos de item), composição (Biblioteca contém repositórios/serviços), propriedades (estado calculado do empréstimo) e dunder methods (representação e igualdade).
Entregáveis (checklist)
- Diagrama simples de classes (texto/ASCII) com relações de herança e composição.
- Implementação incremental em etapas, com commits mentais: entidades → repositórios → serviços → interface de uso.
- Casos de uso: criar, editar e listar (usuários, itens, empréstimos).
- Validações e erros: entradas inválidas, duplicidade, item indisponível, devolução indevida.
- Exemplos de execução: um script que demonstra o fluxo.
- Testes/checagens mínimas: asserts simples ou unittest para regras críticas.
- Refatorações finais: nomes, responsabilidades, redução de duplicação e melhoria de legibilidade.
Diagrama simples de classes (texto)
Item (base) <|-- Livro
<|-- Revista
Usuario
Emprestimo (compõe: Usuario + Item)
RepositorioBase[T] (base) <|-- RepositorioItens
<|-- RepositorioUsuarios
<|-- RepositorioEmprestimos
ServicoBiblioteca (compõe: repos de itens/usuarios/emprestimos)
Erros de domínio:
- DomainError (base)
- ValidationError
- NotFoundError
- ConflictError
- BusinessRuleErrorLeitura do diagrama: Livro e Revista especializam Item. Emprestimo referencia (compõe) um Usuario e um Item. ServicoBiblioteca compõe repositórios e concentra os casos de uso.
Estrutura de pacotes sugerida
biblioteca/
__init__.py
domain/
__init__.py
errors.py
item.py
usuario.py
emprestimo.py
infra/
__init__.py
repositorio_base.py
repositorios.py
services/
__init__.py
biblioteca_service.py
app/
__init__.py
demo.py
tests/
__init__.py
test_regras_basicas.pyEssa separação ajuda a manter o domínio (regras e entidades) independente de detalhes de armazenamento e da camada de aplicação.
Passo a passo prático: implementação incremental
Passo 1 — Erros de domínio (tratamento de erros consistente)
Centralize exceções para padronizar mensagens e facilitar testes.
- Ouça o áudio com a tela desligada
- Ganhe Certificado após a conclusão
- + de 5000 cursos para você explorar!
Baixar o aplicativo
# biblioteca/domain/errors.py
class DomainError(Exception):
"""Erro base do domínio."""
class ValidationError(DomainError):
pass
class NotFoundError(DomainError):
pass
class ConflictError(DomainError):
pass
class BusinessRuleError(DomainError):
passPasso 2 — Entidades do domínio (Item, Livro, Revista, Usuario, Emprestimo)
Você já viu como criar classes, propriedades e dunder methods. Aqui, o foco é integrar tudo com regras de negócio e consistência.
# biblioteca/domain/item.py
from __future__ import annotations
from dataclasses import dataclass
from biblioteca.domain.errors import ValidationError
@dataclass(frozen=True)
class ItemId:
value: str
def __post_init__(self):
if not self.value or not self.value.strip():
raise ValidationError("ItemId não pode ser vazio")
@dataclass
class Item:
item_id: ItemId
titulo: str
def __post_init__(self):
if not self.titulo or not self.titulo.strip():
raise ValidationError("Título do item é obrigatório")
def __str__(self) -> str:
return f"{self.titulo} ({self.item_id.value})"
def __repr__(self) -> str:
return f"Item(item_id={self.item_id!r}, titulo={self.titulo!r})"
@dataclass
class Livro(Item):
autor: str = ""
def __post_init__(self):
super().__post_init__()
if not self.autor.strip():
raise ValidationError("Autor é obrigatório para Livro")
def __repr__(self) -> str:
return f"Livro(item_id={self.item_id!r}, titulo={self.titulo!r}, autor={self.autor!r})"
@dataclass
class Revista(Item):
edicao: int = 1
def __post_init__(self):
super().__post_init__()
if self.edicao <= 0:
raise ValidationError("Edição deve ser > 0")
def __repr__(self) -> str:
return f"Revista(item_id={self.item_id!r}, titulo={self.titulo!r}, edicao={self.edicao!r})"# biblioteca/domain/usuario.py
from __future__ import annotations
from dataclasses import dataclass
from biblioteca.domain.errors import ValidationError
@dataclass(frozen=True)
class UsuarioId:
value: str
def __post_init__(self):
if not self.value or not self.value.strip():
raise ValidationError("UsuarioId não pode ser vazio")
@dataclass
class Usuario:
usuario_id: UsuarioId
nome: str
def __post_init__(self):
if not self.nome or not self.nome.strip():
raise ValidationError("Nome do usuário é obrigatório")
def __str__(self) -> str:
return f"{self.nome} ({self.usuario_id.value})"
def __repr__(self) -> str:
return f"Usuario(usuario_id={self.usuario_id!r}, nome={self.nome!r})"# biblioteca/domain/emprestimo.py
from __future__ import annotations
from dataclasses import dataclass
from datetime import date
from biblioteca.domain.errors import ValidationError, BusinessRuleError
from biblioteca.domain.usuario import Usuario
from biblioteca.domain.item import Item
@dataclass
class Emprestimo:
emprestimo_id: str
usuario: Usuario
item: Item
data_inicio: date
data_prevista_devolucao: date
data_devolucao: date | None = None
def __post_init__(self):
if not self.emprestimo_id or not self.emprestimo_id.strip():
raise ValidationError("emprestimo_id é obrigatório")
if self.data_prevista_devolucao < self.data_inicio:
raise ValidationError("data_prevista_devolucao não pode ser anterior à data_inicio")
if self.data_devolucao is not None and self.data_devolucao < self.data_inicio:
raise ValidationError("data_devolucao não pode ser anterior à data_inicio")
@property
def ativo(self) -> bool:
return self.data_devolucao is None
@property
def atrasado(self) -> bool:
if not self.ativo:
return False
return date.today() > self.data_prevista_devolucao
def devolver(self, quando: date | None = None) -> None:
if not self.ativo:
raise BusinessRuleError("Empréstimo já foi devolvido")
self.data_devolucao = quando or date.today()
def __str__(self) -> str:
status = "ativo" if self.ativo else f"devolvido em {self.data_devolucao.isoformat()}"
return f"Emprestimo({self.emprestimo_id}): {self.usuario.nome} -> {self.item.titulo} [{status}]"
def __repr__(self) -> str:
return (
"Emprestimo("
f"emprestimo_id={self.emprestimo_id!r}, usuario={self.usuario!r}, item={self.item!r}, "
f"data_inicio={self.data_inicio!r}, data_prevista_devolucao={self.data_prevista_devolucao!r}, "
f"data_devolucao={self.data_devolucao!r})"
)Passo 3 — Repositórios em memória (CRUD com validações de duplicidade)
Repositórios encapsulam armazenamento e oferecem operações previsíveis. Aqui, usaremos dicionários em memória para manter o projeto simples e testável.
# biblioteca/infra/repositorio_base.py
from __future__ import annotations
from typing import Dict, Generic, Iterable, TypeVar, Callable
from biblioteca.domain.errors import ConflictError, NotFoundError
T = TypeVar("T")
K = TypeVar("K")
class RepositorioBase(Generic[K, T]):
def __init__(self, key_fn: Callable[[T], K]):
self._data: Dict[K, T] = {}
self._key_fn = key_fn
def add(self, obj: T) -> None:
key = self._key_fn(obj)
if key in self._data:
raise ConflictError(f"Objeto com chave {key!r} já existe")
self._data[key] = obj
def get(self, key: K) -> T:
try:
return self._data[key]
except KeyError as e:
raise NotFoundError(f"Objeto com chave {key!r} não encontrado") from e
def update(self, obj: T) -> None:
key = self._key_fn(obj)
if key not in self._data:
raise NotFoundError(f"Objeto com chave {key!r} não encontrado")
self._data[key] = obj
def list_all(self) -> Iterable[T]:
return list(self._data.values())# biblioteca/infra/repositorios.py
from __future__ import annotations
from biblioteca.infra.repositorio_base import RepositorioBase
from biblioteca.domain.item import Item
from biblioteca.domain.usuario import Usuario
from biblioteca.domain.emprestimo import Emprestimo
class RepositorioItens(RepositorioBase[str, Item]):
def __init__(self):
super().__init__(key_fn=lambda item: item.item_id.value)
class RepositorioUsuarios(RepositorioBase[str, Usuario]):
def __init__(self):
super().__init__(key_fn=lambda u: u.usuario_id.value)
class RepositorioEmprestimos(RepositorioBase[str, Emprestimo]):
def __init__(self):
super().__init__(key_fn=lambda e: e.emprestimo_id)Passo 4 — Serviço de aplicação (casos de uso criar/editar/listar)
O serviço orquestra regras: verifica existência, disponibilidade, cria empréstimos, edita dados e lista informações. Ele compõe repositórios e expõe uma API simples para a camada de interface (script/CLI/web futuramente).
# biblioteca/services/biblioteca_service.py
from __future__ import annotations
from datetime import date, timedelta
from biblioteca.domain.errors import BusinessRuleError, ValidationError
from biblioteca.domain.item import ItemId, Livro, Revista
from biblioteca.domain.usuario import UsuarioId, Usuario
from biblioteca.domain.emprestimo import Emprestimo
from biblioteca.infra.repositorios import RepositorioItens, RepositorioUsuarios, RepositorioEmprestimos
class ServicoBiblioteca:
def __init__(
self,
itens: RepositorioItens,
usuarios: RepositorioUsuarios,
emprestimos: RepositorioEmprestimos,
):
self._itens = itens
self._usuarios = usuarios
self._emprestimos = emprestimos
# --- Usuários (criar/editar/listar) ---
def criar_usuario(self, usuario_id: str, nome: str) -> Usuario:
usuario = Usuario(usuario_id=UsuarioId(usuario_id), nome=nome)
self._usuarios.add(usuario)
return usuario
def editar_usuario_nome(self, usuario_id: str, novo_nome: str) -> Usuario:
if not novo_nome or not novo_nome.strip():
raise ValidationError("novo_nome é obrigatório")
usuario = self._usuarios.get(usuario_id)
usuario.nome = novo_nome
self._usuarios.update(usuario)
return usuario
def listar_usuarios(self) -> list[Usuario]:
return list(self._usuarios.list_all())
# --- Itens (criar/editar/listar) ---
def cadastrar_livro(self, item_id: str, titulo: str, autor: str) -> Livro:
livro = Livro(item_id=ItemId(item_id), titulo=titulo, autor=autor)
self._itens.add(livro)
return livro
def cadastrar_revista(self, item_id: str, titulo: str, edicao: int) -> Revista:
revista = Revista(item_id=ItemId(item_id), titulo=titulo, edicao=edicao)
self._itens.add(revista)
return revista
def editar_titulo_item(self, item_id: str, novo_titulo: str):
if not novo_titulo or not novo_titulo.strip():
raise ValidationError("novo_titulo é obrigatório")
item = self._itens.get(item_id)
item.titulo = novo_titulo
self._itens.update(item)
return item
def listar_itens(self):
return list(self._itens.list_all())
# --- Empréstimos (criar/editar/listar) ---
def _item_disponivel(self, item_id: str) -> bool:
for e in self._emprestimos.list_all():
if e.item.item_id.value == item_id and e.ativo:
return False
return True
def criar_emprestimo(
self,
emprestimo_id: str,
usuario_id: str,
item_id: str,
dias: int = 7,
inicio: date | None = None,
) -> Emprestimo:
if dias <= 0:
raise ValidationError("dias deve ser > 0")
if not self._item_disponivel(item_id):
raise BusinessRuleError("Item indisponível (já emprestado)")
usuario = self._usuarios.get(usuario_id)
item = self._itens.get(item_id)
data_inicio = inicio or date.today()
previsto = data_inicio + timedelta(days=dias)
emprestimo = Emprestimo(
emprestimo_id=emprestimo_id,
usuario=usuario,
item=item,
data_inicio=data_inicio,
data_prevista_devolucao=previsto,
)
self._emprestimos.add(emprestimo)
return emprestimo
def devolver_item(self, emprestimo_id: str, quando: date | None = None) -> Emprestimo:
emprestimo = self._emprestimos.get(emprestimo_id)
emprestimo.devolver(quando=quando)
self._emprestimos.update(emprestimo)
return emprestimo
def listar_emprestimos(self, somente_ativos: bool = False) -> list[Emprestimo]:
emprestimos = list(self._emprestimos.list_all())
if somente_ativos:
emprestimos = [e for e in emprestimos if e.ativo]
return emprestimosPasso 5 — Script de demonstração (exemplos de execução)
Este arquivo simula o uso do sistema e serve como “manual vivo” do comportamento esperado.
# biblioteca/app/demo.py
from biblioteca.infra.repositorios import RepositorioItens, RepositorioUsuarios, RepositorioEmprestimos
from biblioteca.services.biblioteca_service import ServicoBiblioteca
from biblioteca.domain.errors import DomainError
def main():
service = ServicoBiblioteca(
itens=RepositorioItens(),
usuarios=RepositorioUsuarios(),
emprestimos=RepositorioEmprestimos(),
)
try:
service.criar_usuario("u1", "Ana")
service.criar_usuario("u2", "Bruno")
service.cadastrar_livro("i1", "Python para Projetos", "Carla Silva")
service.cadastrar_revista("i2", "Tech Monthly", 42)
e1 = service.criar_emprestimo("e1", "u1", "i1", dias=3)
print(e1)
print("Itens:")
for item in service.listar_itens():
print("-", item)
print("Empréstimos ativos:")
for e in service.listar_emprestimos(somente_ativos=True):
print("-", e)
service.devolver_item("e1")
print("Empréstimos (todos):")
for e in service.listar_emprestimos():
print("-", e)
except DomainError as e:
print("Erro:", e)
if __name__ == "__main__":
main()Validações e tratamento de erros: o que checar e onde
Para manter o sistema previsível, distribua validações em camadas com responsabilidades claras:
| Camada | Validações típicas | Exemplos no projeto |
|---|---|---|
| Entidades (domain) | Invariantes do objeto | Título obrigatório, edição > 0, datas coerentes |
| Serviço (services) | Regras de negócio envolvendo múltiplos objetos | Item disponível, dias > 0, devolução apenas se ativo |
| Repositório (infra) | Consistência de armazenamento | Duplicidade ao adicionar, inexistência ao buscar/atualizar |
Uma regra prática: se a validação depende apenas do próprio objeto, fica na entidade; se depende de consultar outros objetos (ex.: disponibilidade), fica no serviço.
Casos de uso exigidos (criar/editar/listar): roteiro de verificação
Criar
- Criar usuário com
criar_usuario. - Cadastrar livro/revista com
cadastrar_livro/cadastrar_revista. - Criar empréstimo com
criar_emprestimo(falha se item já estiver emprestado).
Editar
- Editar nome do usuário com
editar_usuario_nome. - Editar título do item com
editar_titulo_item. - Editar estado do empréstimo via ação de negócio:
devolver_item(em vez de “setar” campos diretamente).
Listar
- Listar usuários com
listar_usuarios. - Listar itens com
listar_itens. - Listar empréstimos com
listar_emprestimose filtrosomente_ativos.
Conjunto mínimo de testes/checagens (garantindo comportamento)
Aqui vai um conjunto pequeno de testes com assert (rápido para começar). Você pode migrar para unittest depois sem mudar o design.
# biblioteca/tests/test_regras_basicas.py
from biblioteca.infra.repositorios import RepositorioItens, RepositorioUsuarios, RepositorioEmprestimos
from biblioteca.services.biblioteca_service import ServicoBiblioteca
from biblioteca.domain.errors import BusinessRuleError, ConflictError
def make_service():
return ServicoBiblioteca(
itens=RepositorioItens(),
usuarios=RepositorioUsuarios(),
emprestimos=RepositorioEmprestimos(),
)
def test_nao_emprestar_item_indisponivel():
s = make_service()
s.criar_usuario("u1", "Ana")
s.cadastrar_livro("i1", "X", "Autor")
s.criar_emprestimo("e1", "u1", "i1")
try:
s.criar_emprestimo("e2", "u1", "i1")
assert False, "Deveria falhar: item indisponível"
except BusinessRuleError:
assert True
def test_nao_permitir_ids_duplicados():
s = make_service()
s.criar_usuario("u1", "Ana")
try:
s.criar_usuario("u1", "Outra")
assert False, "Deveria falhar: usuário duplicado"
except ConflictError:
assert True
def test_devolver_duas_vezes_falha():
s = make_service()
s.criar_usuario("u1", "Ana")
s.cadastrar_revista("i2", "R", 1)
s.criar_emprestimo("e1", "u1", "i2")
s.devolver_item("e1")
try:
s.devolver_item("e1")
assert False, "Deveria falhar: devolução duplicada"
except BusinessRuleError:
assert TrueChecagens mínimas recomendadas para este projeto:
- Não emprestar item já emprestado (regra central).
- Não permitir IDs duplicados em repositórios.
- Não permitir devolução duas vezes.
- Validar entradas essenciais (strings vazias, números inválidos, datas incoerentes).
Refatorações finais orientadas (legibilidade e reutilização)
1) Reduzir duplicação em edição de campos
Se você notar padrões como “buscar → validar → alterar → update”, considere criar métodos utilitários internos no serviço (ex.: _get_usuario, _get_item) ou funções de validação reutilizáveis.
2) Melhorar a regra de disponibilidade
Hoje, _item_disponivel percorre todos os empréstimos. Para evoluir, você pode:
- Manter um índice (ex.: dict item_id → emprestimo ativo).
- Adicionar método no repositório de empréstimos:
find_ativo_por_item(item_id).
3) Tornar IDs mais consistentes
Você já tem ItemId e UsuarioId. Se quiser reforçar consistência, crie EmprestimoId também, seguindo o mesmo padrão.
4) Ajustar dunder methods para depuração
Garanta que __repr__ seja informativo (bom para logs e testes) e que __str__ seja amigável (bom para interface). Se notar saídas confusas, refine formatos e inclua campos relevantes.
5) Separar “comandos” de “consultas”
Para crescer o projeto, uma refatoração útil é separar métodos que mudam estado (comandos) de métodos que apenas retornam dados (consultas). Isso facilita testes e reduz efeitos colaterais inesperados.