CrackedRuby CrackedRuby

Overview

The Adapter pattern addresses interface incompatibility between classes or systems. When existing code cannot directly communicate with new components due to mismatched interfaces, an adapter class acts as a translator between them. This pattern appeared in the Gang of Four Design Patterns book (1994) as one of seven structural patterns.

The pattern solves a common software development challenge: integrating components that were designed independently without knowledge of each other. Rather than modifying existing code—which may be unavailable, risky, or violate the Open/Closed Principle—an adapter wraps one interface to make it compatible with another.

Two primary forms exist: class adapters use multiple inheritance to adapt one interface to another, while object adapters use composition to wrap the adaptee. Ruby supports only single inheritance, so Ruby developers implement adapters through composition and delegation.

The pattern appears frequently when:

  • Integrating third-party libraries with different interfaces than the application expects
  • Maintaining legacy code while migrating to new APIs
  • Creating reusable components that work with various backend implementations
  • Building abstraction layers between business logic and external services
# Without adapter: direct coupling to incompatible interface
class LegacyPrinter
  def print_text(content)
    # Legacy printing implementation
  end
end

# Application expects different interface
class DocumentProcessor
  def process(printer)
    printer.output("Document content")  # Expects output method
  end
end

# This fails - LegacyPrinter doesn't have output method
processor = DocumentProcessor.new
processor.process(LegacyPrinter.new)  # NoMethodError

Key Principles

The Adapter pattern consists of four primary components that interact to bridge interface gaps:

Target Interface: The interface that clients expect. This defines the domain-specific interface the client code uses. In Ruby, this may be an explicit interface through duck typing or a module defining expected behavior.

Client: The code that interacts with objects through the target interface. The client remains unaware of the adapter's existence and treats the adapted object as if it natively implements the target interface.

Adaptee: The existing class with an incompatible interface that requires adaptation. The adaptee contains the functionality needed by the client but presents it through different method names, parameters, or return types.

Adapter: The class that implements the target interface and wraps the adaptee. The adapter receives calls through the target interface and translates them into calls the adaptee understands, handling any necessary data transformations.

The pattern operates through delegation: when a client calls a method on the adapter, the adapter performs any necessary preprocessing, calls the appropriate adaptee method, performs any necessary postprocessing, and returns the result in the format the client expects.

# Target interface (implicit through duck typing)
# Expected interface: log(level, message)

# Adaptee with incompatible interface
class ThirdPartyLogger
  def write_log(severity, text, timestamp)
    puts "[#{timestamp}] #{severity}: #{text}"
  end
end

# Adapter translating between interfaces
class LoggerAdapter
  def initialize(logger)
    @logger = logger
  end
  
  def log(level, message)
    @logger.write_log(level, message, Time.now)
  end
end

# Client uses expected interface
class Application
  def initialize(logger)
    @logger = logger
  end
  
  def run
    @logger.log('INFO', 'Application started')
  end
end

# Adapter makes incompatible logger work with client
adapted_logger = LoggerAdapter.new(ThirdPartyLogger.new)
app = Application.new(adapted_logger)
app.run
# => [2025-01-15 10:30:00] INFO: Application started

The pattern follows the Single Responsibility Principle by separating interface conversion from business logic. Each class maintains one reason to change: clients change when business requirements change, adapters change when interface mismatches change, and adaptees change when underlying implementation changes.

Adapters support the Open/Closed Principle by allowing systems to integrate new components without modifying existing code. Adding a new adaptee requires creating a new adapter rather than changing client code or existing adapters.

The pattern creates a dependency structure where clients depend on the target interface abstraction rather than concrete adaptee implementations. This reduces coupling and increases flexibility, as different adaptees can be substituted by providing appropriate adapters.

Ruby Implementation

Ruby implements the Adapter pattern through composition and delegation, taking advantage of Ruby's flexible object model and duck typing. Unlike statically-typed languages requiring explicit interface declarations, Ruby adapters work through method signature compatibility.

The standard implementation wraps the adaptee in an adapter class that delegates method calls:

