CrackedRuby CrackedRuby

Structural Patterns Overview

Overview

Structural patterns address how classes and objects compose to form larger structures. These patterns focus on simplifying relationships between entities, making systems easier to understand and maintain. Unlike creational patterns that deal with object instantiation or behavioral patterns that handle communication, structural patterns emphasize composition and interface design.

The seven canonical structural patterns emerged from the Gang of Four design patterns work: Adapter, Bridge, Composite, Decorator, Facade, Flyweight, and Proxy. Each pattern solves specific composition problems while maintaining loose coupling between components.

Structural patterns operate at two levels: class-based patterns use inheritance to compose interfaces and implementations, while object-based patterns describe ways to assemble objects to realize new functionality. Class patterns establish relationships at compile time through inheritance, while object patterns create relationships at runtime through composition.

These patterns prove essential when building systems that must adapt to changing requirements. A Facade simplifies complex subsystem interfaces. An Adapter makes incompatible interfaces work together. A Decorator adds responsibilities to objects dynamically. Understanding when and how to apply each pattern determines the flexibility and maintainability of software architecture.

Key Principles

Structural patterns share fundamental principles that distinguish them from other pattern categories. The primary principle centers on managing relationships between entities to form larger structures without creating tight coupling.

Composition Over Inheritance

Structural patterns favor object composition over class inheritance. Composition provides greater flexibility by allowing behavior changes at runtime. Where inheritance creates static relationships at compile time, composition creates dynamic relationships through object references.

# Inheritance approach - rigid
class TextView
  def display
    "Displaying text"
  end
end

class BorderedTextView < TextView
  def display
    "Border: #{super}"
  end
end

# Composition approach - flexible
class TextView
  def display
    "Displaying text"
  end
end

class BorderDecorator
  def initialize(component)
    @component = component
  end
  
  def display
    "Border: #{@component.display}"
  end
end

view = TextView.new
bordered = BorderDecorator.new(view)

Interface Segregation

Structural patterns emphasize separating interfaces from implementations. Clients depend on abstractions rather than concrete classes, enabling substitution without modifying client code. This principle manifests differently across patterns but remains central to structural design.

The Bridge pattern exemplifies this by decoupling abstraction from implementation so both can vary independently. The Adapter pattern applies interface segregation by creating a new interface that clients expect while wrapping an existing incompatible interface.

Transparency and Uniformity

Several structural patterns maintain transparency in how clients interact with individual objects versus compositions of objects. The Composite pattern treats individual objects and compositions uniformly through a common interface. Decorators maintain the same interface as the components they wrap, allowing decorators to stack transparently.

# Composite maintains uniform interface
class File
  def initialize(name, size)
    @name = name
    @size = size
  end
  
  def size
    @size
  end
end

class Directory
  def initialize(name)
    @name = name
    @children = []
  end
  
  def add(component)
    @children << component
  end
  
  def size
    @children.sum(&:size)
  end
end

# Both File and Directory respond to size
file = File.new("doc.txt", 100)
dir = Directory.new("folder")
dir.add(file)

puts file.size    # => 100
puts dir.size     # => 100

Lazy Loading and Resource Optimization

The Flyweight and Proxy patterns address resource optimization through sharing and deferred operations. Flyweight shares common state across multiple objects to reduce memory consumption. Proxy controls access to objects, enabling lazy initialization, access control, or remote access.

Single Responsibility Through Delegation

Structural patterns distribute responsibilities across objects through delegation. Rather than creating monolithic classes that handle multiple concerns, these patterns delegate specific responsibilities to dedicated objects. The Facade delegates complex subsystem operations through a simplified interface. The Decorator delegates to wrapped components while adding new behavior.

Common Patterns

The seven structural patterns each solve distinct composition problems. Understanding the specific problem each addresses determines proper pattern selection.

Adapter Pattern

