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.
| Ruim | Melhor | Por quê |
|---|---|---|
Manager | InvoiceService | Indica o domínio e a responsabilidade |
process() | calculate_total() | Explicita o resultado |
items | line_items | Evita 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”.
- Ouça o áudio com a tela desligada
- Ganhe Certificado após a conclusão
- + de 5000 cursos para você explorar!
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 orderAcoplamento: 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.
| Escolha | Tende a funcionar melhor quando... | Riscos |
|---|---|---|
| Herança | Há uma variação clara de especialização e um contrato comum | Hierarquias profundas, acoplamento ao pai, overrides perigosos |
| Composição | Comportamentos são combináveis e mudam com frequência | Mais 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.totalcalculado 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.
| Abordagem | Use quando... | Cuidado |
|---|---|---|
| Exceções | Falha é erro de uso, dados inválidos, invariantes quebradas | Nã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
DataManagerrealmente faz? (ex.:OrderService). - Passo 2: remova estado global: substitua
DISCOUNTpor 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 totalTarefas 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_ordere para o componente de frete, incluindo regras e erros.