class PaymentGatewayAdapter
  def initialize(gateway)
    @gateway = gateway
  end
  
  def charge(amount, currency, card_token)
    # Convert to format expected by gateway
    result = @gateway.process_payment(
      amount_cents: (amount * 100).to_i,
      currency_code: currency.upcase,
      payment_method: card_token
    )
    
    # Convert result to format expected by client
    {
      success: result[:status] == 'approved',
      transaction_id: result[:reference_number],
      message: result[:response_message]
    }
  end
end

# Adaptee with different interface
class StripeGateway
  def process_payment(amount_cents:, currency_code:, payment_method:)
    # Stripe API call simulation
    {
      status: 'approved',
      reference_number: 'ch_3xyz',
      response_message: 'Payment successful'
    }
  end
end

adapter = PaymentGatewayAdapter.new(StripeGateway.new)
result = adapter.charge(29.99, 'usd', 'tok_visa')
# => {:success=>true, :transaction_id=>"ch_3xyz", :message=>"Payment successful"}

Ruby's method_missing enables dynamic adapters that handle arbitrary method calls:

class DynamicAdapter
  def initialize(adaptee, method_map)
    @adaptee = adaptee
    @method_map = method_map
  end
  
  def method_missing(method, *args, &block)
    if @method_map.key?(method)
      target_method = @method_map[method]
      @adaptee.send(target_method, *args, &block)
    else
      super
    end
  end
  
  def respond_to_missing?(method, include_private = false)
    @method_map.key?(method) || super
  end
end

class LegacyDatabase
  def select_rows(table, conditions)
    "Selecting from #{table} where #{conditions}"
  end
  
  def insert_row(table, data)
    "Inserting into #{table}: #{data}"
  end
end

# Map new interface to legacy methods
adapter = DynamicAdapter.new(
  LegacyDatabase.new,
  find: :select_rows,
  create: :insert_row
)

adapter.find('users', 'active = true')
# => "Selecting from users where active = true"
adapter.create('users', {name: 'Alice'})
# => "Inserting into users: {:name=>\"Alice\"}"

SimpleDelegator from Ruby's standard library provides built-in adapter functionality:

require 'delegate'

class MetricCollector < SimpleDelegator
  def initialize(logger)
    super(logger)
    @call_count = Hash.new(0)
  end
  
  def log(level, message)
    @call_count[level] += 1
    __getobj__.log(level, message)
  end
  
  def stats
    @call_count
  end
end

# Wrap any logger with metric collection
collector = MetricCollector.new(StandardLogger.new)
collector.log('ERROR', 'Connection failed')
collector.log('ERROR', 'Timeout occurred')
collector.log('INFO', 'Request completed')
collector.stats
# => {"ERROR"=>2, "INFO"=>1}

Module mixins create adapters through interface extension:

module RESTClientAdapter
  def get(path)
    http_get(build_url(path))
  end
  
  def post(path, body)
    http_post(build_url(path), serialize(body))
  end
  
  private
  
  def build_url(path)
    "#{base_url}#{path}"
  end
  
  def serialize(data)
    JSON.generate(data)
  end
end

class LegacyHTTPClient
  def http_get(url)
    "GET #{url}"
  end
  
  def http_post(url, data)
    "POST #{url} with #{data}"
  end
  
  def base_url
    'https://api.example.com'
  end
end

client = LegacyHTTPClient.new
client.extend(RESTClientAdapter)
client.get('/users/1')
# => "GET https://api.example.com/users/1"
client.post('/users', {name: 'Bob'})
# => "POST https://api.example.com/users with {\"name\":\"Bob\"}"

Practical Examples

Database Connection Adapter: Applications often need to support multiple database backends with unified interfaces. Database adapters translate generic operations into vendor-specific SQL or API calls.

class PostgreSQLAdapter
  def initialize(connection)
    @connection = connection
  end
  
  def find_all(table, conditions = {})
    where_clause = build_where_clause(conditions)
    query = "SELECT * FROM #{table}#{where_clause}"
    @connection.exec(query).to_a
  end
  
  def insert(table, attributes)
    columns = attributes.keys.join(', ')
    placeholders = attributes.keys.each_with_index.map { |_, i| "$#{i + 1}" }.join(', ')
    query = "INSERT INTO #{table} (#{columns}) VALUES (#{placeholders}) RETURNING id"
    @connection.exec_params(query, attributes.values).first['id']
  end
  
  def update(table, id, attributes)
    set_clause = attributes.keys.each_with_index.map { |k, i| "#{k} = $#{i + 2}" }.join(', ')
    query = "UPDATE #{table} SET #{set_clause} WHERE id = $1"
    @connection.exec_params(query, [id] + attributes.values)
  end
  
  private
  
  def build_where_clause(conditions)
    return '' if conditions.empty?
    clauses = conditions.map { |k, v| "#{k} = '#{v}'" }.join(' AND ')
    " WHERE #{clauses}"
  end
