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 |