Overview
Composition and inheritance represent two fundamental approaches to code reuse and relationship modeling in object-oriented programming. Inheritance creates "is-a" relationships where a subclass inherits behavior from a parent class, while composition creates "has-a" relationships where an object contains other objects to delegate behavior.
The composition versus inheritance debate addresses a core design question: how should objects acquire and share behavior? Inheritance establishes a class hierarchy where child classes automatically receive parent class methods and can override them. Composition builds objects from smaller, independent components that work together through delegation.
# Inheritance: Employee is-a Person
class Person
def initialize(name)
@name = name
end
def introduce
"I'm #{@name}"
end
end
class Employee < Person
def initialize(name, employee_id)
super(name)
@employee_id = employee_id
end
end
# Composition: Employee has-a Person
class Employee
def initialize(person, employee_id)
@person = person
@employee_id = employee_id
end
def introduce
@person.introduce
end
end
The Gang of Four design patterns book popularized the principle "favor composition over inheritance," but this guideline requires context. Each approach offers distinct advantages and constraints that affect maintainability, flexibility, and code clarity.
Key Principles
Inheritance Hierarchy: Inheritance creates a taxonomic relationship where subclasses form specializations of parent classes. The subclass inherits all public and protected methods from the parent, gaining access to the parent's interface and implementation. Changes to parent classes propagate automatically to all descendants, creating tight coupling across the hierarchy.
Composition Structure: Composition assembles objects from component parts, each responsible for specific behavior. The containing object delegates work to its components through explicit method calls. Components remain independent and can be replaced or modified without affecting the containing object's interface.
Coupling Differences: Inheritance couples subclasses to parent implementation details. A child class depends on parent method signatures, return types, and even internal state in some cases. Composition creates loose coupling through interfaces—the containing object only depends on component method signatures, not their internal implementation.
Substitutability: Inheritance supports polymorphism through the Liskov Substitution Principle—any code accepting a parent class instance should work correctly with subclass instances. This enables runtime polymorphism where different subclasses respond differently to the same method calls.
class Animal
def speak
raise NotImplementedError
end
end
class Dog < Animal
def speak
"Woof"
end
end
class Cat < Animal
def speak
"Meow"
end
end
def make_animal_speak(animal)
puts animal.speak
end
make_animal_speak(Dog.new) # => Woof
make_animal_speak(Cat.new) # => Meow
Interface Contracts: Composition establishes contracts through interfaces or duck typing. Components must implement expected methods, but their internal structure remains hidden. Ruby's duck typing eliminates the need for explicit interface declarations—any object responding to the required methods satisfies the contract.
class Printer
def initialize(formatter)
@formatter = formatter
end
def print(data)
puts @formatter.format(data)
end
end
class JsonFormatter
def format(data)
data.to_json
end
end
class CsvFormatter
def format(data)
data.join(',')
end
end
# Both formatters work because they implement format()
printer = Printer.new(JsonFormatter.new)
printer.print({name: "Alice"})
Single vs Multiple Behavior Sources: Inheritance in Ruby supports single inheritance only—each class inherits from exactly one parent. Composition allows objects to delegate to multiple components, each providing different capabilities. This makes composition more flexible when combining behavior from multiple sources.
Reusability Models: Inheritance reuses code through class hierarchies where common behavior moves to parent classes. Composition reuses code through independent, composable components that can be mixed and matched across different contexts.
Design Considerations
When Inheritance Applies: Inheritance works best when modeling true taxonomic relationships where subclasses represent specialized versions of a general concept. If the relationship satisfies "is-a" semantics naturally and subclasses need to override or extend parent behavior in predictable ways, inheritance provides clear modeling and polymorphic behavior.
The Liskov Substitution Principle tests inheritance appropriateness: subclass instances must be usable anywhere the parent class is expected without breaking functionality. Violations indicate that inheritance forces an unnatural relationship.
# Good inheritance: Square is-a Shape
class Shape
def area
raise NotImplementedError
end
end
class Square < Shape
def initialize(side)
@side = side
end
def area
@side * @side
end
end
# Problematic inheritance: Stack is-not-a Array
class Stack < Array
# Inherits push, pop, but also [], []=, each, etc.
# These break stack semantics
end
When Composition Applies: Composition excels when assembling objects from independent capabilities that don't form natural hierarchies. If different object types need to share some behavior but not others, composition allows selective inclusion of components without forcing unrelated classes into artificial inheritance relationships.
Composition handles behavior changes at runtime. Objects can swap components dynamically, changing their behavior without modifying class definitions. Inheritance locks behavior at compile time through the class hierarchy.
class TextEditor
def initialize
@spell_checker = nil
@auto_saver = nil
end
def enable_spell_check(language)
@spell_checker = SpellChecker.new(language)
end
def enable_auto_save(interval)
@auto_saver = AutoSaver.new(interval)
end
def check_spelling(text)
@spell_checker.check(text) if @spell_checker
end
end
Multiple Inheritance Alternative: Ruby's single inheritance limitation often forces composition for combining multiple behaviors. Rather than attempting complex mixin chains, composition explicitly models each behavior source as a component. This creates clearer dependencies and avoids method resolution order confusion.
Deep Hierarchies Problem: Inheritance hierarchies deeper than 2-3 levels become difficult to understand and maintain. Each level adds coupling and makes it harder to predict method behavior. Composition flattens these structures by delegating to specialized components rather than chaining through parent classes.
Fragile Base Class Problem: Changes to parent classes risk breaking subclasses in unexpected ways. A parent method modification might affect dozens of subclasses, requiring careful testing of all descendants. Composition isolates changes to individual components—modifying one component affects only objects directly using it.
Testing Implications: Composition simplifies testing by allowing component isolation. Tests can mock or stub individual components without involving the entire object graph. Inheritance requires testing subclasses in the context of their parent hierarchy, increasing test complexity.
Ruby Implementation
Class Inheritance Syntax: Ruby implements inheritance through the < operator. Subclasses inherit all methods from their parent except private methods. The super keyword calls the parent implementation of overridden methods.
class Vehicle
def initialize(make, model)
@make = make
@model = model
end
def description
"#{@make} #{@model}"
end
def fuel_efficiency
raise "Must be implemented by subclass"
end
end
class ElectricVehicle < Vehicle
def initialize(make, model, battery_capacity)
super(make, model)
@battery_capacity = battery_capacity
end
def fuel_efficiency
"#{@battery_capacity} kWh capacity"
end
def description
"#{super} (Electric)"
end
end
ev = ElectricVehicle.new("Tesla", "Model 3", 75)
puts ev.description # => Tesla Model 3 (Electric)
Module Mixins: Ruby modules provide a middle ground between inheritance and composition. Including a module copies its methods into the class, similar to inheritance. The method resolution order checks included modules before the parent class but after the class itself.
module Swimmable
def swim
"Swimming at #{swim_speed} mph"
end
def swim_speed
10
end
end
module Flyable
def fly
"Flying at #{fly_speed} mph"
end
def fly_speed
50
end
end
class Duck
include Swimmable
include Flyable
def swim_speed
5
end
end
duck = Duck.new
puts duck.swim # => Swimming at 5 mph
puts duck.fly # => Flying at 50 mph
Delegation Pattern: Ruby's Forwardable module simplifies composition by automatically generating delegation methods. This reduces boilerplate when forwarding method calls to components.
require 'forwardable'
class Document
extend Forwardable
def initialize(content, formatter)
@content = content
@formatter = formatter
end
# Delegate format method to @formatter
def_delegator :@formatter, :format
# Delegate multiple methods to @content
def_delegators :@content, :length, :empty?, :size
end
class MarkdownFormatter
def format(text)
"**#{text}**"
end
end
doc = Document.new("Hello", MarkdownFormatter.new)
puts doc.format("World") # => **World**
puts doc.length # => 5
SimpleDelegator: Ruby's SimpleDelegator class wraps another object and forwards all method calls to it by default. This creates a transparent proxy that can selectively override methods while delegating the rest.
require 'delegate'
class TimestampedArray < SimpleDelegator
def initialize(array)
super(array)
@created_at = Time.now
end
def push(item)
puts "[#{Time.now}] Adding #{item}"
super
end
def created_at
@created_at
end
end
arr = TimestampedArray.new([1, 2, 3])
arr.push(4) # => [2025-10-06 ...] Adding 4
puts arr[0] # => 1 (delegated to wrapped array)
puts arr.length # => 4 (delegated to wrapped array)
Composition with Dependency Injection: Ruby's dynamic typing enables flexible composition through dependency injection. Objects receive components as constructor arguments or through setter methods, allowing component substitution without modifying the containing class.
class OrderProcessor
def initialize(payment_gateway:, notification_service:, logger:)
@payment_gateway = payment_gateway
@notification_service = notification_service
@logger = logger
end
def process(order)
@logger.info("Processing order #{order.id}")
if @payment_gateway.charge(order.amount)
@notification_service.send_confirmation(order)
@logger.info("Order #{order.id} completed")
true
else
@logger.error("Payment failed for order #{order.id}")
false
end
end
end
# Different components injected at runtime
processor = OrderProcessor.new(
payment_gateway: StripeGateway.new,
notification_service: EmailService.new,
logger: Rails.logger
)
Prepend for Composition: The prepend method inserts a module before the class in the method resolution order. This allows modules to wrap class methods, creating a composition-like pattern where the module can call the original implementation through super.
module Cacheable
def fetch(key)
@cache ||= {}
@cache[key] ||= super
end
end
class Database
def fetch(key)
puts "Fetching #{key} from database"
"value_#{key}"
end
end
Database.prepend(Cacheable)
db = Database.new
puts db.fetch("user_1") # => Fetching user_1 from database
puts db.fetch("user_1") # => Returns cached value
Practical Examples
Example 1: Logger with Multiple Outputs: A logging system needs to write messages to different destinations. Inheritance would create a rigid hierarchy (FileLogger, ConsoleLogger, DatabaseLogger), making it impossible to log to multiple destinations simultaneously. Composition allows combining multiple output handlers.
class Logger
def initialize
@handlers = []
end
def add_handler(handler)
@handlers << handler
end
def log(level, message)
timestamp = Time.now.strftime("%Y-%m-%d %H:%M:%S")
formatted = "[#{timestamp}] #{level.upcase}: #{message}"
@handlers.each { |handler| handler.write(formatted) }
end
def info(message)
log("info", message)
end
def error(message)
log("error", message)
end
end
class FileHandler
def initialize(filename)
@file = File.open(filename, 'a')
end
def write(message)
@file.puts(message)
@file.flush
end
end
class ConsoleHandler
def write(message)
puts message
end
end
class EmailHandler
def initialize(email_address)
@email = email_address
end
def write(message)
# Only send emails for errors
if message.include?("ERROR")
# Send email logic here
puts "Emailing #{@email}: #{message}"
end
end
end
logger = Logger.new
logger.add_handler(FileHandler.new("app.log"))
logger.add_handler(ConsoleHandler.new)
logger.add_handler(EmailHandler.new("admin@example.com"))
logger.info("Application started")
logger.error("Database connection failed")
Example 2: Payment Processing Hierarchy: An e-commerce system processes different payment types. Inheritance models the payment type hierarchy naturally, as all payment processors share common validation and recording logic but differ in processing implementation.
class PaymentProcessor
def initialize(amount, customer)
@amount = amount
@customer = customer
@processed_at = nil
end
def process
return false unless validate
result = execute_payment
if result
@processed_at = Time.now
record_transaction
end
result
end
private
def validate
@amount > 0 && @customer.active?
end
def execute_payment
raise "Must be implemented by subclass"
end
def record_transaction
Transaction.create(
customer_id: @customer.id,
amount: @amount,
processed_at: @processed_at,
payment_type: self.class.name
)
end
end
class CreditCardProcessor < PaymentProcessor
def initialize(amount, customer, card_number, cvv)
super(amount, customer)
@card_number = card_number
@cvv = cvv
end
private
def execute_payment
# Charge credit card
CreditCardGateway.charge(@card_number, @cvv, @amount)
end
end
class PayPalProcessor < PaymentProcessor
def initialize(amount, customer, paypal_email)
super(amount, customer)
@paypal_email = paypal_email
end
private
def execute_payment
# Process PayPal payment
PayPalAPI.transfer(@paypal_email, @amount)
end
end
class CryptoProcessor < PaymentProcessor
def initialize(amount, customer, wallet_address, currency)
super(amount, customer)
@wallet_address = wallet_address
@currency = currency
end
private
def execute_payment
# Process cryptocurrency payment
CryptoAPI.send_payment(@wallet_address, @amount, @currency)
end
end
processor = CreditCardProcessor.new(100, customer, "1234...", "123")
processor.process
Example 3: Game Entity System: A game needs entities with different combinations of abilities. A player might move and shoot, enemies might move and take damage, and decorative objects might only render. Inheritance forces choosing a single primary behavior, while composition allows combining any abilities.
class Position
attr_accessor :x, :y
def initialize(x, y)
@x = x
@y = y
end
def distance_to(other)
Math.sqrt((@x - other.x)**2 + (@y - other.y)**2)
end
end
class Movement
def initialize(position, speed)
@position = position
@speed = speed
end
def move_to(x, y)
direction_x = x - @position.x
direction_y = y - @position.y
distance = Math.sqrt(direction_x**2 + direction_y**2)
if distance > 0
@position.x += (direction_x / distance) * @speed
@position.y += (direction_y / distance) * @speed
end
end
end
class Health
attr_reader :current, :maximum
def initialize(maximum)
@maximum = maximum
@current = maximum
end
def take_damage(amount)
@current = [@current - amount, 0].max
end
def heal(amount)
@current = [@current + amount, @maximum].min
end
def alive?
@current > 0
end
end
class Combat
def initialize(damage)
@damage = damage
end
def attack(target_health)
target_health.take_damage(@damage)
end
end
class Entity
attr_reader :position
def initialize(x, y)
@position = Position.new(x, y)
@movement = nil
@health = nil
@combat = nil
end
def add_movement(speed)
@movement = Movement.new(@position, speed)
self
end
def add_health(maximum)
@health = Health.new(maximum)
self
end
def add_combat(damage)
@combat = Combat.new(damage)
self
end
def move_to(x, y)
@movement.move_to(x, y) if @movement
end
def take_damage(amount)
@health.take_damage(amount) if @health
end
def attack(target)
@combat.attack(target) if @combat && target.respond_to?(:take_damage)
end
def alive?
@health ? @health.alive? : true
end
end
# Create different entity types by composing abilities
player = Entity.new(0, 0)
.add_movement(5)
.add_health(100)
.add_combat(25)
enemy = Entity.new(10, 10)
.add_movement(3)
.add_health(50)
.add_combat(15)
decoration = Entity.new(5, 5) # No abilities added
player.move_to(8, 8)
player.attack(enemy.instance_variable_get(:@health))
puts "Enemy health: #{enemy.instance_variable_get(:@health).current}"
Example 4: Report Generation with Strategies: A reporting system generates data in different formats. The report structure and data gathering use inheritance, while output formatting uses composition to allow runtime format selection.
class Report
def initialize(data_source)
@data_source = data_source
end
def generate
data = gather_data
calculate_metrics(data)
end
private
def gather_data
raise "Must be implemented by subclass"
end
def calculate_metrics(data)
{
total: data.sum,
average: data.sum / data.length.to_f,
count: data.length
}
end
end
class SalesReport < Report
private
def gather_data
@data_source.sales_for_period(start_date, end_date)
end
def start_date
Date.today - 30
end
def end_date
Date.today
end
end
class InventoryReport < Report
private
def gather_data
@data_source.current_inventory_levels
end
def calculate_metrics(data)
super.merge(
low_stock: data.count { |level| level < 10 },
out_of_stock: data.count { |level| level == 0 }
)
end
end
class ReportFormatter
def initialize(report, formatter)
@report = report
@formatter = formatter
end
def output
metrics = @report.generate
@formatter.format(metrics)
end
end
class JsonFormatter
def format(metrics)
require 'json'
JSON.pretty_generate(metrics)
end
end
class HtmlFormatter
def format(metrics)
html = "<table>\n"
metrics.each do |key, value|
html += " <tr><td>#{key}</td><td>#{value}</td></tr>\n"
end
html += "</table>"
end
end
class CsvFormatter
def format(metrics)
require 'csv'
CSV.generate do |csv|
csv << metrics.keys
csv << metrics.values
end
end
end
# Inheritance for report types, composition for output format
sales = SalesReport.new(data_source)
inventory = InventoryReport.new(data_source)
# Generate same report in different formats
puts ReportFormatter.new(sales, JsonFormatter.new).output
puts ReportFormatter.new(sales, HtmlFormatter.new).output
puts ReportFormatter.new(inventory, CsvFormatter.new).output
Common Patterns
Strategy Pattern: The strategy pattern replaces conditional logic with composition by encapsulating algorithms in separate objects. The context object holds a reference to a strategy and delegates algorithm execution to it.
class DataCompressor
def initialize(strategy)
@strategy = strategy
end
def compress(data)
@strategy.compress(data)
end
def strategy=(new_strategy)
@strategy = new_strategy
end
end
class ZipStrategy
def compress(data)
# Zip compression logic
"zip(#{data})"
end
end
class GzipStrategy
def compress(data)
# Gzip compression logic
"gzip(#{data})"
end
end
compressor = DataCompressor.new(ZipStrategy.new)
puts compressor.compress("hello") # => zip(hello)
compressor.strategy = GzipStrategy.new
puts compressor.compress("hello") # => gzip(hello)
Decorator Pattern: The decorator pattern adds behavior to objects dynamically through composition. Each decorator wraps an object and adds functionality while maintaining the same interface.
class Coffee
def cost
2.00
end
def description
"Coffee"
end
end
class MilkDecorator
def initialize(coffee)
@coffee = coffee
end
def cost
@coffee.cost + 0.50
end
def description
@coffee.description + ", Milk"
end
end
class SugarDecorator
def initialize(coffee)
@coffee = coffee
end
def cost
@coffee.cost + 0.25
end
def description
@coffee.description + ", Sugar"
end
end
# Build coffee with decorators
coffee = Coffee.new
coffee_with_milk = MilkDecorator.new(coffee)
coffee_with_milk_and_sugar = SugarDecorator.new(coffee_with_milk)
puts coffee_with_milk_and_sugar.description # => Coffee, Milk, Sugar
puts coffee_with_milk_and_sugar.cost # => 2.75
Template Method Pattern: The template method pattern uses inheritance to define an algorithm's structure in a base class while allowing subclasses to override specific steps.
class DataProcessor
def process(file)
data = read_file(file)
parsed = parse_data(data)
validated = validate_data(parsed)
transform_data(validated)
end
private
def read_file(file)
File.read(file)
end
def parse_data(data)
raise "Must be implemented by subclass"
end
def validate_data(parsed)
# Default validation
parsed.compact
end
def transform_data(validated)
raise "Must be implemented by subclass"
end
end
class CsvProcessor < DataProcessor
private
def parse_data(data)
require 'csv'
CSV.parse(data, headers: true).map(&:to_h)
end
def transform_data(validated)
validated.map { |row| row.transform_keys(&:upcase) }
end
end
class JsonProcessor < DataProcessor
private
def parse_data(data)
require 'json'
JSON.parse(data)
end
def validate_data(parsed)
super
parsed.select { |item| item['valid'] == true }
end
def transform_data(validated)
validated.map { |item| item.slice('id', 'name', 'value') }
end
end
Adapter Pattern: The adapter pattern uses composition to make incompatible interfaces work together by wrapping an object and translating method calls.
# Existing legacy system
class LegacyPrinter
def print_document(doc, copies)
copies.times { puts "Printing: #{doc}" }
end
end
# New interface expected by application
class ModernPrinterAdapter
def initialize(legacy_printer)
@legacy_printer = legacy_printer
end
def print(document)
@legacy_printer.print_document(document, 1)
end
def print_multiple(document, count)
@legacy_printer.print_document(document, count)
end
end
# Application code uses modern interface
def send_to_printer(printer, doc)
printer.print(doc)
end
legacy = LegacyPrinter.new
adapter = ModernPrinterAdapter.new(legacy)
send_to_printer(adapter, "report.pdf")
Bridge Pattern: The bridge pattern separates abstraction from implementation using composition, allowing them to vary independently.
class RemoteControl
def initialize(device)
@device = device
end
def power_on
@device.turn_on
end
def power_off
@device.turn_off
end
def set_volume(level)
@device.volume = level
end
end
class AdvancedRemote < RemoteControl
def mute
@device.volume = 0
end
def max_volume
@device.volume = 100
end
end
class Television
attr_accessor :volume
def turn_on
puts "TV is on"
end
def turn_off
puts "TV is off"
end
end
class Radio
attr_accessor :volume
def turn_on
puts "Radio is on"
end
def turn_off
puts "Radio is off"
end
end
# Different remotes can control different devices
tv_remote = AdvancedRemote.new(Television.new)
tv_remote.power_on
tv_remote.mute
radio_remote = RemoteControl.new(Radio.new)
radio_remote.power_on
radio_remote.set_volume(50)
Common Pitfalls
Premature Composition: Converting all inheritance to composition creates unnecessary indirection and boilerplate. Simple inheritance hierarchies with clear "is-a" relationships often provide better readability than composition with multiple delegation layers.
# Unnecessary composition
class Dog
def initialize(animal)
@animal = animal
end
def speak
@animal.speak
end
end
class Animal
def speak
"Generic sound"
end
end
# Better with inheritance for true is-a relationship
class Dog < Animal
def speak
"Woof"
end
end
Over-Delegating with Forwardable: Forwarding every method from a component creates tight coupling without inheritance's benefits. The containing class becomes a thin wrapper that exposes the entire component interface, defeating composition's encapsulation purpose.
# Over-delegation exposes too much
class UserAccount
extend Forwardable
def initialize(user)
@user = user
end
# Forwarding everything couples UserAccount to User's interface
def_delegators :@user, :name, :email, :password_hash, :created_at,
:updated_at, :admin?, :banned?, :verified?
end
# Better: expose only necessary methods
class UserAccount
def initialize(user)
@user = user
end
def display_name
@user.name
end
def contact_email
@user.email
end
# Internal methods not exposed
end
Breaking Liskov Substitution: Subclasses that violate parent class contracts break polymorphism. Common violations include throwing exceptions where the parent succeeds, returning different types, or requiring stronger preconditions.
class Rectangle
attr_accessor :width, :height
def area
width * height
end
end
# Violates Liskov Substitution
class Square < Rectangle
def width=(value)
@width = @height = value
end
def height=(value)
@width = @height = value
end
end
def test_rectangle(rect)
rect.width = 5
rect.height = 10
rect.area == 50 # Fails for Square
end
# Better: use composition
class Square
attr_accessor :side
def area
side * side
end
end
Mixin Confusion: Ruby's module inclusion inserts methods into the inheritance chain, causing confusion when multiple modules define the same method. The last included module wins, but this behavior is not obvious from the class definition.
module A
def greet
"Hello from A"
end
end
module B
def greet
"Hello from B"
end
end
class MyClass
include A
include B # B's greet overrides A's greet
end
puts MyClass.new.greet # => Hello from B
# Better: explicit composition makes behavior clear
class MyClass
def initialize
@greeter = B.new
end
def greet
@greeter.greet
end
end
Ignoring Inheritance When Appropriate: Forcing composition for clear taxonomic relationships adds complexity without benefit. Payment processors, geometric shapes, and exception classes naturally form hierarchies that inheritance models effectively.
# Forced composition creates unnecessary complexity
class PaymentProcessor
def initialize(processor_type)
@processor = case processor_type
when :credit_card then CreditCardProcessor.new
when :paypal then PayPalProcessor.new
when :crypto then CryptoProcessor.new
end
end
def process(amount)
@processor.process(amount)
end
end
# Better: use inheritance for natural hierarchy
class PaymentProcessor
def process(amount)
raise NotImplementedError
end
end
class CreditCardProcessor < PaymentProcessor
def process(amount)
# Process credit card
end
end
Component State Synchronization: Objects composed of multiple components must coordinate state changes across components. Failing to maintain consistency between components leads to invalid object states.
# Problematic: position and movement can become inconsistent
class Entity
attr_reader :position, :movement
def initialize
@position = Position.new(0, 0)
@movement = Movement.new(5) # Movement has no reference to position
end
def teleport(x, y)
@position.x = x
@position.y = y
# Movement component doesn't know position changed
end
end
# Better: components share state or coordinate through entity
class Entity
def initialize
@position = Position.new(0, 0)
@movement = Movement.new(@position, 5) # Movement references position
end
def teleport(x, y)
@position.x = x
@position.y = y
@movement.reset_path # Coordinate state change
end
end
Reference
Comparison Matrix
| Aspect | Inheritance | Composition |
|---|---|---|
| Relationship Type | is-a | has-a |
| Coupling | Tight coupling to parent | Loose coupling through interfaces |
| Flexibility | Fixed at class definition | Can change at runtime |
| Code Reuse | Vertical reuse through hierarchy | Horizontal reuse through components |
| Ruby Support | Single inheritance with super | Direct delegation or Forwardable |
| Testing | Must test with parent context | Components tested independently |
| Complexity | Simple hierarchies, complex at depth | More objects, clearer dependencies |
| Polymorphism | Built-in through class hierarchy | Manual through duck typing |
| Method Resolution | Automatic through inheritance chain | Explicit delegation required |
Decision Criteria
| Use Inheritance When | Use Composition When |
|---|---|
| True is-a relationship exists | has-a or uses-a relationship exists |
| Shared behavior applies to all subclasses | Behavior shared by unrelated classes |
| Liskov Substitution Principle satisfied | Objects need runtime behavior changes |
| Hierarchy depth stays under 3 levels | Combining multiple independent behaviors |
| Subclasses override specific methods | Building objects from reusable parts |
| Polymorphic behavior needed | Interface-based programming preferred |
| Template Method pattern applies | Strategy or Decorator pattern applies |
| Natural taxonomy exists | Mixing capabilities from multiple sources |
Ruby Delegation Techniques
| Technique | Syntax | Use Case |
|---|---|---|
| Manual delegation | def method; @component.method; end | Full control over delegation |
| Forwardable | def_delegator :@component, :method | Multiple specific methods |
| SimpleDelegator | class MyClass < SimpleDelegator | Transparent proxy with overrides |
| DelegateClass | class MyClass < DelegateClass(Array) | Delegate to specific class |
| Method missing | def method_missing(name, *args) | Dynamic delegation |
Common Composition Patterns
| Pattern | Structure | Ruby Implementation |
|---|---|---|
| Strategy | Context delegates to strategy | Object holds strategy reference |
| Decorator | Wrapper adds behavior | Nested objects with same interface |
| Adapter | Wrapper translates interface | Object wraps incompatible class |
| Bridge | Abstraction separated from implementation | Object references implementation |
| Composite | Tree of objects with same interface | Recursive component structure |
Inheritance Patterns
| Pattern | Structure | Ruby Implementation |
|---|---|---|
| Template Method | Base class defines algorithm | Abstract methods overridden by subclass |
| Factory Method | Subclasses create objects | Override factory method in subclass |
| Hook Methods | Base class calls empty methods | Subclasses override hooks as needed |
| Abstract Base Class | Parent defines interface | Parent raises NotImplementedError |
Module Inclusion Order
| Method | Position in Chain | Effect |
|---|---|---|
| include | After class, before parent | Module methods available, class overrides |
| prepend | Before class | Module wraps class methods with super |
| extend | Adds to singleton class | Module methods become class methods |
Method Resolution Order
When Ruby resolves a method call, it searches in this order:
- Prepended modules (most recent first)
- The class itself
- Included modules (most recent first)
- Parent class (repeat from step 1)
- Object
- Kernel
- BasicObject
Refactoring Guidelines
| From | To | Trigger |
|---|---|---|
| Inheritance | Composition | Deep hierarchy (3+ levels) |
| Inheritance | Composition | Multiple unrelated behaviors needed |
| Inheritance | Composition | Subclass uses only subset of parent |
| Composition | Inheritance | True is-a relationship discovered |
| Composition | Inheritance | Polymorphic substitution required |
| Manual delegation | Forwardable | 3+ delegated methods |
| Forwardable | Manual | Need to transform delegated data |