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.