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.
Se assomiglia ad una papera...
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.
Il duck-typing in Ruby
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!
Anche le sedie possono camminare
L'oggetto sedia non cammina, quindi non è conforme al behavior acts_as_walking
; ma cosa succederebbe se invece alcune delle sue istanze lo fossero? Supponiamo di dover gestire un catalogo di sedie che comprenda anche alcuni stravaganti esperimenti di laboratorio come la 'robot chair', dotata di deambulazione autonoma; l'approccio duck-typing, non essendo basato sulla classe ma sul behavior, ci consente anche di gestire questi casi intervenendo direttamente sull'istanza interessata; proseguiamo in coda al listato precedente aggiungendo:
wooden_base = Chair.new("Continuous Arm Windsor Chair", 600) massage = Chair.new("Massage Chair" , 1500) simple = Chair.new("Simple Chair" , 30) robot_chair = Chair.new("WL-16 Robot Chair" , 9999) def robot_chair.walk @distance = @distance.to_f + 2 end marathon(wooden_base,Man.new) marathon(robot_chair,Man.new)
Eseguendo lo script noteremo come mentre la wooden chair non è adatta a camminare, e quindi nemmeno a competere in una maratona, la sedia robotica viene riconosciuta come aderente al behavior e quindi ammessa alla gara.
Questo risultato è ottenibile intervenendo come fatto sulla singola istanza in modo da renderla conforme alle specifiche del comportamento stabilito.
Conclusioni
Il duck typing mischia sapientemente caratteristiche molto potenti e lati oscuri; questa tecnica incarna una metodologia semplice, snella ed elegante per catturare le essenze degli oggetti di business e condividerle in modo orizzontale all'interno di un applicazione.
I benefici di questo approccio sono molti: ogni singola istanza in ogni momento ha i presupposti per essere perfettamente aderente ai compiti che deve onorare. D'altro canto tutta questa flessibilità porta con se un prezzo da pagare: ogni sviluppatore sul progetto deve essere a conoscenza del funzionamento di tutti i behaviors implementati, sia nel caso voglia aggiungerne qualcuno ai propri oggetti, sia per evitare che alcuni metodi sviluppati sovrascrivano comportamenti esistenti.