Adapter converts one interface into another interface that clients expect. This pattern enables classes with incompatible interfaces to work together. Two variants exist: class adapters use multiple inheritance, while object adapters use composition.

# Target interface expected by client
class MediaPlayer
  def play(filename)
    raise NotImplementedError
  end
end

# Adaptee with incompatible interface
class VLCPlayer
  def play_vlc(filename)
    puts "Playing VLC: #{filename}"
  end
end

# Adapter makes VLCPlayer compatible with MediaPlayer
class MediaAdapter < MediaPlayer
  def initialize(player_type)
    @advanced_player = case player_type
    when :vlc
      VLCPlayer.new
    end
  end
  
  def play(filename)
    @advanced_player.play_vlc(filename)
  end
end

adapter = MediaAdapter.new(:vlc)
adapter.play("movie.vlc")  # => Playing VLC: movie.vlc

Bridge Pattern

Bridge decouples abstraction from implementation, allowing both to vary independently. This pattern splits a class into two hierarchies: abstraction and implementation. The abstraction maintains a reference to the implementation and delegates operations to it.

# Implementation hierarchy
class DrawingAPI
  def draw_circle(x, y, radius)
    raise NotImplementedError
  end
end

class DrawingAPI1 < DrawingAPI
  def draw_circle(x, y, radius)
    "API1 circle at (#{x},#{y}) radius #{radius}"
  end
end

class DrawingAPI2 < DrawingAPI
  def draw_circle(x, y, radius)
    "API2 circle at (#{x},#{y}) radius #{radius}"
  end
end

# Abstraction hierarchy
class Shape
  def initialize(drawing_api)
    @drawing_api = drawing_api
  end
end

class Circle < Shape
  def initialize(x, y, radius, drawing_api)
    super(drawing_api)
    @x = x
    @y = y
    @radius = radius
  end
  
  def draw
    @drawing_api.draw_circle(@x, @y, @radius)
  end
end

circle1 = Circle.new(1, 2, 3, DrawingAPI1.new)
circle2 = Circle.new(5, 7, 11, DrawingAPI2.new)

Composite Pattern

Composite composes objects into tree structures to represent part-whole hierarchies. Clients treat individual objects and compositions uniformly through a common interface. This pattern works when representing hierarchical structures where operations apply recursively.

class Component
  def operation
    raise NotImplementedError
  end
  
  def add(component)
    raise NotImplementedError
  end
  
  def remove(component)
    raise NotImplementedError
  end
end

class Leaf < Component
  def initialize(name)
    @name = name
  end
  
  def operation
    @name
  end
end

class Composite < Component
  def initialize(name)
    @name = name
    @children = []
  end
  
  def operation
    results = [@name]
    @children.each do |child|
      results << child.operation
    end
    results.join(" > ")
  end
  
  def add(component)
    @children << component
  end
  
  def remove(component)
    @children.delete(component)
  end
end

root = Composite.new("root")
branch1 = Composite.new("branch1")
branch2 = Composite.new("branch2")
leaf1 = Leaf.new("leaf1")
leaf2 = Leaf.new("leaf2")

root.add(branch1)
root.add(branch2)
branch1.add(leaf1)
branch2.add(leaf2)

puts root.operation  # => root > branch1 > leaf1 > branch2 > leaf2

Decorator Pattern

Decorator attaches additional responsibilities to objects dynamically. Decorators provide a flexible alternative to subclassing for extending functionality. Each decorator wraps a component and adds behavior before or after delegating to the wrapped component.

class Coffee
  def cost
    2.0
  end
  
  def description
    "Coffee"
  end
end

class CoffeeDecorator
  def initialize(coffee)
    @coffee = coffee
  end
  
  def cost
    @coffee.cost
  end
  
  def description
    @coffee.description
  end
end

class Milk < CoffeeDecorator
  def cost
    @coffee.cost + 0.5
  end
  
  def description
    "#{@coffee.description}, Milk"
  end
