AiTechWorlds
AiTechWorlds
University learning companion
Object-Oriented Programming (OOP) · Composition vs Inheritance: Choosing the Right Tool
Use this course-level map to place “Composition vs Inheritance: Choosing the Right Tool” in the larger system. It complements—not replaces—a topic-specific worked diagram.
Imagine an engineer tasked with building a FlyingCar. The obvious approach: make it inherit from both Car and Plane. It IS A Car. It IS A Plane. Problem solved, right?
Not quite. A car has a steering wheel, four wheels, and drives on roads. A plane has wings, landing gear, and needs a runway. A FlyingCar inheriting from both gets everything the landing gear conflicts with the car wheels, the wings interfere with the doors, and the engine systems are incompatible. You've created a Frankenstein class that's harder to maintain than either parent.
Now imagine a different approach. Instead of the FlyingCar being a car and a plane, it has a drive system and a flight system. You plug in the components you need. The components are independent, testable, and swappable. That is composition.
The core question in OOP design is always: does this relationship make logical sense?
Dog IS A Animal. SavingsAccount IS A BankAccount. The child is genuinely a specialized version of the parent.Car HAS A Engine. User HAS A EmailAddress. The class contains another object as one of its parts.If the sentence "X IS A Y" sounds wrong or awkward, you probably want composition.
"A Logger IS A Database" sounds wrong. "A Logger HAS A DatabaseConnection" sounds right.
Inheritance creates tight coupling. When a base class changes, every subclass inherits that change whether they want it or not.
class Animal:
def __init__(self, name: str):
self.name = name
def describe(self):
# Original developer adds a feature: log every description
print(f"[LOG] describe() called")
print(f"I am {self.name}")
class Dog(Animal):
def describe(self):
super().describe() # Calls Animal.describe()
print(f"I am also a dog!")If someone changes Animal.describe() to add unexpected behavior, every single subclass is affected. With hundreds of subclasses, this becomes a maintenance nightmare. This is the fragile base class problem: the base class is fragile because changing it breaks subclasses in unpredictable ways.
Let's build a notification system. Users can receive emails, SMS messages, and push notifications.
class Notifier:
def send(self, message: str):
raise NotImplementedError
class EmailNotifier(Notifier):
def send(self, message: str):
print(f"Email: {message}")
class SMSNotifier(Notifier):
def send(self, message: str):
print(f"SMS: {message}")
# What if a user wants BOTH email AND SMS?
class EmailAndSMSNotifier(EmailNotifier, SMSNotifier):
# Multiple inheritance — which send() wins?
# Python uses MRO (Method Resolution Order) — confusing!
def send(self, message: str):
EmailNotifier.send(self, message) # Must call each manually
SMSNotifier.send(self, message)
# What about email + push? SMS + push? All three?
# You'd need: EmailAndPushNotifier, SMSAndPushNotifier,
# EmailSMSAndPushNotifier... combinatorial explosion!The problem: every new combination requires a new class. With 4 notification channels, you could need up to 15 combination classes.
# Each notification channel is its own independent class
class EmailChannel:
def __init__(self, email_address: str):
self.email_address = email_address # Target email
def deliver(self, message: str):
print(f"Email to {self.email_address}: {message}")
class SMSChannel:
def __init__(self, phone_number: str):
self.phone_number = phone_number # Target phone
def deliver(self, message: str):
print(f"SMS to {self.phone_number}: {message}")
class PushChannel:
def __init__(self, device_token: str):
self.device_token = device_token # Device identifier
def deliver(self, message: str):
print(f"Push to device {self.device_token[:8]}...: {message}")
# The Notifier CONTAINS channel objects — it doesn't inherit from them
class Notifier:
def __init__(self):
self.channels = [] # List to hold any number of channel objects
def add_channel(self, channel):
self.channels.append(channel) # Plug in any channel at runtime
return self # Enable method chaining
def notify(self, message: str):
for channel in self.channels: # Deliver via every registered channel
channel.deliver(message)Using the composed system:
# User wants email + SMS
user_notifier = Notifier()
user_notifier.add_channel(EmailChannel("alice@example.com"))
user_notifier.add_channel(SMSChannel("+1-555-0100"))
user_notifier.notify("Your order has shipped!")Output:
Email to alice@example.com: Your order has shipped!
SMS to +1-555-0100: Your order has shipped!Adding push notifications requires zero class changes just add_channel(PushChannel(...)).
Sometimes you want to add behavior to a class without full inheritance. Mixins are small, focused classes designed to be mixed into other classes.
class JSONMixin:
"""Adds JSON serialization to any class."""
def to_json(self):
import json
return json.dumps(self.__dict__, default=str) # Serialize object attributes
class TimestampMixin:
"""Adds created_at and updated_at tracking."""
from datetime import datetime
def touch(self):
self.updated_at = str(__import__('datetime').datetime.now()) # Set timestamp
class User(JSONMixin, TimestampMixin):
def __init__(self, name: str, email: str):
self.name = name # User's display name
self.email = email # User's email address
user = User("Alice", "alice@example.com")
print(user.to_json())
# Output: {"name": "Alice", "email": "alice@example.com"}Mixins work well when the behavior is truly orthogonal it applies to many unrelated classes without modification.
| Situation | Use | Reason |
|---|---|---|
| Child is a genuine specialization of parent | Inheritance | IS A relationship is clear and stable |
| Child needs behavior from multiple unrelated classes | Composition | Avoids multiple inheritance confusion |
| Base class changes frequently | Composition | Isolates changes, prevents fragile base class |
| You need to swap parts at runtime | Composition | Objects can be replaced without subclassing |
| Adding small, reusable behaviors | Mixin | Clean way to share orthogonal functionality |
| Hierarchy is more than 2-3 levels deep | Composition | Deep hierarchies are hard to reason about |
Building a FlyingCar by inheriting from Car and Plane creates chaos. Building one that has a drive system and a flight system creates a vehicle you can actually maintain.
University learning companion
Object-Oriented Programming (OOP) · Composition vs Inheritance: Choosing the Right Tool
Complete the steps without looking at the answer first; record evidence rather than only a final result.
These are primary or established learning references for this subject. Check the relevant chapter or specification for the exact topic and version.
Get this course's notes on Telegram!
Free cheat sheets, summaries & practice exercises