rubyでいいかんじにDIするhabuの紹介

## TL;DR

rubyでいい感じにDIするgemを作りました。使い方を紹介します。いいなと思ったらStarください。
https://github.com/hanachin/habu

ユーザーがある。

```user.rb
User = Struct.new(:name)
```

新しくユーザーを作るサービスがある。これは`user_repository`に依存している。コンストラクタ(`#initialize`)を`@Inject`でアノテーションする。

```new_user_service.rb
class NewUserService
  @Inject
  def initialize(user_repository)
    @user_repository = user_repository
  end

  def call(*params)
    @user_repository.new(*params)
  end
end
```

まずはじめに先頭で`require "habu/setup"`する。必要なソースを`require`する。

```app.rb
require 'habu/setup'
require_relative 'user'
require_relative 'new_user_service'
```

DIコンテナを作成する。

```app.rb
# Create a new container
container = Habu::Container.new
```

`Habu::Container#[]`を呼び出し`user_repository`のつくりかたをブロックで定義する。

```app.rb
# Register user_repository service by passing the block as service factory
container[:user_repository] { User }
```

`Habu::Container#new`に`UserService`クラスを渡すと`user_repository`をコンストラクタ注入する。いい感じにうごく。

```app.rb
# Call Habu::Container#new to get instance
new_user = container.new(NewUserService).call("hanachin")
# => #<struct User name="hanachin">
```

`Habu::Container#[]`に渡されたブロックは引数として`Habu::Container`自身をうけとる。
`Habu::Container#[]`をブロックなしで呼び出すと定義時に渡されたブロックを呼び出した結果を返す。

これにより`new_user`の定義時に`user_repository`を参照できる。

```app.rb
# Factory block take a container as argument
container[:new_user] do |c|
  # You can get the service object by calling Container#[](service_name)
  NewUserService.new(c[:user_repository])
end
```

ブロックなしでよびだす、いい感じに動く。

```app.rb
new_user = container[:new_user].call("hanachin")
# => #<struct User name="hanachin">
```

DIコンテナを`Habu::Container#to_refinements`でRefinements化して`using`する。
`@Inject`したコンストラクタがオーバーライドされているので単に`.new`するだけで依存性が注入された状態で呼び出せる。
`Habu::Container#new`経由でインスタンス化する必要はない。

```rb
# Using container as refinements for shorthand for container.new
using container.to_refinements
new_user = NewUserService.new.call("hanachin")
# => #<struct User name="hanachin">
```

## 動機

「Ruby on Railsの正体と向き合い方」
https://speakerdeck.com/yasaichi/what-is-ruby-on-rails-and-how-to-deal-with-it?slide=59
https://youtu.be/ecpq0U4zkWE?t=1648

密結合→疎結合に移行できると良いのではみたいな文脈でLaravelのDIコンテナに言及があった。
あるとべんりそうだしつくるか〜。

## DIとは

他でいい感じの記事よんでください。
https://scala-text.github.io/scala_text/advanced-trait-di.html#%E4%BE%9D%E5%AD%98%E6%80%A7%E3%81%AE%E6%B3%A8%E5%85%A5%E3%81%A8%E3%81%AF%EF%BC%9F

## †闇の動機†

Javaの引数がないアノテーションはrubyだとインスタンス変数なのでSyntax OK

```inject_annotation.rb
class C
  @Inject
  def initialize(c)
    @c = c
  end
end
```

```
% ruby -c inject_annotation.rb
Syntax OK
```

インスタンス変数による注釈が実装できるとPythonのデコレータの実装もできる。

Rubyをキメると気持ちいい
`@Inject`でRubyキメたい。
†注釈術士†になりたい。

## rubyで`@Inject`の位置を取得する

### TracePoint

まずみなさんが試すのがTracePointだと思います。

```get_inject_location_by_trace_point.rb
TracePoint.trace(:line) do |tp|
  raise if File.read(tp.path).lines[tp.lineno-1].match?(/@Inject/)
end

class C
  @Inject
  def initialize(c)
    @c = c
  end
end
```

残念ながら取れません。

```
% ruby get_inject_location_by_trace_point.rb
% 
```

### InstructionSequence

TracePointを使っても実行時に取れないということは? InstructionSequenceを覗いてみましょう。

値を返すときは`getinstancevariable`がある。

```rb
% ruby --dump=insns -e '@Inject'
== disasm: #<ISeq:<main>@-e:1 (1,0)-(1,7)> (catch: FALSE)
0000 getinstancevariable          :@Inject, <is:0>                    (   1)[Li]
0003 leave
```

返さないときは`getinstancevariable`がない。

```rb
% ruby --dump=insns -e '@Inject; nil'
== disasm: #<ISeq:<main>@-e:1 (1,0)-(1,12)> (catch: FALSE)
0000 putnil                                                           (   1)[Li]
0001 leave
```

残念ながら取れません。

### RubyVM::AbstractSyntaxTree

InstructionSequenceにコンパイルされる前なら取れる。