end

class Sugar < CoffeeDecorator
  def cost
    @coffee.cost + 0.2
  end
  
  def description
    "#{@coffee.description}, Sugar"
  end
end

coffee = Coffee.new
coffee_with_milk = Milk.new(coffee)
coffee_with_milk_and_sugar = Sugar.new(coffee_with_milk)

puts coffee_with_milk_and_sugar.description  # => Coffee, Milk, Sugar
puts coffee_with_milk_and_sugar.cost         # => 2.7

Facade Pattern

Facade provides a unified interface to a set of interfaces in a subsystem. This pattern defines a higher-level interface that makes the subsystem easier to use by hiding its complexity. Facades reduce dependencies between clients and subsystem implementations.

class CPU
  def freeze
    puts "CPU frozen"
  end
  
  def execute
    puts "CPU executing"
  end
end

class Memory
  def load(position, data)
    puts "Memory loaded at #{position} with #{data}"
  end
end

class HardDrive
  def read(sector, size)
    "Data from sector #{sector}"
  end
end

class ComputerFacade
  def initialize
    @cpu = CPU.new
    @memory = Memory.new
    @hard_drive = HardDrive.new
  end
  
  def start
    @cpu.freeze
    data = @hard_drive.read(0, 1024)
    @memory.load(0, data)
    @cpu.execute
  end
end

computer = ComputerFacade.new
computer.start
# Output:
# CPU frozen
# Data from sector 0
# Memory loaded at 0 with Data from sector 0
# CPU executing

Flyweight Pattern

Flyweight uses sharing to support large numbers of fine-grained objects efficiently. This pattern separates intrinsic state (shared) from extrinsic state (context-specific). The pattern maintains a pool of shared objects and requires clients to pass extrinsic state during operations.

class TreeType
  attr_reader :name, :color, :texture
  
  def initialize(name, color, texture)
    @name = name
    @color = color
    @texture = texture
  end
  
  def draw(x, y)
    puts "Drawing #{@name} tree at (#{x},#{y}) - #{@color}, #{@texture}"
  end
end

class TreeFactory
  def initialize
    @tree_types = {}
  end
  
  def get_tree_type(name, color, texture)
    key = "#{name}_#{color}_#{texture}"
    @tree_types[key] ||= TreeType.new(name, color, texture)
  end
  
  def total_tree_types
    @tree_types.size
  end
end

class Tree
  def initialize(x, y, tree_type)
    @x = x
    @y = y
    @tree_type = tree_type
  end
  
  def draw
    @tree_type.draw(@x, @y)
  end
end

factory = TreeFactory.new
forest = []

# Create 1000 trees with only 3 TreeType objects
1000.times do |i|
  type = case i % 3
  when 0
    factory.get_tree_type("Oak", "Green", "Rough")
  when 1
    factory.get_tree_type("Pine", "Dark Green", "Smooth")
  else
    factory.get_tree_type("Birch", "White", "Peeling")
  end
  forest << Tree.new(rand(100), rand(100), type)
end

puts "Total unique tree types: #{factory.total_tree_types}"  # => 3

Proxy Pattern

Proxy provides a surrogate or placeholder for another object to control access to it. Several proxy variants exist: virtual proxies delay expensive object creation, protection proxies control access rights, and remote proxies represent objects in different address spaces.

class RealImage
  def initialize(filename)
    @filename = filename
    load_from_disk
  end
  
  def display
    puts "Displaying #{@filename}"
  end
  
  private
  
  def load_from_disk
    puts "Loading #{@filename} from disk..."
    sleep(1)  # Simulate expensive operation
  end
end

class ImageProxy
  def initialize(filename)
    @filename = filename
    @real_image = nil
  end
  
  def display
    @real_image ||= RealImage.new(@filename)
    @real_image.display
  end
end

# Client code
image1 = ImageProxy.new("photo1.jpg")
image2 = ImageProxy.new("photo2.jpg")

