Modelagem de problemas comuns com Python Orientado a Objetos: do requisito ao código

Capítulo 13

Tempo estimado de leitura: 6 minutos

+ Exercício

Do requisito ao modelo: um roteiro de decisão

Modelar com Orientação a Objetos é transformar um conjunto de necessidades (requisitos) em um conjunto de tipos (classes) com responsabilidades claras e uma API previsível. O objetivo não é “criar muitas classes”, e sim reduzir ambiguidades: quem faz o quê, onde ficam as regras, como o sistema evolui sem quebrar.

Um processo prático e repetível costuma seguir estas etapas:

  • Levantar requisitos: o que o sistema precisa fazer, quais regras não podem ser violadas, quais cenários são críticos.
  • Identificar entidades e valores: “coisas” com identidade (entidades) e “dados” sem identidade própria (value objects).
  • Definir responsabilidades: cada classe deve ter um motivo principal para mudar.
  • Desenhar relacionamentos: composição (tem-um), associação (usa-um) e herança (é-um) quando houver especialização real.
  • Decidir onde ficam regras de negócio: invariantes e validações perto dos dados; orquestração em serviços/casos de uso; persistência fora do domínio.
  • Criar uma API consistente: nomes, retornos, erros, e fluxo de uso previsível.
  • Refatorar guiado por extensibilidade: reduzir duplicação, isolar variações e estabilizar contratos.

Checklist rápido para evitar modelos frágeis

  • Evite “classes anêmicas”: classes que só guardam dados e deixam toda regra espalhada em funções externas.
  • Evite “classes Deus”: uma classe que sabe e faz tudo (alta complexidade e acoplamento).
  • Prefira composição para montar comportamentos e herança apenas quando a substituição for segura.
  • Regras importantes devem ser testáveis sem depender de banco, rede ou interface.

Passo a passo de modelagem (com um template reutilizável)

1) Levantamento de requisitos: transforme frases em cenários

Comece com 5 a 10 cenários em linguagem natural. Exemplo (biblioteca):

  • Um usuário pode pegar emprestado um exemplar disponível.
  • Um empréstimo tem data de início e data prevista de devolução.
  • Não é possível emprestar um exemplar já emprestado.
  • Ao devolver, o exemplar volta a ficar disponível.
  • Um usuário não pode ter mais que N empréstimos ativos.

Em seguida, separe:

  • Regras (invariantes): “não emprestar exemplar indisponível”, “limite de empréstimos”.
  • Eventos: “emprestar”, “devolver”.
  • Consultas: “listar empréstimos ativos”, “ver disponibilidade”.

2) Identificação de entidades, value objects e serviços

Uma heurística simples:

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

  • Entidade: tem identidade e muda ao longo do tempo (Ex.: Usuário, Exemplar, Empréstimo).
  • Value object: representa um conceito imutável/sem identidade (Ex.: ISBN, Período, Dinheiro).
  • Serviço de domínio/caso de uso: coordena ações entre entidades (Ex.: Serviço de Empréstimo).

Evite criar entidade para tudo. “Título do livro” pode ser apenas um atributo; “ISBN” pode ser um value object se houver validação e uso recorrente.

3) Responsabilidades: escreva “verbo + objeto” por classe

Para cada candidato a classe, escreva 3 a 6 responsabilidades. Exemplo:

  • Exemplar: marcar como emprestado; marcar como devolvido; informar disponibilidade.
  • Usuário: registrar empréstimo ativo; remover empréstimo; verificar limite.
  • Empréstimo: registrar devolução; verificar se está ativo; calcular atraso (se existir regra).

Se uma classe tiver responsabilidades demais, divida. Se tiver responsabilidades vagas (“gerenciar sistema”), recomece.

4) Relacionamentos: composição vs herança

Use composição quando um objeto “tem” outro como parte (Carrinho tem Itens). Use herança quando há especialização substituível (PagamentoCartao é um Pagamento) e quando a API do tipo base faz sentido para todos os tipos.

SinalPrefiraExemplo
Variação de comportamentoComposição/estratégiaFrete calculado por regra pluggável
Hierarquia natural e substituívelHerançaNotificaçãoEmail/NotificaçãoSMS
Parte-todoComposiçãoBiblioteca possui Catálogo

5) Onde ficam as regras de negócio

Uma divisão prática:

  • Entidades/value objects: invariantes e validações locais (ex.: não permitir estado inválido).
  • Serviços/casos de uso: orquestração entre objetos (ex.: emprestar envolve usuário + exemplar + criação de empréstimo).
  • Infra: persistência, IO, integrações (não misturar com regra).

Isso ajuda a manter o domínio testável e independente.

6) API consistente: nomes, retornos e erros

