Composição em Python Orientado a Objetos: construir sistemas por partes reutilizáveis

Capítulo 8

Tempo estimado de leitura: 7 minutos

+ Exercício

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.

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

class Pedido:    def __init__(self, pagamento):        self._pagamento = pagamento    def pagar(self, valor):        return self._pagamento.processar(valor)  # delegação

Exemplo 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

ComponenteResponsabilidadePor que é uma boa “peça” reutilizável
ItemPedidoRepresentar um item e calcular subtotalPode ser testado isoladamente; lógica simples e coesa
EnderecoDados e validação de entregaPode ser usado em outros contextos (cadastro, devolução)
Pagamento (interface)Contrato para processar pagamentoPermite trocar estratégia (cartão, pix, boleto) sem mexer no pedido
PedidoOrquestrar itens, endereço e pagamentoFica 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.quantidade

2) 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) == 2

3) 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 recibo

5) 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:

  • Pedido não muda quando surge um novo meio de pagamento; basta criar uma nova classe com processar.
  • 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 Endereco

Use 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:

  • RelatorioPDF e RelatorioCSV em um sistema de relatórios.
  • UsuarioAdmin e UsuarioComum em um sistema com permissões.
  • PedidoComDescontoNatal, PedidoComDescontoClienteVIP e PedidoComFreteGratis (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 NotificadorSMS herdar de NotificadorEmail é um mau sinal.
  • Reescreva usando composição: crie um componente Canal (email/sms) e um decorador/“wrapper” de log que receba qualquer canal e delegue enviar.
  • 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: CalculadoraFrete com método calcular(endereco, itens) e faça Pedido.total() opcionalmente incluir frete.
  • Opção B: ValidadorPedido que centralize regras (ex.: limite de itens, valor mínimo) e o Pedido.pagar() delega a validação para ele.

Justifique: por que esse novo comportamento não deveria ser uma subclasse de Pedido?

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

Em um sistema onde um Pedido precisa aceitar diferentes meios de pagamento (cartão, PIX etc.) sem criar várias subclasses, qual abordagem melhor atende essa necessidade e por quê?

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

Você errou! Tente novamente.

A composição permite que Pedido “tenha um” componente de pagamento e delegue o processamento via um método como processar(valor). Assim, é possível trocar a estratégia de pagamento sem criar subclasses e com menor acoplamento.

Próximo capitúlo

Métodos especiais (dunder methods) em POO Python: __repr__, __str__, __eq__ e coleções

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

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.