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 |