Ecco la storia completa, che spiega i concetti di metaprogrammazione necessari per capire perché l'inclusione dei moduli funziona nel modo in cui funziona in Ruby.
Cosa succede quando viene incluso un modulo?
L'inclusione di un modulo in una classe aggiunge il modulo agli antenati della classe. Puoi guardare gli antenati di qualsiasi classe o modulo chiamando il suo ancestors
metodo:
module M
def foo; "foo"; end
end
class C
include M
def bar; "bar"; end
end
C.ancestors
#=> [C, M, Object, Kernel, BasicObject]
# ^ look, it's right here!
Quando chiami un metodo su un'istanza di C
, Ruby esaminerà ogni elemento di questo elenco di antenati per trovare un metodo di istanza con il nome fornito. Da quando abbiamo incluso M
in C
, M
ora è un antenato di C
, quindi quando chiamiamofoo
un'istanza di C
, Ruby troverà quel metodo in M
:
C.new.foo
#=> "foo"
Notare che l'inclusione non copia alcuna istanza o metodo di classe nella classe - aggiunge semplicemente una "nota" alla classe che dovrebbe anche cercare metodi di istanza nel modulo incluso.
E i metodi di "classe" nel nostro modulo?
Poiché l'inclusione cambia solo il modo in cui vengono inviati i metodi di istanza, l'inclusione di un modulo in una classe rende disponibili solo i metodi di istanza su quella classe. I metodi di "classe" e altre dichiarazioni nel modulo non vengono automaticamente copiate nella classe:
module M
def instance_method
"foo"
end
def self.class_method
"bar"
end
end
class C
include M
end
M.class_method
#=> "bar"
C.new.instance_method
#=> "foo"
C.class_method
#=> NoMethodError: undefined method `class_method' for C:Class
In che modo Ruby implementa i metodi di classe?
In Ruby, le classi ei moduli sono oggetti semplici: sono istanze della classe Class
e Module
. Ciò significa che puoi creare dinamicamente nuove classi, assegnarle a variabili, ecc .:
klass = Class.new do
def foo
"foo"
end
end
#=> #<Class:0x2b613d0>
klass.new.foo
#=> "foo"
Anche in Ruby, hai la possibilità di definire i cosiddetti metodi singleton sugli oggetti. Questi metodi vengono aggiunti come nuovi metodi di istanza alla classe singleton speciale e nascosta dell'oggetto:
obj = Object.new
# define singleton method
def obj.foo
"foo"
end
# here is our singleton method, on the singleton class of `obj`:
obj.singleton_class.instance_methods(false)
#=> [:foo]
Ma anche classi e moduli non sono solo semplici oggetti? In effetti lo sono! Ciò significa che possono avere anche metodi singleton? Sì, lo fa! Ed è così che nascono i metodi di classe:
class Abc
end
# define singleton method
def Abc.foo
"foo"
end
Abc.singleton_class.instance_methods(false)
#=> [:foo]
Oppure, il modo più comune per definire un metodo di classe è quello di utilizzare self
all'interno del blocco di definizione della classe, che si riferisce all'oggetto della classe creato:
class Abc
def self.foo
"foo"
end
end
Abc.singleton_class.instance_methods(false)
#=> [:foo]
Come includo i metodi di classe in un modulo?
Come abbiamo appena stabilito, i metodi di classe sono in realtà solo metodi di istanza sulla classe singleton dell'oggetto classe. Questo significa che possiamo semplicemente includere un modulo nella classe singleton per aggiungere un gruppo di metodi di classe? Sì, lo fa!
module M
def new_instance_method; "hi"; end
module ClassMethods
def new_class_method; "hello"; end
end
end
class HostKlass
include M
self.singleton_class.include M::ClassMethods
end
HostKlass.new_class_method
#=> "hello"
Questa self.singleton_class.include M::ClassMethods
riga non sembra molto carina, quindi ha aggiunto Ruby Object#extend
, che fa lo stesso, ovvero include un modulo nella classe singleton dell'oggetto:
class HostKlass
include M
extend M::ClassMethods
end
HostKlass.singleton_class.included_modules
#=> [M::ClassMethods, Kernel]
# ^ there it is!
Spostando il file extend
chiamata nel modulo
Questo esempio precedente non è un codice ben strutturato, per due motivi:
- Ora dobbiamo chiamare entrambi
include
e extend
nella HostClass
definizione per includere correttamente il nostro modulo. Questo può diventare molto complicato se devi includere molti moduli simili.
HostClass
riferimenti diretti M::ClassMethods
, che è un dettaglio di implementazione del modulo M
che HostClass
non dovrebbe essere necessario conoscere o preoccuparsi.
Allora che ne dici di questo: quando chiamiamo include
sulla prima riga, in qualche modo notifichiamo al modulo che è stato incluso, e gli diamo anche il nostro oggetto classe, in modo che possa chiamare extend
se stesso. In questo modo, è compito del modulo aggiungere i metodi di classe se lo desidera.
Questo è esattamente lo scopo del metodo specialeself.included
. Ruby chiama automaticamente questo metodo ogni volta che il modulo viene incluso in un'altra classe (o modulo) e passa l'oggetto della classe host come primo argomento:
module M
def new_instance_method; "hi"; end
def self.included(base) # `base` is `HostClass` in our case
base.extend ClassMethods
end
module ClassMethods
def new_class_method; "hello"; end
end
end
class HostKlass
include M
def self.existing_class_method; "cool"; end
end
HostKlass.singleton_class.included_modules
#=> [M::ClassMethods, Kernel]
# ^ still there!
Ovviamente, l'aggiunta di metodi di classe non è l'unica cosa che possiamo fare self.included
. Abbiamo l'oggetto classe, quindi possiamo chiamare qualsiasi altro metodo (classe) su di esso:
def self.included(base) # `base` is `HostClass` in our case
base.existing_class_method
#=> "cool"
end