Adapter

Convert the interface of a class into another interface clients expect. Adapter lets classes work together that couldn't otherwise because of incompatible interfaces.

Also known as: Wrapper

Participants

RoleIn this exampleResponsibility
Target#draw(x, y, text)The interface the game expects. AnsiScreen already speaks it.
AdapteeCursesWindowA useful object with the wrong interface (curses' setpos and addstr).
AdapterCursesScreenWraps the adaptee and translates calls into the target interface.
ClientStatusLineWorks with anything that speaks the target interface.

Ruby implementation

adapter.rb
# The Target: the game's own screen interface. Anything with #draw(x, y, text).
# AnsiScreen speaks it natively, using terminal escape codes.
class AnsiScreen
  attr_reader :output

  def initialize
    @output = +""
  end

  def draw(x, y, text)
    @output << "\e[#{y + 1};#{x + 1}H#{text}"
  end
end

# The Adaptee: a curses-style window with its own interface.
# It moves a cursor to (row, column) first, then adds a string there.
class CursesWindow
  attr_reader :calls

  def initialize
    @calls = []
  end

  def setpos(row, column) = @calls << [:setpos, row, column]
  def addstr(text) = @calls << [:addstr, text]
end

# The Adapter wraps the window and translates #draw into curses calls.
class CursesScreen
  def initialize(window)
    @window = window
  end

  def draw(x, y, text)
    @window.setpos(y, x)
    @window.addstr(text)
  end
end

# The Client only knows about #draw.
class StatusLine
  def initialize(gold:, lives:)
    @gold = gold
    @lives = lives
  end

  def render(screen)
    screen.draw(0, 0, "Gold: #{@gold}  Lives: #{@lives}")
  end
end

In use

status = StatusLine.new(gold: 120, lives: 20)

status.render(AnsiScreen.new)
status.render(CursesScreen.new(CursesWindow.new))

StatusLine never learns that one screen is really a curses window. The adapter also hides a small trap: curses takes row then column, the opposite order to the game’s x, y.

Notes

Ruby idioms

  • Duck typing does half the work. The target is just “responds to #draw”, so the adapter needs no shared superclass.
  • Prefer a wrapper to a monkey patch. Reopening the curses window class to add #draw works, but changes it for every caller. A wrapper keeps the translation in one place.
  • In Rails, Active Record database adapters and Active Job queue adapters are this pattern at scale.

Reach for it when

  • Integrating a third-party library, legacy class or external API whose shape you don’t control.
  • You want your own code to depend on your interface, not a vendor’s.

Watch for

  • Adapters that start adding behaviour. Translation only. If it starts doing more, it’s turning into a Decorator or a Facade.

Use an adapter to let your code keep its own interface. Keep it to translation, and give new behaviour to other objects.