# Images not loaded yet
puts "Images created"

# Load and display only when needed
image1.display  # Triggers loading
image1.display  # Uses cached image

Ruby Implementation

Ruby's dynamic nature and metaprogramming capabilities influence how structural patterns manifest in idiomatic Ruby code. The language features enable pattern implementations that differ from static language approaches.

Duck Typing and Adapter

Ruby's duck typing reduces the need for explicit adapters in many cases. When an object responds to required methods, no adapter may be necessary. However, adapters remain useful when integrating third-party libraries or maintaining explicit architectural boundaries.

# Without adapter - duck typing sufficient
class XMLProcessor
  def process_xml(data)
    "XML: #{data}"
  end
end

class JSONProcessor
  def process_json(data)
    "JSON: #{data}"
  end
end

# With adapter - explicit interface contract
class DataProcessor
  def process(data)
    raise NotImplementedError
  end
end

class JSONAdapter < DataProcessor
  def initialize(json_processor)
    @processor = json_processor
  end
  
  def process(data)
    @processor.process_json(data)
  end
end

# Client expects process method
def handle_data(processor, data)
  processor.process(data)
end

Modules and Decorator

Ruby modules enable decorator-like behavior through module inclusion and extension. SimpleDelegator from the standard library provides decorator infrastructure.

require 'delegate'

class Text
  attr_reader :content
  
  def initialize(content)
    @content = content
  end
  
  def render
    @content
  end
end

class BoldDecorator < SimpleDelegator
  def render
    "**#{__getobj__.render}**"
  end
end

class ItalicDecorator < SimpleDelegator
  def render
    "*#{__getobj__.render}*"
  end
end

text = Text.new("Hello")
bold = BoldDecorator.new(text)
italic_bold = ItalicDecorator.new(bold)

puts italic_bold.render  # => ***Hello***

Refinements and Adapter

Refinements provide lexically scoped modifications to classes, creating adapter-like behavior without global modifications.

module StringAdapterRefinement
  refine String do
    def to_data_format
      {type: "string", value: self, length: length}
    end
  end
  
  refine Integer do
    def to_data_format
      {type: "integer", value: self}
    end
  end
end

class DataProcessor
  using StringAdapterRefinement
  
  def process(input)
    input.to_data_format
  end
end

processor = DataProcessor.new
puts processor.process("test")  # => {:type=>"string", :value=>"test", :length=>4}
puts processor.process(42)      # => {:type=>"integer", :value=>42}

# Outside refinement scope, methods unavailable
# "test".to_data_format  # NoMethodError

Singleton Class and Proxy

Ruby's singleton class enables per-object behavior modification, supporting proxy implementations.

class Document
  attr_accessor :content
  
  def initialize(content)
    @content = content
  end
  
  def display
    puts @content
  end
end

def create_logging_proxy(obj)
  obj.singleton_class.class_eval do
    alias_method :original_display, :display
    
    define_method :display do
      puts "[LOG] Display called"
      original_display
      puts "[LOG] Display complete"
    end
  end
  obj
end

doc = Document.new("Important document")
proxy = create_logging_proxy(doc)
proxy.display
# Output:
# [LOG] Display called
# Important document
# [LOG] Display complete

Forwardable and Delegation

The Forwardable module from the standard library simplifies delegation patterns common in structural implementations.

require 'forwardable'

class Protection
  def initialize
    @allowed_methods = [:read, :write]
  end
  
  def allowed?(method)
    @allowed_methods.include?(method)
  end
end

class Resource
  def read
    "Reading resource"
  end
  
  def write(data)
    "Writing: #{data}"
  end
  
  def delete
    "Deleting resource"
  end
end

class ProtectedResourceProxy
  extend Forwardable
  
  def_delegators :@resource, :read, :write
  
  def initialize(resource, protection)
    @resource = resource
    @protection = protection
  end
  
  def delete
    if @protection.allowed?(:delete)
      @resource.delete
    else
      "Access denied"
    end
  end
