The Adapter Pattern in Ruby

Jan 6, 2022 - 5 min read
The Adapter Pattern in Ruby
Share

The Adapter pattern allows you to make incompatible objects work well together by using an adapter object.

Here's a video version of this article if you prefer to watch instead:

And you can check out the code on Github if you want to try it out.

This first example is an application that needs to talk to a payment platform like PayPal.

# lib/01_no_adapter.rb
require_relative "./paypal_lib"
require_relative "./user"

# Client code is coupled with the 3rd party library.
class ClientApp
  def self.call(user:)
    PaypalLib.subscription(email: user.email)
    PaypalLib.customer(
      fname: user.first_name,
      lname: user.last_name
    )
  end
end

ClientApp.call(user: User.new)

So our class is called ClientApp, and the 3rd party library for talking with PayPal is called PaypalLib to indicate it's something that we don't have control over. It's a library.

In this example it's not a real library, but it's a placeholder for one. It will just print some messages to the screen when we call its methods.

# lib/paypal_lib.rb
class PaypalLib
  class << self
    def subscription(email:)
      puts "Creating subscription for #{email}..."
    end

    def customer(fname:, lname:)
      puts "Creating customer for #{fname} #{lname}..."
    end
  end
end

We also have a User object which we'll use to extract data from. Like email, first-name, last-name, and so forth.

# lib/user.rb
class User
  attr_reader :id, :first_name, :last_name, :full_name, :email

  def initialize
    @id = 1
    @first_name = "John"
    @last_name = "Doe"
    @full_name = "John Doe"
    @email = "jdoe@email.com"
  end
end

And as you can see, the client code is using the library directly through it's interface, which makes it tightly coupled to that 3rd party code.

What this coupling does is, when the time comes to extend our application to support another payment platform, or even upgrade the 3rd party library to a new version, we need to change the client code.

And not just in this class, but all the other classes we might have and that are coupled with the 3rd party library.

But let's assume we want to extend the application to support a new platform, namely Braintree.

So just like the PaypalLib class, we have another one called BraintreeLib that is also a fake library, and it too prints something to the screen when we call its methods.

# lib/braintree_lib.rb
class BraintreeLib
  class << self
    def user(full_name:)
      puts "Creating user #{full_name}..."
    end

    def subscribe(external_id:)
      puts "Creating subscription for ID: #{external_id}..."
    end
  end
end

But its methods and arguments (in other words, its API) is different than the PayPal one. And that is on purpose, because that's the problem we are trying to solve.

We want to align these different interfaces so that we can make future extensions easy.

But in order to extend our current implementation to support this new library, we'll have to go in, and add a condition so that we can identify the platform we want to use.

Note the "code changing" part. That's a red flag.

# lib/02_no_adapter.rb
require_relative "./paypal_lib"
require_relative "./braintree_lib"
require_relative "./user"

class ClientApp
  def self.call(platform:, user:)
    if platform == :paypal
      PaypalLib.subscription(email: user.email)
      PaypalLib.customer(
        fname: user.first_name,
        lname: user.last_name
      )
    elsif platform == :braintree
      BraintreeLib.user(full_name: user.full_name)
      BraintreeLib.subscribe(external_id: user.id)
    else
      raise "Wrong platform!"
    end
  end
end

ClientApp.call(platform: :braintree, user: User.new)
# ClientApp.call(platform: :paypal, user: User.new)

So we introduce the platform argument, and an if/else expression that gets to call the correct 3rd party library code.

The reason this approach is a red flag is it goes against the Open/Closed principle which says our classes need to be open for extension and closed for modification.

In other words, we should be able to extend our application to support future extensions (namely, multiple payment platforms) without having to change our ClientApp class.

And the way to do that is to create an adapter class which decouples our client code from the 3rd party library code.

# lib/paypal_adapter.rb
require_relative "./paypal_lib"

class PaypalAdapter
  class << self
    def subscribe(user)
      PaypalLib.subscription(email: user.email)
    end

    def register(user)
      PaypalLib.customer(
        fname: user.first_name,
        lname: user.last_name
      )
    end
  end
end
# lib/braintree_adapter.rb
require_relative "./braintree_lib"

class BraintreeAdapter
  class << self
    def subscribe(user)
      BraintreeLib.subscribe(external_id: user.id)
    end

    def register(user)
      BraintreeLib.user(full_name: user.full_name)
    end
  end
end
# lib/03_adapter.rb
require_relative "./paypal_adapter"
require_relative "./braintree_adapter"
require_relative "./user"

# Client code is decoupled fron any 3rd party library.
class ClientApp
  def self.call(platform:, user:)
    platform.subscribe(user)
    platform.register(user)
  end
end

ClientApp.call(platform: BraintreeAdapter, user: User.new)
# ClientApp.call(platform: PaypalAdapter, user: User.new)

By introducing an adapter, we can have a unified API for all current and future payment platforms we may want to add.

No matter how incompatible the new 3rd party library code will be, we can deal with it in its adapter class.

The client code (namely the ClientApp class) will have no knowledge about the details of each adapter, it will use them all the same way.

And because of that, it will conform to the open/closed principle. We'll be able to extend it without changing its implementation.

All we need to do is provide it with a different adapter class as it's first argument.

12 Project Ideas
Cezar Halmagean
Software development consultant with over a decade of experience in helping growing companies scale large Ruby on Rails applications. Has written about the process of building Ruby on Rails applications in RubyWeekly, SemaphoreCI, and Foundr.