CrackedRuby CrackedRuby

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