Hibariya

「設定」を設計するための資料

プログラムは、なるべく何もしなくても良い感じに動いてくれるのが理想的だけど、実際には何らかのかたちでユーザの設定を必要とすることがある。 Rails を使うときは config/application.rb でタイムゾーンを指定したり、DB へ接続するための情報を config/database.yml に指定する。 Bundler の挙動を変えたければ bundle config で設定を変更する。 Gem をインストールするときに毎回指定したいオプションがあれば、~/.gemrc に追記する。

もし自分の関わるプロダクトに「設定」のAPIが必要になったとき、何を判断の基準にして設計すればいいだろう。 ちょっと近所を見渡すだけでも、「設定」のやり方には色々ありそうだ。 設定という視点から、Rubyist にとって身近なプロダクトたちを資料として眺めてみた。

(NOTE: ちょっと悩みながら「設定」という言葉を選んだけど、もしかしたら「入力」と言った方が良いかもしれない。ここで言いたかった「設定」というのは「プログラムが適切に動作するために必要なあらゆる入力」の中の色々だ。)

IRB: グローバルなハッシュを使う

irb は起動時に ~/.irbrc ファイルを読む。 ファイルの中身は Ruby スクリプトとして評価されるが、基本的には IRB.conf でアクセスできる Hash オブジェクトを変更することで設定を行なう。 起動時に tapp gem を require したいなら以下のように書く。

IRB.conf[:LOAD_MODULES] = %w(tapp)

IRB.conf には、どこからでもグローバルにアクセスできる。 irb は内部で IRB.conf をさまざまな場所からさまざまなタイミングで呼ぶ。 毎回直接呼ぶこともあれば、オブジェクトが生成されるタイミングにだけ呼んでインスタンス変数としてセットすることもある。

設定情報をひとつのグローバルな Hash オブジェクトとして保持するという方法は、 シンプルで比較的実装が簡単 そうだ。

RubyGems: 情報をデータの性格に応じて分ける

gem コマンドの挙動を変更したいなら、コマンドへオプションを渡す他に ~/.gemrc ファイルに設定を書く方法がある。 gem installgem update コマンドへ常に --no-document オプションを渡したいなら以下のように書く。 ファイルの中身はYAMLだ。

install: --no-document
update: --no-document

設定の内容は、 Gem.configurationGem::ConfigFile オブジェクト (Hash オブジェクトのようにアクセスできる) として保持される。 Gem.configuration は RubyGems 内の様々な場所から呼び出される。 例として gem install コマンドが実行される際には、コマンドラインオプションと Gem.configuration[:install] の中身がマージされる。

Gem.configuration が保持する情報は、 ~/.gemrc で設定できる項目だけではない。 rubygems.org 上の gem を管理する際に使う API キーも保持している。 ハッシュっぽいアクセス方法だと ~/.gemrc で設定できる一般的な設定を扱えるし、そうでない普通のメソッド呼び出しでは他の情報にもアクセスできる。

Gem.configuration[:install] # => "--no-document"
Gem.configuration.api_keys  # => {:rubygems=>"****************************"}

api_keys の値は ~/.gem/credentials から読み込まれたものだ。 慎重に扱うべき情報は明確に分けられ、別物として扱われている。 こうすることによって、例えば、通常の設定ファイル (~/.gemrc) から api_keys を指定できてしまうというような 望まない使われ方を簡単にはっきりと予防する ことになりそうだ。

ちなみに Gem.configuration にハッシュっぽくアクセスする際には、文字列もシンボルも同じキーとして扱われる。

Rack: オブジェクトに包む

rackup コマンドはカレントディレクトリの config.ru ファイルを読み込む。 ファイルの中身は Ruby スクリプトだ。

# rack に添付されている、ロブスターを表示するサンプル
require 'rack/lobster'

use Rack::ShowExceptions
run Rack::Lobster.new

