Classes abstratas e contratos em POO Python: abc, métodos abstratos e design orientado a interface

Capítulo 10

Tempo estimado de leitura: 9 minutos

+ Exercício

O que são classes abstratas (ABCs) e por que elas existem

Em projetos pequenos, muitas vezes basta “funcionar”: se um objeto tem os métodos esperados, você usa (duck typing). Em projetos maiores, com múltiplas equipes, módulos e integrações, é comum precisar de um contrato formal: uma definição explícita do que uma implementação deve oferecer. Em Python, esse contrato pode ser expresso com classes abstratas (ABCs, de Abstract Base Classes), usando o módulo abc.

Uma ABC permite:

  • Definir uma API mínima obrigatória (métodos/atributos que precisam existir).
  • Falhar cedo: instanciar uma classe incompleta gera erro claro.
  • Guiar implementações e facilitar refatorações, porque o contrato fica centralizado.
  • Melhorar a legibilidade: ao ler a ABC, você entende o “papel” (interface) esperado.

Duck typing vs ABC: quando usar cada um

CenárioPreferirMotivo
Código interno simples, poucas implementações, baixo riscoDuck typingMenos cerimônia, mais flexibilidade
Biblioteca/SDK, plugin system, integrações externasABCsContrato explícito e erros antecipados
Equipe grande, várias implementações do mesmo papelABCsPadronização e manutenção
Você quer aceitar “qualquer coisa” que tenha certos métodosDuck typingInteroperabilidade sem herança

Um ponto importante: ABC não elimina duck typing. Você pode usar ABC para documentar e validar internamente, e ainda aceitar objetos “compatíveis” em pontos específicos. A diferença é que a ABC torna o contrato explícito.

Ferramentas do módulo abc: ABC e abstractmethod

O módulo abc fornece:

  • ABC: classe base para criar uma ABC.
  • @abstractmethod: decorador que marca métodos obrigatórios.

Regras práticas:

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

  • Uma classe que herda de uma ABC e não implementa todos os métodos abstratos não pode ser instanciada.
  • Métodos abstratos podem ter corpo (por exemplo, para compartilhar lógica), mas continuam obrigatórios.
  • Você pode combinar métodos abstratos com métodos concretos auxiliares na ABC.

Exemplo prático: contratos para Pagamento, Repositório e Exportação

Vamos montar três “interfaces” (ABCs) comuns em sistemas reais:

  • GatewayDePagamento: processa pagamentos e estorna.
  • RepositorioDeDados: salva e busca entidades.
  • Exportador: exporta dados em um formato.

Passo 1: definir tipos de domínio simples (DTOs) e exceções

from __future__ import annotations
from dataclasses import dataclass
from decimal import Decimal
from typing import Protocol, Iterable


@dataclass(frozen=True)
class Pagamento:
    id: str
    valor: Decimal
    moeda: str


@dataclass(frozen=True)
class Recibo:
    pagamento_id: str
    status: str  # ex.: "aprovado", "recusado"
    mensagem: str


class ContratoNaoAtendidoError(TypeError):
    """Erro de contrato para deixar falhas mais claras em tempo de execução."""


class PagamentoRecusadoError(RuntimeError):
    pass

Observação: ContratoNaoAtendidoError é opcional, mas útil para padronizar mensagens quando você validar contratos manualmente em pontos críticos.

Passo 2: criar as ABCs (contratos formais)

from abc import ABC, abstractmethod
from typing import Optional


class GatewayDePagamento(ABC):
    @abstractmethod
    def cobrar(self, pagamento: Pagamento) -> Recibo:
        """Processa a cobrança e retorna um recibo."""

    @abstractmethod
    def estornar(self, pagamento_id: str) -> None:
        """Solicita estorno de um pagamento já processado."""


class RepositorioDeDados(ABC):
    @abstractmethod
    def salvar_pagamento(self, pagamento: Pagamento) -> None:
        """Persiste o pagamento."""

    @abstractmethod
    def obter_pagamento(self, pagamento_id: str) -> Optional[Pagamento]:
        """Busca um pagamento por id. Retorna None se não existir."""


class Exportador(ABC):
    @abstractmethod
    def exportar(self, pagamentos: list[Pagamento]) -> str:
        """Exporta pagamentos e retorna o conteúdo gerado (texto)."""

Essas ABCs são o “centro” do design orientado a interface: o restante do sistema depende delas, não de implementações concretas.

Passo 3: implementar múltiplas classes concretas

