Duck Typing, tipizzazione per comportamenti

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.

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!
Se vuoi aggiornamenti su Duck Typing, tipizzazione per comportamenti inserisci la tua e-mail nel box qui sotto:
 
X
Se vuoi aggiornamenti su Duck Typing, tipizzazione per comportamenti

inserisci la tua e-mail nel box qui sotto:

Ho letto e acconsento l'informativa sulla privacy

Acconsento al trattamento di cui al punto 3 dell'informativa sulla privacy