config.ru の内容は Rack::Builder オブジェクトの中で評価される (instance_eval)。 Rack::Builder には Rack アプリケーションを構築するためのシンプルな DSL が実装されていて、 use map run を使えば、呼び出したいアプリケーションや挿し込みたいミドルウェアを指定できる仕組みになっている。

config.ru の中身は普通の Ruby スクリプトなので、簡単な Rack アプリケーションなら直接書いてもいい。 以下は固定の文字列を返すだけの単純な例だ。

run ->(env) {
  body = 'Hi, Rack.'

  [200, {'Content-Type' => 'text/plain', 'Content-Length' => body.length.to_s}, [body]]
}

config.ru を読み込む処理には eval 以外にもちょっとした仕掛けがあり、ファイルの先頭でコマンドラインオプションを指定できる。 以下の例の 1 行目は shebang ではないし、Ruby 的にはコメントだが、なんと rackup すると 4423 番ポートで起動する (デフォルトは 9292)。

#\ --port 4423

run ->(env) {
  # (省略...)
}

Rackが config.ru を評価し終えると、できあがった Rack アプリケーションがすぐに起動をはじめる。 必要なもの (サーバの起動オプションやミドルウェアの設定、そしてアプリケーションそのもの) は すべて Rack::Builder オブジェクトか Rack::Server オブジェクトのインスタンス変数として保持 されている。 グローバルにアクセスできる IRB.confGem.configuration のようなものは無い。

Rack::Config

Rack そのものの設定ではないが、Rack の仕組みを使って「Rack アプリケーションやミドルウェア間でやりとりするための」グローバルな設定を共有できる。 ほんの数十行からなる Rack::Config は、他のミドルウェアが必要としている値を設定するのに使える。

use Rack::Config do |env|
  env['greeting.default'] = 'Hi'
end

use GreetingSupportMiddleware # このミドルウェアが env['greeting.default'] を必要としている
run GreetingApp

Bundler

Gemfile: 内部 DSL を使う

依存したい gem を Bundler に教えるには Gemfile を使う。Gemfile の中身は以下のような感じだ。

source 'https://rubygems.org'

ruby '2.1.1'

gem 'middleman', '~> 3.2.2'

group :development do
  gem 'rake'
end

Bundler は Gemfile を読み込んで、依存関係の情報を Bundler.definitionBundler::Definition オブジェクトとして保持する。 IRB.confGem.configuration もそうだけど、プロセス毎にせいぜい 1 つしか持たなくていいような設定は、こんな風にモジュールで直接保持して問題無さそうだ。

Gemfile のための DSL は Bundler::Dsl に実装されている。Bundler::Dsl には、依存したい gem を指定するための gemgroup というようなメソッドが用意されている。 Gemfile には Ruby スクリプトを自由に書けるとはいえ、独自の コードを書く機会は滅多に無い。 そして Gemfile に書く内容は、 JSON や XML でも書けそうな内容だ。 それでも、 Gemfile のために設計された専用の書き方 はそのぶん読み書きがしやすい (ように思う)。

.bundle/config: 柔軟な指定を受け付ける

依存を指定するには Gemfile を使った。Bundler の具体的な動作を設定するには .bundle/config を使う。 ここには、gem をどこにどうやってインストールするか、のようなマシン毎に異なる情報を設定できる (そのためリポジトリには含まれないことが多い)。

.bundle/config の中身は以下のように YAML に似た KEY: VALUE 形式で記述される。KEY には BUNDLE_ というプリフィクスがつく。

---
BUNDLE_PATH: put/gems/here

このファイルは手で編集してもいいが、 bundle config コマンドで設定した方が簡単だ (詳細: bundle help config)。

設定を保持できる場所は .bundle/config 以外にもある。それぞれ影響する範囲が異なり、優先順位がある (上の方が優先される)。

これをどう実現しているかは実装を見た方が早い。value に代入している箇所がそれだ。

