The Composition Workshop: Building with “Has-A” Relationships


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
Enter fullscreen mode

Exit fullscreen mode

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
Enter fullscreen mode

Exit fullscreen mode

“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()}'
Enter fullscreen mode

Exit fullscreen mode

“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
Enter fullscreen mode

Exit fullscreen mode

“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)
Enter fullscreen mode

Exit fullscreen mode

“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())
Enter fullscreen mode

Exit fullscreen mode

“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
Enter fullscreen mode

Exit fullscreen mode

“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
Enter fullscreen mode

Exit fullscreen mode

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
Enter fullscreen mode

Exit fullscreen mode

“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
Enter fullscreen mode

Exit fullscreen mode



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.



Source link

Leave a Reply

Your email address will not be published. Required fields are marked *