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

Il mixin in Ruby

Orientarsi tra moduli, include ed extend e singleton
Orientarsi tra moduli, include ed extend e singleton
Link copiato negli appunti

Nella "guida Ruby" abbiamo già incontrato i moduli ed abbiamo visto un semplice esempio di mixin. In questo articolo approfondiamo l'utilizzo dei moduli e della parola chiave include, con ulteriori implementazioni dell'ereditarietà multipla. Vediamo anche come modificare il comportamento delle istanze, oltre che delle classi, grazie ad extend e come realizzare metodi singleton.

Ereditarietà multipla

Iniziamo subito con la funzione principale del mixin: quella di permettere l'ereditarietà multipla attraverso i moduli e il metodo include. Prendiamo ad esempio un modello del comportamento dei cani.

module Aggressive
  def snarl
    "Grrr."
  end
end

class Dog
  include Aggressive
  
  def bark
    "Bau, bau."
  end
end

I metodi d'istanza del modulo Aggressive saranno ereditati dagli oggetti della classe Dog come metodi d'istanza.

> Dog.included_modules
=> [Aggressive, Kernel]
> Dog.instance_methods
=> ["bark", "snarl", ...]
> rowdy = Dog.new
=> #<Dog:0xb7c63f4c>
> rowdy.bark
=> "Bau, bau."
> rowdy.snarl
=> "Grrr."

L'output di Dog.instance_methods è ridotto per questioni di spazio e chiarezza.

Internamente quando includiamo un modulo, Ruby crea, a partire dai suoi metodi, una classe che diviene l'immediato antenato della nostra classe. Ad esempio se abbiamo una classe Dog che ha come classe padre Animal

Dog <- Animal

dopo l'inclusione del modulo Aggressive avremo indicativamente una situazione del genere:

Dog <- Aggressive <- Animal

Senza l'inclusione del modulo

> Dog.ancestors
=> [Dog, Animal, Object, Kernel]

Dopo l'inclusione del modulo "Agressive"

> Dog.ancestors
=> [Dog, Aggressive, Animal, Object, Kernel]

Se si includono in una classe due moduli che definiscono dei metodi con lo stesso nome solo uno di essi sarà ereditato dalla classe e precisamente quello incluso per ultimo.

module InEnglish
  def bark
    "woof, woof"
  end
end

module InEsperanto
  def bark
    "boj, boj"
  end
end

class Dog
  include InEnglish
  include InEsperanto
end

Proviamo ad istanziare un oggetto Dog e a chiamare il metodo bark. Otteniamo la versione in esperanto.

> rowdy = Dog.new
=> #<Dog:0xb7cc7d30>
> rowdy.bark
=> "boj, boj"

Quindi anche se InEnglish compare tra gli antenati di Dog il suo metodo bark non è disponibile per gli oggetti di tipo Dog.

> Dog.ancestors
=> [Dog, InEsperanto, InEnglish, Object, Kernel]

L'unica soluzione per averli entrambi è quella di dargli nomi distinti in modo da evitare ambiguità.

È possibile utilizzare il meccanismo del mixin anche in altre interessanti circostanze sia attraverso il metodo include, sia attraverso extend.

Oltre che alle classi è possibile aggiungere funzionalità anche ai singoli oggetti utilizzando il metodo extend.

Il metodo extend

Il metodo extend permette di aggiungere ad un oggetto, istanza di una classe, i metodi di istanza appartenenti ad un modulo passato come argomento. In questo modo è possibile differenziare il comportamento degli oggetti di una stessa classe.

Un esempio banale: proviamo a ridefinire un metodo di un oggetto

module InJapanese
  def meow
    "Nya, nya."
  end
end

class Cat
  def meow
    "Miao, miao."
  end
end

zorba = Cat.new
puts zorba.meow
zorba.extend(InJapanese)
puts zorba.meow
fritz = Cat.new
puts fritz.meow

Come è facile immaginare in outupt viene fuori:

Miao, miao.
Nya, nya.
Miao, miao.

In pratica il metodo meow del modulo InJapanese è reso disponibile da extend solo all'oggetto zorba che lo ha invocato, tutti gli altri oggetti della stessa classe continueranno ad usare il metodo meow della classe originaria.

extend può essere utilizzato anche per trasformare i metodi di istanza di un modulo in metodi di classe. Basta invocare extend dall'interno della definizione della classe:

module InJapanese
  def meow
    "Nya, nya."
  end
end

class Cat
  extend InJapanese
end

In questo modo possiamo scrivere Cat.meow che produrrà in output la stringa "Nya, nya.". Equivalentemente è possibile estendere una classe utilizzando la sintassi:

Cat.extend(InJapanese)

Lo stesso comportamento può essere ottenuto utilizzando include in questo modo:

module InJapanese
  def meow
    "Nya, nya."
  end
end

class Cat
  class << self
    include InJapanese
  end
end

Oltre che attraverso extend è possibile aggiungere direttamente dei metodi a degli specifici oggetti. I metodi definiti in questo modo sono detti singleton.

Metodi singleton

I metodi singleton sono utili quando un particolare oggetto prevede un comportamento diverso da quello definito dalla sua classe. Riprendendo l'esempio precedente dotiamo il gatto zorba di un nuovo metodo meow:

class Cat
  def meow
    "Miao, miao."
  end
end

zorba = Cat.new
fritz = Cat.new

def zorba.meow
  "Nya, nya."
end

Il metodo meow chiamato dall'oggetto zorba avrà quindi un comportamento diverso da quello dei membri di Cat senza metodi singleton:

> fritz.meow
=> "Miao, miao."
> zorba.meow
=> "Nya, nya."

Ovviamente oltre che ridefinire metodi già esistenti nella classe base è possibile anche definire metodi completamente nuovi:

def zorba.purr
  "Grrrr"
end
> zorba.purr
=> "Grrrr"
> zorba.singleton_methods
=> ["meow", "purr"]

I metodi singleton vanno utilizzati utilmente in alternativa a extend quando occorre aggiungere un solo metodo ad un solo oggetto o in casi analoghi. In definitiva quando si estende un oggetto con extend non si fa altro che "trasformare" tutti i metodi d'istanza del modulo in metodi singleton per l'oggetto chiamante.

Metodi di classe

Un particolare tipo di metodi singleton sono i metodi di classe. In questo caso l'oggetto sul quale è definito il metodo singleton è la classe stessa. I metodi di classe vanno definiti facendo precedere il nome della classe a quello del metodo:

class Cat
  def Cat.family
    "Felidae"
  end
end

Questi metodi vanno chiamati normalmente:

> Cat.family
=> "Felidae"

Ovviamente il metodo family non sarà disponibile per gli oggetti di tipo Cat. I metodi di classe sono molto usati nella libreria standard soprattutto nelle classi File, Dir, Regexp, IO, Thread.

Anche in questo caso si può dire che quando si estende una classe con extend non si fa altro che "trasformare" tutti i metodi d'istanza del modulo in metodi di classe. Dei metodi analoghi ai metodi di classe possono essere definiti anche per i moduli:

module InJapanese
  def InJapanese.country
    "Japan"
  end
end

Il metodo può essere chiamato con:

> InJapanese.country
=> "Japan"

ma a differenza dei metodi d'istanza non viene ereditato dalle classi che includono il modulo InJapanese.

module_function

Concludiamo con un interessante utilizzo del meccanismo del mixin che ci permette di creare delle funzioni che sono allo stesso tempo funzioni del modulo e metodi d'istanza per le classi che includono il modulo. Un esempio di tale comportamento lo ritroviamo nei metodi di Math per i quali ad esempio è possibile scrivere sia

> Math.exp(1)
=> 2.71828182845905

sia

> include Math
=> Object
> exp(1)
=> 2.71828182845905

Per ottenere un tale comportamento basta definire normalmente un metodo d'istanza del modulo e quindi chiamare module_function per trasformarlo in una funzione del modulo:

module InJapanese
  def country
    "Japan"
  end
  module_function :country
end

In questo modo country diventerà un metodo di classe pubblico e allo stesso tempo un metodo d'istanza privato. Se chiamato senza argomenti module_function si comporterà allo stesso modo rendendo funzioni del modulo tutti i metodi definiti successivamente.

Tornando all'esempio:

class Cat
  include InJapanese
  def nation
    country
  end
end

Possiamo dunque scrivere:

> InJapanese.country
=> "Japan"
> zorba = Cat.new
=> #<Cat:0xb7cc4f2c>
> zorba.nation
=> "Japan"

mentre non possiamo chiamare il metodo country che è un metodo d'istanza privato:

> zorba.country
NoMethodError: private method 'country' called for #<Cat:0xb7cc4f2c>

Quelli illustrati sono solo alcuni esempi di utilizzo del meccanismo del mixin che come visto va ben oltre la semplice implementazione dell'ereditarietà multipla e offre numerose possibilità che permettono operare in modo semplice, e per quanto possibile non ambiguo, sugli oggetti, sulle classi e sui moduli.

Ti consigliamo anche