There are two ways to implement the Adaptor pattern in Ruby: through delegation or through Ruby's open classes. The former is the most common implementation of the Adapter pattern and is the most general. The latter is more concise but is not always possible.
In demonstrating each type of adaption we will use the following contrived example. We want to put a SquarePeg into a RoundHole by passing it to the hole's peg_fits? method. The peg_fits? method checks the radius attribute of the peg, but a SquarePeg does not have a radius. Therefore we need to adapt the interface of the SquarePeg to meet the requirements of the RoundHole.
class SquarePeg
attr_reader :width
def initialize( width )
@width = width
end
end
class RoundPeg
attr_reader :radius
def initialize( radius )
@radius = radius
end
end
class RoundHole
attr_reader :radius
def initialize( r )
@radius = r
end
def peg_fits?( peg )
peg.radius <= radius
end
end
Adaption by Delegation
To implement an Adaptor with delegation, create a new adaptor class that implements the required interface by making calls to the adapted object.
class SquarePegAdaptor
def initialize( square_peg )
@peg = square_peg
end
def radius
Math.sqrt(((@peg.width/2) ** 2)*2)
end
end
hole = RoundHole.new( 4.0 )
4.upto(7) do |i|
peg = SquarePegAdaptor.new( SquarePeg.new(i.to_f) )
if hole.peg_fits?( peg )
puts "peg #{peg} fits in hole #{hole}"
else
puts "peg #{peg} does not fit in hole #{hole}"
end
end
This produces the output
peg #<SquarePegAdaptor:0xa038b10> fits in hole #<RoundHole:0xa038bd0> peg #<SquarePegAdaptor:0xa038990> fits in hole #<RoundHole:0xa038bd0> peg #<SquarePegAdaptor:0xa0388a0> does not fit in hole #<RoundHole:0xa038bd0> peg #<SquarePegAdaptor:0xa038720> does not fit in hole #<RoundHole:0xa038bd0>
Adaption through Open Classes
Ruby's open classes let you add the required methods to the class that you want to adapt. This approach is far more concise than the use of adaptor classes, and also has the advantage that you don't need to create another object.
class SquarePeg
def radius
Math.sqrt( ((width/2) ** 2) * 2 )
end
end
hole = RoundHole.new( 4.0 )
4.upto(7) do |i|
peg = SquarePeg.new(i.to_f)
if hole.peg_fits?( peg )
puts "peg #{peg} fits in hole #{hole}"
else
puts "peg #{peg} does not fit in hole #{hole}"
end
end
This produces the output:
peg #<SquarePeg:0xa038618> fits in hole #<RoundHole:0xa038b88> peg #<SquarePeg:0xa0384b0> fits in hole #<RoundHole:0xa038b88> peg #<SquarePeg:0xa0383d8> does not fit in hole #<RoundHole:0xa038b88> peg #<SquarePeg:0xa038270> does not fit in hole #<RoundHole:0xa038b88>
This approach is not possible if the adapted object and required interface both have a method with the same name, but different semantics. For example, if the RoundPeg class also had an width attribute that returned the diameter of the peg and was checked by the RoundHole's peg_fits? method it would have the same name as, but a different meaning from, the width parameter of the SquarePeg class. There would be no way of implementing both the RoundPeg and the SquarePeg interfaces in the same class.
-- NatPryce
I've put some information on what I consider a generalized adaptor (one that lets you choose which interface you want to use) in AdaptorPattern/Generalized. -- MauricioFernandez