Defina convenções antes de codar:

  • Métodos de comando (mudam estado): emprestar(), devolver(), adicionar_item().
  • Métodos de consulta (não mudam estado): esta_disponivel(), total(), itens().
  • Erros previsíveis: use exceções específicas do domínio (ex.: ExemplarIndisponivel).

Caso 1: Biblioteca de empréstimos (do requisito ao código)

Requisitos mínimos (recorte)

  • Emprestar um exemplar disponível para um usuário.
  • Impedir empréstimo se o usuário atingiu o limite.
  • Devolver um exemplar e encerrar o empréstimo.

Modelo inicial (classes e relações)

  • Usuario (entidade): mantém empréstimos ativos (composição).
  • Exemplar (entidade): estado de disponibilidade.
  • Emprestimo (entidade): liga Usuario e Exemplar, com datas.
  • ServicoEmprestimo (caso de uso): coordena a operação emprestar/devolver.

Implementação enxuta com regras no lugar certo

from dataclasses import dataclass, field
from datetime import date, timedelta

class RegraDeNegocioError(Exception):
    pass

class ExemplarIndisponivel(RegraDeNegocioError):
    pass

class LimiteDeEmprestimosAtingido(RegraDeNegocioError):
    pass

@dataclass
class Exemplar:
    id: str
    titulo: str
    _emprestado: bool = False

    def esta_disponivel(self) -> bool:
        return not self._emprestado

    def marcar_emprestado(self) -> None:
        if self._emprestado:
            raise ExemplarIndisponivel(f"Exemplar {self.id} indisponível")
        self._emprestado = True

    def marcar_devolvido(self) -> None:
        self._emprestado = False

@dataclass
class Emprestimo:
    id: str
    usuario_id: str
    exemplar_id: str
    inicio: date
    previsto_para: date
    devolvido_em: date | None = None

    def esta_ativo(self) -> bool:
        return self.devolvido_em is None

    def registrar_devolucao(self, quando: date) -> None:
        if not self.esta_ativo():
            return
        self.devolvido_em = quando

@dataclass
class Usuario:
    id: str
    nome: str
    limite_emprestimos: int = 3
    emprestimos: list[Emprestimo] = field(default_factory=list)

    def emprestimos_ativos(self) -> list[Emprestimo]:
        return [e for e in self.emprestimos if e.esta_ativo()]

    def pode_emprestar(self) -> bool:
        return len(self.emprestimos_ativos()) < self.limite_emprestimos

    def adicionar_emprestimo(self, emprestimo: Emprestimo) -> None:
        if not self.pode_emprestar():
            raise LimiteDeEmprestimosAtingido(
                f"Usuário {self.id} atingiu o limite"
            )
        self.emprestimos.append(emprestimo)

class ServicoEmprestimo:
    def emprestar(self, *, usuario: Usuario, exemplar: Exemplar, dias: int = 7) -> Emprestimo:
        if not usuario.pode_emprestar():
            raise LimiteDeEmprestimosAtingido(f"Usuário {usuario.id} atingiu o limite")

        exemplar.marcar_emprestado()

        hoje = date.today()
        emprestimo = Emprestimo(
            id=f"E-{usuario.id}-{exemplar.id}-{hoje.isoformat()}",
            usuario_id=usuario.id,
            exemplar_id=exemplar.id,
            inicio=hoje,
            previsto_para=hoje + timedelta(days=dias),
        )
        usuario.adicionar_emprestimo(emprestimo)
        return emprestimo

    def devolver(self, *, usuario: Usuario, exemplar: Exemplar, emprestimo: Emprestimo) -> None:
        emprestimo.registrar_devolucao(date.today())
        exemplar.marcar_devolvido()

Note a separação: Exemplar protege sua disponibilidade; Usuario protege o limite; o serviço coordena a sequência e cria o empréstimo.

Teste de comportamento (focado em regra)

def test_nao_empresta_exemplar_indisponivel():
    usuario = Usuario(id="U1", nome="Ana")
    exemplar = Exemplar(id="X1", titulo="Python")
    svc = ServicoEmprestimo()

    svc.emprestar(usuario=usuario, exemplar=exemplar)

    try:
        svc.emprestar(usuario=usuario, exemplar=exemplar)
        assert False, "Deveria falhar"
    except ExemplarIndisponivel:
        assert True

Caso 2: Carrinho de compras (modelagem e API consistente)

Requisitos mínimos (recorte)

  • Adicionar/remover itens com quantidade.
  • Calcular subtotal, desconto e total.
  • Aplicar cupom com regras (ex.: percentual, valor fixo, mínimo de compra).

Decisões de modelagem

  • Produto como entidade simples (id, nome, preço).
  • ItemCarrinho como composição (produto + quantidade).
  • Carrinho como agregador de itens e regras de cálculo.
  • Cupom como objeto de regra (política de desconto). Em vez de herança obrigatória, podemos usar composição com um “aplicador” (estratégia).

