Boas práticas de POO em Python: legibilidade, reutilização, acoplamento e coesão

Capítulo 12

Tempo estimado de leitura: 12 minutos

+ Exercício

O que torna um código OO “sustentável”

Boas práticas em POO não são regras rígidas: são escolhas que aumentam a chance de o código continuar legível e modificável com segurança. Neste capítulo, o foco é reduzir o custo de mudança por meio de: legibilidade (entender rápido), reutilização (aproveitar sem copiar), baixo acoplamento (mudar uma parte sem quebrar outras) e alta coesão (cada classe faz bem uma coisa).

Princípios práticos (sem slogans)

  • Classes pequenas e coesas: uma classe deve ter um motivo principal para mudar.
  • Métodos curtos: cada método deve caber na cabeça; quando cresce, extraia funções/métodos auxiliares.
  • Nomes claros: nomes devem revelar intenção e evitar ambiguidade.
  • Baixo acoplamento: dependa de abstrações simples (protocolos/contratos) e injete dependências.
  • Evitar estado global: estado global cria dependências invisíveis e dificulta testes.
  • Preferir imutabilidade quando fizer sentido: objetos imutáveis reduzem bugs por efeitos colaterais.
  • Docstrings úteis: documente o “porquê”, invariantes e exemplos de uso.

Legibilidade: nomes, tamanho e intenção

Nomes que descrevem comportamento

Evite nomes genéricos como data, info, manager, handler. Prefira nomes que indiquem papel e domínio.

RuimMelhorPor quê
ManagerInvoiceServiceIndica o domínio e a responsabilidade
process()calculate_total()Explicita o resultado
itemsline_itemsEvita ambiguidade

Métodos curtos: extração guiada por intenção

Quando um método faz várias coisas (validar, transformar, persistir, notificar), ele vira um “mini-script” difícil de testar. Um passo a passo comum para reduzir:

  • Identifique blocos com comentários do tipo “agora valida”, “agora calcula”, “agora salva”.
  • Extraia cada bloco para um método privado com nome descritivo.
  • Deixe o método público como orquestrador (alto nível).
class CheckoutService:
    def place_order(self, cart, payment_method):
        self._validate_cart(cart)
        total = self._calculate_total(cart)
        payment = self._charge(payment_method, total)
        order = self._create_order(cart, payment)
        self._notify(order)
        return order

    def _validate_cart(self, cart):
        ...

    def _calculate_total(self, cart):
        ...

Coesão: uma classe, uma ideia central

Coesão alta significa que os atributos e métodos “andam juntos”: eles existem para cumprir um objetivo claro. Sinais de baixa coesão:

  • A classe tem muitos métodos que usam subconjuntos diferentes de atributos.
  • Você precisa passar muitos parâmetros repetidos entre métodos da mesma classe.
  • Você tem dificuldade de dar um nome específico para a classe.

Exemplo: classe “inchada” e como fatiar

Considere uma classe que mistura regras de desconto, cálculo de frete e persistência. Uma refatoração orientada a coesão separa responsabilidades por “motivo de mudança”.

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

# Antes (sintoma): tudo em um lugar
class OrderProcessor:
    def __init__(self, db, shipping_api):
        self.db = db
        self.shipping_api = shipping_api

    def process(self, order):
        # calcula desconto
        # calcula frete
        # salva no banco
        # envia e-mail
        ...

Passo a passo para melhorar:

  • Liste as responsabilidades: desconto, frete, persistência, notificação.
  • Crie classes/serviços menores para cada responsabilidade.
  • Faça o orquestrador depender dessas partes (injeção).
# Depois: peças coesas
class DiscountPolicy:
    def discount_for(self, order):
        ...

class ShippingCalculator:
    def quote(self, order):
        ...

class OrderRepository:
    def save(self, order):
        ...

class Notifier:
    def order_created(self, order):
        ...

class OrderService:
    def __init__(self, discounts, shipping, repo, notifier):
        self.discounts = discounts
        self.shipping = shipping
        self.repo = repo
        self.notifier = notifier

    def create_order(self, order):
        order.apply_discount(self.discounts.discount_for(order))
        order.set_shipping(self.shipping.quote(order))
        self.repo.save(order)
        self.notifier.order_created(order)
        return order

