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.
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.
Perhaps the most frustrating situations that seem to call for an adapter are those where the interface you have almost—but 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)
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
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.
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.