DEV Community

Sergei Malykh
Sergei Malykh

Posted on

1

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)

AWS Security LIVE!

Tune in for AWS Security LIVE!

Join AWS Security LIVE! for expert insights and actionable tips to protect your organization and keep security teams prepared.

Learn More