end

class MongoDBAdapter
  def initialize(collection)
    @collection = collection
  end
  
  def find_all(table, conditions = {})
    @collection.find(conditions).to_a
  end
  
  def insert(table, attributes)
    result = @collection.insert_one(attributes)
    result.inserted_id.to_s
  end
  
  def update(table, id, attributes)
    @collection.update_one(
      { _id: BSON::ObjectId(id) },
      { '$set' => attributes }
    )
  end
end

# Application uses unified interface
class UserRepository
  def initialize(adapter)
    @adapter = adapter
  end
  
  def all
    @adapter.find_all('users')
  end
  
  def create(attributes)
    @adapter.insert('users', attributes)
  end
  
  def update(id, attributes)
    @adapter.update('users', id, attributes)
  end
end

# Switch databases by changing adapter
postgres_repo = UserRepository.new(PostgreSQLAdapter.new(pg_connection))
mongo_repo = UserRepository.new(MongoDBAdapter.new(mongo_collection))

Message Format Adapter: Systems integrating with external APIs often encounter different message formats (JSON, XML, Protocol Buffers). Format adapters normalize these differences.

class JSONMessageAdapter
  def initialize(client)
    @client = client
  end
  
  def send_message(recipient, content, priority)
    payload = {
      to: recipient,
      body: content,
      priority: priority,
      timestamp: Time.now.iso8601
    }
    @client.post('/messages', JSON.generate(payload))
  end
  
  def receive_messages(limit = 10)
    response = @client.get("/messages?limit=#{limit}")
    messages = JSON.parse(response)
    messages.map do |msg|
      {
        from: msg['sender'],
        content: msg['body'],
        received_at: Time.parse(msg['timestamp'])
      }
    end
  end
end

class XMLMessageAdapter
  def initialize(client)
    @client = client
  end
  
  def send_message(recipient, content, priority)
    xml = <<~XML
      <message>
        <recipient>#{recipient}</recipient>
        <text>#{content}</text>
        <priority>#{priority}</priority>
        <sent>#{Time.now.iso8601}</sent>
      </message>
    XML
    @client.send(xml)
  end
  
  def receive_messages(limit = 10)
    response = @client.receive(limit)
    doc = Nokogiri::XML(response)
    doc.xpath('//message').map do |node|
      {
        from: node.at_xpath('sender').text,
        content: node.at_xpath('text').text,
        received_at: Time.parse(node.at_xpath('sent').text)
      }
    end
  end
end

# Messaging service uses unified interface
class MessagingService
  def initialize(adapter)
    @adapter = adapter
  end
  
  def broadcast(recipients, message)
    recipients.each do |recipient|
      @adapter.send_message(recipient, message, 'normal')
    end
  end
  
  def fetch_new_messages
    @adapter.receive_messages(50)
  end
end

Legacy System Integration: When modernizing applications, adapters bridge old and new architectures without requiring complete rewrites.

# Legacy system with procedural interface
class LegacyOrderSystem
  def self.create_order(customer_id, items_list, shipping_addr)
    order_id = generate_order_id
    items_list.split(',').each do |item|
      add_item_to_order(order_id, item.strip)
    end
    set_shipping_address(order_id, shipping_addr)
    order_id
  end
  
  def self.get_order_status(order_id)
    # Returns numeric status codes
    fetch_status_code(order_id)
  end
  
  private_class_method def self.generate_order_id
    "ORD-#{rand(10000)}"
  end
  
  private_class_method def self.add_item_to_order(order_id, item)
    # Legacy database insert
  end
  
  private_class_method def self.set_shipping_address(order_id, address)
    # Legacy database update
  end
  
  private_class_method def self.fetch_status_code(order_id)
    1  # 0=pending, 1=processing, 2=shipped, 3=delivered
  end