Implementação com estratégia de desconto (composição)

from dataclasses import dataclass, field

class CupomInvalido(Exception):
    pass

@dataclass(frozen=True)
class Produto:
    id: str
    nome: str
    preco: float

@dataclass
class ItemCarrinho:
    produto: Produto
    quantidade: int

    def subtotal(self) -> float:
        return self.produto.preco * self.quantidade

class RegraDesconto:
    def calcular(self, subtotal: float) -> float:
        return 0.0

@dataclass(frozen=True)
class DescontoPercentual(RegraDesconto):
    percentual: float  # 0.10 = 10%

    def calcular(self, subtotal: float) -> float:
        return max(0.0, subtotal * self.percentual)

@dataclass(frozen=True)
class DescontoValorFixo(RegraDesconto):
    valor: float

    def calcular(self, subtotal: float) -> float:
        return min(subtotal, max(0.0, self.valor))

@dataclass(frozen=True)
class Cupom:
    codigo: str
    regra: RegraDesconto
    minimo: float = 0.0

    def desconto(self, subtotal: float) -> float:
        if subtotal < self.minimo:
            return 0.0
        return self.regra.calcular(subtotal)

@dataclass
class Carrinho:
    itens: list[ItemCarrinho] = field(default_factory=list)
    cupom: Cupom | None = None

    def adicionar(self, produto: Produto, quantidade: int = 1) -> None:
        if quantidade <= 0:
            raise ValueError("quantidade deve ser > 0")
        for item in self.itens:
            if item.produto.id == produto.id:
                item.quantidade += quantidade
                return
        self.itens.append(ItemCarrinho(produto=produto, quantidade=quantidade))

    def remover(self, produto_id: str) -> None:
        self.itens = [i for i in self.itens if i.produto.id != produto_id]

    def subtotal(self) -> float:
        return sum(i.subtotal() for i in self.itens)

    def desconto(self) -> float:
        if not self.cupom:
            return 0.0
        return self.cupom.desconto(self.subtotal())

    def total(self) -> float:
        return self.subtotal() - self.desconto()

    def aplicar_cupom(self, cupom: Cupom) -> None:
        self.cupom = cupom

Observe como a variação (tipos de desconto) foi isolada em RegraDesconto. O carrinho não precisa conhecer detalhes de cada desconto, apenas chamar calcular.

Caso 3: Agenda de tarefas (regras, estados e extensibilidade)

Requisitos mínimos (recorte)

  • Criar tarefas com título e prazo opcional.
  • Marcar como concluída e reabrir.
  • Listar tarefas por status e por vencimento.
  • Regra: não permitir concluir tarefa já concluída (ou tornar idempotente).

Modelagem sugerida

  • Tarefa: entidade com estado (aberta/concluída), datas e operações.
  • ListaTarefas: coleção com operações de consulta e organização.
  • Filtro/Ordenação: pode ser função ou objeto (comece simples e extraia se crescer).

Implementação com API previsível

from dataclasses import dataclass, field
from datetime import date

@dataclass
class Tarefa:
    id: str
    titulo: str
    prazo: date | None = None
    concluida_em: date | None = None

    def esta_concluida(self) -> bool:
        return self.concluida_em is not None

    def concluir(self, quando: date | None = None) -> None:
        if self.esta_concluida():
            return
        self.concluida_em = quando or date.today()

    def reabrir(self) -> None:
        self.concluida_em = None

    def esta_atrasada(self, hoje: date | None = None) -> bool:
        hoje = hoje or date.today()
        return (self.prazo is not None) and (not self.esta_concluida()) and (self.prazo < hoje)

@dataclass
class ListaTarefas:
    tarefas: list[Tarefa] = field(default_factory=list)

    def adicionar(self, tarefa: Tarefa) -> None:
        self.tarefas.append(tarefa)

    def abertas(self) -> list[Tarefa]:
        return [t for t in self.tarefas if not t.esta_concluida()]

    def concluidas(self) -> list[Tarefa]:
        return [t for t in self.tarefas if t.esta_concluida()]

    def atrasadas(self, hoje: date | None = None) -> list[Tarefa]:
        return [t for t in self.tarefas if t.esta_atrasada(hoje=hoje)]

Essa modelagem deixa claro onde estão as regras de estado (na Tarefa) e onde estão as consultas de coleção (na ListaTarefas).

Refatorações guiadas para melhorar extensibilidade

Refatoração 1: “if” crescendo demais → isolar variação

Sintoma: um método começa a ter muitos if tipo == ... para tratar regras diferentes (ex.: descontos, tipos de notificação, cálculo de multa). Ação: extrair para estratégia (composição) ou para classes especializadas.

Exemplo de sintoma no carrinho (evitar):

def desconto(self):
    if self.cupom.tipo == "percentual":
        ...
    elif self.cupom.tipo == "fixo":
        ...

