Aqui está a história completa, explicando os conceitos de metaprogramação necessários para entender por que a inclusão de módulo funciona da maneira que funciona em Ruby.
O que acontece quando um módulo é incluído?
Incluir um módulo em uma classe adiciona o módulo aos ancestrais da classe. Você pode ver os ancestrais de qualquer classe ou módulo chamando seu ancestors
método:
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 você chama um método em uma instância de C
, Ruby vai olhar para cada item desta lista de ancestrais para encontrar um método de instância com o nome fornecido. Como incluímos M
em C
, M
agora é um ancestral de C
, então, quando chamarmos foo
uma instância de C
, Ruby encontrará esse método em M
:
C.new.foo
#=> "foo"
Observe que a inclusão não copia nenhuma instância ou método de classe para a classe - ela apenas adiciona uma "nota" à classe de que também deve procurar métodos de instância no módulo incluído.
E os métodos de "classe" em nosso módulo?
Como a inclusão apenas muda a maneira como os métodos de instância são despachados, incluir um módulo em uma classe apenas torna seus métodos de instância disponíveis nessa classe. Os métodos de "classe" e outras declarações no módulo não são copiados automaticamente para a 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
Como Ruby implementa métodos de classe?
Em Ruby, classes e módulos são objetos simples - são instâncias da classe Class
e Module
. Isso significa que você pode criar dinamicamente novas classes, atribuí-las a variáveis, etc .:
klass = Class.new do
def foo
"foo"
end
end
#=> #<Class:0x2b613d0>
klass.new.foo
#=> "foo"
Também em Ruby, você tem a possibilidade de definir os chamados métodos singleton em objetos. Esses métodos são adicionados como novos métodos de instância à classe singleton especial oculta do objeto:
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]
Mas as classes e os módulos também não são objetos simples? Na verdade, eles são! Isso significa que eles também podem ter métodos singleton? Sim! E é assim que os métodos de classe nascem:
class Abc
end
# define singleton method
def Abc.foo
"foo"
end
Abc.singleton_class.instance_methods(false)
#=> [:foo]
Ou, a maneira mais comum de definir um método de classe é usar self
dentro do bloco de definição de classe, que se refere ao objeto de classe que está sendo criado:
class Abc
def self.foo
"foo"
end
end
Abc.singleton_class.instance_methods(false)
#=> [:foo]
Como faço para incluir os métodos de classe em um módulo?
Como acabamos de estabelecer, os métodos de classe são, na verdade, apenas métodos de instância na classe singleton do objeto de classe. Isso significa que podemos apenas incluir um módulo na classe singleton para adicionar vários métodos de classe? Sim!
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"
Esta self.singleton_class.include M::ClassMethods
linha não parece muito boa, então Ruby adicionou Object#extend
, que faz o mesmo - ou seja, inclui um módulo na classe singleton do objeto:
class HostKlass
include M
extend M::ClassMethods
end
HostKlass.singleton_class.included_modules
#=> [M::ClassMethods, Kernel]
# ^ there it is!
Movendo a extend
chamada para o módulo
Este exemplo anterior não é um código bem estruturado, por dois motivos:
- Agora temos que chamar ambos
include
e extend
na HostClass
definição para que nosso módulo seja incluído corretamente. Isso pode ser muito complicado se você tiver que incluir muitos módulos semelhantes.
HostClass
referências diretas M::ClassMethods
, que é um detalhe de implementação do módulo M
que HostClass
não deve ser necessário saber ou se preocupar.
Então, que tal isto: quando chamamos include
na primeira linha, de alguma forma notificamos o módulo que ele foi incluído, e também damos a ele nosso objeto de classe, para que ele possa chamar a extend
si mesmo. Dessa forma, é função do módulo adicionar os métodos da classe, se desejar.
É exatamente para isso que serve o método especialself.included
. Ruby chama automaticamente esse método sempre que o módulo é incluído em outra classe (ou módulo) e passa o objeto de classe host como o primeiro argumento:
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!
Claro, adicionar métodos de classe não é a única coisa que podemos fazer self.included
. Temos o objeto de classe, portanto, podemos chamar qualquer outro método (classe) nele:
def self.included(base) # `base` is `HostClass` in our case
base.existing_class_method
#=> "cool"
end