end

resource = Resource.new
protection = Protection.new
proxy = ProtectedResourceProxy.new(resource, protection)

puts proxy.read           # => Reading resource
puts proxy.delete         # => Access denied

Method Missing and Proxy

The method_missing hook enables transparent proxy implementations that forward unknown methods to wrapped objects.

class LazyProxy
  def initialize(&initializer)
    @initializer = initializer
    @real_object = nil
  end
  
  def method_missing(method, *args, &block)
    ensure_initialized
    @real_object.send(method, *args, &block)
  end
  
  def respond_to_missing?(method, include_private = false)
    ensure_initialized
    @real_object.respond_to?(method, include_private)
  end
  
  private
  
  def ensure_initialized
    @real_object ||= @initializer.call
  end
end

# Expensive object creation deferred
proxy = LazyProxy.new do
  puts "Creating expensive object..."
  Array.new(1000000) { rand }
end

puts "Proxy created"
puts proxy.size  # Triggers initialization
puts proxy.size  # Uses existing object

Design Considerations

Selecting appropriate structural patterns requires analyzing the specific composition problem and evaluating trade-offs between different approaches.

Pattern Selection Criteria

Choose Adapter when integrating existing classes with incompatible interfaces. The pattern proves most valuable when working with third-party libraries or legacy code that cannot be modified. Adapter adds a level of indirection but preserves existing code.

Select Bridge when both abstractions and implementations need independent variation. If a system requires multiple implementations across multiple abstractions, Bridge prevents a proliferation of subclasses. The pattern increases complexity through additional indirection but provides flexibility for future extensions.

Apply Composite when representing hierarchical structures where clients should treat individual objects and compositions uniformly. File systems, organizational structures, and UI component trees benefit from Composite. The pattern simplifies client code but can make designs overly general if type safety matters.

Use Decorator when adding responsibilities to individual objects dynamically without affecting other objects. Decorators provide more flexibility than static inheritance but can result in many small objects and complexity when unwrapping decorators.

Implement Facade when simplifying complex subsystems or providing a unified interface to disparate systems. Facades reduce compilation dependencies and hide implementation details. However, facades can become god objects if they accumulate too much responsibility.

Choose Flyweight when applications use large numbers of objects and storage costs are significant. The pattern works when objects share substantial intrinsic state. Flyweight increases complexity and requires careful management of extrinsic state.

Select Proxy when controlling access to objects, whether for lazy initialization, access control, remote access, or monitoring. Proxies add indirection but provide control over object access and lifecycle.

Combining Patterns

Structural patterns often combine with each other or with patterns from other categories. A Composite might use Decorator to add responsibilities to individual components. A Facade might use Adapter to integrate legacy subsystems. A Proxy might use Flyweight to share proxy instances.

# Composite with Decorator
class ComponentDecorator
  def initialize(component)
    @component = component
  end
  
  def operation
    @component.operation
  end
end

class LoggingDecorator < ComponentDecorator
  def operation
    puts "Logging operation"
    result = super
    puts "Operation complete"
    result
  end
end

composite = Composite.new("root")
leaf = Leaf.new("item")
composite.add(leaf)

# Decorate the composite
decorated = LoggingDecorator.new(composite)
decorated.operation

Performance Trade-offs

Structural patterns introduce varying performance costs. Adapter and Facade add minimal overhead—typically a single method call. Bridge adds one level of indirection per operation. Composite incurs recursive traversal costs proportional to tree depth. Decorator overhead accumulates with each decorator layer. Flyweight trades computation time for memory savings. Proxy costs depend on the proxy type—virtual proxies add initialization overhead, while remote proxies add network latency.

Testing Implications

