An adapter is an object that crosses the chasm between the interface that you have and the interface that you need. So the gist of this pattern is to convert an object to return a hash/object that has some fields/methods we expected for the next usage (EmailService in the next example).

They exist to soak up the differences between the interfaces that we need and the objects that we have. An adapter supports the interface that we need on the outside, but it implements that interface by making calls to an object hidden inside—an object that does everything we need it to do, but does it via the wrong interface.

Filling in the Gaps with the Adapter

Suppose we have an EmailService class that sends emails, but it only accepts email data in a specific format with to, from, subject, and body fields. However, our application generates email data in a different format with recipient, sender, title, and content fields. To bridge this gap, we can create an adapter class that converts email data from our format to the format expected by the EmailService.

class EmailService
  def send_email(to:, from:, subject:, body:)
    # Assume email sending logic here
    puts "Sending email to #{to} from #{from} with subject '#{subject}' and body '#{body}'"
  end
end

class EmailData
  attr_accessor :recipient, :sender, :title, :content

  def initialize(recipient, sender, title, content)
    @recipient = recipient
    @sender = sender
    @title = title
    @content = content
  end
end

class EmailDataAdapter
  def initialize(email_data)
    @email_data = email_data
  end

  def to_email_service_format
    {
      to: email_data.recipient,
      from: email_data.sender,
      subject: email_data.title,
      body: email_data.content
    }
  end

  private

  attr_reader :email_data
end

# Usage:
email_data = EmailData.new("[email protected]", "[email protected]", "Important news", "Hello Jane, we have some important news for you!")
adapter = EmailDataAdapter.new(email_data)
email_service_data = adapter.to_email_service_format

service = EmailService.new
service.send_email(email_service_data)

In this example, we create an adapter class called EmailDataAdapter that takes in email data in our format and converts it to the format expected by the EmailService. We pass the converted email data to an instance of the EmailService class to send the email.

The near misses

Perhaps the most frustrating situations that seem to call for an adapter are those where the interface you have almostbut not quite—lines up with the interface that you need. For example, suppose we are writing a class to render text on the screen:

class Renderer
  def self.render(text_object)
    text = text_object.text
    size = text_object.size_inches
    color = text_object.color
    # render the text ...
  end
end

# Clearly, Renderer is looking to render objects that look something like this
class TextObject
  attr_reader :text, :size_inches, :color
  def initialize(text, size_inches, color)
    @text = text
    @size_inches = size_inches
    @color = color
	end 
end

# Unfortunately, we discover that some of the text that we need to render is 
# contained in an object that looks more like this:
class BritishTextObject
  attr_reader :string, :size_mm, :colour
	# ... 
end

The good news is that BritishTextObject contains fundamentally everything we need to render the text. The bad news is that the text is stored in a field called string, not text; that the size of the text is in millimetres, not inches; and that the colour attribute has that bonus “u”. So it still follows the initiative ideas, we need to convert it to an interface that we need, but we have almost values.

class BritishTextObjectAdapter < TextObject
  def initialize(bto)
		@bto = bto 
	end
  
	def text
    return @bto.string
	end
  
	def size_inches
    return @bto.size_mm / 25.4
	end
  
	def color
    return @bto.colour
	end 
end

# That is, we easily use it to convert BritishTextObject to be an argument for Renderer
british_text_object = BritishTextObject.new(text, size_inches, color)
adapter = BritishTextObjectAdapter.new(british_text_object)

Render.render(adapter)

Modifying a Single Instance

If modifying an entire class on the fly seems a little extreme, Ruby provides another, perhaps less invasive alternative. Instead of modifying an entire class, you can modify the behavior of a single instance like the way we did with singleton class.

bto = BritishTextObject.new('hello', 50.8, :blue)

class << bto
  def color
    colour
	end

  def text
    string
	end

  def size_inches
    return size_mm/25.4
	end 
end

Screenshot 2023-03-25 at 11.18.34.png

As shown in Figure above, singleton class is actually the first place where Ruby looks when you call a method, so any method defined in the singleton class will override the methods in the regular class.

Adapters in the Wild

You can find a classic application of the Adapter pattern buried in ActiveRecord, the object relational mapper used by Ruby on Rails. ActiveRecord has to deal with the fact that it needs to talk to a whole crowd of different database systems: MYSQL and Oracle and Postgres, not to mention SQLServer. All of these database systems provide a Ruby API—which is good. But all of the APIs are different—which is bad.

# To execute some SQL, you need to call the query method:
results = mysql_connection.query(sql)

# But if you are talking to Sybase, you need to use the sql method:
results = sybase_connection.sql(sql)

Meanwhile, if you are dealing with Oracle, you call the execute method and get back a cursor to the results instead of the results themselves. It is almost as if the authors got together and conspired to ensure that there was no overlap.