Design Patterns in Ruby: Adapter
12:13Metodologie, Ruby, SvliluppoStefano
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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | #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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | #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:
1 2 3 4 5 6 7 8 9 10 11 | #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
1 2 3 4 5 6 7 8 9 10 11 | #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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | #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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 | #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) |
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
Post precedenti della serie:
Design Patterns in Ruby: Introduzione
Design Patterns in Ruby: Abstract Factory
Excellent door hangers online printing available at PsPrint
Tags: design patterns, ruby

















[...] ORIGINAL POST [...]
[...] This post was mentioned on Twitter by Jérémie Anderlin, Stefano Mancini. Stefano Mancini said: My new article on design patterns in Ruby: Adapter. http://ping.fm/fADFu [...]
I linked to this post at the Drink Rails blog as one of the top Ruby on Rails related posts of the day.
Thanks so much Sohan!
Brilliant! I didn’t realise I had so much more to learn!