DEV Community

Sergei Malykh
Sergei Malykh

Posted on

Custom DI

Экспериментирую с 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
Enter fullscreen mode Exit fullscreen mode

Метод 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>
Enter fullscreen mode Exit fullscreen mode

В чем проблема? Интуитивно понятно, что где-то нарушается порядок выполенения операторов. Но где именно? Попробуем разобраться. В ruby есть средства для анализа собственного кода. Чтобы понять, как код воспринимается виртуальной машиной, можно воспользоваться модулем RubyVM::AbstractSyntaxTree:

RubyVM::AbstractSyntaxTree.parse(<<~RUBY)
  include DI.new { def external_api = :api_result }
RUBY
Enter fullscreen mode Exit fullscreen mode

На выходе получим синтаксическое дерево:

=> (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)))
Enter fullscreen mode Exit fullscreen mode

Многовато текста. Изолируем код, уберем излишнюю реализацию:

RubyVM::AbstractSyntaxTree.parse(<<~RUBY)
  include Class.new(Module) { 1 }
RUBY
Enter fullscreen mode Exit fullscreen mode

Отформатировано и обрезано для лучшего восприятия:

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
  )
Enter fullscreen mode Exit fullscreen mode

Метод include принимает список аргументов LIST. Единственным переданным аргументом является элемент ITER. Его предназначение сокрыто где-то в исходниках ruby, но очевидно, что он объединяет вызов метода (Class.new в данном случае) с переданным в него блоком кода SCOPE. В итоге, сначала инициализируется класс с блоком, затем результат передается в include.

Сравним с реализацией Receiver2:

RubyVM::AbstractSyntaxTree.parse(<<~RUBY)
  include Class.new(Module) do
    1
  end
RUBY
Enter fullscreen mode Exit fullscreen mode
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)
  )
Enter fullscreen mode Exit fullscreen mode

В этом случае блок SCOPE ассоциирован непосредственно с include. То есть, класс инициализируется без блока, который вместо этого идет как аргумент в include.

Как всегда после дебага, можно заглянуть в документацию, и найти там подтверждения разного приоритета операторов блока:

{ ... } blocks have priority below all listed operations, but do ... end blocks have lower priority.
Enter fullscreen mode Exit fullscreen mode

Возвращаясь к начальной задаче, код можно было бы переписать, используя скобки. Но это не красивое и хрупкое решение, продолжаю поиски =)

class Receiver2
  include(
    DI.new do
      def external_api = ExternalApi.call
    end
  )
end

Receiver2.new.external_api # :api_result
Enter fullscreen mode Exit fullscreen mode

Top comments (0)