# lib/bundler/settings.rb の一部
module Bundler
  class Settings
    # (中略...)
    def [](key)
      the_key = key_for(key)
      value = (@local_config[the_key] || ENV[the_key] || @global_config[the_key])
      is_bool(key) ? to_bool(value) : value
    end
    # (中略...)
  end
end

.bundle/config ファイルには ERB で式を埋め込むようなことはできないが、必要に応じて 環境変数で指定したり、プロジェクト毎に異なる指定ができる

読み込まれた設定は、Bundler.settingsBundler::Settings オブジェクトとして保持される。 このオブジェクトにはハッシュっぽくアクセスできる。 が、普通の Hash オブジェクトとは違い、:path'path''PATH' も同一のキーとして扱われる。

ちなみに、 Bundler::Settings には 設定を書き込むための実装 もある。Bundler::Settings#[]= は local な .bundle/config ファイルへ、Bundler::Settings#set_global は global な ~/.bundle/config ファイルへそれぞれ値を上書きする。

Ruby on Rails

database.yml: 式を埋め込める YAML

SQLite、PostgreSQL や MySQL などのデータベースへの接続情報は config/database.yml に設定する。 内容は YAML 文書で、かつ ERB で Ruby の式を埋め込む ことが可能だ。 実行する環境によって接続するデータベースを変えたり、デプロイ先での接続情報を環境変数などから読み込みたいときに使われる。

こういった設定方法をお手軽に採用したいときには Settingslogic のような既存の gem を使えそうだ。

YAML を設定に使う利点として特筆したいのは、アンカーとエイリアスによって似たような内容をまとめられる点だ。 このおかげで、似たような設定項目があるときにすっきり書ける。

development: &development
  adapter: postgresql
  username: postgres
  encoding: unicode
  pool: 5
  database: app_development

test:
  <<: *development # ここに development の内容がドカッと入る
  database: app_test

Railtie: 複数のコンポーネントと協調する

Railtie は、ActiveRecord など Rails の各コンポーネント、そして Rails アプリケーションの基盤となるコアだ。 プラグインや Engine など Rails を拡張するための何かを実装したいとき、Rails の世界で初期化処理をいいタイミングに実行したいとき、そして Rails アプリケーションと設定をやりとりしたいときには、Railtie の機能が使える。

Railtie で設定を共有する

Railtie を使って Rails を拡張するには Rails::Railtie クラスを継承する。 Rails::Railtie.config は設定を出し入れするためのオブジェクト (Rails::Railtie::Configuration) を返す。 このオブジェクトに設定した値は、 Rails アプリケーション全体でグローバルに共有される。 例として、Rails を起動する直前に以下のようなコードを読み込んでみよう。

# config/environment.rb の先頭あたりでこのコードを require する
module MyExtention
  class Railtie < ::Rails::Railtie
    config.my_extention_value = 'Hi'
  end
end

設定した my_extention_value には様々な場所からアクセスできる。 例えば config/environments/development.rb の中から。

# config/environments/development.rb
Hi::Application.configure do
  config.my_extention_value # => "Hi"
  # (中略...)
end

MyExtention と Rails アプリケーションとの間でうまく設定を共有できていることが確認できる。 この値には Rails.application.config でRails アプリケーションのどこからでもアクセスできる。

ところで、この例でアクセスしている config と MyExtention::Railtie 内の config は、実は異なる別々のオブジェクトだ。 にもかかわらず、 事前に設定した値に問題無くアクセスできている。 何が起こっているのかより詳しく言うと、 Rails::Railtie::Configuration オブジェクトにセットした設定に Rails::Application::Configuration オブジェクトを通してアクセスできている。 とても不思議だ。

なぜそんなことができるのかというと、実は config に出し入れした値は Rails::Railtie::Configurationクラス変数 として保持されていて、「config オブジェクト」のクラスはみなこのクラスを継承しているからだった。

Railtie の config オブジェクトを見ていると、クラス変数をうまく使うことで 同じ設定を共有しつつ異なる API を提供できる ことがわかる。 例えば、 Rails::Railtie::Configuration はデータベースの接続情報など持たないが、 Rails::Application::Configurationdatabase_configuration というメソッドを持つ。 そういった違いがありつつも、ふたつのクラスは同じ設定 my_extention_value を共有している。

