Overview
The Bridge Pattern separates an abstraction from its implementation so that the two can vary independently. This structural pattern involves an interface that acts as a bridge between the abstraction class and implementer classes. The abstraction contains a reference to the implementer, delegating the actual work to the implementation object rather than inheriting from it.
The pattern emerged from the need to avoid a permanent binding between an abstraction and its implementation. Without the Bridge Pattern, extending a system with new abstractions and implementations leads to an exponential growth of classes. For instance, a shape system supporting multiple rendering methods would require a class for each shape-renderer combination: CircleOpenGL, CircleDirect3D, SquareOpenGL, SquareDirect3D, and so on.
The Bridge Pattern solves this by splitting the system into two separate hierarchies: one for the abstraction (shapes) and one for the implementation (renderers). Each hierarchy evolves independently. Adding a new shape does not require creating classes for every renderer, and adding a new renderer does not require creating classes for every shape.
The pattern applies to any scenario where:
- Both abstractions and implementations need to extend independently
- Changes in implementation should not affect client code using the abstraction
- Implementation details should be hidden from clients
- Multiple implementations need to be shared among objects
- A proliferation of classes from combined variations must be avoided
# Without Bridge: explosion of classes
class CircleOpenGL < Shape
def draw
# OpenGL-specific circle drawing
end
end
class CircleDirect3D < Shape
def draw
# Direct3D-specific circle drawing
end
end
# With Bridge: composition separates concerns
class Circle
def initialize(renderer)
@renderer = renderer
end
def draw
@renderer.render_circle(@radius, @x, @y)
end
end
Key Principles
The Bridge Pattern operates on four fundamental components that work together to achieve separation of concerns.
Abstraction defines the high-level control interface that clients interact with. It maintains a reference to an object of the Implementor type. The abstraction delegates primitive operations to the implementor object rather than implementing them directly. In Ruby, abstractions typically expose domain-specific methods that clients call.
Refined Abstraction extends the interface defined by Abstraction. These classes add specialized behavior while still delegating implementation details to the implementor. Multiple refined abstractions can exist, each offering different high-level functionality while sharing the same implementation hierarchy.
Implementor defines the interface for implementation classes. This interface does not need to correspond directly to the Abstraction's interface. The Implementor provides primitive operations, and the Abstraction defines higher-level operations based on these primitives. The Implementor interface focuses on implementation-specific operations.
Concrete Implementor provides specific implementations of the Implementor interface. Multiple concrete implementors can exist, each offering different ways to perform the primitive operations. The abstraction works with any concrete implementor through the Implementor interface.
The relationship between these components follows a composition structure. The Abstraction contains a reference to an Implementor, establishing the bridge between the two hierarchies. This containment relationship replaces inheritance, providing greater flexibility.
# Abstraction
class RemoteControl
def initialize(device)
@device = device
end
def toggle_power
if @device.enabled?
@device.disable
else
@device.enable
end
end
def volume_up
@device.set_volume(@device.volume + 10)
end
end
# Refined Abstraction
class AdvancedRemoteControl < RemoteControl
def mute
@device.set_volume(0)
end
end
# Implementor interface (duck typing in Ruby)
# Concrete Implementor
class Television
attr_reader :volume
def initialize
@enabled = false
@volume = 50
end
def enabled?
@enabled
end
def enable
@enabled = true
end
def disable
@enabled = false
end
def set_volume(level)
@volume = [0, [level, 100].min].max
end
end
The pattern establishes an explicit separation of interface and implementation. Clients program against the Abstraction interface, remaining unaware of the concrete implementation details. The Implementor hierarchy can change without affecting client code, and new implementations can be added without modifying existing abstractions.
The Bridge Pattern differs from other structural patterns in specific ways. Unlike the Adapter Pattern, which makes incompatible interfaces work together after design, the Bridge Pattern separates interface and implementation from the start. Unlike the Strategy Pattern, which focuses on algorithmic variation, the Bridge Pattern handles structural decomposition of an entire subsystem.
Design Considerations
The Bridge Pattern applies most effectively when both abstractions and implementations require independent extension. Systems with multiple dimensions of variation benefit significantly from this separation. The pattern prevents class explosion that occurs when using inheritance to combine multiple variations.
When to Apply the Bridge Pattern:
Select the Bridge Pattern when the system exhibits platform-specific implementations. Cross-platform applications often need platform-specific code for graphics rendering, file operations, or network communication. The Bridge Pattern isolates platform-specific code in concrete implementors while keeping the abstraction platform-independent.
Use the pattern when switching implementations at runtime provides value. Applications may need to change rendering engines, storage backends, or processing algorithms based on user preferences, system resources, or runtime conditions. The Bridge Pattern makes this switching transparent to clients.
Apply the pattern when implementations should be shared among multiple objects. Creating expensive implementations (such as database connections or graphics contexts) for every abstraction instance wastes resources. The Bridge Pattern allows multiple abstraction objects to share a single implementation instance.
Choose the pattern when hiding implementation details from clients matters. Compiled libraries, APIs, and frameworks benefit from completely hiding implementation details. Changes to the implementation do not require recompiling client code.
Trade-offs and Considerations:
The Bridge Pattern increases the number of objects in the system. Each abstraction instance requires a corresponding implementor instance. This additional object creation and delegation adds minor runtime overhead. For small systems or performance-critical code with simple variations, simpler alternatives may suffice.
The pattern introduces an additional layer of indirection. Every operation on an abstraction delegates to an implementor. This delegation creates a slight performance cost compared to direct implementation. Profile the application to verify that this cost remains acceptable.
The pattern requires careful interface design. The Implementor interface must remain stable and general enough to support future implementations. Poor interface design leads to frequent changes across all concrete implementors. Design the Implementor interface after understanding the primitive operations needed across all expected implementations.
Alternatives to Consider:
The Strategy Pattern works better for single-dimension algorithmic variation. When only the algorithm changes while the context remains constant, Strategy provides a simpler solution. The Bridge Pattern handles multiple dimensions of variation across both abstraction and implementation.
The Abstract Factory Pattern applies when creating families of related objects. When the application needs to work with variants of multiple related objects, Abstract Factory provides better support. The Bridge Pattern focuses on separating a single abstraction from its implementation.
The State Pattern fits when an object's behavior depends on its state. State transitions drive behavior changes in the State Pattern, while the Bridge Pattern separates structural concerns. State may work within either side of a Bridge.
Decision Framework:
Evaluate the number of variation dimensions. Single-dimension variation suggests simpler patterns. Two or more dimensions of independent variation indicate the Bridge Pattern.
Assess the extension frequency. Systems requiring frequent addition of new abstractions and implementations benefit most from the Bridge Pattern. Stable systems with fixed variations do not justify the added complexity.
Consider the client coupling requirements. When clients must remain decoupled from implementation details, the Bridge Pattern provides strong isolation. When clients can directly depend on implementations, simpler approaches work.
Measure the cost of class explosion. Calculate the number of classes required with inheritance versus the Bridge Pattern. Systems with N abstractions and M implementations require N×M classes with inheritance but only N+M classes with the Bridge Pattern.
# Alternative: Strategy for single-dimension variation
class Report
def initialize(formatter)
@formatter = formatter
end
def generate(data)
@formatter.format(data)
end
end
# Alternative: State for behavior driven by state
class Connection
def initialize
@state = DisconnectedState.new(self)
end
def connect
@state.connect
end
def change_state(state)
@state = state
end
end
Ruby Implementation
Ruby's dynamic nature and duck typing simplify Bridge Pattern implementation. Ruby does not require explicit interface declarations, allowing implementors to conform to the expected interface through method presence rather than formal contracts.
Basic Bridge Structure:
Ruby implementations typically use instance variables to store the implementor reference. The abstraction receives the implementor through initialization or setter methods. Method delegation forwards calls from the abstraction to the implementor.
class Window
def initialize(graphics)
@graphics = graphics
end
def draw_text(text, x, y)
@graphics.render_text(text, x, y)
end
def draw_rectangle(x, y, width, height)
@graphics.render_rectangle(x, y, width, height)
end
end
class IconWindow < Window
def draw_icon(icon, x, y)
icon_data = load_icon_bitmap(icon)
@graphics.render_bitmap(icon_data, x, y)
end
private
def load_icon_bitmap(icon)
# Icon loading logic
icon.bitmap_data
end
end
Implementor Hierarchy:
Concrete implementors in Ruby implement the required methods without inheriting from a formal interface. Ruby's duck typing verifies the interface at runtime. Module extraction creates a shared interface specification when desired.
module GraphicsRenderer
def render_text(text, x, y)
raise NotImplementedError, "Subclass must implement render_text"
end
def render_rectangle(x, y, width, height)
raise NotImplementedError, "Subclass must implement render_rectangle"
end
def render_bitmap(bitmap, x, y)
raise NotImplementedError, "Subclass must implement render_bitmap"
end
end
class OpenGLGraphics
include GraphicsRenderer
def render_text(text, x, y)
# OpenGL text rendering implementation
glRasterPos2f(x, y)
glCallLists(text.length, GL_UNSIGNED_BYTE, text)
end
def render_rectangle(x, y, width, height)
glBegin(GL_QUADS)
glVertex2f(x, y)
glVertex2f(x + width, y)
glVertex2f(x + width, y + height)
glVertex2f(x, y + height)
glEnd()
end
def render_bitmap(bitmap, x, y)
glRasterPos2f(x, y)
glDrawPixels(bitmap.width, bitmap.height, GL_RGB, GL_UNSIGNED_BYTE, bitmap.data)
end
end
class SVGGraphics
include GraphicsRenderer
def initialize
@elements = []
end
def render_text(text, x, y)
@elements << "<text x='#{x}' y='#{y}'>#{text}</text>"
end
def render_rectangle(x, y, width, height)
@elements << "<rect x='#{x}' y='#{y}' width='#{width}' height='#{height}'/>"
end
def render_bitmap(bitmap, x, y)
@elements << "<image x='#{x}' y='#{y}' href='data:image/png;base64,#{bitmap.base64}'/>"
end
def to_svg
"<svg>#{@elements.join}</svg>"
end
end
Runtime Implementation Switching:
Ruby's dynamic typing allows changing the implementor at runtime through simple assignment. This provides flexibility for applications that need to switch implementations based on conditions.
class DataExporter
attr_writer :formatter
def initialize(formatter)
@formatter = formatter
end
def export(data)
@formatter.format(data)
end
def export_with(formatter, data)
original_formatter = @formatter
@formatter = formatter
result = export(data)
@formatter = original_formatter
result
end
end
class JSONFormatter
def format(data)
require 'json'
data.to_json
end
end
class CSVFormatter
def format(data)
require 'csv'
CSV.generate do |csv|
data.each { |row| csv << row }
end
end
end
class XMLFormatter
def format(data)
require 'builder'
xml = Builder::XmlMarkup.new(indent: 2)
xml.data do
data.each_with_index do |item, i|
xml.item(item, index: i)
end
end
end
end
# Usage
exporter = DataExporter.new(JSONFormatter.new)
json_output = exporter.export([{name: "Alice", age: 30}, {name: "Bob", age: 25}])
# Switch to CSV for specific export
csv_output = exporter.export_with(CSVFormatter.new, [["Alice", 30], ["Bob", 25]])
Implementor Factory:
Factory methods or classes select appropriate implementors based on configuration, environment, or runtime conditions. This encapsulates the selection logic and simplifies client code.
class StorageFactory
def self.create_storage(type)
case type
when :file
FileStorage.new
when :database
DatabaseStorage.new
when :memory
MemoryStorage.new
else
raise ArgumentError, "Unknown storage type: #{type}"
end
end
def self.create_default_storage
create_storage(ENV['STORAGE_TYPE']&.to_sym || :file)
end
end
class DocumentRepository
def initialize(storage = nil)
@storage = storage || StorageFactory.create_default_storage
end
def save(document)
@storage.write(document.id, document.to_hash)
end
def find(id)
data = @storage.read(id)
Document.from_hash(data)
end
end
class FileStorage
def initialize(base_path = './storage')
@base_path = base_path
Dir.mkdir(@base_path) unless Dir.exist?(@base_path)
end
def write(key, value)
File.write(File.join(@base_path, "#{key}.json"), value.to_json)
end
def read(key)
JSON.parse(File.read(File.join(@base_path, "#{key}.json")))
end
end
class MemoryStorage
def initialize
@store = {}
end
def write(key, value)
@store[key] = value
end
def read(key)
@store[key]
end
end
Module-Based Implementors:
Ruby modules provide an alternative to class-based implementors. Modules containing implementation logic can be mixed into abstraction classes, creating a bridge through composition rather than inheritance.
module EmailSender
def send_email(to, subject, body)
raise NotImplementedError
end
end
module SMTPEmailSender
include EmailSender
def send_email(to, subject, body)
require 'net/smtp'
message = <<~MESSAGE
From: noreply@example.com
To: #{to}
Subject: #{subject}
#{body}
MESSAGE
Net::SMTP.start('smtp.example.com', 25) do |smtp|
smtp.send_message(message, 'noreply@example.com', to)
end
end
end
module SendGridEmailSender
include EmailSender
def send_email(to, subject, body)
require 'sendgrid-ruby'
# SendGrid API implementation
end
end
class NotificationService
def initialize(email_sender)
@email_sender = email_sender
end
def notify_user(user, message)
@email_sender.send_email(user.email, "Notification", message)
end
end
Practical Examples
Multi-Platform UI Framework:
A user interface framework needs to render controls on multiple platforms (Windows, macOS, Linux) while maintaining consistent abstraction. The Bridge Pattern separates UI control abstractions from platform-specific rendering.
class Button
attr_accessor :text, :x, :y, :width, :height
def initialize(platform_renderer, text)
@platform_renderer = platform_renderer
@text = text
@x = 0
@y = 0
@width = 100
@height = 30
@enabled = true
end
def render
if @enabled
@platform_renderer.draw_button(@text, @x, @y, @width, @height)
else
@platform_renderer.draw_disabled_button(@text, @x, @y, @width, @height)
end
end
def enable
@enabled = true
end
def disable
@enabled = false
end
end
class Checkbox < Button
def initialize(platform_renderer, text)
super(platform_renderer, text)
@checked = false
end
def render
state = @checked ? :checked : :unchecked
@platform_renderer.draw_checkbox(@text, @x, @y, state, @enabled)
end
def toggle
@checked = !@checked
end
end
class WindowsRenderer
def draw_button(text, x, y, width, height)
puts "Windows: Drawing button '#{text}' at (#{x}, #{y}) with Win32 API"
# Win32 API calls here
end
def draw_disabled_button(text, x, y, width, height)
puts "Windows: Drawing grayed-out button '#{text}'"
end
def draw_checkbox(text, x, y, state, enabled)
checkmark = state == :checked ? "[X]" : "[ ]"
puts "Windows: Drawing checkbox #{checkmark} '#{text}' with Win32"
end
end
class MacOSRenderer
def draw_button(text, x, y, width, height)
puts "macOS: Drawing button '#{text}' with Cocoa at (#{x}, #{y})"
# Cocoa API calls here
end
def draw_disabled_button(text, x, y, width, height)
puts "macOS: Drawing inactive button '#{text}'"
end
def draw_checkbox(text, x, y, state, enabled)
checkmark = state == :checked ? "☑" : "☐"
puts "macOS: Drawing checkbox #{checkmark} '#{text}' with NSButton"
end
end
class LinuxRenderer
def draw_button(text, x, y, width, height)
puts "Linux: Drawing button '#{text}' at (#{x}, #{y}) with GTK+"
# GTK+ API calls here
end
def draw_disabled_button(text, x, y, width, height)
puts "Linux: Drawing insensitive button '#{text}'"
end
def draw_checkbox(text, x, y, state, enabled)
checkmark = state == :checked ? "[✓]" : "[ ]"
puts "Linux: Drawing checkbox #{checkmark} '#{text}' with GtkCheckButton"
end
end
# Usage
renderer = MacOSRenderer.new
ok_button = Button.new(renderer, "OK")
cancel_button = Button.new(renderer, "Cancel")
remember_me = Checkbox.new(renderer, "Remember me")
ok_button.render
# => macOS: Drawing button 'OK' with Cocoa at (0, 0)
remember_me.toggle
remember_me.render
# => macOS: Drawing checkbox ☑ 'Remember me' with NSButton
Database Abstraction Layer:
An application needs to support multiple database backends (PostgreSQL, MySQL, SQLite) while providing a consistent query interface. The Bridge Pattern separates query construction from database-specific execution.
class Query
def initialize(database)
@database = database
@conditions = []
@limit_value = nil
@order_by = nil
end
def where(condition, *params)
@conditions << @database.build_condition(condition, params)
self
end
def limit(value)
@limit_value = value
self
end
def order(column, direction = :asc)
@order_by = @database.build_order(column, direction)
self
end
def execute(table)
sql = build_sql(table)
@database.execute(sql)
end
private
def build_sql(table)
parts = ["SELECT * FROM #{table}"]
parts << "WHERE #{@conditions.join(' AND ')}" unless @conditions.empty?
parts << @order_by if @order_by
parts << "LIMIT #{@limit_value}" if @limit_value
parts.join(" ")
end
end
class PostgreSQLDatabase
def connect(config)
# PostgreSQL-specific connection
@connection = PG.connect(config)
end
def build_condition(condition, params)
# PostgreSQL uses $1, $2 for parameters
condition.gsub('?') { |_| "$#{params.index($1) + 1}" }
end
def build_order(column, direction)
"ORDER BY #{column} #{direction.to_s.upcase} NULLS LAST"
end
def execute(sql)
@connection.exec(sql)
end
def last_insert_id
@connection.exec("SELECT lastval()").first['lastval']
end
end
class MySQLDatabase
def connect(config)
@connection = Mysql2::Client.new(config)
end
def build_condition(condition, params)
# MySQL uses ? for parameters
condition
end
def build_order(column, direction)
"ORDER BY #{column} #{direction.to_s.upcase}"
end
def execute(sql)
@connection.query(sql)
end
def last_insert_id
@connection.last_id
end
end
class SQLiteDatabase
def connect(config)
@connection = SQLite3::Database.new(config[:database])
end
def build_condition(condition, params)
condition
end
def build_order(column, direction)
"ORDER BY #{column} #{direction.to_s.upcase}"
end
def execute(sql)
@connection.execute(sql)
end
def last_insert_id
@connection.last_insert_row_id
end
end
# Usage
db = PostgreSQLDatabase.new
db.connect(host: 'localhost', database: 'myapp')
query = Query.new(db)
results = query
.where("status = ?", 'active')
.where("created_at > ?", '2025-01-01')
.order('name', :asc)
.limit(10)
.execute('users')
Message Queue Abstraction:
A distributed system needs to support different message queue implementations (RabbitMQ, AWS SQS, Redis) while maintaining consistent publishing and consumption interfaces.
class MessagePublisher
def initialize(queue_backend)
@queue = queue_backend
end
def publish(topic, message)
payload = serialize_message(message)
@queue.send_message(topic, payload)
end
def publish_batch(topic, messages)
messages.each { |msg| publish(topic, msg) }
end
private
def serialize_message(message)
{
id: SecureRandom.uuid,
timestamp: Time.now.to_i,
body: message
}
end
end
class MessageConsumer
def initialize(queue_backend)
@queue = queue_backend
end
def consume(topic, &block)
@queue.receive_messages(topic) do |raw_message|
message = deserialize_message(raw_message)
block.call(message)
end
end
private
def deserialize_message(raw_message)
# Parse and validate message
raw_message[:body]
end
end
class RabbitMQQueue
def initialize(config)
@connection = Bunny.new(config)
@connection.start
@channel = @connection.create_channel
end
def send_message(topic, payload)
exchange = @channel.topic(topic, durable: true)
exchange.publish(
payload.to_json,
routing_key: topic,
persistent: true
)
end
def receive_messages(topic, &block)
queue = @channel.queue(topic, durable: true)
queue.subscribe(manual_ack: true) do |delivery_info, properties, body|
message = JSON.parse(body, symbolize_names: true)
block.call(message)
@channel.ack(delivery_info.delivery_tag)
end
end
end
class AWSQueue
def initialize(config)
@sqs = Aws::SQS::Client.new(region: config[:region])
@queue_url = config[:queue_url]
end
def send_message(topic, payload)
@sqs.send_message(
queue_url: @queue_url,
message_body: payload.to_json,
message_attributes: {
topic: { string_value: topic, data_type: 'String' }
}
)
end
def receive_messages(topic, &block)
loop do
response = @sqs.receive_message(
queue_url: @queue_url,
max_number_of_messages: 10,
wait_time_seconds: 20
)
response.messages.each do |msg|
message = JSON.parse(msg.body, symbolize_names: true)
block.call(message)
@sqs.delete_message(
queue_url: @queue_url,
receipt_handle: msg.receipt_handle
)
end
end
end
end
class RedisQueue
def initialize(config)
@redis = Redis.new(config)
end
def send_message(topic, payload)
@redis.lpush(topic, payload.to_json)
end
def receive_messages(topic, &block)
loop do
_, message_json = @redis.brpop(topic, timeout: 0)
message = JSON.parse(message_json, symbolize_names: true)
block.call(message)
end
end
end
# Usage
queue_backend = RabbitMQQueue.new(host: 'localhost')
publisher = MessagePublisher.new(queue_backend)
consumer = MessageConsumer.new(queue_backend)
publisher.publish('orders.created', {
order_id: 12345,
customer_id: 67890,
total: 99.99
})
consumer.consume('orders.created') do |message|
puts "Processing order: #{message[:order_id]}"
end
Common Patterns
Shared Implementor Pattern:
Multiple abstraction instances share a single implementor instance when the implementor maintains no per-abstraction state. This reduces memory usage and initialization costs for expensive implementors.
class SharedGraphicsContext
@@instances = {}
def self.get_instance(type)
@@instances[type] ||= create_instance(type)
end
def self.create_instance(type)
case type
when :opengl
OpenGLContext.new
when :directx
DirectXContext.new
end
end
end
class Shape
def initialize(graphics_type)
@graphics = SharedGraphicsContext.get_instance(graphics_type)
end
end
# Multiple shapes share the same graphics context
circle1 = Shape.new(:opengl)
circle2 = Shape.new(:opengl)
# Both share the same OpenGL context instance
Implementor Chain Pattern:
Chain multiple implementors together where each implementor processes the operation and optionally delegates to the next implementor. This creates processing pipelines within the implementation hierarchy.
class Logger
def initialize(output)
@output = output
end
def log(level, message)
formatted = format_message(level, message)
@output.write(formatted)
end
private
def format_message(level, message)
"[#{Time.now}] #{level}: #{message}"
end
end
class FileOutput
def initialize(filepath, next_output = nil)
@file = File.open(filepath, 'a')
@next_output = next_output
end
def write(message)
@file.puts(message)
@file.flush
@next_output&.write(message)
end
end
class ConsoleOutput
def initialize(next_output = nil)
@next_output = next_output
end
def write(message)
puts message
@next_output&.write(message)
end
end
class RemoteOutput
def initialize(endpoint, next_output = nil)
@endpoint = endpoint
@next_output = next_output
end
def write(message)
# Send to remote logging service
HTTP.post(@endpoint, body: message)
@next_output&.write(message)
end
end
# Chain outputs: console -> file -> remote
output = ConsoleOutput.new(
FileOutput.new('app.log',
RemoteOutput.new('https://logs.example.com/ingest')
)
)
logger = Logger.new(output)
logger.log('ERROR', 'Database connection failed')
# Logs to all three outputs
Implementor Factory with Configuration:
Select implementors based on configuration files, environment variables, or runtime detection. This centralizes implementor selection logic and simplifies switching between implementations.
class CacheFactory
def self.create_from_config(config)
case config['type']
when 'redis'
RedisCache.new(
host: config['host'],
port: config['port'],
db: config['database']
)
when 'memcached'
MemcachedCache.new(config['servers'])
when 'memory'
MemoryCache.new(max_size: config['max_size'])
else
raise "Unknown cache type: #{config['type']}"
end
end
def self.create_from_env
config = {
'type' => ENV['CACHE_TYPE'] || 'memory',
'host' => ENV['CACHE_HOST'],
'port' => ENV['CACHE_PORT'],
'database' => ENV['CACHE_DB']
}
create_from_config(config)
end
end
class CacheService
def initialize(cache_backend = nil)
@cache = cache_backend || CacheFactory.create_from_env
end
def get(key)
@cache.read(key)
end
def set(key, value, ttl: 3600)
@cache.write(key, value, ttl)
end
end
Lazy Implementor Initialization:
Delay implementor creation until first use. This improves startup time and avoids initializing expensive resources that may never be used.
class ReportGenerator
def initialize(renderer_type)
@renderer_type = renderer_type
@renderer = nil
end
def generate(data)
renderer.render(data)
end
private
def renderer
@renderer ||= create_renderer
end
def create_renderer
case @renderer_type
when :pdf
PDFRenderer.new
when :html
HTMLRenderer.new
when :excel
ExcelRenderer.new
end
end
end
# Renderer only created when generate is called
report = ReportGenerator.new(:pdf)
# No PDF renderer created yet
result = report.generate(data)
# PDF renderer created on first use
Anti-Pattern: Breaking the Bridge:
Abstraction classes that directly access implementor internal state break encapsulation and create tight coupling. This defeats the purpose of the Bridge Pattern.
# ANTI-PATTERN: Don't do this
class Document
def initialize(storage)
@storage = storage
end
def save_fast
# Directly accessing storage internals
if @storage.instance_of?(DatabaseStorage)
@storage.connection.execute("INSERT /*+ APPEND */ ...")
end
end
end
# CORRECT: Define proper interface
class Document
def initialize(storage)
@storage = storage
end
def save(mode: :normal)
@storage.write(self.data, mode: mode)
end
end
class DatabaseStorage
def write(data, mode: :normal)
query = mode == :fast ? fast_insert_query(data) : normal_insert_query(data)
@connection.execute(query)
end
end
Reference
Pattern Components
| Component | Responsibility | Implementation |
|---|---|---|
| Abstraction | Defines high-level interface for clients | Class with implementor reference |
| Refined Abstraction | Extends abstraction with specialized behavior | Subclass of abstraction |
| Implementor | Defines interface for implementation classes | Module or class (duck typed) |
| Concrete Implementor | Provides specific implementation | Class implementing implementor interface |
When to Use Bridge Pattern
| Scenario | Reason |
|---|---|
| Multiple platform support | Isolates platform-specific code |
| Runtime implementation switching | Allows dynamic behavior changes |
| Avoiding class explosion | Reduces N×M classes to N+M classes |
| Hiding implementation details | Completely decouples interface from implementation |
| Sharing implementations | Multiple abstractions use same implementor |
| Independent hierarchy evolution | Abstractions and implementations extend separately |
Bridge vs Other Patterns
| Pattern | Primary Purpose | Key Difference |
|---|---|---|
| Adapter | Make incompatible interfaces work together | Applied after design, not from start |
| Strategy | Vary algorithm independently | Single dimension of variation |
| State | Behavior changes based on state | State transitions drive behavior |
| Abstract Factory | Create families of related objects | Creates multiple related objects |
| Decorator | Add responsibilities dynamically | Wraps objects with additional behavior |
Implementation Checklist
| Step | Action | Verification |
|---|---|---|
| 1 | Identify orthogonal dimensions | Can abstractions and implementations vary independently? |
| 2 | Define implementor interface | Does it provide primitive operations? |
| 3 | Create concrete implementors | Do they implement full interface? |
| 4 | Design abstraction interface | Does it use implementor through delegation? |
| 5 | Create refined abstractions | Do they add behavior without changing implementor? |
| 6 | Test with different combinations | Can any abstraction work with any implementor? |
Common Implementor Operations
| Operation Type | Example Methods | Purpose |
|---|---|---|
| Data Access | read, write, query, update | Storage operations |
| Rendering | draw, render, paint, display | Visual output |
| Communication | send, receive, connect, disconnect | Network operations |
| Processing | process, transform, convert, encode | Data manipulation |
| Resource Management | allocate, release, acquire, dispose | System resources |
Ruby-Specific Considerations
| Aspect | Implementation | Example |
|---|---|---|
| Interface Definition | Duck typing, optional modules | Objects respond to required methods |
| Implementor Storage | Instance variables | @implementor = storage_backend |
| Dynamic Switching | attr_writer or setter methods | exporter.formatter = JSONFormatter.new |
| Factory Selection | Class methods | StorageFactory.create_storage(:redis) |
| Lazy Initialization | Memoization pattern | @renderer |
| Module-Based Implementation | Include or extend modules | include EmailSender |
Performance Impact
| Aspect | Impact | Mitigation |
|---|---|---|
| Delegation overhead | Slight increase in method call time | Acceptable for most applications |
| Additional objects | More memory for implementor instances | Share implementors when stateless |
| Initialization cost | May initialize unused implementors | Use lazy initialization |
| Runtime switching | Minimal cost to reassign references | Switch only when necessary |
Testing Strategy
| Test Type | Focus | Approach |
|---|---|---|
| Unit tests | Individual implementors | Test each concrete implementor independently |
| Integration tests | Abstraction-implementor interaction | Test abstractions with each implementor |
| Contract tests | Interface compliance | Verify all implementors satisfy interface |
| Combination tests | All abstraction-implementor pairs | Ensure any combination works correctly |
| Switch tests | Runtime implementation changes | Verify switching does not break behavior |