end

# Modern adapter providing object-oriented interface
class OrderAdapter
  STATUS_CODES = {
    0 => :pending,
    1 => :processing,
    2 => :shipped,
    3 => :delivered
  }.freeze
  
  def create(customer_id, items, shipping_address)
    items_string = items.map { |item| item[:sku] }.join(', ')
    address_string = format_address(shipping_address)
    
    order_id = LegacyOrderSystem.create_order(
      customer_id,
      items_string,
      address_string
    )
    
    Order.new(order_id, self)
  end
  
  def status(order_id)
    code = LegacyOrderSystem.get_order_status(order_id)
    STATUS_CODES[code] || :unknown
  end
  
  private
  
  def format_address(address)
    "#{address[:street]}, #{address[:city]}, #{address[:zip]}"
  end
end

class Order
  attr_reader :id
  
  def initialize(id, adapter)
    @id = id
    @adapter = adapter
  end
  
  def status
    @adapter.status(@id)
  end
end

# Modern application code
adapter = OrderAdapter.new
order = adapter.create(
  12345,
  [{sku: 'ITEM-001'}, {sku: 'ITEM-002'}],
  {street: '123 Main St', city: 'Boston', zip: '02101'}
)
order.status  # => :processing

Design Considerations

The Adapter pattern applies when interface incompatibility prevents direct integration. Choosing between creating an adapter and modifying existing code requires evaluating several factors.

When to Use Adapters: The pattern becomes appropriate when the adaptee code cannot be modified. This includes third-party libraries, legacy systems, or code maintained by other teams. Adapters also make sense when multiple incompatible implementations need to work with the same client code, such as supporting multiple payment processors or multiple cloud storage providers.

The pattern provides value when interface incompatibility is the only integration obstacle. If the adaptee lacks necessary functionality entirely, an adapter cannot add it—that requires a decorator or wrapper with additional implementation.

When to Avoid Adapters: Creating adapters for code under direct control adds unnecessary indirection. If modifying the original class is straightforward and safe, direct changes provide clearer code than adaptation layers.

Adapters prove unnecessary when classes already share compatible interfaces through duck typing. Ruby's dynamic nature often eliminates the need for formal adaptation when methods happen to match.

The pattern introduces overhead unsuitable for performance-critical paths. Each adapter adds method dispatch latency and memory allocation. Critical inner loops should avoid adapter indirection when possible.

Trade-offs Analysis: Adapters increase flexibility at the cost of indirection. Adding an abstraction layer simplifies client code but requires jumping through additional method calls to reach actual functionality. This trade-off favors maintainability over raw performance.

The pattern reduces coupling between clients and adaptees but creates coupling to the adapter itself. Clients depend on the adapter's interface, so adapter interface changes ripple through client code. Stable adapter interfaces minimize this coupling.

Adapters concentrate interface translation logic in one location, supporting the Single Responsibility Principle. However, this concentration creates a central point of change—adapters must update whenever either the client's expected interface or the adaptee's provided interface changes.

Comparison with Alternative Patterns: The Facade pattern also wraps complex subsystems but serves a different purpose. Facades simplify interfaces by providing a higher-level abstraction, while adapters match existing interfaces without simplification.

The Decorator pattern wraps objects to add behavior, not to change interfaces. Decorators and adapters both use composition and delegation, but decorators preserve the original interface while adding functionality.

The Proxy pattern controls access to objects without changing interfaces. Proxies add concerns like lazy loading, access control, or caching while maintaining interface compatibility. Adapters explicitly change interfaces to resolve incompatibility.

Bridge pattern separates abstraction from implementation, while Adapter makes incompatible interfaces work together. Bridges are designed upfront for flexibility; adapters are added retroactively to solve compatibility problems.

Interface Design Considerations: Adapter interfaces should match client expectations precisely, not adaptee capabilities. This means the adapter interface derives from client requirements, with the adapter implementation handling whatever transformation the adaptee needs.

Adapter interfaces benefit from being narrow and focused. Wide interfaces with many methods create large adapter classes harder to implement and maintain. When adapting interfaces with many methods, consider creating multiple small adapters rather than one large adapter.