Rails.application.config.class                     # => Rails::Application::Configuration
Rails.application.config.my_extention_value        # => "Hi"
Rails.application.config.database_configuration    # => {"development"=>{"adapter"=>"sqlite3", "database"=>"db/...}

MyExtention::Railtie.config.class                  # => Rails::Railtie::Configuration
MyExtention::Railtie.config.my_extention_value     # => "Hi"
MyExtention::Railtie.config.database_configuration # NoMethodError

sprockets-rails の例

Rails::Railtie::Configuration (とそれを継承したクラスたち) は、Rails アプリケーション起動時の様々なタイミングでフックできるメソッドを供えている。 これを利用すれば、アプリケーションの初期化前にデフォルト値を設定しておき、初期化中にユーザ (プログラマ) によってカスタマイズされた設定値を、初期化後に受け取って利用することが可能になる。

具体的な例を見てみる。 sprockets-rails の Sprockets::Railtie は、config.assets.prefix という値を扱う。 該当個所を以下に抜き出してコメントを書いてみた。

module Sprockets
  class Railtie < ::Rails::Railtie
    # (中略)

    # 1. sprockets-rails が require されるとこの辺が実行される
    config.assets = OrderedOptions.new
    config.assets._blocks    = []
    config.assets.paths      = []
    config.assets.prefix     = "/assets" # 2. ここでデフォルト値が設定される
    # (中略)
    config.after_initialize do |app|
      # 3. Rails アプリケーションの初期化が完了すると、このブロックが実行される
      config = app.config
      # (中略)
      ActionView::Base.instance_eval do
        include Sprockets::Rails::Helper

        # Copy relevant config to AV context
        self.debug_assets  = config.assets.debug
        self.digest_assets = config.assets.digest
        self.assets_prefix = config.assets.prefix # 4. ここで最終的に設定された値がセットされる
        # (中略)
      end
      # (中略)
    end
  end
end

config.assets.prefix というのは precompile した assetsをどこに配置するか指定するもので、デフォルトは '/assets' になっている。 その結果、通常は precompile 済みの assets が public/assets 下に配置される。 この値は Rails アプリケーションの設定 (config/application.rb など) で、以下のように上書きできる。

# config/application.rb

# (中略)
module Hi
  class Application < Rails::Application
    config.assets.prefix = '/put-assets-here'
    # (中略)
  end
end

上記のコードが実行されアプリケーションの初期化処理も完了すると、Sprockets::Railtie クラスで定義された config.after_initialize フックが呼び出される。 こうしてカスタマイズされた値 '/put-assets-here'ActionView::Base.assets_prefix にセットされるという寸法だ。

Railtie が提供するフックポイントは色々ある。 before_initializebefore_configuration など、after_initialize 以外にも様々だ。

ActiveSupport::OrderedOptions

これは Railtie とはあまり関係無いけれど、もしも MyExtention に関する設定が複数あり、それも階層で表現できた方がより都合が良いときは ActiveSupport::OrderedOptions を利用できる。これはハッシュのような API を備えていて、かつ、キーの名前をメソッド名のようにして呼び出すことができる。

module MyExtention
  class Railtie < ::Rails::Railtie
    config.my_extention = ActiveSupport::OrderedOptions.new
    config.my_extention.alpha = 'Alpha'
    config.my_extention.bravo = 'Bravo'

    config.my_extention.alpha # => 'Alpha'
    config.my_extention.bravo # => 'Bravo'
  end
end

最後に

「設定」の API はユーザに公開する「変更が大変な部分」なので、なるべく先を見越して設計しておきたい。 「この項目は本当にこんな名前で、こういう扱いでいいのか」ということを悩んだときには、ここで挙げたような先人の知恵が助けになりそうだ。

バージョン情報