Melhoria: Cupom delega para regra.calcular() (como no exemplo).

Refatoração 2: regras duplicadas em vários lugares → mover para o dono do dado

Sintoma: a mesma validação aparece em serviço, controller e entidade. Ação: a regra deve ficar onde o estado é alterado. Ex.: disponibilidade do exemplar deve ser protegida pelo próprio Exemplar.

Refatoração 3: acoplamento com infraestrutura → criar portas (interfaces) simples

Sintoma: o serviço de empréstimo já “salva no banco” dentro do método. Ação: introduzir um repositório como dependência (porta), mantendo o domínio testável. Mesmo sem entrar em detalhes de arquitetura, você pode definir um contrato mínimo e usar um fake em testes.

class EmprestimoRepo:
    def salvar(self, emprestimo: Emprestimo) -> None:
        raise NotImplementedError

class ServicoEmprestimo:
    def __init__(self, repo: EmprestimoRepo):
        self.repo = repo

    def emprestar(self, *, usuario: Usuario, exemplar: Exemplar, dias: int = 7) -> Emprestimo:
        ...
        self.repo.salvar(emprestimo)
        return emprestimo

Refatoração 4: API inconsistente → padronizar comandos e consultas

Sintoma: métodos com nomes ambíguos (processar, gerenciar), retornos imprevisíveis e efeitos colaterais escondidos. Ação: padronizar:

  • Comandos retornam None ou o objeto criado (ex.: emprestar retorna Emprestimo).
  • Consultas não alteram estado.
  • Exceções do domínio para violações de regra.

Exercícios de modelagem (com critérios de avaliação)

Exercício 1: Biblioteca com reserva

Requisitos:

  • Se um exemplar estiver indisponível, o usuário pode criar uma reserva.
  • Quando o exemplar for devolvido, a primeira reserva da fila tem prioridade por 24h.
  • Um usuário não pode reservar o mesmo exemplar duas vezes.

Tarefas:

  • Liste entidades e value objects.
  • Desenhe relações (quem compõe quem).
  • Defina a API mínima: métodos e exceções.
  • Escreva 3 testes de comportamento (cenários).

Critérios de avaliação:

  • Clareza: nomes e responsabilidades evidentes; regras localizadas.
  • Reutilização: fila de reservas não acoplada ao empréstimo; regras isoladas.
  • Testes de comportamento: cobrem prioridade de 24h, duplicidade e ordem da fila.

Exercício 2: Carrinho com frete e impostos

Requisitos:

  • Frete varia por região e peso total.
  • Imposto varia por categoria do produto.
  • O total final = subtotal + frete + impostos − descontos.

Tarefas:

  • Modele CalculadoraFrete e CalculadoraImposto como estratégias (composição).
  • Garanta que o carrinho não precise de if por região/categoria.
  • Crie testes para duas regiões e duas categorias.

Critérios de avaliação:

  • Clareza: carrinho com API simples (total()), dependências explícitas.
  • Reutilização: estratégias plugáveis; fácil adicionar nova região/categoria.
  • Testes de comportamento: validam cálculo final e regras de variação.

Exercício 3: Agenda com recorrência

Requisitos:

  • Tarefas podem ser recorrentes (diária, semanal, mensal).
  • Ao concluir uma tarefa recorrente, a próxima ocorrência é criada automaticamente.
  • Deve ser possível pausar a recorrência.

Tarefas:

  • Decida se recorrência é herança (TarefaRecorrente) ou composição (tarefa + regra de recorrência). Justifique.
  • Defina como a criação da próxima ocorrência acontece (na tarefa? em um serviço?).
  • Escreva testes para: concluir gera próxima; pausar impede geração.

Critérios de avaliação:

  • Clareza: regra de recorrência explícita e isolada.
  • Reutilização: fácil adicionar novo tipo de recorrência.
  • Testes de comportamento: cobrem geração, pausa e datas.

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

Em uma modelagem orientada a objetos, qual alternativa melhor representa a separação correta entre regras de negócio e orquestração, mantendo o domínio testável e com responsabilidades claras?

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

Você errou! Tente novamente.

A abordagem recomendada coloca regras e invariantes nas entidades/value objects (evitando estados inválidos) e usa serviços para orquestrar fluxos entre objetos. Persistência e IO ficam fora do domínio, o que melhora testabilidade e reduz acoplamento.

Próximo capitúlo

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

Arrow Right Icon
Baixe o app para ganhar Certificação grátis e ouvir os cursos em background, mesmo com a tela desligada.
  • Leia este curso no aplicativo para ganhar seu Certificado Digital!
  • Ouça este curso no aplicativo sem precisar ligar a tela do celular;
  • Tenha acesso 100% gratuito a mais de 4000 cursos online, ebooks e áudiobooks;
  • + Centenas de exercícios + Stories Educativos.