Agora criamos implementações diferentes para cada contrato.

GatewayDePagamento: implementação fake (para testes) e uma implementação “simulada”

class GatewayFake(GatewayDePagamento):
    def __init__(self, aprovar: bool = True):
        self._aprovar = aprovar
        self._estornos: set[str] = set()

    def cobrar(self, pagamento: Pagamento) -> Recibo:
        if not self._aprovar:
            return Recibo(pagamento_id=pagamento.id, status="recusado", mensagem="Pagamento recusado")
        return Recibo(pagamento_id=pagamento.id, status="aprovado", mensagem="OK")

    def estornar(self, pagamento_id: str) -> None:
        self._estornos.add(pagamento_id)


class GatewaySimulado(GatewayDePagamento):
    def cobrar(self, pagamento: Pagamento) -> Recibo:
        # Simula uma regra: valores acima de 1000 são recusados
        if pagamento.valor > Decimal("1000"):
            return Recibo(pagamento_id=pagamento.id, status="recusado", mensagem="Limite excedido")
        return Recibo(pagamento_id=pagamento.id, status="aprovado", mensagem="Aprovado")

    def estornar(self, pagamento_id: str) -> None:
        # Em um gateway real, aqui haveria chamada externa
        return None

RepositorioDeDados: memória e “arquivo” (simples)

import json
from pathlib import Path


class RepositorioEmMemoria(RepositorioDeDados):
    def __init__(self):
        self._db: dict[str, Pagamento] = {}

    def salvar_pagamento(self, pagamento: Pagamento) -> None:
        self._db[pagamento.id] = pagamento

    def obter_pagamento(self, pagamento_id: str):
        return self._db.get(pagamento_id)


class RepositorioEmArquivoJSON(RepositorioDeDados):
    def __init__(self, caminho: Path):
        self._caminho = caminho
        if not self._caminho.exists():
            self._caminho.write_text(json.dumps({}), encoding="utf-8")

    def _ler(self) -> dict:
        return json.loads(self._caminho.read_text(encoding="utf-8"))

    def _escrever(self, data: dict) -> None:
        self._caminho.write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8")

    def salvar_pagamento(self, pagamento: Pagamento) -> None:
        data = self._ler()
        data[pagamento.id] = {"id": pagamento.id, "valor": str(pagamento.valor), "moeda": pagamento.moeda}
        self._escrever(data)

    def obter_pagamento(self, pagamento_id: str):
        data = self._ler()
        item = data.get(pagamento_id)
        if not item:
            return None
        return Pagamento(id=item["id"], valor=Decimal(item["valor"]), moeda=item["moeda"])

Exportador: CSV e JSON

class ExportadorCSV(Exportador):
    def exportar(self, pagamentos: list[Pagamento]) -> str:
        linhas = ["id,valor,moeda"]
        for p in pagamentos:
            linhas.append(f"{p.id},{p.valor},{p.moeda}")
        return "\n".join(linhas)


class ExportadorJSON(Exportador):
    def exportar(self, pagamentos: list[Pagamento]) -> str:
        data = [
            {"id": p.id, "valor": str(p.valor), "moeda": p.moeda}
            for p in pagamentos
        ]
        return json.dumps(data, ensure_ascii=False, indent=2)

Passo a passo: compondo o sistema a partir dos contratos

Agora criamos um serviço que depende apenas das interfaces (ABCs). Isso facilita trocar implementações sem alterar a regra de negócio.

class ServicoDePagamentos:
    def __init__(
        self,
        gateway: GatewayDePagamento,
        repo: RepositorioDeDados,
        exportador: Exportador,
    ):
        self._gateway = gateway
        self._repo = repo
        self._exportador = exportador

    def processar(self, pagamento: Pagamento) -> Recibo:
        recibo = self._gateway.cobrar(pagamento)
        if recibo.status != "aprovado":
            raise PagamentoRecusadoError(recibo.mensagem)
        self._repo.salvar_pagamento(pagamento)
        return recibo

    def exportar_pagamentos(self, ids: list[str]) -> str:
        pagamentos: list[Pagamento] = []
        for pid in ids:
            p = self._repo.obter_pagamento(pid)
            if p is not None:
                pagamentos.append(p)
        return self._exportador.exportar(pagamentos)

Uso com diferentes combinações:

from decimal import Decimal

servico = ServicoDePagamentos(
    gateway=GatewaySimulado(),
    repo=RepositorioEmMemoria(),
    exportador=ExportadorCSV(),
)

