Design Patterns in Ruby: Adapter

Questo secondo post della serie abbandona momentaneamente i creational patterns e tratta uno degli structural pattern più importanti: l'Adapter. Lo scopo di un adapter è quello di convertire l'interfaccia di una classe in una richiesta dall'oggetto client. Grazie ad un adapter è quindi possibile far interagire due classi dotate di interfacce tra loro incompatibili. Supponiamo dunque di avere due classi, PalGame e NtscGame, che estendono una superclasse Game. Queste sottoclassi espongono rispettivamente due metodi: _play_ e _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

Abbiamo poi una classe Console estesa da PalConsole e NtscConsole. Queste due classi si aspettano di dialogare rispettivamente con giochi di tipo PalGame e 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

Come si puo' infatti notare, il metodo play_game di una console PalConsole chiamerà il metodo _play_ del gioco passato come argomento; la console NtscConsole chiamerà invece il metodo _run_. Il nostro obiettivo è quello di parmettere ad una PalConsole di poter lanciare dei giochi di tipo NtscGame. L'approccio che segue la linea tracciata dai GoF prevede di costruire una classe Adapter in grado di fornire alla PalConsole l'interfaccia _play_ di cui ha bisogno:


#adapters.rb

class NtscToPalAdatper

attr_accessor :game

def initialize(game)

@game = game

end
 
 def play

@game.run

end

end

Vediamo dunque che l'adapter espone semplicemente un metodo _play_ che invocherà il metodo _run_ del gioco NtscGame. Il seguente codice


#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)

fornirà il seguente output: I am the NTSC version of Final Fantasy and I am running! A questo punto mostriamo delle alternative in grado di sfruttare maggiormente le potenzialità di Ruby. Una prima possibilità è quella di utilizzare le "open classes" di Ruby: il linguaggio permette infatti di aggiungere metodi ad una classe già caricata, in questo modo:


#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)

Come possiamo notare, abbiamo aggiunto a runtime il metodo _play_ al gioco NtscGame. Il codice sopra produce il seguente 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! Notiamo però che con questa soluzione abbiamo aggiunto il metodo _play_ all'intera classe NtscGame. Tutte le istanze che verranno create, saranno dunque dotate del metodo _play_. Questo tipo di intervento è comunque molto rischioso, in quanto potrebbe poi non garantire la compatibilità con eventuali altre librerie a causa di quelche name clash. Un'altra alternativa potrebbe essere quella di utilizzare delle "singleton classes". Ruby permette infatti di modificare una singola istanza di una classe, creando una classe anonima come sua superclasse che implementa il nuovo metodo dichiarato. Esistono numerose possibilità per implementare queste singleton classes. Vediamo nello snippet sottostante le varie implementazioni.


#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 <
def play

run

end

EOT

console.play_game(dragons_lair)

Tutte li implementazioni forniscono la stessa tipologia di 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 questo post abbiamo dunque visto l'utilità di questo design pattern e come Ruby permetta allo sviluppatore di "uscire" dall'implementazione classica suggerita in modo da poter sfruttare maggiormente le potenzialità del linguaggio. Codice sorgente "qui":http://github.com/devinterface/design_patterns_in_ruby Post precedenti della serie: Design Patterns in Ruby: Introduzione Design Patterns in Ruby: Abstract Factory