O Que Você Vai Aprender
Nesta aula, você explorará um princípio fundamental da Programação Orientada a Objetos (POO): a relação entre objetos através da Composição e como ela se compara à Herança. Entenderá o porquê de "preferir composição à herança" em muitos cenários de design de software.
🤝 Entender a Composição
Compreender o relacionamento "tem um" e como ele é implementado em POO.
🆚 Comparar com Herança
Distinguir a composição da herança ("é um") e quando usar cada uma.
💡 Princípio de Design
Entender o famoso princípio "prefira a composição à herança" e suas vantagens.
Composição vs. Herança: Tipos de Relacionamento
Em Programação Orientada a Objetos, a forma como os objetos se relacionam é crucial para um bom design. Existem duas abordagens principais para criar esses relacionamentos: **Composição** e **Herança**.
Composição: O Relacionamento "Tem Um"
Um objeto contém ou utiliza outros objetos. Isso representa uma relação "tem um".
- **Exemplo:** Uma `Casa` tem um `Quarto`. Um `Carro` tem um `Motor`.
- **Vantagem:** Maior flexibilidade e reutilização de código, pois os componentes podem ser facilmente trocados ou reutilizados em diferentes contextos.
Herança: O Relacionamento "É Um"
Uma classe deriva de outra, herdando suas características e comportamentos. Isso representa uma relação "é um".
- **Exemplo:** Um `Cachorro` é um `Animal`. Um `Carro` é um `Veículo`.
- **Uso:** Ideal quando há uma clara hierarquia de tipos e a subclasse é realmente uma versão mais específica da superclasse.
Princípio Famoso: "Prefira a Composição à Herança." Este é um princípio de design de software que sugere que, na maioria dos casos, usar a composição para reutilizar funcionalidades resulta em designs mais flexíveis e menos acoplados do que a herança.
Exemplo Prático: Composição (Casa e Quarto)
Vamos ver a composição em ação com o exemplo de uma `Casa` que "tem" vários `Quartos`. A `Casa` não herda de `Quarto`, mas sim contém objetos `Quarto` como parte de sua estrutura.
Código Python para Composição
# classes.py
class Quarto:
def __init__(self, nome, area):
self.nome = nome
self.area = area
def __str__(self):
return f"Quarto: {self.nome} ({self.area} m²)"
class Casa:
def __init__(self, endereco):
self.endereco = endereco
self.quartos = [] # Uma Casa "tem um" ou "tem vários" Quartos
def adicionar_quarto(self, quarto):
if isinstance(quarto, Quarto):
self.quartos.append(quarto)
# print(f"Quarto '{quarto.nome}' adicionado à casa em {self.endereco}.")
else:
print("Isso não é um quarto válido!")
def calcular_area_total(self):
total_area = sum(q.area for q in self.quartos)
return total_area
def listar_quartos(self):
if not self.quartos:
return "Nenhum quarto nesta casa."
return "\n".join([str(q) for q in self.quartos])
# Exemplo de uso:
# meu_quarto1 = Quarto("Sala de Estar", 30)
# meu_quarto2 = Quarto("Cozinha", 15)
# minha_casa = Casa("Rua da Programação, 123")
# minha_casa.adicionar_quarto(meu_quarto1)
# minha_casa.adicionar_quarto(meu_quarto2)
# print(minha_casa.listar_quartos())
# print(f"Área total da casa: {minha_casa.calcular_area_total()} m²")
Simulador: Casa e Quartos (Composição)
Crie uma casa e adicione quartos a ela. Veja como a casa "tem" os quartos.
Exemplo: Herança (Animal)
Para contraste, aqui está um breve exemplo de herança. Um `Cachorro` é um tipo mais específico de `Animal` e, portanto, herda comportamentos gerais de `Animal` (como fazer som) e pode especializá-los.
Código Python para Herança
# classes.py
class Animal:
def __init__(self, nome):
self.nome = nome
def fazer_som(self):
return "Som genérico de animal"
class Cachorro(Animal): # Cachorro "é um" Animal
def __init__(self, nome, raca):
super().__init__(nome) # Chama o construtor da classe pai
self.raca = raca
def fazer_som(self): # Sobrescreve o método da classe pai
return "Au au!"
# Exemplo de uso:
# meu_cachorro = Cachorro("Buddy", "Golden Retriever")
# print(meu_cachorro.nome) # Saída: Buddy
# print(meu_cachorro.fazer_som()) # Saída: Au au!
#
# um_animal = Animal("Leo")
# print(um_animal.fazer_som()) # Saída: Som genérico de animal
Na herança, a subclasse (Cachorro) reutiliza o código da superclasse (Animal) através de uma relação de especialização.
Vantagens da Composição
Embora a herança seja útil para hierarquias "é um", a composição frequentemente leva a designs mais robustos e flexíveis. Aqui estão algumas de suas principais vantagens:
🤸 Maior Flexibilidade
Composição permite alterar o comportamento de um objeto em tempo de execução, trocando os componentes. Com herança, o comportamento é fixo na hierarquia da classe.
♻️ Reutilização de Código Aprimorada
Componentes (objetos que são "tidos por" outros) são unidades independentes que podem ser facilmente reutilizadas em diversas classes, sem as restrições da hierarquia de herança.
⬇️ Acoplamento Fraco
Objetos compostos são menos dependentes um do outro. Alterações em um componente geralmente não exigem grandes modificações no objeto que o contém, ao contrário da herança, onde mudanças na classe pai podem afetar todas as subclasses.
🚫 Menos Problemas de Herança Múltipla
Linguagens como Python permitem herança múltipla, mas ela pode levar a complexidades (problema do diamante). Composição oferece uma alternativa mais limpa para combinar funcionalidades sem os riscos da herança múltipla.
Desafios para Continuar
Agora que você explorou a composição e a herança, é hora de aplicar esses conceitos! Resolva estes problemas no seu ambiente de desenvolvimento Python para solidificar seu aprendizado.
-
✓
1. Composição: Carro e Motor
Crie duas classes em Python: `Motor` e `Carro`.
- A classe `Motor` deve ter atributos como `tipo` (ex: "Combustão", "Elétrico") e `potencia`. Deve ter um método `ligar()` que imprime "Motor
ligado!". - A classe `Carro` deve ter atributos como `modelo` e `ano`. Em vez de herdar de `Motor`, um `Carro` deve conter uma instância de `Motor` (composição).
- A classe `Carro` deve ter um método `iniciar_viagem()` que chama o método `ligar()` do seu objeto `Motor` interno.
# Exemplo de Estrutura: class Motor: def __init__(self, tipo, potencia): self.tipo = tipo self.potencia = potencia def ligar(self): print(f"Motor {self.tipo} ligado!") class Carro: def __init__(self, modelo, ano, motor): # 'motor' é uma instância de Motor self.modelo = modelo self.ano = ano self.motor = motor # Composição: Carro tem um Motor def iniciar_viagem(self): print(f"Iniciando viagem com o {self.modelo}...") self.motor.ligar() # Criar e testar: # motor_eletrico = Motor("Elétrico", "200cv") # meu_carro = Carro("Tesla Model 3", 2023, motor_eletrico) # meu_carro.iniciar_viagem()
- A classe `Motor` deve ter atributos como `tipo` (ex: "Combustão", "Elétrico") e `potencia`. Deve ter um método `ligar()` que imprime "Motor
-
✓
2. Discutir: Qual Abordagem para um Sistema de Notificações?
Imagine que você está projetando um sistema de notificações que pode enviar mensagens por SMS, E-mail ou Push (aplicativo).
- Você criaria uma classe `NotificacaoBase` e faria `NotificacaoSMS`, `NotificacaoEmail`, `NotificacaoPush` herdarem dela (Herança)?
- Ou você criaria uma classe `Notificacao` que `tem um` (compõe) um `SenderSMS`, um `SenderEmail` e um `SenderPush`?