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