The adapter interface should not leak adaptee implementation details. Clients should remain unaware they are working with an adapter, treating adapted objects identically to objects natively implementing the target interface.

Common Patterns

Object Adapter: The standard implementation uses composition to wrap the adaptee. This approach provides maximum flexibility since Ruby supports only single inheritance.

class StandardAdapter
  def initialize(adaptee)
    @adaptee = adaptee
  end
  
  def target_method(arg)
    # Transform input
    adapted_arg = transform_input(arg)
    
    # Call adaptee
    result = @adaptee.adaptee_method(adapted_arg)
    
    # Transform output
    transform_output(result)
  end
  
  private
  
  def transform_input(arg)
    # Input transformation logic
  end
  
  def transform_output(result)
    # Output transformation logic
  end
end

Two-Way Adapter: Implements both interfaces to work as an adapter in either direction, allowing the same object to be used in contexts expecting either interface.

class TwoWayAdapter
  def initialize(object_a, object_b)
    @object_a = object_a
    @object_b = object_b
  end
  
  # Interface A methods delegating to B
  def method_a1(arg)
    @object_b.method_b1(transform_a_to_b(arg))
  end
  
  def method_a2
    transform_b_to_a(@object_b.method_b2)
  end
  
  # Interface B methods delegating to A
  def method_b1(arg)
    @object_a.method_a1(transform_b_to_a(arg))
  end
  
  def method_b2
    transform_a_to_b(@object_a.method_a2)
  end
  
  private
  
  def transform_a_to_b(value)
    # Transform from A format to B format
  end
  
  def transform_b_to_a(value)
    # Transform from B format to A format
  end
end

Pluggable Adapter: Uses dependency injection to make adapters interchangeable, supporting multiple backend implementations through a unified interface.

class NotificationService
  def initialize(adapter)
    @adapter = adapter
  end
  
  def notify(user, message)
    @adapter.send(user.contact_info, message)
  end
end

class EmailAdapter
  def send(email_address, message)
    # Send via email
    "Email sent to #{email_address}: #{message}"
  end
end

class SMSAdapter
  def send(phone_number, message)
    # Send via SMS
    "SMS sent to #{phone_number}: #{message}"
  end
end

class PushAdapter
  def send(device_token, message)
    # Send push notification
    "Push sent to #{device_token}: #{message}"
  end
end

# Configure service with appropriate adapter
email_service = NotificationService.new(EmailAdapter.new)
sms_service = NotificationService.new(SMSAdapter.new)
push_service = NotificationService.new(PushAdapter.new)

Adapter Registry: Maintains a collection of adapters mapped to specific adaptee types, enabling runtime adapter selection based on the object type.

class AdapterRegistry
  def initialize
    @adapters = {}
  end
  
  def register(type, adapter_class)
    @adapters[type] = adapter_class
  end
  
  def adapt(object)
    type = object.class.name.to_sym
    adapter_class = @adapters[type]
    raise "No adapter registered for #{type}" unless adapter_class
    adapter_class.new(object)
  end
end

# Register adapters for different types
registry = AdapterRegistry.new
registry.register(:LegacyUser, ModernUserAdapter)
registry.register(:LegacyOrder, ModernOrderAdapter)
registry.register(:LegacyProduct, ModernProductAdapter)

# Automatically adapt objects based on type
legacy_objects.map { |obj| registry.adapt(obj) }

Transparent Adapter: Uses method_missing to dynamically forward method calls, adapting only methods that need translation while passing through others unchanged.

class TransparentAdapter
  def initialize(adaptee)
    @adaptee = adaptee
  end
  
  # Only override methods requiring adaptation
  def new_method_name(*args)
    @adaptee.old_method_name(*args)
  end
  
  # Pass through all other methods
  def method_missing(method, *args, &block)
    @adaptee.send(method, *args, &block)
  end
  
  def respond_to_missing?(method, include_private = false)
    @adaptee.respond_to?(method, include_private) || super
  end
end

Composite Adapter: Combines multiple adapters to handle complex interface translations requiring multiple transformation steps.