Testing structural patterns requires different strategies based on pattern characteristics. Adapters need tests verifying correct interface translation. Composites require recursive operation testing. Decorators need verification that each decorator adds expected behavior without breaking the component interface. Facades benefit from integration tests covering subsystem interactions. Flyweight tests must verify correct state separation. Proxies require testing both proxy behavior and eventual delegation to real objects.

# Testing a decorator chain
require 'minitest/autorun'

class DecoratorTest < Minitest::Test
  def test_decorator_chain
    base = Text.new("test")
    decorated = ItalicDecorator.new(BoldDecorator.new(base))
    
    assert_equal "***test***", decorated.render
  end
  
  def test_decorator_independence
    base = Text.new("test")
    bold = BoldDecorator.new(base)
    italic = ItalicDecorator.new(base)
    
    assert_equal "**test**", bold.render
    assert_equal "*test*", italic.render
  end
end

Practical Examples

Real-world applications demonstrate how structural patterns solve concrete design problems in production systems.

Web Framework Middleware Stack

Web frameworks implement middleware as a Decorator chain, where each middleware adds behavior to request processing.

class Application
  def call(request)
    {status: 200, body: "Response for #{request[:path]}"}
  end
end

class Middleware
  def initialize(app)
    @app = app
  end
  
  def call(request)
    @app.call(request)
  end
end

class LoggingMiddleware < Middleware
  def call(request)
    puts "[#{Time.now}] #{request[:method]} #{request[:path]}"
    response = super
    puts "[#{Time.now}] Status: #{response[:status]}"
    response
  end
end

class AuthenticationMiddleware < Middleware
  def call(request)
    if request[:headers]['Authorization']
      super
    else
      {status: 401, body: "Unauthorized"}
    end
  end
end

class CachingMiddleware < Middleware
  def initialize(app)
    super
    @cache = {}
  end
  
  def call(request)
    cache_key = "#{request[:method]}:#{request[:path]}"
    @cache[cache_key] ||= super
  end
end

# Build middleware stack
app = Application.new
app = CachingMiddleware.new(app)
app = AuthenticationMiddleware.new(app)
app = LoggingMiddleware.new(app)

# Process requests
request1 = {method: "GET", path: "/users", headers: {'Authorization' => 'token'}}
request2 = {method: "GET", path: "/users", headers: {'Authorization' => 'token'}}
request3 = {method: "GET", path: "/posts", headers: {}}

response1 = app.call(request1)
response2 = app.call(request2)  # Cached
response3 = app.call(request3)  # Unauthorized

Database Connection Pool with Proxy

Connection pools use Proxy to manage expensive database connections, providing lazy initialization and resource control.

class DatabaseConnection
  attr_reader :id
  
  def initialize(id)
    @id = id
    puts "Opening database connection #{id}"
    sleep(0.1)  # Simulate connection overhead
  end
  
  def query(sql)
    "Result of: #{sql}"
  end
  
  def close
    puts "Closing connection #{@id}"
  end
end

class ConnectionProxy
  def initialize(pool, connection)
    @pool = pool
    @connection = connection
  end
  
  def query(sql)
    @connection.query(sql)
  end
  
  def close
    @pool.release(self)
  end
  
  def real_connection
    @connection
  end
end

class ConnectionPool
  def initialize(size)
    @size = size
    @connections = []
    @available = []
    @in_use = []
  end
  
  def acquire
    connection = @available.pop || create_connection
    @in_use << connection
    ConnectionProxy.new(self, connection)
  end
  
  def release(proxy)
    connection = proxy.real_connection
    @in_use.delete(connection)
    @available << connection
  end
  
  def shutdown
    (@available + @in_use).each(&:close)
  end
  
  private
  
  def create_connection
    raise "Pool exhausted" if @connections.size >= @size
    connection = DatabaseConnection.new(@connections.size + 1)
    @connections << connection
    connection
  end
end

pool = ConnectionPool.new(3)

