VOOZH about

URL: https://www.sitepoint.com/using-and-testing-the-adapter-design-pattern/

⇱ Improve Your Ruby with the Adapter Design Pattern — SitePoint


This metrics tool terrifies bad developers

Start free trial

This metrics tool terrifies bad developers

Start free trial
SitePoint Premium
Stay Relevant and Grow Your Career in Tech
  • Premium Results
  • Publish articles on SitePoint
  • Daily curated jobs
  • Learning Paths
  • Discounts to dev tools
Start Free Trial

7 Day Free Trial. Cancel Anytime.

Key Takeaways

  • The Adapter Design Pattern in Ruby allows for greater flexibility and easier addition of new methods by separating implementations into modules, rather than using conditional branching.
  • The Adapter Design Pattern can be seen in action in gems such as multi_json, ActiveRecord, and moneta, which parse JSON, interact with databases, and provide a unified interface to key-value stores, respectively.
  • The Adapter Design Pattern can also be used to create a gem that allows the user to choose an adapter for a rudimentary CSV parser, as demonstrated in the tutorial section of the text.
  • The Adapter Design Pattern promotes code reusability and flexibility, allows for interaction with a uniform interface regardless of the underlying class or interface, and enables classes to work together that couldn’t otherwise due to incompatible interfaces.

👁 Image

Imagine we have some code where we want to accomplish things in a variety of ways. One way to do this is with conditional branching:

class Animal
 def speak(kind)
 puts case kind
 when :dog then "woof!"
 when :cat then "meow!"
 when :owl then "hoo!"
 end
 end
end
Animal.new.speak(:dog)

This works, but what if a developer wants to add a new way? With conditional branching, the entire method would need to be overwritten. Instead, we can separate the implementations into modules:

class Animal
 module Adapter
 module Dog
 def self.speak
 puts "woof!"
 end
 end
 module Cat
 def self.speak
 puts "meow!"
 end
 end
 end
 def speak
 self.adapter.speak
 end
 def adapter
 return @adapter if @adapter
 self.adapter = :dog
 @adapter
 end
 def adapter=(adapter)
 @adapter = Animal::Adapter.const_get(adapter.to_s.capitalize)
 end
end
animal = Animal.new
animal.speak
animal.adapter = :cat
aanimal.speak

This is a lot more code! However, if we want to add another module, it’s not too bad and a lot more flexible:

class Animal
 module Adapter
 module Owl
 def self.speak
 puts "hoo!"
 end
 end
 end
end
animal.adapter = :owl
animal.speak

This new module could even go in a separate gem – and with its own dependencies! Organizing things this way is called the adapter design pattern. Let’s look at a few examples of this pattern in the wild.

multi_json

A good example is the multi_json gem which parses JSON with the fastest available backend. In multi_json, each backend is contained in an class that descends from Adapter. Here’s multi_json/lib/multi_json/adapters/gson.rb.

require 'gson'
require 'stringio'
require 'multi_json/adapter'
module MultiJson
 module Adapters
 # Use the gson.rb library to dump/load.
 class Gson < Adapter
 ParseError = ::Gson::DecodeError
 def load(string, options = {})
 ::Gson::Decoder.new(options).decode(string)
 end
 def dump(object, options = {})
 ::Gson::Encoder.new(options).encode(object)
 end
 end
 end
end

Here, load executes each library’s method for turning a JSON string into an object, and dump executes the method for turning an object into a string.

ActiveRecord

ActiveRecord is Rails’ ORM library for interacting with relational databases. It relies on the adapter pattern to allow the developer to interact with any supported database using the same methods. We can find this pattern in ActiveRecord‘s connection_adapters.

