Custom DI
Tags: ruby, experiment
Published: 2023-11-20 14:09:32

---
title: Custom DI
published: true
description: 
tags: #ruby #experiment
# cover_image: ![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/wm4wvbg836o3co6x65o4.png)
# Use a ratio of 100:42 for best results.
# published_at: 2023-11-20 13:49 +0000
---

Экспериментирую с Dependency Injection. Хочется сделать такой код, который помимо основной функции, изоляции зависимостей, также будет хорошо восприниматься IDE (с переходами по коду). Интерфейс видится примерно таким:

```ruby
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`. Все работает, как задумано. Чуть-чуть модифицируем код:

```ruby
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`:

```ruby
RubyVM::AbstractSyntaxTree.parse(<<~RUBY)
  include DI.new { def external_api = :api_result }
RUBY
```

На выходе получим синтаксическое дерево:
```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)))
```

Многовато текста. Изолируем код, уберем излишнюю реализацию:
```ruby
RubyVM::AbstractSyntaxTree.parse(<<~RUBY)
  include Class.new(Module) { 1 }
RUBY
```

Отформатировано и обрезано для лучшего восприятия:
```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`:
```ruby
RubyVM::AbstractSyntaxTree.parse(<<~RUBY)
  include Class.new(Module) do
    1
  end
RUBY
```

```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`.

Как всегда после дебага, можно заглянуть [в документацию](https://ruby-doc.org/core-3.1.0/doc/syntax/precedence_rdoc.html), и найти там подтверждения разного приоритета операторов блока:

```
{ ... } blocks have priority below all listed operations, but do ... end blocks have lower priority.
```

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

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

Receiver2.new.external_api # :api_result
```