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.