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
は与えられたブロックに処理を委譲する機能を持つ(メソッド呼び出し(super・ブロック付き・yield) (Ruby 2.6.0))。
そして 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の特異メソッドとして追加する機能を持つ(instance method Object#extend (Ruby 2.6.0))。
つまりこの場合だと 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