Composition vs Inheritance
Choose the right relationship: 'is-a' vs 'has-a'.
Composition and inheritance are two fundamental ways to build relationships between classes. Understanding when to use each is crucial for writing maintainable, flexible code.
Understanding the Difference
Section titled “Understanding the Difference”- Inheritance creates an “is-a” relationship (Car is-a Vehicle)
- Composition creates a “has-a” relationship (Car has-a Engine)
Inheritance: “Is-A” Relationship
Section titled “Inheritance: “Is-A” Relationship”Inheritance is used when a class is a specialized version of another class.
class Vehicle: def __init__(self, brand: str, model: str): self.brand = brand self.model = model
def start(self): return f"{self.brand} {self.model} started."
class Car(Vehicle): """Car IS-A Vehicle""" def __init__(self, brand: str, model: str, num_doors: int): super().__init__(brand, model) self.num_doors = num_doors
car = Car("Toyota", "Camry", 4)print(car.start()) # Inherited methodComposition: “Has-A” Relationship
Section titled “Composition: “Has-A” Relationship”Composition is used when a class contains or uses another class.
class Engine: def start(self): return "Engine started."
def stop(self): return "Engine stopped."
class Car: """Car HAS-A Engine""" def __init__(self, brand: str, model: str): self.brand = brand self.model = model self.engine = Engine() # Composition
def start(self): return f"{self.brand} {self.model}: {self.engine.start()}"
car = Car("Toyota", "Camry")print(car.start()) # "Toyota Camry: Engine started."When to Use Inheritance
Section titled “When to Use Inheritance”Use inheritance when:
- The relationship is truly “is-a”
- You need to override methods
- You want to share common behavior
- The subclass is a specialized version of the parent
class Animal: def make_sound(self): raise NotImplementedError
class Dog(Animal): """Dog IS-A Animal - good use of inheritance""" def make_sound(self): return "Woof!"
class Cat(Animal): """Cat IS-A Animal - good use of inheritance""" def make_sound(self): return "Meow!"When to Use Composition
Section titled “When to Use Composition”Use composition when:
- The relationship is “has-a” or “uses-a”
- You need flexibility to change behavior at runtime
- You want to avoid deep inheritance hierarchies
- The relationship is more about functionality than identity
class Engine: def start(self): return "Engine started"
class ElectricMotor: def start(self): return "Motor started"
class Car: """Car HAS-A engine/motor - good use of composition""" def __init__(self, brand: str, model: str, power_source): self.brand = brand self.model = model self.power_source = power_source # Can be Engine or ElectricMotor
def start(self): return f"{self.brand} {self.model}: {self.power_source.start()}"
# Flexible - can use different power sourcesgas_car = Car("Toyota", "Camry", Engine())electric_car = Car("Tesla", "Model 3", ElectricMotor())
print(gas_car.start()) # "Toyota Camry: Engine started"print(electric_car.start()) # "Tesla Model 3: Motor started"Real-World Example: Employee Management
Section titled “Real-World Example: Employee Management”Using Inheritance (Not Always Best)
Section titled “Using Inheritance (Not Always Best)”class Employee: def __init__(self, name: str, employee_id: int): self.name = name self.employee_id = employee_id
def calculate_salary(self): return 0
class FullTimeEmployee(Employee): def __init__(self, name: str, employee_id: int, salary: float): super().__init__(name, employee_id) self.salary = salary
def calculate_salary(self): return self.salary
class PartTimeEmployee(Employee): def __init__(self, name: str, employee_id: int, hourly_rate: float, hours: float): super().__init__(name, employee_id) self.hourly_rate = hourly_rate self.hours = hours
def calculate_salary(self): return self.hourly_rate * self.hours
# Problem: What if an employee changes from full-time to part-time?# You'd need to create a new object - not flexible!Using Composition (More Flexible)
Section titled “Using Composition (More Flexible)”class SalaryCalculator: """Strategy pattern using composition""" def calculate(self, employee_data: dict) -> float: raise NotImplementedError
class FullTimeSalaryCalculator(SalaryCalculator): def calculate(self, employee_data: dict) -> float: return employee_data["salary"]
class PartTimeSalaryCalculator(SalaryCalculator): def calculate(self, employee_data: dict) -> float: return employee_data["hourly_rate"] * employee_data["hours"]
class ContractSalaryCalculator(SalaryCalculator): def calculate(self, employee_data: dict) -> float: return employee_data["contract_amount"]
class Employee: """Employee HAS-A salary calculator - flexible!""" def __init__(self, name: str, employee_id: int, calculator: SalaryCalculator): self.name = name self.employee_id = employee_id self.calculator = calculator # Composition self.employee_data = {}
def set_employee_data(self, **kwargs): self.employee_data = kwargs
def calculate_salary(self): return self.calculator.calculate(self.employee_data)
def change_calculator(self, new_calculator: SalaryCalculator): """Can change salary calculation at runtime!""" self.calculator = new_calculator
# Usage - much more flexiblefull_time_calc = FullTimeSalaryCalculator()employee = Employee("Alice", 101, full_time_calc)employee.set_employee_data(salary=50000)print(employee.calculate_salary()) # 50000
# Change to part-time without creating new objectpart_time_calc = PartTimeSalaryCalculator()employee.change_calculator(part_time_calc)employee.set_employee_data(hourly_rate=25, hours=20)print(employee.calculate_salary()) # 500Comparison: Inheritance vs Composition
Section titled “Comparison: Inheritance vs Composition”Real-World Example: Notification System
Section titled “Real-World Example: Notification System”Inheritance Approach
Section titled “Inheritance Approach”class Notification: def send(self, message: str): raise NotImplementedError
class EmailNotification(Notification): def send(self, message: str): print(f"Sending email: {message}")
class SMSNotification(Notification): def send(self, message: str): print(f"Sending SMS: {message}")
# Problem: What if you need multiple notification channels?# You'd need to create multiple classes or use multiple inheritanceComposition Approach (Better)
Section titled “Composition Approach (Better)”class EmailService: def send(self, recipient: str, message: str): print(f"Email to {recipient}: {message}")
class SMSService: def send(self, phone: str, message: str): print(f"SMS to {phone}: {message[:50]}...")
class PushService: def send(self, device_id: str, message: str): print(f"Push to {device_id}: {message}")
class Notification: """Notification HAS-A list of services - flexible!""" def __init__(self): self.services = [] # Composition - can have multiple
def add_service(self, service): self.services.append(service)
def send(self, message: str, **kwargs): """Send through all services""" for service in self.services: if isinstance(service, EmailService): service.send(kwargs.get("email"), message) elif isinstance(service, SMSService): service.send(kwargs.get("phone"), message) elif isinstance(service, PushService): service.send(kwargs.get("device_id"), message)
# Usage - very flexiblenotification = Notification()notification.add_service(EmailService())notification.add_service(SMSService())notification.add_service(PushService())
notification.send( "Your order has shipped!", email="user@example.com", phone="+1234567890", device_id="device-123")The “Favor Composition Over Inheritance” Principle
Section titled “The “Favor Composition Over Inheritance” Principle”This principle suggests that composition is often more flexible than inheritance:
Problems with Deep Inheritance
Section titled “Problems with Deep Inheritance”class Animal: pass
class Mammal(Animal): pass
class Dog(Mammal): pass
class Labrador(Dog): pass
class GoldenRetriever(Dog): pass
# Problem: What if a Labrador needs behavior from GoldenRetriever?# You can't easily share code without creating more complex hierarchiesSolution with Composition
Section titled “Solution with Composition”class Behavior: """Composable behaviors""" def execute(self): raise NotImplementedError
class FetchBehavior(Behavior): def execute(self): return "Fetching ball..."
class GuardBehavior(Behavior): def execute(self): return "Guarding territory..."
class PlayfulBehavior(Behavior): def execute(self): return "Playing fetch..."
class Dog: """Dog HAS-A behavior - can change at runtime!""" def __init__(self, name: str, behavior: Behavior): self.name = name self.behavior = behavior # Composition
def perform_behavior(self): return self.behavior.execute()
def change_behavior(self, new_behavior: Behavior): """Change behavior dynamically""" self.behavior = new_behavior
# Flexible - can change behaviorsdog = Dog("Buddy", FetchBehavior())print(dog.perform_behavior()) # "Fetching ball..."
dog.change_behavior(GuardBehavior())print(dog.perform_behavior()) # "Guarding territory..."Key Takeaways
Section titled “Key Takeaways”- Deep inheritance hierarchies can become hard to maintain
- Composition allows you to change behavior at runtime
- Both have their place - choose based on the specific use case
Remember: “Favor composition over inheritance” is a guideline, not a rule. Use inheritance when it makes sense, but prefer composition when you need flexibility.