Acoplamento: dependências visíveis e substituíveis

Acoplamento é o quanto uma parte do sistema “sabe demais” sobre outra. Quanto mais detalhes internos você precisa conhecer para usar uma classe, maior o acoplamento.

Reduzindo acoplamento com injeção de dependências

Evite criar dependências internas rígidas (instanciar diretamente dentro da classe). Prefira receber dependências por parâmetro (construtor ou método), mantendo a classe testável e flexível.

# Acoplado: a classe decide qual gateway usar
class PaymentService:
    def __init__(self):
        self.gateway = StripeGateway()  # difícil trocar em testes

# Menos acoplado: dependência vem de fora
class PaymentService:
    def __init__(self, gateway):
        self.gateway = gateway

    def charge(self, amount):
        return self.gateway.charge(amount)

Depender de “o que faz”, não de “quem é”

Na prática, isso significa depender de uma interface/contrato simples (por exemplo, um objeto que tenha charge(amount)), em vez de depender de uma implementação específica. Assim, você troca a implementação sem reescrever o código consumidor.

Evitar estado global: previsibilidade e testes

Estado global (variáveis globais mutáveis, singletons com cache interno, configurações alteradas em runtime) cria efeitos colaterais difíceis de rastrear. Prefira:

  • Passar configurações explicitamente (por exemplo, um objeto Settings).
  • Concentrar estado mutável em poucos lugares bem definidos.
  • Evitar que métodos dependam de variáveis externas invisíveis.
# Evite
TAX_RATE = 0.2

class Invoice:
    def total(self, subtotal):
        return subtotal * (1 + TAX_RATE)

# Prefira (dependência explícita)
class Invoice:
    def __init__(self, tax_rate):
        self.tax_rate = tax_rate

    def total(self, subtotal):
        return subtotal * (1 + self.tax_rate)

Imutabilidade quando fizer sentido: menos efeitos colaterais

Objetos imutáveis são úteis quando representam “valores” (ex.: dinheiro, coordenadas, intervalo de datas, identificadores). Benefícios: mais segurança em concorrência, menos bugs por alterações inesperadas, mais facilidade de cache.

Passo a passo: transformar um “value object” em imutável

  • Identifique classes que representam valores e não “entidades” com ciclo de vida.
  • Remova setters e métodos que alteram estado.
  • Faça métodos retornarem novas instâncias ao “modificar”.
from dataclasses import dataclass

@dataclass(frozen=True)
class Money:
    amount: int  # centavos
    currency: str

    def add(self, other: "Money") -> "Money":
        if self.currency != other.currency:
            raise ValueError("currency mismatch")
        return Money(self.amount + other.amount, self.currency)

Trade-off: imutabilidade pode gerar mais objetos e exigir um estilo mais funcional. Use quando o ganho de segurança/clareza compensar.

Docstrings: documentar intenção, invariantes e exemplos

Docstrings são mais úteis quando explicam regras e contratos, não o óbvio. Boas docstrings incluem:

  • O que a classe/método garante (pós-condições).
  • O que espera (pré-condições).
  • Invariantes importantes.
  • Exemplos curtos de uso.
class ShippingCalculator:
    """Calcula frete com base no destino e no peso total.

    Regras:
        - Peso total deve ser > 0.
        - CEP deve estar no formato 'NNNNN-NNN'.

    """

    def quote(self, destination_zip: str, total_weight_kg: float) -> int:
        """Retorna o frete em centavos.

        Levanta:
            ValueError: se o CEP for inválido ou o peso for <= 0.
        """
        ...

Trade-offs comuns em POO (e como decidir)

Herança vs composição

Use herança quando existe uma relação “é-um” estável e você quer polimorfismo com substituição segura. Use composição quando você quer montar comportamento por partes e evitar hierarquias rígidas.

