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.rb
の attr_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::Configuration
は Gruf
に extend されることになる。
つまり、ここで定義されているインスタンス変数は Gruf
の クラスインスタンス変数になる。
もしインスタンス変数で保持していた場合は、newで作成したインスタンスを保持しそのインスタンスを介して更新・参照をする必要が出てしまう。設定値のようなグローバルな情報を管理するにはやや不都合だと言えそうだ(そもそも Gruf::Configuration
はモジュールなのでインスタンス化できないが...)。
クラスインスタンス変数で保持することで Gruf
モジュールを介してどこからでもアクセスができるようになる。
実際に、server_binding_url
という設定項目は lib/gruf/cli/executor.rb
や lib/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
を介して読み書き可能なインスタンス変数として定義していたGruf
がGruf::Configuration
をextend
することで、設定項目をクラスインスタンス変数として管理していた。これにより、各所から更新・参照ができる状態になっていた。