module ActiveRecord
 module ConnectionAdapters # :nodoc:
 extend ActiveSupport::Autoload
 autoload :Column
 autoload :ConnectionSpecification
 autoload_at 'active_record/connection_adapters/abstract/schema_definitions' do
 autoload :IndexDefinition
 autoload :ColumnDefinition
 autoload :ChangeColumnDefinition
 autoload :ForeignKeyDefinition
 autoload :TableDefinition
 autoload :Table
 autoload :AlterTable
 autoload :ReferenceDefinition
 end
 autoload_at 'active_record/connection_adapters/abstract/connection_pool' do
 autoload :ConnectionHandler
 autoload :ConnectionManagement
 end
 autoload_under 'abstract' do
 autoload :SchemaStatements
 autoload :DatabaseStatements
 autoload :DatabaseLimits
 autoload :Quoting
 autoload :ConnectionPool
 autoload :QueryCache
 autoload :Savepoints
 end
 ...
 class AbstractAdapter
 ADAPTER_NAME = 'Abstract'.freeze
 include Quoting, DatabaseStatements, SchemaStatements
 include DatabaseLimits
 include QueryCache
 include ActiveSupport::Callbacks
 include ColumnDumper
 SIMPLE_INT = /\A\d+\z/
 define_callbacks :checkout, :checkin
 attr_accessor :visitor, :pool
 attr_reader :schema_cache, :owner, :logger
 alias :in_use? :owner
 ...
 attr_reader :prepared_statements
 def initialize(connection, logger = nil, config = {}) # :nodoc:
 super()
 @connection = connection
 @owner = nil
 @instrumenter = ActiveSupport::Notifications.instrumenter
 @logger = logger
 @config = config
 @pool = nil
 @schema_cache = SchemaCache.new self
 @visitor = nil
 @prepared_statements = false
 end
 ...

ActiveRecord includes many adapters, including MySQL and PostgreSQL here. Look through a couple of those to see great examples of this pattern.

Moneta

One of my favorite gems is moneta which is a unified interface to key-value stores, such as Redis. Here is an example of using a file as a key-value store:

require 'moneta'
# Create a simple file store
store = Moneta.new(:File, dir: 'moneta')
# Store some entries
store['key'] = 'value'
# Read entry
store.key?('key') # returns true
store['key'] # returns 'value'
store.close

From the user’s perspective, accessing both redis and daybreak is as simple as reading or modifying a hash. Here’s what the daybreak adapter looks like (comments removed to save space):

require 'daybreak'
module Moneta
 module Adapters
 class Daybreak < Memory
 def initialize(options = {})
 @backend = options[:backend] ||
 begin
 raise ArgumentError, 'Option :file is required' unless
options[:file]
 ::Daybreak::DB.new(options[:file], serializer:
::Daybreak::Serializer::None)
 end
 end
 def load(key, options = {})
 @backend.load if options[:sync]
 @backend[key]
 end
 def store(key, value, options = {})
 @backend[key] = value
 @backend.flush if options[:sync]
 value
 end
 def increment(key, amount = 1, options = {})
 @backend.lock { super }
 end
 def create(key, value, options = {})
 @backend.lock { super }
 end
 def close
 @backend.close
 end
 end
 end
end

Creating an Adapter Gem

Let’s make a gem that allows the user to choose an adapter for a rudimentary CSV parser. Here’s what our folder structure will look like:

├── Gemfile
├── Rakefile
├── lib
│   ├── table_parser
│   │   └── adapters
│   │   ├── scan.rb
│   │   └── split.rb
│   └── table_parser.rb
└── test
 ├── helper.rb
 ├── scan_adapter_test.rb
 ├── split_adapter_test.rb
 └── table_parser_test.rb

Dependencies

Add minitest and ruby "2.3.0" to the Gemfile:

# Gemfile
source "https://rubygems.org"
ruby "2.3.0"
gem "minitest", "5.8.3"

Ruby 2.3 adds the new squiggly heredoc syntax which will be useful in this case as it prevents unnecessary leading whitespace. Adding it to the Gemfile will not install it. It will need to be installed separately with a command like (if you use RVM):

$ rvm install 2.3.0

Test Support

Add a Rakefile that lets use rake to run all of our tests:

# Rakefile
require "rake/testtask"
Rake::TestTask.new do |t|
 t.pattern = "test/*_test.rb"
 t.warning = true
 t.libs << 'test'
end
task default: :test

t.libs << 'test' adds the test folder to our $LOAD_PATH when running the task. The lib folder is included by default.

The Main Module

lib/table_parser.rb will implement what the user accesses when they use the gem:

