Экспериментирую с Dependency Injection. Хочется сделать такой код, который помимо основной функции, изоляции зависимостей, также будет хорошо восприниматься IDE (с переходами по коду). Интерфейс видится примерно таким:
class ExternalApi
def self.call = :api_result
end
class DI < Module
def included(base)
end
end
class Receiver1
include DI.new { def external_api = ExternalApi.call }
end
Receiver1.new.external_api # => :api_result
Метод external_api
добавляется как зависимость в инстанс Receiver1
. Все работает, как задумано. Чуть-чуть модифицируем код:
class Receiver2
include DI.new do
def external_api = ExternalApi.call
end
end
Receiver2.new.external_api # => NoMethodError: undefined method `external_api' for #<Receiver2:0x00007f06274aa7e8>
В чем проблема? Интуитивно понятно, что где-то нарушается порядок выполенения операторов. Но где именно? Попробуем разобраться. В ruby есть средства для анализа собственного кода. Чтобы понять, как код воспринимается виртуальной машиной, можно воспользоваться модулем RubyVM::AbstractSyntaxTree
:
RubyVM::AbstractSyntaxTree.parse(<<~RUBY)
include DI.new { def external_api = :api_result }
RUBY
На выходе получим синтаксическое дерево:
=> (SCOPE@1:0-1:49
tbl: []
args: nil
body:
(FCALL@1:0-1:49 :include
(LIST@1:8-1:49
(ITER@1:8-1:49 (CALL@1:8-1:14 (CONST@1:8-1:10 :DI) :new nil)
(SCOPE@1:15-1:49
tbl: []
args: nil
body:
(DEFN@1:17-1:47
mid: :external_api
body: (SCOPE@1:17-1:47 tbl: [] args: (ARGS@1:17-1:33 pre_num: 0 pre_init: nil opt: nil first_post: nil post_num: 0 post_init: nil rest: nil kw: nil kwrest: nil block: nil) body: (LIT@1:36-1:47 :api_result))))) nil)))
Многовато текста. Изолируем код, уберем излишнюю реализацию:
RubyVM::AbstractSyntaxTree.parse(<<~RUBY)
include Class.new(Module) { 1 }
RUBY
Отформатировано и обрезано для лучшего восприятия:
FCALL@1:0-1:29 :include
(
LIST@1:8-1:29
(
ITER@1:8-1:29
(CALL@1:8-1:25 (CONST@1:8-1:13 :Class) :new (LIST@1:18-1:24 (CONST@1:18-1:24 :Module) nil))
(SCOPE@1:26-1:29 tbl: [] args: nil body: (LIT@1:27-1:28 1))
)
nil
)
Метод include
принимает список аргументов LIST
. Единственным переданным аргументом является элемент ITER
. Его предназначение сокрыто где-то в исходниках ruby, но очевидно, что он объединяет вызов метода (Class.new
в данном случае) с переданным в него блоком кода SCOPE
. В итоге, сначала инициализируется класс с блоком, затем результат передается в include
.
Сравним с реализацией Receiver2
:
RubyVM::AbstractSyntaxTree.parse(<<~RUBY)
include Class.new(Module) do
1
end
RUBY
ITER@1:0-3:3
(
FCALL@1:0-1:25 :include
(
LIST@1:8-1:25
(
CALL@1:8-1:25 (CONST@1:8-1:13 :Class) :new (LIST@1:18-1:24 (CONST@1:18-1:24 :Module) nil)
)
nil
)
)
(
SCOPE@1:26-3:3 tbl: [] args: nil body: (LIT@2:2-2:3 1)
)
В этом случае блок SCOPE
ассоциирован непосредственно с include
. То есть, класс инициализируется без блока, который вместо этого идет как аргумент в include
.
Как всегда после дебага, можно заглянуть в документацию, и найти там подтверждения разного приоритета операторов блока:
{ ... } blocks have priority below all listed operations, but do ... end blocks have lower priority.
Возвращаясь к начальной задаче, код можно было бы переписать, используя скобки. Но это не красивое и хрупкое решение, продолжаю поиски =)
class Receiver2
include(
DI.new do
def external_api = ExternalApi.call
end
)
end
Receiver2.new.external_api # :api_result
Top comments (0)