Вы сталкиваетесь с нюансами иерархии объектов Ruby и с тем, как поиск методов взаимодействует с включенными модулями.
Когда вы вызываете метод для объекта, Ruby просматривает ancestors
список класса объекта в поисках класса-предка или модуля, который отвечает на этот метод. Когда вы вызываете super
в этом методе, вы фактически продолжаете обход дерева ancestors
в поисках следующего объекта, который отвечает на то же имя метода.
Древо предков для ваших классов X
и Y
выглядит следующим образом:
p X.ancestors #=> [ X, A, Object, Kernel, BaseObject ]
p Y.ancestors #=> [ Y, X, A, Object, Kernel, BaseObject ]
Проблема в том, что include
включение модуля во второй раз в дочернем классе не вводит вторую копию модуля в цепочку предков.
По сути, когда вы вызываете Y.new.blah
, Ruby начинает искать класс, который отвечает на blah
. Он проходит мимо Y
и X
и приземляется на A
, что представляет собой метод blah
. Когда A#blah
вызывает super
, «указатель» в вашем списке предков уже указывает на A
, и Ruby продолжает искать с этой точки другой объект, отвечающий на blah
, начиная с Object
, Kernel
и затем BaseObject
. Ни в одном из этих классов нет метода blah
, поэтому ваш вызов super
завершится ошибкой.
То же самое происходит, если модуль A
включает в себя модуль B
, а затем класс включает в себя оба модуля A
и B
. Модуль B
не включен дважды:
module A; end
module B; include A; end
class C
include A
include B
end
p C.ancestors # [ C, B, A, Object, Kernel, BaseObject ]
Обратите внимание, что это C, B, A
, а не C, A, B, A
.
Казалось бы, цель состоит в том, чтобы позволить вам безопасно вызывать super
внутри любого из методов A
, не беспокоясь о том, что потребляющие иерархии классов могут непреднамеренно включить A
дважды.
Есть несколько экспериментов, демонстрирующих различные аспекты этого поведения. Первый — это добавление метода blah
в Object, который позволяет передать вызов super
:
class Object; def blah; puts "Object::blah"; end; end
module A
def blah
puts "A::blah"
super
end
end
class X
include A
end
class Y < X
include A
end
Y.new.blah
# Output
# A::blah
# Object::blah
Второй эксперимент заключается в использовании двух модулей, BaseA
и A
, что действительно приводит к правильной вставке модулей дважды в цепочку ancestors
:
module BaseA
def blah
puts "BaseA::blah"
end
end
module A
def blah
puts "A::blah"
super
end
end
class X
include BaseA
end
class Y < X
include A
end
p Y.ancestors # [ Y, A, X, BaseA, Object, ...]
Y.new.blah
# Output
# A::blah
# BaseA::blah
В третьем эксперименте используется prepend
вместо include
, что помещает модуль перед объекта в иерархии ancestors
и, что интересно, действительно вставляет дубликат модуля. Это позволяет нам достичь точки, в которой Y::blah
эффективно вызывает X::blah
, что не удается, поскольку Object::blah
не существует:
require 'pry'
module A
def blah
puts "A::blah"
begin
super
rescue
puts "no super"
end
end
end
class X
prepend A
end
class Y < X
prepend A
end
p Y.ancestors # [ A, Y, A, X, Object, ... ]
Y.new.blah
# Output
# A::blah (from the A before Y)
# A::blah (from the A before X)
# no super (from the rescue clause in A::blah)
03.01.2017