Por que composição é uma alternativa frequente à herança
Em muitos sistemas, um objeto precisa usar capacidades de outros objetos (enviar e-mail, calcular frete, validar endereço, processar pagamento), mas isso não significa que ele seja esses objetos. Nesses casos, a composição costuma ser mais adequada do que herança.
Composição é o design em que um objeto (o “todo”) contém outros objetos (as “partes”) e delega responsabilidades a eles. Em vez de criar uma hierarquia de subclasses para cada variação, você monta o comportamento combinando componentes.
- Herança: reutiliza comportamento por especialização (“é um”).
- Composição: reutiliza comportamento por montagem e delegação (“tem um”).
Sinais de que composição pode ser melhor
- Você quer trocar um comportamento em tempo de execução (ex.: trocar o meio de pagamento).
- Você tem muitas combinações possíveis (ex.: vários tipos de notificação × vários provedores).
- Você quer reduzir acoplamento e manter responsabilidades pequenas e testáveis.
- A hierarquia de herança começa a crescer com subclasses “só para mudar um detalhe”.
Agregação, composição forte e delegação
Agregação (relação “tem um”, ciclo de vida independente)
Na agregação, o objeto “todo” referencia uma parte que pode existir independentemente. Exemplo: um Pedido usa um Endereco que pode ser reutilizado em outros pedidos e continuar existindo fora do pedido.
Composição forte (relação “parte de”, ciclo de vida dependente)
Na composição forte, as partes são criadas/gerenciadas pelo “todo” e fazem sentido principalmente dentro dele. Exemplo: ItemPedido normalmente existe como parte de um Pedido.
Delegação de métodos (encaminhar chamadas para componentes)
Delegação é quando o objeto “todo” expõe um método e, internamente, chama o método do componente responsável. Isso mantém a interface do “todo” simples, sem duplicar lógica.
- Ouça o áudio com a tela desligada
- Ganhe Certificado após a conclusão
- + de 5000 cursos para você explorar!
Baixar o aplicativo
class Pedido: def __init__(self, pagamento): self._pagamento = pagamento def pagar(self, valor): return self._pagamento.processar(valor) # delegaçãoExemplo prático: Pedido com Itens, Pagamento e Endereço
Vamos modelar um cenário com múltiplas responsabilidades, separando cada preocupação em um componente. O objetivo é que Pedido coordene o fluxo, mas não “saiba” detalhes de pagamento, validação de endereço ou cálculo de totais de itens.
Visão geral das responsabilidades
| Componente | Responsabilidade | Por que é uma boa “peça” reutilizável |
|---|---|---|
ItemPedido | Representar um item e calcular subtotal | Pode ser testado isoladamente; lógica simples e coesa |
Endereco | Dados e validação de entrega | Pode ser usado em outros contextos (cadastro, devolução) |
Pagamento (interface) | Contrato para processar pagamento | Permite trocar estratégia (cartão, pix, boleto) sem mexer no pedido |
Pedido | Orquestrar itens, endereço e pagamento | Fica focado no fluxo do domínio (criar, totalizar, pagar) |
Passo a passo
1) Criar o componente de item (composição forte)
Um item pertence ao pedido e costuma ser criado/adicionado por ele. O item sabe calcular seu subtotal.
from dataclasses import dataclass @dataclass(frozen=True) class ItemPedido: sku: str descricao: str preco_unitario: float quantidade: int = 1 def subtotal(self) -> float: return self.preco_unitario * self.quantidade2) Criar o componente Endereco (agregação)
O endereço pode existir fora do pedido (por exemplo, vindo do perfil do cliente). Aqui colocamos uma validação simples para ilustrar a ideia de responsabilidade isolada.
from dataclasses import dataclass @dataclass(frozen=True) class Endereco: rua: str numero: str cidade: str uf: str cep: str def valido(self) -> bool: # Exemplo simples: CEP com 8 dígitos cep_digits = ''.join(ch for ch in self.cep if ch.isdigit()) return len(cep_digits) == 8 and len(self.uf) == 23) Definir um “contrato” de pagamento e implementações (composição + polimorfismo por duck typing)
O pedido não precisa herdar de nada. Ele apenas recebe um objeto que tenha o método processar(valor). Assim, você troca o meio de pagamento sem alterar o pedido.
class PagamentoCartao: def __init__(self, ultimos4: str): self.ultimos4 = ultimos4 def processar(self, valor: float) -> str: return f"Pago R$ {valor:.2f} no cartão ****{self.ultimos4}" class PagamentoPix: def __init__(self, chave: str): self.chave = chave def processar(self, valor: float) -> str: return f"Pago R$ {valor:.2f} via PIX ({self.chave})"4) Montar o Pedido com composição e delegação
O pedido contém itens (composição forte), referencia um endereço (agregação) e recebe um processador de pagamento (composição). Ele delega o pagamento ao componente.
class Pedido: def __init__(self, endereco: Endereco, pagamento): self._endereco = endereco # agregação self._pagamento = pagamento # composição (estratégia) self._itens: list[ItemPedido] = [] # composição forte self._pago = False def adicionar_item(self, sku: str, descricao: str, preco_unitario: float, quantidade: int = 1) -> None: self._itens.append(ItemPedido(sku, descricao, preco_unitario, quantidade)) def total(self) -> float: return sum(item.subtotal() for item in self._itens) def pode_finalizar(self) -> bool: return bool(self._itens) and self._endereco.valido() and not self._pago def pagar(self) -> str: if not self._endereco.valido(): raise ValueError("Endereço inválido") if not self._itens: raise ValueError("Pedido sem itens") if self._pago: raise ValueError("Pedido já está pago") valor = self.total() recibo = self._pagamento.processar(valor) # delegação self._pago = True return recibo5) Usar e trocar componentes sem alterar o Pedido
O mesmo Pedido funciona com diferentes pagamentos, porque ele depende de um comportamento (método processar), não de uma classe base específica.
endereco = Endereco(rua="Av. Central", numero="100", cidade="Curitiba", uf="PR", cep="80000-000") pedido1 = Pedido(endereco=endereco, pagamento=PagamentoCartao("1234")) pedido1.adicionar_item("SKU1", "Camiseta", 59.90, 2) pedido1.adicionar_item("SKU2", "Meia", 19.90, 1) print(pedido1.total()) print(pedido1.pagar()) pedido2 = Pedido(endereco=endereco, pagamento=PagamentoPix("email@exemplo.com")) pedido2.adicionar_item("SKU3", "Boné", 49.90, 1) print(pedido2.pagar())Onde a composição reduziu acoplamento (e evitou “explosão” de subclasses)
Sem composição, seria comum cair em herança para representar variações: PedidoComCartao, PedidoComPix, PedidoComBoleto… e depois variações com regras extras, gerando combinações difíceis de manter.
Com composição:
Pedidonão muda quando surge um novo meio de pagamento; basta criar uma nova classe comprocessar.- Validação de endereço fica em
Endereco, não espalhada pelo pedido. - Cálculo de subtotal fica em
ItemPedido, evitando duplicação.
Delegação na prática: expondo uma interface mais simples
Às vezes você quer que o objeto “todo” ofereça atalhos para operações comuns dos componentes, sem expor o componente diretamente. Isso é delegação de métodos.
class Pedido: # ... def endereco_valido(self) -> bool: return self._endereco.valido() # delega para EnderecoUse delegação com cuidado: se você começar a “replicar” muitos métodos do componente, pode estar criando um “objeto Deus” ou uma fachada excessiva. Delegue apenas o que faz sentido como parte da API do “todo”.
Exercícios (com foco em trocar herança por composição)
1) Diagnóstico de design: “é um” ou “tem um”?
Para cada caso, responda se você usaria herança ou composição e justifique em 3 a 5 linhas:
RelatorioPDFeRelatorioCSVem um sistema de relatórios.UsuarioAdmineUsuarioComumem um sistema com permissões.PedidoComDescontoNatal,PedidoComDescontoClienteVIPePedidoComFreteGratis(podendo combinar descontos).
2) Refatoração guiada: de herança para composição
Você recebeu este código (simplificado):
class NotificadorEmail: def enviar(self, msg: str) -> None: print("EMAIL:", msg) class NotificadorSMS(NotificadorEmail): def enviar(self, msg: str) -> None: print("SMS:", msg) class NotificadorEmailComLog(NotificadorEmail): def enviar(self, msg: str) -> None: print("LOG: enviando") super().enviar(msg)Tarefas:
- Explique por que
NotificadorSMSherdar deNotificadorEmailé um mau sinal. - Reescreva usando composição: crie um componente
Canal(email/sms) e um decorador/“wrapper” de log que receba qualquer canal e delegueenviar. - Mostre como combinar “SMS com log” sem criar uma nova subclasse.
3) Extensão do exemplo do Pedido
Implemente mais um componente e conecte ao Pedido por composição:
- Opção A:
CalculadoraFretecom métodocalcular(endereco, itens)e façaPedido.total()opcionalmente incluir frete. - Opção B:
ValidadorPedidoque centralize regras (ex.: limite de itens, valor mínimo) e oPedido.pagar()delega a validação para ele.
Justifique: por que esse novo comportamento não deveria ser uma subclasse de Pedido?