# lib/table_parser.rb
module TableParser
 extend self
 def parse(text)
 self.adapter.parse(text)
 end
 def adapter
 return @adapter if @adapter
 self.adapter = :split
 @adapter
 end
 def adapter=(adapter)
 require "table_parser/adapters/#{adapter}"
 @adapter = TableParser::Adapter.const_get(adapter.to_s.capitalize)
 end
end

::adapter sets a default adapter the first time it is called. Notice that adapters are not loaded until they are set. This avoids exposing developers to bugs in unused adapters and allows adapters in the same project to use their own dependencies without requiring all dependencies for all adapters up front.

The Adapters

The first adapter parses using the scan method with an appropriate regular expression. The regex delimiter searches for either anything that is not a comma or two consecutive commas:

# lib/table_parser/adapters/scan.rb
module TableParser
 module Adapter
 module Scan
 extend self
 def parse(text)
 delimiter = /[^,]+|,,/
 lines = text.split(/\n/)
 keys = lines.shift.scan(delimiter).map { |key| key.strip }
 rows = lines.map do |line|
 row = {}
 fields = line.scan(delimiter)
 keys.each do |key|
 row[key] = fields.shift.strip
 row[key] = "" if row[key] == ",,"
 end
 row
 end
 return rows
 end
 end
 end
end

The second adapter parses using the split method with another regular expression:

# lib/table_parser/adapters/split.rb
module TableParser
 module Adapter
 module Split
 extend self
 def parse(text)
 delimiter = / *, */
 lines = text.split(/\n/)
 keys = lines.shift.split(delimiter, -1)
 rows = lines.map do |line|
 row = {}
 fields = line.split(delimiter, -1)
 keys.each { |key| row[key] = fields.shift }
 row
 end
 return rows
 end
 end
 end
end

Test Helper

The main thing that needs to be done here is setting up the minitest dependencies, and we can go ahead and load the project code as well. This is not really a necessary file here, but it’s common in larger projects.

# test/test_helper.rb
require "minitest/autorun"
require "table_parser"

Shared Test Examples

We should avoid duplicating all tests for each adapter. Instead, we will write shared examples that will be pulled in from simpler adapter test files:

# test/table_parser_test.rb
require "test_helper"
module TableParserTest
 def test_parse_columns_and_rows
 text = <<~TEXT
 Name,LastName
 John,Doe
 Jane,Doe
 TEXT
 john, jane = TableParser.parse(text)
 assert_equal "John", john["Name"]
 assert_equal "Jane", jane["Name"]
 assert_equal "Doe", john["LastName"]
 assert_equal "Doe", jane["LastName"]
 end
 def test_empty
 text = <<~TEXT
 Name,LastName
 TEXT
 result = TableParser.parse(text)
 assert_equal [], result
 end
 def test_removes_leading_and_trailing_whitespace
 text = <<~TEXT
 , Name,LastName
 ,John , Doe
 , Jane, Doe
 TEXT
 john, jane = TableParser.parse(text)
 assert_equal "John", john["Name"]
 assert_equal "Jane", jane["Name"]
 assert_equal "Doe", john["LastName"]
 assert_equal "Doe", jane["LastName"]
 end
end

Adapter Test Files

Next, for each adapter we need a test that will run the shared examples on it. Since the test examples are in a separate file and shared, these are pretty compact.

First, one for the scanning adapter:

# test/scan_adapter_test.rb
require "table_parser_test"
class TableParser::ScanAdapterTest < Minitest::Test
 include TableParserTest
 def setup
 TableParser.adapter = :scan
 end
end

Next, for the splitting adapter:

# test/split_adapter_test.rb
require "table_parser_test"
class TableParser::SplitAdapterTest < Minitest::Test
 include TableParserTest
 def setup
 TableParser.adapter = :split
 end
end

Running the Test Suite

Thanks to the Rakefile, verifying that both of the adapters work is easy;

$ rake
Run options: --seed 26993
# Running:
......
Fabulous run in 0.001997s, 3004.7896 runs/s, 9014.3689 assertions/s.
6 runs, 18 assertions, 0 failures, 0 errors, 0 skips

Conclusion

