This post is part of a series of reviews on the book Design Patterns in Ruby. Check out the Introduction post for a full table of contents along with some generic principles regarding Design Patterns.
An Adapter does what you might expect: it attaches itself into 2 ends which aren’t compatible between them. The classic analogy to this is a plug adapter, that thing you use to turn 3-way plugs compatible into a 2-way socket. Without the adapter, you’d be screwed and things just wouldn’t work, and it’s no different on software.
That class diagrams translates into this: the client thinks he’s talking to his target, but he’s actually talking to an adapter which redirects calls to the real target (adaptee). Let’s start simple with the plugs and sockets example, first we’ll try to fit in a 3-way plug into a 2-way socket:
class TwoWaySocket def initialize(plug) @plug = plug end def plug_in begin @plug.two_way_plug_in rescue puts "Plug doesn't fit!!" end end end class ThreeWayPlug def three_way_plug_in puts "All 3 pins plugged" end end socket = TwoWaySocket.new(ThreeWayPlug.new) socket.plug_in
So I’m not lying, it really doesn’t fit. Adapters to the rescue!
class TwoWaySocket def initialize(plug) @plug = plug end def plug_in begin @plug.two_way_plug_in rescue puts "Plug doesn't fit!!" end end end class ThreeWayPlug def three_way_plug_in puts "All 3 pins plugged" end end class PlugAdapter def initialize(three_way_plug) @three_way_plug = three_way_plug end def two_way_plug_in @three_way_plug.three_way_plug_in end end #we'll now call a socket with the adapter instead socket = TwoWaySocket.new(PlugAdapter.new(ThreeWayPlug.new)) socket.plug_in
As you can see, we’re just creating an object that matches one interface with the other so all things can work gracefully.
Now we get to the cool part where we do things the Ruby way. Instead of creating an intermediate class, we could…cheat! Yes, why use a plug adapter if we can get a screwdriver and open up the 3-way plug, remove a pin and adjust things inside so it’ll work with our 2-way socket. In real life that might seem hard, but in this case:
#make sure the original class is loaded require 'three_way_plug' class ThreeWayPlug def two_way_plug_in three_way_plug_in end end socket = TwoWaySocket.new(ThreeWayPlug.new) socket.plug_in
There, we simply reopened the freakin’ class at runtime and said “you WILL have what the socket requires, you WILL fit!”. The cool thing about doing this is that you don’t have to change the code to use an adapter and you don’t have to create yet another class just to handle the situation, you just reopen and cirurgically change whatever you want.
Does changing the whole class seems too drastic? What if you wanted just that one object to adapt instead of everyone? No problem, Ruby gives you the power to edit instances much like you’d edit classes:
three_way_plug = ThreeWayPlug.new socket = TwoWaySocket.new(three_way_plug) socket.plug_in #this won't work class << three_way_plug def two_way_plug_in three_way_plug_in end end socket.plug_in #but this will
Or if you think class << is scary, you can define a new method on the instance by putting the instance name before the method name:
three_way_plug = ThreeWayPlug.new socket = TwoWaySocket.new(three_way_plug) socket.plug_in #this won't work def three_way_plug.two_way_plug_in three_way_plug_in end socket.plug_in #but this will
As usual, there are pros and cons about both ways. Class/instance redefition in certainly simpler and easier, but that's untrue when it comes to complex classes that you need to adapt, specially when you don't have a full understanding of that class, so, for those cases, stick with the classic, good ol' GoF way.
Case any of these last 2/3 chunks of code look unfamiliar to you, I suggest reading my post on the Ruby Object Model and Metaprogramming, it will help you understand and learn the powers of Ruby reflection, which is constantly used due to Ruby’s dynamic typing style.