EscolhaTende a funcionar melhor quando...Riscos
HerançaHá uma variação clara de especialização e um contrato comumHierarquias profundas, acoplamento ao pai, overrides perigosos
ComposiçãoComportamentos são combináveis e mudam com frequênciaMais objetos para gerenciar, mais “cola” no orquestrador

Propriedade vs método

Mesmo que você já conheça property, a decisão de design é: quando algo deve parecer um atributo e quando deve parecer uma ação?

  • Propriedade: acesso barato, sem efeitos colaterais, sem dependências externas, sem falhas “normais”. Ex.: order.total calculado a partir de itens já em memória.
  • Método: pode ser custoso, pode falhar, pode depender de I/O, pode ter parâmetros. Ex.: shipping.quote() consultando tabela externa.
# Bom como propriedade: determinístico e barato
class Order:
    @property
    def total_cents(self) -> int:
        return sum(item.total_cents for item in self.items)

# Bom como método: pode falhar e depende de serviço
class ShippingCalculator:
    def quote(self, order) -> int:
        ...

Exceções vs retornos sentinela

Sentinelas (como None, -1, "") podem simplificar fluxos, mas frequentemente escondem erro e espalham if pelo código. Exceções são melhores quando a falha é realmente excepcional ou quando você quer forçar o tratamento.

AbordagemUse quando...Cuidado
ExceçõesFalha é erro de uso, dados inválidos, invariantes quebradasNão use para controle de fluxo comum
Sentinela“Não encontrado” é esperado e frequente (ex.: busca opcional)Documente e trate explicitamente
# Sentinela aceitável: busca opcional
user = repo.find_user_by_email(email)
if user is None:
    ...

# Exceção apropriada: violação de regra
if total_weight_kg <= 0:
    raise ValueError("total_weight_kg must be > 0")

Checklist de revisão de POO (para aplicar em qualquer classe)

Legibilidade

  • O nome da classe descreve claramente o que ela representa?
  • Os métodos têm nomes verbais e específicos?
  • Os métodos têm tamanho pequeno e uma intenção principal?
  • Há duplicação de lógica que poderia virar um método/função?

Coesão

  • A classe tem um motivo principal para mudar?
  • Quase todos os métodos usam os mesmos dados centrais?
  • Se eu remover um método, o restante ainda faz sentido como “uma coisa só”?

Acoplamento

  • As dependências estão explícitas (injeção) em vez de criadas internamente?
  • O código depende de contratos simples em vez de detalhes de implementação?
  • É fácil substituir dependências em testes?

Estado e mutabilidade

  • Existe estado global mutável influenciando o comportamento?
  • O estado mutável está concentrado e bem protegido?
  • Algum objeto poderia ser imutável (value object) para reduzir bugs?

Contratos e documentação

  • Pré-condições e invariantes estão claras (validação e docstring)?
  • Erros são comunicados de forma consistente (exceção vs sentinela)?
  • Há exemplos mínimos de uso na docstring quando necessário?

Exercício guiado: revisar e melhorar um conjunto de classes

A seguir há um conjunto de classes com problemas típicos: baixa coesão, alto acoplamento, estado global e nomes genéricos. Sua tarefa é aplicar o checklist e refatorar.

Código para revisão (intencionalmente problemático)

DISCOUNT = 0.1

class DataManager:
    def __init__(self, db):
        self.db = db
        self.cache = {}

    def process(self, user_id, cart, cep):
        # pega usuário
        user = self.db.get_user(user_id)

        # calcula total
        total = 0
        for item in cart:
            total += item["price"] * item["qty"]

        # desconto global
        total = total - (total * DISCOUNT)

        # frete (simulado)
        if cep.startswith("0"):
            shipping = 3000
        else:
            shipping = 5000

        # salva
        order_id = self.db.insert_order({
            "user": user,
            "items": cart,
            "total": total,
            "shipping": shipping,
        })

        # cache global da instância
        self.cache[user_id] = order_id
        return order_id

    def get_last_order(self, user_id):
        return self.cache.get(user_id)