class CompositeAdapter
  def initialize(primary_adapter, *additional_adapters)
    @adapters = [primary_adapter] + additional_adapters
  end
  
  def process(data)
    @adapters.reduce(data) do |result, adapter|
      adapter.transform(result)
    end
  end
end

# Chain multiple transformations
adapter = CompositeAdapter.new(
  JSONToHashAdapter.new,
  HashNormalizerAdapter.new,
  HashToObjectAdapter.new
)

Common Pitfalls

Excessive Adapter Chains: Creating adapters that wrap other adapters creates deep delegation chains that obscure program flow and degrade performance. Each additional adapter layer adds method dispatch overhead and makes debugging more difficult.

# Problematic: adapter wrapping adapter wrapping adapter
first_adapter = BasicAdapter.new(legacy_system)
second_adapter = EnhancedAdapter.new(first_adapter)
third_adapter = FinalAdapter.new(second_adapter)

# Better: single adapter handling all transformations
unified_adapter = UnifiedAdapter.new(legacy_system)

Long adapter chains indicate design problems. The solution involves consolidating transformation logic into fewer, more comprehensive adapters or reconsidering the interface boundaries.

Leaking Adaptee Details: Adapters that expose adaptee implementation details through their interface break encapsulation. Clients become coupled to the adaptee despite the adapter's presence.

# Problematic: adapter exposing adaptee type
class BadAdapter
  def initialize(adaptee)
    @adaptee = adaptee
  end
  
  def process(data)
    # Returns adaptee object directly
    @adaptee.internal_process(data)
  end
  
  def adaptee
    @adaptee  # Exposing internal state
  end
end

# Better: adapter completely hiding adaptee
class GoodAdapter
  def initialize(adaptee)
    @adaptee = adaptee
  end
  
  def process(data)
    result = @adaptee.internal_process(data)
    # Transform to expected format
    transform_result(result)
  end
  
  private
  
  def transform_result(result)
    # Convert adaptee result to target format
  end
end

Incomplete Interface Implementation: Adapters implementing only subset of the expected interface cause runtime errors when clients call unimplemented methods.

# Problematic: partial implementation
class PartialAdapter
  def initialize(adaptee)
    @adaptee = adaptee
  end
  
  def method_one
    @adaptee.adapted_method_one
  end
  
  # Missing method_two, method_three expected by clients
end

# Client code fails when calling missing methods
adapter = PartialAdapter.new(legacy_system)
adapter.method_one  # Works
adapter.method_two  # NoMethodError

# Better: complete implementation or explicit unsupported methods
class CompleteAdapter
  def initialize(adaptee)
    @adaptee = adaptee
  end
  
  def method_one
    @adaptee.adapted_method_one
  end
  
  def method_two
    @adaptee.adapted_method_two
  end
  
  def method_three
    raise NotImplementedError, "method_three not supported by this adapter"
  end
end

Stateful Adapters: Maintaining state in adapters creates unexpected behavior when adapters are reused or shared. Adapters should be stateless translators, with state management belonging to either clients or adaptees.

# Problematic: stateful adapter
class StatefulAdapter
  def initialize(adaptee)
    @adaptee = adaptee
    @cache = {}
  end
  
  def fetch(key)
    @cache[key] ||= @adaptee.get(key)  # Caching in adapter
  end
end

# Cache state causes stale data
adapter = StatefulAdapter.new(data_source)
adapter.fetch('user:1')  # Fetches from source
data_source.update('user:1', new_data)
adapter.fetch('user:1')  # Returns stale cached data

# Better: stateless adapter
class StatelessAdapter
  def initialize(adaptee)
    @adaptee = adaptee
  end
  
  def fetch(key)
    @adaptee.get(key)  # No caching, no state
  end
end

Bidirectional Coupling: Creating adapters that depend on both the adaptee and the target interface couples the adapter to both sides, making it fragile to changes in either.

# Problematic: adapter coupled to specific target class
class CoupledAdapter
  def initialize(adaptee, target_class)
    @adaptee = adaptee
    @target_class = target_class  # Tight coupling
  end
  
  def convert
    @target_class.new(@adaptee.data)
  end
end

# Better: adapter coupled only to interfaces
class DecoupledAdapter
  def initialize(adaptee)
    @adaptee = adaptee
  end
  
  def data
    # Returns data in expected format
    {
      id: @adaptee.identifier,
      name: @adaptee.title
    }
  end
