XP on Rails Extreme Programming Blog

7Jul/101

Design Patterns in Ruby: Adapter

ORIGINAL POST

This second post of the series leaves for a moment the creational patterns and speaks about one of the most important structural pattern: the Adapter.

The purpose of an adapter is “to convert the interface of a class into another interface clients expect. Adapter lets classes work together that couldn’t otherwise because of incompatible interfaces.”

Suppose therefore to have two classes, PalGame and NtscGame, that extend a superclass Game. These subclasses respectively expose two methods: play and run.

#models.rb
class Game
  attr_accessor :title
  def initialize(title)
    @title = title
  end
end

class PalGame < Game
  def play
    puts "I am the Pal version of #{@title} and I am running!"  
  end
end

class NtscGame < Game  
  def run
    puts "I am the NTSC version of #{@title} and I am running!"  
  end
end

We then subclass a Console class with PalConsole and NtscConsole. These two classes are expected to talk with, respectively, games of kind PalGame and NtscGame.

#models.rb
class Console
end

class PalConsole < Console
  def play_game(game)
    game.play  
  end
end

class NtscConsole < Console
  def run_game(game)
    game.run  
  end
end

How can see, the method play_game of a PalConsole will call the play method of the game, the NtscConsole instead will invoke the run method.

Our goal is to let a PalConsole to run games of kind NtscGame.

Below we’ll follow the line traced by the GoF and we’ll build an Adapter class that provides the play method needed by the PalConsole’s interface:

#adapters.rb
class NtscToPalAdatper
  attr_accessor :game
  def initialize(game)
    @game = game
  end
 
  def play
    @game.run  
  end  
end

As you can see, the adapter exposes a simple play method that calls the run method of the NTSCGame.

The following code:

#main.rb
require 'models.rb'
require 'adapters.rb'

console = PalConsole.new

final_fantasy = NtscGame.new("Final Fantasy")

adapter = NtscToPalAdatper.new(final_fantasy)

console.play_game(adapter)

will produce this output:
I am the NTSC version of Final Fantasy and I am running!

We see at this point some alternatives to further exploit the potential of Ruby.

One possibility is to take advantage of the Ruby’s “open classes”. The language makes possible to add methods to an already loaded class in this way:

#main2.rb
require 'models.rb'

console = PalConsole.new

class NtscGame < Game  
  def play
    run
  end
 
  # alternatively for this simple example we can define an alias:
  # alias play run
end

final_fantasy = NtscGame.new("Final Fantasy")

double_dragon = NtscGame.new("Double Dragon")

console.play_game(final_fantasy)

console.play_game(double_dragon)

As we can see, we’ve added at runtime the method play for a NtscGame game.

The above code produces the following output:
I am the NTSC version of Final Fantasy and I am running!
I am the NTSC version of Double Dragon and I am running!

Note however that with this solution we added the play method to the entire class NtscGame. All instances that are created will be equipped with the play method.
This type of action is very risky because it could not guarantee compatibility with other libraries, for example because of name clash.

Another alternative would be to use the “singleton classes”.
Ruby makes it possible to change a single instance of a class, by creating an anonymous class as its superclass that implements the new defined method.

There are several possibilities to implement these singleton classes. Let’s see in the following snippet some implementations.

#main3.rb
require 'models.rb'

console = PalConsole.new

#1 - creating a singleton class
final_fantasy = NtscGame.new("Final Fantasy")

def final_fantasy.play
  run  
end

console.play_game(final_fantasy)


#2 - adding methods opening the singleton class directly
winning_eleven = NtscGame.new("Winning Eleven")

class << winning_eleven
  def play
    run
  end
end

console.play_game(winning_eleven)


#3 - adding methods from a module
thunderforce = NtscGame.new("Thunderforce")

module Foo
  def play
    run
  end
end

thunderforce.extend(Foo)

console.play_game(thunderforce)


#4 - adding methods inside an instance_eval call
dragons_lair = NtscGame.new("Dragons Lair")

dragons_lair.instance_eval <<EOT
  def play
    run
  end
EOT

console.play_game(dragons_lair)

All implementations will provide the same type of output:
I am the NTSC version of Final Fantasy and I am running!
I am the NTSC version of Winning Eleven and I am running!
I am the NTSC version of Thunderforce and I am running!
I am the NTSC version of Dragons Lair and I am running!

In this post we have therefore seen the usefulness of this design patterns and how Ruby allows the developer to “leave” the classic implementation suggested in order to better take advantage of the potentiality of the language.

source code here

Previous posts from this series:
Design Patterns in Ruby: Introduction
Design Patterns in Ruby: Abstract Factory

About stefano

Independent Information Technology and Services Professional
Comments (1) Trackbacks (0)
  1. You know how to search using telephone number with the aim of hit upon particulars a propos the owner with a quash search search or turn around mobile phone book. There are several websites as soon as provide this service in favor of on hand at no loss, nevertheless they are limited with the aim of landline cell phone numbers simply in important cases.


Leave a comment


No trackbacks yet.