Nessun risultato. Prova con un altro termine.
Guide
Notizie
Software
Tutorial

Duck Typing, tipizzazione per comportamenti

Riconoscere un oggetto non dal tipo ma dai metodi che espone, quindi dal suo "comportamento"
Riconoscere un oggetto non dal tipo ma dai metodi che espone, quindi dal suo "comportamento"
Link copiato negli appunti

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.


Ti consigliamo anche