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
| Role | In this example | Responsibility |
|---|---|---|
| Target | #draw(x, y, text) | The interface the game expects. AnsiScreen already speaks it. |
| Adaptee | CursesWindow | A useful object with the wrong interface (curses' setpos and addstr). |
| Adapter | CursesScreen | Wraps the adaptee and translates calls into the target interface. |
| Client | StatusLine | Works with anything that speaks the target interface. |
Ruby implementation
# 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
endIn 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
#drawworks, 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.