Improve Your Ruby with the Adapter Design Pattern

Robert Qualls
Robert Qualls
Share

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.