2019-02-25

Rubyライブラリでよく見る `configure do ... end` による設定管理の仕組み

Rubyのライブラリで設定を管理するときに、次のようなパターンのコードをよく見ると思う。

Xyz.configure do |config|
  config.xxx = 'xxx'
  config.yyy = 'yyy'
end

このパターンを採用しているライブラリの1つである gruf というRuby製gRPCフレームワークのコードを読んで、その仕組みを追ってみたい。

configure を使ったサンプルコード

まずはイメージを具体的にするため、configure を呼び出している部分を確認しよう。 以下は gruf のREADME.mdに書いてあるサンプルコードだ。

require 'gruf'

Gruf.configure do |c|
  c.server_binding_url = 'grpc.service.com:9003'
end

以降、このサンプルコードを前提として話を進める。

configure メソッドの実装

早速 configure メソッドの実装を覗いてみよう。 def configure でgrepをかけるとlib/gruf/configuration.rb に定義されていることがわかった。

# lib/gruf/configuration.rb
module Gruf
  module Configuration
    # ...

    def configure
      yield self
    end

    # ...     
  end
end

yield self のたった1行だったが、これをちゃんと読み解いてみよう。 yield は与えられたブロックに処理を委譲する機能を持つ( https://docs.ruby-lang.org/ja/latest/doc/spec=2fcall.html#yield )。
そして self は、見たところ Gruf::Configuration だ。

yield の引数として渡される値は、呼び出す側にとってはブロック引数として受け取れる部分となる。今回のサンプルコードを思い浮かべると、ブロック引数 c の実体は Gruf::Configuration であるということだ。

# 再掲
require 'gruf'

# `c` は Gruf::Configuration
Gruf.configure do |c|
  c.server_binding_url = 'grpc.service.com:9003'
end

Configuration モジュールの取り込みとextend

ここでちょっと立ち止まる。
サンプルコードでは、Gruf.configure .. というように configure のレシーバは Gruf だった。しかし、今見つけ出した configure メソッドは Gruf::Configuration 以下に生えている。
このことから、何らかの方法で Gruf 側に Configuration を取り込んでいることが予想できる。それを追ってみよう。

すると lib/gruf.rb に次の定義を見つけることができた。

# lib/gruf.rb
module Gruf
  extend Configuration
end

Gruf モジュールが、Configuration モジュールを extend している。extend は引数のモジュールをselfの特異メソッドとして追加する機能を持つ( https://docs.ruby-lang.org/ja/latest/method/Object/i/extend.html )。
つまりこの場合だと Gruf モジュールの特異メソッドとして Configuration モジュールを取り込んでいるということだ。

設定項目の管理とクラスインスタンス変数

次に、具体的な設定項目(サンプルコードでは server_binding_url )がどう実装されているかを見てみよう。 これは lib/gruf/configuration.rbattr_accessor で定義されているインスタンス変数であることがすぐにわかる。

# lib/gruf/configuration.rb
module Gruf
  module Configuration
    VALID_CONFIG_KEYS = {
      # ...
      server_binding_url: '0.0.0.0:9001',
      # ...
    }.freeze

    attr_accessor *VALID_CONFIG_KEYS.key

    # ...
  end
end

設定値は参照と更新ができなければならないので、attr_accessor で定義されているのは自然だ。
そして、先ほど見たように Gruf::ConfigurationGruf に extend されることになる。 つまり、ここで定義されているインスタンス変数は Gruf の クラスインスタンス変数になる。

もしインスタンス変数で保持していた場合は、newで作成したインスタンスを保持しそのインスタンスを介して更新・参照をする必要が出てしまう。設定値のようなグローバルな情報を管理するにはやや不都合だと言えそうだ(そもそも Gruf::Configuration はモジュールなのでインスタンス化できないが...)。
クラスインスタンス変数で保持することで Gruf モジュールを介してどこからでもアクセスができるようになる。

実際に、server_binding_url という設定項目は lib/gruf/cli/executor.rblib/gruf/server.rb からも更新・参照されている

# lib/gruf/cli/executor.rb
module Gruf
  module Cli
    class Executor
      def setup!
        # ...

        Gruf.server_binding_url = opts[:host] if opts[:host]

        # ...
      end
    end
  end
end
# lib/gruf/server.rb
module Gruf
  class Server
    def initialize(opts = {})
      # ...

      @hostname = opts.fetch(:hostname, Gruf.server_binding_url)

      # ...
    end
end

まとめ

  • configure の実体は yield self の1行で、selfは Gruf::Configuration というモジュールだった
  • Gruf::Configuration は設定項目を attr_accessor を介して読み書き可能なインスタンス変数として定義していた
  • GrufGruf::Configurationextend することで、設定項目をクラスインスタンス変数として管理していた。これにより、各所から更新・参照ができる状態になっていた。