```
% ruby --dump=parsetree -e '@Inject; nil'
###########################################################
## Do NOT use this node dump for any purpose other than  ##
## debug and research.  Compatibility is not guaranteed. ##
###########################################################

# @ NODE_SCOPE (line: 1, location: (1,0)-(1,12))
# +- nd_tbl: (empty)
# +- nd_args:
# |   (null node)
# +- nd_body:
#     @ NODE_BLOCK (line: 1, location: (1,0)-(1,12))
#     +- nd_head (1):
#     |   @ NODE_IVAR (line: 1, location: (1,0)-(1,7))*
#     |   +- nd_vid: :@Inject
#     +- nd_head (2):
#         @ NODE_NIL (line: 1, location: (1,9)-(1,12))*
```

## InstructionSequenceのコンパイル時にフックを仕掛けてASTを取得する

ASTの状態ならアノテーション(と勝手に呼んでいるただのインスタンス変数)が取れることがわかりました。
コンパイルされる前に `@Inject` を取る方法を紹介します。

`RubyVM::InstructionSequence.load_iseq`を使います。

参考: [YARV Maniacs 【第 13 回】 事前コンパイルへの道](https://magazine.rubyist.net/articles/0053/0053-YarvManiacs.html)

`.load_iseq`は引数でファイル名を取ります。
次のような感じでファイル名を出力したあと`RubyVM::InstructionSequence.compile_file`でファイルをコンパイルし返す処理を`prepend`で差し込んでみます。

```iseq_patch.rb
iseq_patch = Module.new do
  def load_iseq(fname)
    p fname
    RubyVM::InstructionSequence.compile_file(fname)
  end
end
RubyVM::InstructionSequence.singleton_class.prepend(iseq_patch)

pp true
```

フックできたようですね。

```
% ruby iseq_patch.rb
"/home/sei/.rbenv/versions/myruby/lib/ruby/2.7.0/pp.rb"
"/home/sei/.rbenv/versions/myruby/lib/ruby/2.7.0/prettyprint.rb"
true
```

ASTを取得してみます。


```iseq_patch_to_get_ast.rb
iseq_patch = Module.new do
  def load_iseq(fname)
    p RubyVM::AbstractSyntaxTree.parse_file(fname)
    RubyVM::InstructionSequence.compile_file(fname)
  end
end
RubyVM::InstructionSequence.singleton_class.prepend(iseq_patch)

pp true
```

無事ASTがとれました。

```rb
% ruby iseq_patch_to_get_ast.rb
#<RubyVM::AbstractSyntaxTree::Node:SCOPE@1:0-593:3>
#<RubyVM::AbstractSyntaxTree::Node:SCOPE@1:0-556:3>
true
```

ちなみに`super`は呼べません。

```iseq_patch_call_super.rb
iseq_patch = Module.new do
  def load_iseq(fname)
    super
  end
end
RubyVM::InstructionSequence.singleton_class.prepend(iseq_patch)

pp true
```

```
% ruby iseq_patch_call_super.rb
Traceback (most recent call last):
        4: from iseq_patch_call_super.rb:8:in `<main>'
        3: from <internal:prelude>:210:in `pp'
        2: from /home/sei/.rbenv/versions/myruby/lib/ruby/2.7.0/rubygems/core_ext/kernel_require.rb:54:in `require'
        1: from /home/sei/.rbenv/versions/myruby/lib/ruby/2.7.0/rubygems/core_ext/kernel_require.rb:54:in `require'
iseq_patch_call_super.rb:3:in `load_iseq': super: no superclass method `load_iseq' for RubyVM::InstructionSequence:Class (NoMethodError)
```

## ASTから`@Inject`されているクラスの名前を取る

なおnestingや上の階層のモジュールは考慮しないものとする。

Refinementsでおもむろに探索用のメソッドを生やすとべんり。

参考: [akaza/ast\_ext\.rb at master · pocke/akaza](https://github.com/pocke/akaza/blob/master/lib/akaza/ast_ext.rb)

```rb
module Habu
  module AnnotationCollectorHelper
    refine(Object) do
      def traversable?
        false
      end
    end

    refine(RubyVM::AbstractSyntaxTree::Node) do
      def traversable?
        true
      end

      def traverse(&block)
        block.call(self)
        children.each { |child| child.traverse(&block) if child.traversable? }
      end
    end
  end
end
```

case in用の`deconstruct`を生やしていい感じに探索する。

```rb
module Habu
  module AnnotationCollectorHelper
    refine(RubyVM::AbstractSyntaxTree::Node) do
      def deconstruct
        [type, *children]
      end

      def each_constructor_annotations(&block)
        klass_name = nil
        inject_annotation_found = false
        traverse do |ast|
          case ast
          in :CLASS, [:COLON2, nil, klass_name], *_
            # TODO: Support namespace
          in :IVAR, :@Inject
            inject_annotation_found = true
          in :DEFN, :initialize, *_ if inject_annotation_found == true
            block.call(klass_name)
            inject_annotation_found = false
          else
            inject_annotation_found = false
          end
        end
      end
    end
  end
end
```

こうすると直前に出てきたクラス定義のクラス名が`each_constructor_annotations`にわたしたブロックの引数として渡される。

あとはいい感じに溜め込んでいけばよいだけ。load_iseqの中から`AnnnotationCollector`のインスタンスのインスタンス変数につっこんでいく。`define_method`とmethod reference operatorを組み合わせるとこういう、prependしたモジュールから他のオブジェクトのインスタンスに副作用をおこすようなモジュールが手軽にかけてべんり。

```rb
require 'habu/annotation_collector_helper'

module Habu
  class AnnotationCollector
    using ::Habu::AnnotationCollectorHelper

    attr_reader :constructor_annotations

    def initialize
      @constructor_annotations = []
      @iseq_interceptor = new_iseq_interceptor
    end

    def install
      RubyVM::InstructionSequence.singleton_class.prepend(@iseq_interceptor)
    end

    private

    def add_constructor_annotation(klass)
      @constructor_annotations << klass
    end

    def new_iseq_interceptor
      add_constructor_annotation = self.:add_constructor_annotation
      Module.new do
        define_method :load_iseq do |fname|
          ast = RubyVM::AbstractSyntaxTree.parse_file(fname)
          ast.each_constructor_annotations(&add_constructor_annotation)
          RubyVM::InstructionSequence.compile_file(fname)
        end
      end
    end
  end
end
```

これの`install`は`habu/setup`のなかで行う。アノテーションの収集は実質グローバルなので`Habu.annotation_collector`経由でどこからでもアクセスできるようにしておく。

```rb
require 'habu'

module Habu
  module Setup
    class << self
      def install(annotation_collector)
        ::Habu.annotation_collector = annotation_collector
        ::Habu.annotation_collector.install
      end
    end
  end
end

Habu::Setup.install(::Habu::AnnotationCollector.new)
```

## DIコンテナの実装

### 定義・参照

雑にこうした。`#[]`は友達。

```rb
module Habu
  class Container
    def initialize
      @factories = {}
    end

    def [](key, &block)
      if block
        @factories[key] = block
      else
        @factories.fetch(key).call(self)
      end
    end
  end
end
```

参照方法については`Habu::Container#cache`を用意して常に同じインスタンスを参照できるようにしたり、メソッドも合わせて定義したりとかやり方が色々ありそう。定義の方法についてはブロック渡す一択でよいと思う。`#[]`以外のエイリアスを与えるのもあり。

### インスタンス化

メソッド引数との一致を見る。

```rb
module Habu
  class Container
    def new(klass, &block)
      params = klass.instance_method(:initialize).parameters
      klass.new(*params.filter_map { @1 == :req && self[@2] }, &block)
    end
  end
end
```

### コンテナ経由でのインスタンス化は面倒

直接newして手軽に使えたほうがべんりなので`@Inject`されてるクラスの`new`を全部上書きしてコンテナに移譲していく。

```rb
module Habu
  class Container
    def to_refinements
      refinements = Module.new
      refinements.instance_exec(self) do |container|
        Habu.annotation_collector.constructor_annotations.each do |klass_name|
          klass = const_get(klass_name)
          refine(klass.singleton_class) do
            define_method(:new) do |&block|
              container.new(klass, &block)
            end
          end
        end
      end
      refinements
    end
  end
end
```

## 将来的にやりたいこと

- `@Inject`されてるメソッドに関しては静的解析ではなくTracePointを使ってメソッド定義時に動的に取得したい
- コンストラクタインジェクション以外のサポート
- メソッド呼び出し形式での参照のサポート
- シングルトンで参照するようなコンテナの追加

## 真似をしたいrubyでDIライブラリ

Habu実装したあとから調べてみると dry-container/dry-auto_inject がよさそうだた。
やっぱりcontainerに参照できるメソッドを生やすのはよさそう。API真似していきたい。
ただし個人的にはmixinしなくても`delegate_to :container`みたいな形で移譲できるだけで十分なんじゃないかなと思う。

https://dry-rb.org/gems/dry-container/
https://dry-rb.org/gems/dry-auto_inject/

### 使用例

- [dry\-containerとdry\-auto\_injectでDIコンテナを作る \- blog\.kymmt\.com](https://blog.kymmt.com/entry/dry-container-and-dry-auto_inject)
- [Architecture of hanami applications \- RubyKaigi 2018](https://rubykaigi.org/2018/presentations/anton_davydov.html)

## 所感

- habu gemを使うとRefinementsでいいかんじに依存性を注入できる
- Rubyのインスタンス変数は実質アノテーション
- Rubyではソースコード中に書かれた式がRubyVMで実行されるとは限らない
- RubyVMで実行されないような式もASTなら取れる
- `RubyVM::InstructionSequence.load_iseq`を上書きするとRubyVMがソースコードをコンパイルする前にフックできる
- `#to_refinements`でRefinementsのモジュールを動的に作って返す手法はRubyキメた感があってよい


## 最後に

rubyで`@Inject`でキメるgemを作ったので使ってみてください。いいなとおもったらStarボタンよろしくお願いします。
https://github.com/hanachin/habu

Qiitaの他にGitHubもやっているのでよかったらフォローしてください。
https://github.com/hanachin