# Acquire and use connections
conn1 = pool.acquire
puts conn1.query("SELECT * FROM users")
conn1.close  # Returns to pool

conn2 = pool.acquire
conn3 = pool.acquire
puts conn2.query("SELECT * FROM posts")
conn2.close
conn3.close

pool.shutdown

File System with Composite

File systems naturally model as Composite structures, where directories contain files and other directories.

class FileSystemEntry
  attr_reader :name
  
  def initialize(name)
    @name = name
  end
  
  def size
    raise NotImplementedError
  end
  
  def display(indent = 0)
    raise NotImplementedError
  end
end

class File < FileSystemEntry
  def initialize(name, size)
    super(name)
    @file_size = size
  end
  
  def size
    @file_size
  end
  
  def display(indent = 0)
    puts "#{' ' * indent}#{@name} (#{@file_size} bytes)"
  end
end

class Directory < FileSystemEntry
  def initialize(name)
    super(name)
    @entries = []
  end
  
  def add(entry)
    @entries << entry
  end
  
  def remove(entry)
    @entries.delete(entry)
  end
  
  def size
    @entries.sum(&:size)
  end
  
  def display(indent = 0)
    puts "#{' ' * indent}#{@name}/"
    @entries.each { |entry| entry.display(indent + 2) }
  end
  
  def find(name)
    return self if @name == name
    
    @entries.each do |entry|
      if entry.is_a?(Directory)
        result = entry.find(name)
        return result if result
      elsif entry.name == name
        return entry
      end
    end
    
    nil
  end
end

# Build file system structure
root = Directory.new("root")
home = Directory.new("home")
user = Directory.new("user")
documents = Directory.new("documents")

root.add(home)
home.add(user)
user.add(documents)

documents.add(File.new("resume.pdf", 50000))
documents.add(File.new("cover_letter.docx", 30000))
user.add(File.new(".bashrc", 1024))

root.display
puts "\nTotal size: #{root.size} bytes"

found = root.find("resume.pdf")
puts "\nFound: #{found.name}" if found

Graphics Rendering with Bridge

Graphics systems use Bridge to separate rendering abstraction from platform-specific implementation.

# Implementation side
class Renderer
  def render_circle(x, y, radius)
    raise NotImplementedError
  end
  
  def render_rectangle(x, y, width, height)
    raise NotImplementedError
  end
end

class VectorRenderer < Renderer
  def render_circle(x, y, radius)
    puts "Vector: Drawing circle at (#{x},#{y}) radius #{radius}"
  end
  
  def render_rectangle(x, y, width, height)
    puts "Vector: Drawing rectangle at (#{x},#{y}) size #{width}x#{height}"
  end
end

class RasterRenderer < Renderer
  def render_circle(x, y, radius)
    puts "Raster: Drawing pixels for circle at (#{x},#{y}) radius #{radius}"
  end
  
  def render_rectangle(x, y, width, height)
    puts "Raster: Drawing pixels for rectangle at (#{x},#{y}) size #{width}x#{height}"
  end
end

# Abstraction side
class Shape
  def initialize(renderer)
    @renderer = renderer
  end
  
  def draw
    raise NotImplementedError
  end
  
  def resize(factor)
    raise NotImplementedError
  end
end

class Circle < Shape
  def initialize(x, y, radius, renderer)
    super(renderer)
    @x = x
    @y = y
    @radius = radius
  end
  
  def draw
    @renderer.render_circle(@x, @y, @radius)
  end
  
  def resize(factor)
    @radius *= factor
  end
end

class Rectangle < Shape
  def initialize(x, y, width, height, renderer)
    super(renderer)
    @x = x
    @y = y
    @width = width
    @height = height
  end
  
  def draw
    @renderer.render_rectangle(@x, @y, @width, @height)
  end
  
  def resize(factor)
    @width *= factor
    @height *= factor
  end
end