p1 = Pagamento(id="p-001", valor=Decimal("10.50"), moeda="BRL")
recibo = servico.processar(p1)

saida = servico.exportar_pagamentos(["p-001", "p-999"])
print(saida)

Erros claros: como o abc ajuda a falhar cedo

Se alguém criar uma implementação incompleta, o Python impede a instanciação e aponta quais métodos faltam.

class GatewayIncompleto(GatewayDePagamento):
    def cobrar(self, pagamento: Pagamento) -> Recibo:
        return Recibo(pagamento_id=pagamento.id, status="aprovado", mensagem="OK")

# Falta implementar estornar

g = GatewayIncompleto()  # TypeError: Can't instantiate abstract class ... with abstract method estornar

Esse tipo de falha é valioso em projetos grandes: o erro aparece no momento em que a classe é usada (instanciada), e não em um ponto distante do sistema.

Validação adicional de contrato (opcional) para mensagens ainda melhores

ABCs já fornecem validação, mas às vezes você quer validar objetos recebidos dinamicamente (por exemplo, plugins carregados em runtime). Você pode checar se um objeto é instância de uma ABC e lançar um erro padronizado.

def exigir_gateway(obj: object) -> GatewayDePagamento:
    if not isinstance(obj, GatewayDePagamento):
        raise ContratoNaoAtendidoError(
            f"Objeto {type(obj).__name__} não atende ao contrato GatewayDePagamento"
        )
    return obj

Isso é útil quando o objeto vem de fora do seu controle (configuração, import dinâmico, etc.).

Design orientado a interface: boas práticas ao definir contratos

  • Contratos pequenos e coesos: evite ABCs “gigantes”. Se um papel tem responsabilidades demais, divida em interfaces menores.
  • Nomes de métodos orientados ao domínio: contratos são mais fáceis de entender quando refletem o vocabulário do sistema.
  • Evite vazar detalhes de infraestrutura: a ABC deve descrever o que é necessário, não como é feito (ex.: não obrigue “usar requests”).
  • Retornos e erros previsíveis: documente (em docstrings e tipos) o que retorna e quais exceções podem ocorrer.
  • Dependa de interfaces, não de classes concretas: serviços recebem GatewayDePagamento, não GatewaySimulado.

Exercícios (com foco em validar o contrato e gerar erros claros)

1) Implementação incompleta (erro obrigatório)

Crie uma classe ExportadorXML que herda de Exportador, mas propositalmente não implemente exportar. Tente instanciar e observe o erro. Depois implemente exportar retornando uma string XML simples.

2) Contrato com método abstrato + método concreto

Altere Exportador para incluir um método concreto exportar_para_arquivo(caminho: Path, pagamentos: list[Pagamento]) que chama exportar e grava o conteúdo. Mantenha exportar como abstrato. Teste com ExportadorCSV e ExportadorJSON.

3) Plugin carregado dinamicamente (validação manual)

Simule um “plugin” vindo de fora: crie uma classe CoisaQualquer com um método cobrar, mas sem estornar. Passe uma instância para exigir_gateway e garanta que o erro lançado seja ContratoNaoAtendidoError com uma mensagem clara.

4) Repositório alternativo com regra de negócio

Implemente RepositorioSomenteLeitura que herda de RepositorioDeDados. Faça salvar_pagamento lançar PermissionError. Use no ServicoDePagamentos e observe onde o erro ocorre. Em seguida, ajuste o serviço para tratar esse caso e retornar uma mensagem de falha adequada (sem mudar o contrato).

5) Teste de contrato com múltiplas implementações

Escreva uma função teste_gateway(gateway: GatewayDePagamento) que:

  • Cria um Pagamento de baixo valor e verifica que cobrar retorna um Recibo com pagamento_id correto.
  • Chama estornar e verifica que não lança exceção.

Execute esse teste com GatewayFake e GatewaySimulado. A ideia é garantir que todas as implementações respeitam o mesmo contrato e comportamento mínimo esperado.

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

Qual é o principal benefício de usar uma classe abstrata (ABC) para definir o contrato de um GatewayDePagamento em um sistema com múltiplas implementações?

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

Você errou! Tente novamente.

ABCs permitem definir uma API mínima obrigatória. Se uma classe herda da ABC e não implementa todos os métodos abstratos (ex.: estornar), ela não pode ser instanciada, gerando um erro claro e antecipado.

Próximo capitúlo

Organização de projetos Python Orientado a Objetos: módulos, pacotes e arquitetura simples

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

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.