di: Sandro Paganotti 23 Agosto 2010
E se provassimo a definire ogni oggetto partendo dai metodi che contiene e non dalla classe dal quale nasce? Con questo spunto nasce l'idea di Duck Typing, uno stile di tipizzazione che trova in Ruby un partner perfetto e calzante.
Il termine utilizzato per rappresentare questa pratica deriva da un'interessante citazione attribuita a James Whitcomb Riley, poeta statunitense: «when I see a bird that walks like a duck and swims like a duck and quacks like a duck, I call that bird a duck.».
Applicando questa citazione al mondo della programmazione ad oggetti potremmo scrivere che se un oggetto si comporta in modo conforme a delle specifiche allora quell'oggetto non è dissimile da tutti quelli aderenti alle stesse specifiche, indipendentemente dalle loro somiglianze 'strutturali'.
Ne scaturisce quindi che nella pratica del duck-typing non deve essere effettuato un controllo sul tipo di oggetto sul quale stà per essere invocato il metodo ma soltanto sul fatto che tale metodo esista o meno.
Ruby si presta in modo molto naturale a questa pratica di sviluppo; vediamo subito un semplice esempio creando due classi che espongono gli stessi metodi nei confronti di uno specifico comportamento:
class Man
def walk
@distance = @distance.to_i + 1
end
end
class Turtle
def walk # turtles sometimes can boost they speed :)
@distance = @distance.to_f + (rand(30) > 1 ? 0.01 : 10)
end
end
def marathon(p1,p2,distance = 100)
puts "Beginning a race between a #{p1.class} and a #{p2.class}"
loop do
step1, step2 = p1.walk, p2.walk
next if(step1 <= distance && step2 <= distance)
puts(
if (step1 + step2 > distance * 2) then "Draw"
elsif (step1 > distance) then "Player 1 is the winner"
else "Player 2 is the winner"
end ); break
end
end
marathon(Man.new,Turtle.new)
marathon(Man.new,Man.new)
marathon(Turtle.new,Man.new)
In questo caso qualunque istanza di un qualsiasi oggetto può partecipare alla maratona a patto che contenga il metodo walk e che questo si comporti in modo standard sia in termini di parametri in ingresso sia di valori in uscita.
Possiamo riassumere quanto appena detto introducendo il concetto di comportamento (behavior); in questo caso l'uomo e la tartaruga condividono un comportamento simile: 'camminatore'; si può quindi dire che ognuno dei due oggetti si comporta come camminatore: in inglese acts_as_walker.
Un modo molto elegante in Ruby di gestire i behaviors risiede nell'utilizzo della tecnica dei Mixin: un modulo contenente la logica del comportamento 'attivabile' attraverso l'invocazione di un metodo di classe; vediamo come:
module Walker
def self.included(base)
base.send(:extend, WalkerClassMethods)
base.send(:include, WalkerInstanceMethods)
end
module WalkerClassMethods
def acts_as_walker(pr)
define_method(:walk) { @distance = @distance.to_f + pr.call }
end
end
module WalkerInstanceMethods
def acts_as_walker?; respond_to?(:walk) end
end
end
Object.send(:include,Walker)
class Man
acts_as_walker proc{1}
end
class Turtle
acts_as_walker proc{rand(30) > 1 ? 0.01 : 10}
end
#.. il metodo 'marathon' è identico al precedente
Questo approccio inserisce anche un utile strumento di controllo che dà la possibilità a metodi come marathon di riuscire a discriminare a runtime quali istanze abbiano il behavior walker e quali no.
Va notato che il metodo di controllo acts_as_walker? non fà nessun tipo di validazione sulla natura della classe ma soltanto in merito all'effettiva aderenza dell'istanza al comportamento richiesto (che in questo caso si risolve nel certificare la presenza del metodo walk).
Riscriviamo il metodo marathon tenendo conto anche del controllo sul behavior:
def marathon(p1,p2,distance = 100)
puts "Beginning a marathon between a #{p1.class} and a #{p2.class}"
if ![p1,p2].all?{|p|p.acts_as_walker?}
puts "Match invalid! At least one of the players cannot walk! "
return
end
loop do
step1, step2 = p1.walk, p2.walk
next if(step1 <= distance && step2 <= distance)
puts(
if (step1 + step2 > distance * 2) then "Draw"
elsif (step1 > distance) then "Player 1 is the winner"
else "Player 2 is the winner"
end ); break
end
end
Ora sinceriamoci dell'effettivo funzionamento di quanto appena scritto aggiungendo le seguenti istruzioni in coda allo script:
class Chair
def initialize(name,price)
@name, @price = name, price
end
end
marathon(Chair.new("Steel Chair",10),Turtle.new)
Eseguendo il codice finora prodotto dovremmo ottenere un risultato simile al seguente:
Beginning a marathon between a Man and a Turtle Player 1 is the winner Beginning a marathon between a Man and a Man Draw Beginning a marathon between a Turtle and a Man Player 2 is the winner Beginning a marathon between a Chair and a Turtle Match invalid! At least one of the players cannot walk!
Guida ActiveSupportUna panoramica sulle funzionalità più importanti di ActiveSupport:... |
Guida Ruby On Rails 2Scoprire le novità di Ruby on Rails 2, memorizzare i dati con... |
Guida Ruby e il WebUn percorso alla scoperta delle potenzialità offerte da Ruby nella... |
Ogni mercoledì, direttamente nella tua e-mail: articoli, guide e tutorial su Ruby e Ruby on Rails .
Iscriviti alla newsletter