# Use different renderers with same shapes
vector = VectorRenderer.new
raster = RasterRenderer.new

circle1 = Circle.new(10, 10, 5, vector)
circle2 = Circle.new(20, 20, 8, raster)
rect = Rectangle.new(5, 5, 10, 15, vector)

circle1.draw
circle2.draw
rect.draw

circle1.resize(2)
circle1.draw

Reference

Pattern Comparison Matrix

Pattern Primary Intent Structure Type Key Benefit Main Trade-off
Adapter Interface conversion Object or Class Enables incompatible interfaces to work together Additional indirection layer
Bridge Decouple abstraction from implementation Object Independent variation of abstraction and implementation Increased initial complexity
Composite Compose objects into tree structures Object Uniform treatment of individual and composite objects May over-generalize design
Decorator Add responsibilities dynamically Object More flexible than static inheritance Can create many small objects
Facade Provide unified interface Object Simplifies complex subsystems Can become a god object
Flyweight Share objects to reduce memory Object Reduces memory consumption significantly Adds complexity managing extrinsic state
Proxy Control access to objects Object Controls object lifecycle and access Adds method call overhead

Pattern Selection Guide

Requirement Recommended Pattern Alternative Pattern
Integrate legacy code Adapter Facade
Build hierarchical structures Composite None
Add behavior at runtime Decorator Strategy
Simplify complex interfaces Facade Mediator
Defer expensive operations Proxy Lazy initialization
Support multiple implementations Bridge Strategy
Reduce memory usage Flyweight Object pooling
Access control Proxy Decorator

Ruby Implementation Approaches

Pattern Idiomatic Ruby Technique Standard Library Support
Adapter Duck typing, Refinements None specific
Bridge Composition with modules None specific
Composite Enumerable inclusion None specific
Decorator SimpleDelegator, Forwardable delegate library
Facade Class with delegation Forwardable module
Flyweight Hash-based pooling None specific
Proxy method_missing, SimpleDelegator delegate library

Common Implementation Pitfalls

Pattern Common Mistake Solution
Adapter Creating adapters unnecessarily Use duck typing when possible
Bridge Confusing with Adapter Bridge separates hierarchies, Adapter matches interfaces
Composite Breaking uniform interface Ensure all nodes implement same interface
Decorator Infinite decoration loops Validate decorator chains
Facade Too much responsibility Keep focused on subsystem coordination
Flyweight Sharing mutable state Separate intrinsic from extrinsic state
Proxy Forgetting to delegate all methods Use method_missing or Forwardable

Pattern Relationships

Pattern Pair Relationship Combined Use Case
Adapter + Facade Complementary Facade uses Adapters for subsystems
Bridge + Abstract Factory Complementary Factory creates platform-specific implementations
Composite + Decorator Combinable Decorate individual components in tree
Composite + Iterator Complementary Traverse composite structures
Decorator + Strategy Alternative Both add behavior, Strategy changes algorithm
Flyweight + Singleton Related Flyweight factory often Singleton
Proxy + Decorator Similar Proxy controls access, Decorator adds behavior

Ruby Module Support

Module Purpose Pattern Support
Forwardable Method delegation Decorator, Facade, Proxy
SimpleDelegator Object wrapping Decorator, Proxy
DelegateClass Class delegation Decorator
Singleton Single instance Flyweight factory
Observable Observer pattern Proxy notification

Performance Characteristics

Pattern Time Complexity Impact Space Complexity Impact Scalability Impact
Adapter O(1) per call Minimal None
Bridge O(1) per call One additional object None
Composite O(n) for traversal O(n) for tree Linear with nodes
Decorator O(d) where d is decorator depth O(d) for decorator chain Linear with decorators
Facade O(1) per facade call Minimal None
Flyweight O(1) access, O(n) creation Reduces from O(n) to O(unique) Significant for large n
Proxy O(1) plus real object cost One additional object Depends on proxy type