Projeto final em Python Orientado a Objetos: aplicação completa com classes, herança e composição

Capítulo 14

Tempo estimado de leitura: 14 minutos

+ Exercício

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
  - BusinessRuleError

Leitura 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.py

Essa 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.

Continue em nosso aplicativo e ...
  • Ouça o áudio com a tela desligada
  • Ganhe Certificado após a conclusão
  • + de 5000 cursos para você explorar!
ou continue lendo abaixo...
Download App

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):
    pass

Passo 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 emprestimos

Passo 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:

CamadaValidações típicasExemplos no projeto
Entidades (domain)Invariantes do objetoTítulo obrigatório, edição > 0, datas coerentes
Serviço (services)Regras de negócio envolvendo múltiplos objetosItem disponível, dias > 0, devolução apenas se ativo
Repositório (infra)Consistência de armazenamentoDuplicidade 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_emprestimos e filtro somente_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 True

Checagens 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.

Agora responda o exercício sobre o conteúdo:

Ao decidir onde implementar uma validação em um sistema de biblioteca com entidades, serviço e repositórios, qual opção melhor segue a regra de responsabilidade entre camadas?

Você acertou! Parabéns, agora siga para a próxima página

Você errou! Tente novamente.

Invariantes que dependem apenas do estado do próprio objeto pertencem às entidades. Já regras que exigem consultar outros objetos ou repositórios (como disponibilidade do item) devem ficar no serviço, que orquestra os casos de uso.

Capa do Ebook gratuito Python Orientado a Objetos para Iniciantes: Classes, Herança e Boas Práticas
100%

Python Orientado a Objetos para Iniciantes: Classes, Herança e Boas Práticas

Novo curso

14 páginas

Baixe o app para ganhar Certificação grátis e ouvir os cursos em background, mesmo com a tela desligada.