Timothy had embraced inheritance enthusiastically—too enthusiastically. His library catalog system had become a tangled hierarchy where AudiobookWithSubscription
inherited from Audiobook
, which inherited from DigitalBook
, which inherited from Book
. Adding a new feature meant navigating four levels of parent classes.
class Book:
def __init__(self, title, author):
self.title = title
self.author = author
class DigitalBook(Book):
def __init__(self, title, author, file_format):
super().__init__(title, author)
self.file_format = file_format
class Audiobook(DigitalBook):
def __init__(self, title, author, file_format, narrator):
super().__init__(title, author, file_format)
self.narrator = narrator
class AudiobookWithSubscription(Audiobook):
def __init__(self, title, author, file_format, narrator, subscription_service):
super().__init__(title, author, file_format, narrator)
self.subscription_service = subscription_service
Margaret found him debugging a method defined three classes up. “You’re building a tower,” she observed. “Towers are fragile. Come to the Composition Workshop—where we build with blocks, not layers.”
The Problem with Deep Hierarchies
Timothy learned inheritance creates rigid structures:
# What if we need a physical book with a subscription?
# Can't inherit from both AudiobookWithSubscription and Book
# The hierarchy assumes digital+audio+subscription always go together
# What if we need audiobook without digital file format?
# Can't skip DigitalBook in the hierarchy
# The inheritance tree makes assumptions about relationships
# that don't match real-world flexibility
“Inheritance forces you to share everything,” Margaret explained. “When AudiobookWithSubscription
inherits from Audiobook
, it gets ALL of Audiobook’s behavior—wanted or not.”
Composition: “Has-A” Instead of “Is-A”
Margaret showed Timothy an alternative:
class Book:
def __init__(self, title, author):
self.title = title
self.author = author
def get_info(self):
return f'"{self.title}" by {self.author}'
class AudioFeatures:
def __init__(self, narrator, duration_minutes):
self.narrator = narrator
self.duration_minutes = duration_minutes
def get_audio_info(self):
hours = self.duration_minutes / 60
return f'Narrated by {self.narrator}, {hours:.1f} hours'
class SubscriptionFeatures:
def __init__(self, service_name, monthly_cost):
self.service_name = service_name
self.monthly_cost = monthly_cost
def get_subscription_info(self):
return f'Available on {self.service_name} (${self.monthly_cost}/month)'
# Composition - book HAS audio features
class Audiobook:
def __init__(self, title, author, narrator, duration_minutes):
self.book = Book(title, author) # Has-a Book
self.audio = AudioFeatures(narrator, duration_minutes) # Has-a AudioFeatures
def get_full_info(self):
return f'{self.book.get_info()} - {self.audio.get_audio_info()}'
# Can compose features flexibly
class SubscribedAudiobook:
def __init__(self, title, author, narrator, duration_minutes, service, cost):
self.book = Book(title, author)
self.audio = AudioFeatures(narrator, duration_minutes)
self.subscription = SubscriptionFeatures(service, cost)
def get_full_info(self):
return f'{self.book.get_info()} - {self.audio.get_audio_info()} - {self.subscription.get_subscription_info()}'
“With composition,” Margaret explained, “objects contain other objects. An Audiobook
has a Book
and has AudioFeatures
. You choose exactly what to include.”
Delegation: Forwarding Method Calls
Timothy learned to expose contained objects’ methods:
class Audiobook:
def __init__(self, title, author, narrator, duration_minutes):
self.book = Book(title, author)
self.audio = AudioFeatures(narrator, duration_minutes)
# Delegate to book
@property
def title(self):
return self.book.title
@property
def author(self):
return self.book.author
# Delegate to audio
@property
def narrator(self):
return self.audio.narrator
def get_listening_time(self):
return self.audio.duration_minutes
audiobook = Audiobook("Dune", "Herbert", "Scott Brick", 1233)
print(audiobook.title) # Delegates to self.book.title
print(audiobook.narrator) # Delegates to self.audio.narrator
“Delegation forwards method calls to contained objects,” Margaret noted. “The audiobook exposes a clean interface while the real work happens inside composed objects.”
Flexible Composition
Timothy discovered composition allowed runtime flexibility:
class Book:
def __init__(self, title, author):
self.title = title
self.author = author
self.features = [] # Can add features dynamically
def add_feature(self, feature):
self.features.append(feature)
def get_all_info(self):
info = [f'"{self.title}" by {self.author}']
for feature in self.features:
if hasattr(feature, 'get_info'):
info.append(feature.get_info())
return ' - '.join(info)
class AudioFeatures:
def __init__(self, narrator, duration_minutes):
self.narrator = narrator
self.duration_minutes = duration_minutes
def get_info(self):
return f'Narrated by {self.narrator}'
class ReviewFeatures:
def __init__(self, rating, review_count):
self.rating = rating
self.review_count = review_count
def get_info(self):
return f'{self.rating}/5 stars ({self.review_count} reviews)'
# Build up features as needed
book = Book("Dune", "Herbert")
book.add_feature(AudioFeatures("Scott Brick", 1233))
book.add_feature(ReviewFeatures(4.5, 2847))
print(book.get_all_info())
# "Dune" by Herbert - Narrated by Scott Brick - 4.5/5 stars (2847 reviews)
“Composition lets you add capabilities at runtime,” Margaret explained. “Inheritance locks in structure at class definition time.”
Dependency Injection
Margaret showed Timothy how composition enables testing:
class DatabaseStorage:
def save_book(self, book):
# Real database operation
print(f"Saving {book.title} to database")
class FileStorage:
def save_book(self, book):
# Save to file
print(f"Saving {book.title} to file")
class MockStorage:
def save_book(self, book):
# For testing - no actual I/O
print(f"Mock save: {book.title}")
class Library:
def __init__(self, storage):
self.storage = storage # Inject dependency
self.books = []
def add_book(self, book):
self.books.append(book)
self.storage.save_book(book) # Delegate to injected storage
# Production use
prod_library = Library(DatabaseStorage())
prod_library.add_book(Book("Dune", "Herbert"))
# Testing use
test_library = Library(MockStorage())
test_library.add_book(Book("Foundation", "Asimov"))
# Different storage without changing Library code
file_library = Library(FileStorage())
“Dependency injection passes components from outside,” Margaret explained. “The Library
doesn’t create its storage—it receives it. This makes testing easy and keeps code flexible.”
The Strategy Pattern with Composition
Timothy learned to swap behavior dynamically:
class PriceCalculator:
def calculate(self, base_price):
return base_price
class DiscountCalculator:
def __init__(self, discount_percent):
self.discount_percent = discount_percent
def calculate(self, base_price):
return base_price * (1 - self.discount_percent / 100)
class SubscriberCalculator:
def calculate(self, base_price):
return base_price * 0.80 # 20% off for subscribers
class Book:
def __init__(self, title, base_price, price_calculator=None):
self.title = title
self.base_price = base_price
self.price_calculator = price_calculator or PriceCalculator()
def get_price(self):
return self.price_calculator.calculate(self.base_price)
# Different pricing strategies
regular_book = Book("Dune", 25.00)
print(regular_book.get_price()) # 25.00
discount_book = Book("Foundation", 25.00, DiscountCalculator(15))
print(discount_book.get_price()) # 21.25
subscriber_book = Book("1984", 25.00, SubscriberCalculator())
print(subscriber_book.get_price()) # 20.00
“The Strategy pattern uses composition to swap algorithms,” Margaret noted. “Same interface, different implementations—all without inheritance.”
Composition Over Inheritance: When to Use Each
Margaret clarified the decision:
# Use INHERITANCE when:
# - Clear "is-a" relationship (Audiobook IS-A Book)
# - Child needs ALL parent behavior
# - Substitutability matters (pass child where parent expected)
# - Hierarchy is shallow (2-3 levels max)
class Book:
def checkout(self):
pass
class Audiobook(Book):
# Audiobook IS-A Book
# Needs ALL book behavior
# Can be used wherever Book is expected
pass
# Use COMPOSITION when:
# - "Has-a" relationship (Book HAS-A PriceCalculator)
# - Need flexibility to swap components
# - Want to mix and match features
# - Avoiding deep hierarchies
# - Enabling testing through dependency injection
class Book:
def __init__(self, title, price_calculator):
self.title = title
self.calculator = price_calculator # HAS-A calculator
# Can swap calculator at runtime
# Easy to test with mock calculator
Favor composition when:
- You need flexibility to change behavior at runtime
- You want to test components independently
- The relationship is “has-a” or “uses-a”
- You’re avoiding deep inheritance hierarchies
- You want to mix and match features
Use inheritance when:
- There’s a clear “is-a” relationship
- Substitutability is important (Liskov Substitution Principle)
- You need polymorphism (treating different types uniformly)
- The hierarchy is shallow and stable
Real-World Example: Notification System
Margaret demonstrated a practical pattern:
class EmailNotifier:
def send(self, message):
print(f"Sending email: {message}")
class SMSNotifier:
def send(self, message):
print(f"Sending SMS: {message}")
class SlackNotifier:
def send(self, message):
print(f"Posting to Slack: {message}")
class NotificationManager:
def __init__(self):
self.notifiers = []
def add_notifier(self, notifier):
self.notifiers.append(notifier)
def notify_all(self, message):
for notifier in self.notifiers:
notifier.send(message)
class Library:
def __init__(self, notification_manager):
self.notification_manager = notification_manager
self.books = []
def add_book(self, book):
self.books.append(book)
self.notification_manager.notify_all(f"New book: {book.title}")
# Compose notification system
notifier = NotificationManager()
notifier.add_notifier(EmailNotifier())
notifier.add_notifier(SlackNotifier())
library = Library(notifier)
library.add_book(Book("Dune", "Herbert"))
# Sends via both email and Slack
“Composition creates flexible systems,” Margaret explained. “Add notifiers, remove notifiers, swap implementations—all without changing Library
or creating inheritance hierarchies.”
The Adapter Pattern
Timothy learned composition could make incompatible interfaces work together:
# Third-party class we can't modify
class LegacyBookDatabase:
def store(self, book_title, book_author):
print(f"Legacy storing: {book_title} by {book_author}")
# Our interface
class ModernStorage:
def save_book(self, book):
print(f"Modern saving: {book.title}")
# Adapter uses composition to bridge the gap
class LegacyAdapter:
def __init__(self, legacy_db):
self.legacy_db = legacy_db # Composition
def save_book(self, book):
# Adapt our interface to legacy interface
self.legacy_db.store(book.title, book.author)
# Library works with any storage matching our interface
class Library:
def __init__(self, storage):
self.storage = storage
def add_book(self, book):
self.storage.save_book(book)
# Use legacy system with modern interface
legacy_db = LegacyBookDatabase()
adapter = LegacyAdapter(legacy_db)
library = Library(adapter)
library.add_book(Book("Dune", "Herbert"))
# Works! Adapter translates between interfaces
Timothy’s Composition Wisdom
Through exploring the Composition Workshop, Timothy learned essential principles:
Composition means “has-a” relationships: Objects contain other objects as components.
Inheritance means “is-a” relationships: Child classes are specialized versions of parents.
Favor composition over inheritance: More flexible, easier to test, avoids deep hierarchies.
Delegation forwards method calls: Expose contained objects’ methods through wrapper methods.
Dependency injection passes components from outside: Makes testing easy and code flexible.
Composition enables runtime flexibility: Add, remove, or swap components as needed.
Strategy pattern swaps algorithms: Different implementations, same interface.
Adapter pattern bridges incompatible interfaces: Wrap legacy code to match modern expectations.
Composition avoids fragile hierarchies: No rigid tower of parent classes to navigate.
Mix and match features freely: Combine capabilities without inheritance constraints.
Testing becomes easier: Mock individual components independently.
Code stays loosely coupled: Components don’t depend on inheritance hierarchies.
Timothy had discovered that building with blocks—composition—created more flexible, maintainable systems than building towers with inheritance. The Composition Workshop revealed that the strongest structures weren’t layered hierarchies but assemblies of independent, swappable components. He could now build complex library systems from simple, focused objects—each doing one thing well, combined in countless ways.
Aaron Rose is a software engineer and technology writer at tech-reader.blog and the author of Think Like a Genius.