No artigo anterior vimos que herança é uma ferramenta poderosa — mas não é sempre a melhor escolha. Um princípio amplamente aceito no design de software diz: prefira composição à herança. Neste artigo vamos entender a diferença entre essas abordagens, explorar o sistema de protocolos do Python e aprender a construir sistemas mais flexíveis e fáceis de manter.
O Problema da Herança Excessiva
Herança cria um acoplamento forte entre classes. Considere:
class Ave:
def respirar(self):
return "Respirando..."
def voar(self):
return "Voando..."
def cantar(self):
return "Cantando..."
class Pinguim(Ave):
def voar(self):
raise NotImplementedError("Pinguins não voam!")
O Pinguim herda voar() de Ave mas não pode implementá-la — viola o Princípio de Substituição de Liskov: uma subclasse deve poder substituir sua classe pai sem quebrar o programa. Esse é o sinal de que herança não é a ferramenta certa aqui.
Composição
Composição significa construir classes a partir de outras classes, incluindo-as como atributos — relação "tem um" em vez de "é um":
class Motor:
def __init__(self, potencia_cv, combustivel):
self.potencia_cv = potencia_cv
self.combustivel = combustivel
self._ligado = False
def ligar(self):
self._ligado = True
return f"Motor {self.potencia_cv}cv ligado."
def desligar(self):
self._ligado = False
return "Motor desligado."
@property
def ligado(self):
return self._ligado
class SistemaSom:
def __init__(self, marca, potencia_watts):
self.marca = marca
self.potencia = potencia_watts
def tocar(self, musica):
return f"[{self.marca}] Tocando: {musica}"
class Carro:
"""Carro TEM UM motor e TEM UM sistema de som — composição."""
def __init__(self, modelo, motor, som):
self.modelo = modelo
self._motor = motor
self._som = som
def ligar(self):
return self._motor.ligar()
def tocar_musica(self, musica):
if not self._motor.ligado:
return "Ligue o carro primeiro."
return self._som.tocar(musica)
def status(self):
estado = "ligado" if self._motor.ligado else "desligado"
return f"{self.modelo} — {estado}"
motor = Motor(150, "Flex")
som = SistemaSom("Pioneer", 200)
carro = Carro("Honda Civic", motor, som)
print(carro.ligar())
print(carro.tocar_musica("Bohemian Rhapsody"))
print(carro.status())
A vantagem: podemos trocar o motor ou o sistema de som sem modificar a classe Carro. Cada componente é independente e reutilizável.
Interfaces Informais: Duck Typing
Python não tem a palavra-chave interface como Java ou C#. Em vez disso, usa duck typing — qualquer objeto que implemente os métodos esperados pode ser usado:
class RelatorioCSV:
def gerar(self, dados):
linhas = [",".join(str(v) for v in linha) for linha in dados]
return "\n".join(linhas)
def salvar(self, conteudo, caminho):
print(f"Salvando CSV em {caminho}...")
class RelatorioJSON:
def gerar(self, dados):
import json
return json.dumps(dados, ensure_ascii=False, indent=2)
def salvar(self, conteudo, caminho):
print(f"Salvando JSON em {caminho}...")
class RelatorioHTML:
def gerar(self, dados):
linhas = "".join(
f"<tr>{''.join(f'<td>{v}</td>' for v in linha)}</tr>"
for linha in dados
)
return f"<table>{linhas}</table>"
def salvar(self, conteudo, caminho):
print(f"Salvando HTML em {caminho}...")
def exportar(relatorio, dados, caminho):
"""Funciona com qualquer objeto que tenha gerar() e salvar()."""
conteudo = relatorio.gerar(dados)
relatorio.salvar(conteudo, caminho)
return conteudo
dados = [["Ana", 9.5], ["Bruno", 8.0], ["Carla", 7.5]]
exportar(RelatorioCSV(), dados, "notas.csv")
exportar(RelatorioJSON(), dados, "notas.json")
exportar(RelatorioHTML(), dados, "notas.html")
Nenhuma das classes herda de uma interface comum — mas todas funcionam com exportar() porque implementam gerar() e salvar().
Protocolos Formais: typing.Protocol
A partir do Python 3.8, o módulo typing oferece Protocol — uma forma de definir interfaces sem herança, usando structural subtyping (verificação estática pela estrutura):
from typing import Protocol
class Serializavel(Protocol):
def serializar(self) -> str:
...
def deserializar(self, dados: str) -> None:
...
class UsuarioJSON:
def __init__(self, nome, email):
self.nome = nome
self.email = email
def serializar(self) -> str:
import json
return json.dumps({"nome": self.nome, "email": self.email})
def deserializar(self, dados: str) -> None:
import json
obj = json.loads(dados)
self.nome = obj["nome"]
self.email = obj["email"]
class ConfiguracaoINI:
def __init__(self):
self.dados = {}
def serializar(self) -> str:
return "\n".join(f"{k}={v}" for k, v in self.dados.items())
def deserializar(self, dados: str) -> None:
for linha in dados.strip().split("\n"):
k, v = linha.split("=")
self.dados[k.strip()] = v.strip()
def salvar(obj: Serializavel, caminho: str) -> None:
"""Aceita qualquer objeto que implemente o protocolo Serializavel."""
conteudo = obj.serializar()
print(f"Salvando em {caminho}: {conteudo}")
usuario = UsuarioJSON("Ana", "ana@email.com")
config = ConfiguracaoINI()
config.dados = {"tema": "escuro", "idioma": "pt-BR"}
salvar(usuario, "usuario.json")
salvar(config, "config.ini")
UsuarioJSON e ConfiguracaoINI não herdam de Serializavel — mas ferramentas como mypy verificam estaticamente se implementam os métodos exigidos pelo protocolo.
Mixins
Mixins são classes pequenas e focadas que adicionam funcionalidades específicas via herança múltipla — sem representar uma entidade completa:
class LogMixin:
"""Adiciona capacidade de logging a qualquer classe."""
def log(self, mensagem, nivel="INFO"):
from datetime import datetime
agora = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
print(f"[{agora}] [{nivel}] [{type(self).__name__}] {mensagem}")
class ValidacaoMixin:
"""Adiciona validação de campos obrigatórios."""
campos_obrigatorios = []
def validar(self):
erros = []
for campo in self.campos_obrigatorios:
valor = getattr(self, campo, None)
if not valor:
erros.append(f"Campo '{campo}' é obrigatório.")
return erros
class SerializacaoMixin:
"""Adiciona serialização automática para dicionário."""
def para_dict(self):
return {
k: v for k, v in self.__dict__.items()
if not k.startswith("_")
}
class Pedido(LogMixin, ValidacaoMixin, SerializacaoMixin):
campos_obrigatorios = ["cliente", "produto", "quantidade"]
def __init__(self, cliente, produto, quantidade):
self.cliente = cliente
self.produto = produto
self.quantidade = quantidade
def processar(self):
erros = self.validar()
if erros:
for erro in erros:
self.log(erro, "ERRO")
return False
self.log(f"Processando pedido: {self.produto} x{self.quantidade}")
return True
pedido = Pedido("Ana", "Teclado Mecânico", 2)
pedido.processar()
print(pedido.para_dict())
pedido_invalido = Pedido("", "Mouse", 0)
pedido_invalido.validar()
pedido_invalido.processar()
Composição vs. Herança: quando usar cada uma
| Situação | Use |
|---|---|
| A relação é claramente "é um" (Cachorro é um Animal) | Herança |
| A relação é "tem um" (Carro tem um Motor) | Composição |
| Quer adicionar comportamento sem criar hierarquia | Mixin |
| Quer definir um contrato sem forçar herança | Protocol |
| A hierarquia tem mais de 2 níveis | Reveja o design |
| Subclasse precisa desabilitar métodos do pai | Composição |
Exemplo Completo: Sistema de Notificações
from typing import Protocol
from datetime import datetime
class Canal(Protocol):
def enviar(self, destinatario: str, mensagem: str) -> bool:
...
class CanalEmail:
def enviar(self, destinatario: str, mensagem: str) -> bool:
print(f"[EMAIL] Para: {destinatario} | {mensagem}")
return True
class CanalSMS:
def enviar(self, destinatario: str, mensagem: str) -> bool:
if len(mensagem) > 160:
print(f"[SMS] Mensagem muito longa para {destinatario}")
return False
print(f"[SMS] Para: {destinatario} | {mensagem}")
return True
class CanalPush:
def enviar(self, destinatario: str, mensagem: str) -> bool:
print(f"[PUSH] Para: {destinatario} | {mensagem[:50]}...")
return True
class LogMixin:
def _registrar(self, canal, destinatario, sucesso):
status = "OK" if sucesso else "FALHOU"
agora = datetime.now().strftime("%H:%M:%S")
print(f" [{agora}] {canal} → {destinatario}: {status}")
class ServicoNotificacao(LogMixin):
"""Usa composição — recebe canais como dependências."""
def __init__(self, canais: list):
self._canais = canais
def notificar(self, destinatario: str, mensagem: str):
resultados = {}
for canal in self._canais:
nome = type(canal).__name__
sucesso = canal.enviar(destinatario, mensagem)
self._registrar(nome, destinatario, sucesso)
resultados[nome] = sucesso
return resultados
def adicionar_canal(self, canal):
self._canais.append(canal)
servico = ServicoNotificacao([
CanalEmail(),
CanalSMS(),
CanalPush(),
])
print("=== Notificação de boas-vindas ===")
servico.notificar("ana@email.com", "Bem-vinda à plataforma!")
print("\n=== Alerta de segurança ===")
servico.notificar(
"bruno@email.com",
"Detectamos um acesso suspeito na sua conta. "
"Se não foi você, altere sua senha imediatamente."
)
Resumo
- Prefira composição à herança quando a relação entre classes é "tem um" e não "é um"
- Duck typing permite polimorfismo sem herança formal — basta implementar os métodos esperados
typing.Protocolformaliza interfaces sem exigir herança, compatível com verificação estática- Mixins adicionam comportamentos específicos via herança múltipla de forma controlada
- Herança excessiva cria acoplamento forte e hierarquias frágeis
- Composição torna os componentes independentes, substituíveis e testáveis isoladamente
Referências e Leituras Complementares
- typing.Protocol (PEP 544) — https://peps.python.org/pep-0544/
- ABC e herança formal — https://docs.python.org/3/library/abc.html
- Glossário: duck typing — https://docs.python.org/3/glossary.html#term-duck-typing
- dataclasses como alternativa a classes simples — https://docs.python.org/3/library/dataclasses.html
- RAMALHO, Luciano. Fluent Python. 2. ed. O'Reilly Media, 2022. Cap. 13 e 14 — protocolos, ABCs e herança em profundidade.
- PHILLIPS, Dusty; LOTT, Steven F. Python Object-Oriented Programming. 4. ed. Packt, 2021. Cap. 6 — composição, mixins e design avançado.
- MARTIN, Robert C. Clean Architecture. Prentice Hall, 2017. Cap. 10 — princípio de substituição de Liskov e design de interfaces.