end

Overcomplicating Simple Adaptations: Adding adapter layers for trivial interface differences unnecessarily complicates code. Simple method aliases or wrapper methods often suffice.

# Overengineered: full adapter for simple rename
class OverkillAdapter
  def initialize(object)
    @object = object
  end
  
  def process(data)
    @object.execute(data)
  end
end

# Simpler: direct aliasing
class SimpleWrapper
  def initialize(object)
    @object = object
  end
  
  alias process execute
end

# Simplest: use object directly and call execute

Reference

Adapter Pattern Components

Component Responsibility Implementation Notes
Target Defines interface clients expect Implicit in Ruby through duck typing or explicit via module
Adaptee Existing class with incompatible interface Wrapped by adapter, remains unchanged
Adapter Translates target interface to adaptee interface Uses composition and delegation
Client Uses objects through target interface Unaware of adapter, treats adapted objects normally

Implementation Approaches

Approach Mechanism Use Case
Object Adapter Composition and delegation Standard approach in Ruby
Method Missing Dynamic method forwarding Adapting many methods with similar patterns
SimpleDelegator Ruby standard library delegation Quick adapters with minimal custom logic
Module Extension Mixin-based interface addition Adding interface to existing objects

When to Use Adapter Pattern

Scenario Why Adapter Applies Alternative Consideration
Third-party library integration Cannot modify external code Direct wrapper if no interface mismatch
Legacy system integration Preserve existing code during migration Refactoring if code is maintainable
Multiple implementations Support various backends with unified interface Strategy pattern if selecting algorithms
Interface evolution Support old and new interfaces simultaneously Version migration if clients can upgrade

Common Adapter Types by Domain

Domain Adapter Purpose Typical Adaptees
Database Normalize query interfaces across vendors MySQL, PostgreSQL, MongoDB, SQLite clients
API Integration Unify different REST/SOAP clients HTTParty, RestClient, Faraday, Net::HTTP
Logging Standardize logging across frameworks Ruby Logger, Log4r, Syslog, third-party services
Authentication Common interface for various auth mechanisms OAuth providers, LDAP, SAML, API keys
File Storage Abstract cloud storage providers AWS S3, Google Cloud Storage, Azure Blob, local filesystem
Messaging Unify message queue interfaces RabbitMQ, Kafka, Redis, SQS clients

Design Decision Checklist

Question Yes → Adapter No → Alternative
Can adaptee code be modified? Consider modifying adaptee Use adapter
Does adaptee have needed functionality? Use adapter Need decorator or complete implementation
Is interface difference significant? Use adapter Simple wrapper or alias may suffice
Are multiple incompatible implementations needed? Use adapter Single implementation sufficient
Is performance critical? Consider direct integration Use adapter
Will interface changes be frequent? Evaluate cost of maintenance Use adapter

Adapter Implementation Template

class TargetAdapter
  def initialize(adaptee)
    @adaptee = adaptee
  end
  
  def target_method(params)
    # 1. Transform input parameters
    adapted_params = transform_input(params)
    
    # 2. Call adaptee method
    result = @adaptee.adaptee_method(adapted_params)
    
    # 3. Transform output
    transform_output(result)
  end
  
  private
  
  def transform_input(params)
    # Convert target format to adaptee format
  end
  
  def transform_output(result)
    # Convert adaptee format to target format
  end
end

Common Method Mappings

Target Interface Adaptee Method Transformation Required
find(id) get_by_id(id) None, direct delegation
save(object) persist(data_hash) Convert object to hash
delete(id) remove(id) None, direct delegation
all list_all Convert array format
create(attributes) insert(record) Validate and transform attributes
update(id, changes) modify(id, delta) Calculate delta from changes

Testing Adapter Implementations

Test Type Purpose Example
Interface Compliance Verify adapter implements expected methods Check all target interface methods present
Delegation Confirm calls reach adaptee Mock adaptee and verify method calls
Transformation Validate input/output conversion Assert transformed data matches expected format
Error Handling Ensure adaptee errors properly propagated Test exception translation and propagation
Edge Cases Handle nil, empty, invalid inputs Test boundary conditions