Adapters are great ways to incorporate multiple ways of accomplishing something without resorting to mountains of conditional branching. They also let you split approaches into separate libraries that can have their own dependencies. If adapters are loaded in a lazy manner, broken adapters will not affect a project unless they are used.

Stay tuned for more great design pattern articles!

Frequently Asked Questions (FAQs) about the Adapter Design Pattern in Ruby

What is the Adapter Design Pattern in Ruby?

The Adapter Design Pattern in Ruby is a structural design pattern that allows objects with incompatible interfaces to work together. This pattern involves a single class, known as the adapter, which is responsible for communication between the two different interfaces. The adapter wraps the object that needs to be adapted and presents a new interface for it to the outside world.

How does the Adapter Design Pattern work?

The Adapter Design Pattern works by encapsulating an existing class within a new class and exposing a consistent interface. The new class, or adapter, can then be used in place of the original class. This allows the client code to interact with the adapter in a uniform way, regardless of the underlying class or interface.

When should I use the Adapter Design Pattern?

The Adapter Design Pattern is particularly useful when you want to use a class that doesn’t meet the exact requirements of your interfaces. It’s also beneficial when you want to create a reusable class that cooperates with unrelated or unforeseen classes, that is, classes that don’t necessarily have compatible interfaces.

What are the benefits of using the Adapter Design Pattern?

The Adapter Design Pattern promotes code reusability and flexibility. It allows developers to use existing classes even if their interfaces don’t match the ones they need. It also enables classes to work together that couldn’t otherwise because of incompatible interfaces.

Can you provide an example of the Adapter Design Pattern in Ruby?

Sure, let’s consider a simple example. Suppose we have a Printer class that expects to print an array of strings. However, we have a Text class that only provides a single string. We can create an Adapter class that takes an instance of Text and adapts its interface to match what Printer expects.

class Text
def initialize(text)
@text = text
end

def content
@text
end
end

class Printer
def print(texts)
texts.each do |text|
puts text
end
end
end

class TextAdapter
def initialize(text)
@text = text
end

def to_a
[@text.content]
end
end

text = Text.new("Hello, World!")
adapter = TextAdapter.new(text)
Printer.new.print(adapter.to_a)

Are there any drawbacks to using the Adapter Design Pattern?

While the Adapter Design Pattern is useful, it can introduce extra complexity into your code, as you need to create new adapter classes for each class or interface you want to adapt. This can make the code harder to understand and maintain.

How does the Adapter Design Pattern differ from other design patterns?

The Adapter Design Pattern is a structural design pattern, which means it’s concerned with how classes and objects are composed to form larger structures. This differs from creational patterns, which deal with object creation mechanisms, and behavioral patterns, which are concerned with communication between objects.

Can the Adapter Design Pattern be used with other design patterns?

Yes, the Adapter Design Pattern can be used in conjunction with other design patterns. For example, it can be used with the Factory Pattern to create the appropriate adapter at runtime, based on some condition.

Is the Adapter Design Pattern specific to Ruby?

No, the Adapter Design Pattern is a general design pattern that can be implemented in any object-oriented programming language. The implementation details may vary from language to language, but the underlying concept remains the same.

Where can I learn more about the Adapter Design Pattern and other design patterns in Ruby?

There are many resources available online to learn about design patterns in Ruby. Some popular options include online tutorials, blogs, and video courses. You can also refer to books on the subject, such as “Design Patterns in Ruby” by Russ Olsen.

👁 Robert Qualls
Robert Qualls

Robert is a voracious reader, Ruby aficionado, and other big words. He is currently looking for interesting projects to work on and can be found at his website.

SitePoint Premium
Stay Relevant and Grow Your Career in Tech
  • Premium Results
  • Publish articles on SitePoint
  • Daily curated jobs
  • Learning Paths
  • Discounts to dev tools
Start Free Trial

7 Day Free Trial. Cancel Anytime.

Stuff we do
Contact
About
Connect
Subscribe to our newsletter

Get the freshest news and resources for developers, designers and digital creators in your inbox each week

© 2000 – 2026 SitePoint Pty. Ltd.
This site is protected by reCAPTCHA and the Google Privacy Policy and Terms of Service apply.
Privacy PolicyTerms of Service