Passo a passo sugerido de refatoração

  • Passo 1: nomeie melhor: o que DataManager realmente faz? (ex.: OrderService).
  • Passo 2: remova estado global: substitua DISCOUNT por uma dependência explícita (política de desconto).
  • Passo 3: aumente coesão: extraia cálculo de total, desconto e frete para componentes separados.
  • Passo 4: reduza acoplamento: faça o serviço depender de interfaces simples (repo, shipping_calculator, discount_policy).
  • Passo 5: decida exceções vs sentinela: o que acontece se o usuário não existir? E se o CEP for inválido?
  • Passo 6: documente contratos: docstrings com regras (CEP, carrinho vazio, valores negativos).

Uma versão melhor (para comparar com a sua)

class DiscountPolicy:
    """Define como calcular desconto em centavos a partir de um subtotal."""
    def discount_cents(self, subtotal_cents: int) -> int:
        return 0

class PercentageDiscount(DiscountPolicy):
    def __init__(self, rate: float):
        self.rate = rate

    def discount_cents(self, subtotal_cents: int) -> int:
        if not (0 <= self.rate <= 1):
            raise ValueError("rate must be between 0 and 1")
        return int(subtotal_cents * self.rate)

class ShippingCalculator:
    def quote_cents(self, cep: str, total_weight_kg: float) -> int:
        if len(cep) != 9 or cep[5] != "-":
            raise ValueError("invalid CEP format")
        if total_weight_kg <= 0:
            raise ValueError("total_weight_kg must be > 0")
        return 3000 if cep.startswith("0") else 5000

class OrderRepository:
    def __init__(self, db):
        self.db = db

    def get_user(self, user_id: str):
        user = self.db.get_user(user_id)
        if user is None:
            raise LookupError("user not found")
        return user

    def save_order(self, payload: dict) -> str:
        return self.db.insert_order(payload)

class OrderService:
    """Orquestra a criação de pedidos.

    Regras:
        - Carrinho não pode estar vazio.
        - Itens devem ter qty > 0 e price_cents >= 0.
    """

    def __init__(self, repo: OrderRepository, discounts: DiscountPolicy, shipping: ShippingCalculator):
        self.repo = repo
        self.discounts = discounts
        self.shipping = shipping

    def create_order(self, user_id: str, items: list[dict], cep: str, total_weight_kg: float) -> str:
        if not items:
            raise ValueError("items cannot be empty")

        user = self.repo.get_user(user_id)
        subtotal_cents = self._subtotal_cents(items)
        discount_cents = self.discounts.discount_cents(subtotal_cents)
        shipping_cents = self.shipping.quote_cents(cep, total_weight_kg)

        payload = {
            "user": user,
            "items": items,
            "subtotal_cents": subtotal_cents,
            "discount_cents": discount_cents,
            "shipping_cents": shipping_cents,
            "total_cents": subtotal_cents - discount_cents + shipping_cents,
        }
        return self.repo.save_order(payload)

    def _subtotal_cents(self, items: list[dict]) -> int:
        total = 0
        for item in items:
            qty = item["qty"]
            price = item["price_cents"]
            if qty <= 0:
                raise ValueError("qty must be > 0")
            if price < 0:
                raise ValueError("price_cents must be >= 0")
            total += qty * price
        return total

Tarefas do aluno (aplique o checklist)

  • Identifique no código original pelo menos 5 problemas (legibilidade, coesão, acoplamento, estado global, contratos).
  • Refatore em etapas pequenas, garantindo que cada etapa mantém o comportamento.
  • Escolha e justifique: onde usar exceção e onde usar sentinela.
  • Decida: algum componente poderia ser imutável? Onde isso reduziria bugs?
  • Escreva docstrings para OrderService.create_order e para o componente de frete, incluindo regras e erros.

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

Qual mudança melhor reduz o acoplamento e melhora a testabilidade de um serviço que precisa cobrar pagamentos?

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

Você errou! Tente novamente.

Injetar a dependência torna as dependências explícitas e substituíveis, reduzindo acoplamento e facilitando testes (ex.: trocar o gateway por um stub/mock). Instanciar internamente ou usar estado global cria dependências rígidas e difíceis de isolar.

Próximo capitúlo

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

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

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.