「設定」を設計するための資料
プログラムは、なるべく何もしなくても良い感じに動いてくれるのが理想的だけど、実際には何らかのかたちでユーザの設定を必要とすることがある。
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 install
や gem update
コマンドへ常に --no-document
オプションを渡したいなら以下のように書く。
ファイルの中身はYAMLだ。
install: --no-document
update: --no-document
設定の内容は、 Gem.configuration
に Gem::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.conf
や Gem.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.definition
に Bundler::Definition
オブジェクトとして保持する。
IRB.conf
や Gem.configuration
もそうだけど、プロセス毎にせいぜい 1 つしか持たなくていいような設定は、こんな風にモジュールで直接保持して問題無さそうだ。
Gemfile
のための DSL は Bundler::Dsl
に実装されている。Bundler::Dsl
には、依存したい gem を指定するための gem
や group
というようなメソッドが用意されている。
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
以外にもある。それぞれ影響する範囲が異なり、優先順位がある (上の方が優先される)。
.bundle/config
で local (プロジェクト毎) に保持- 環境変数として保持
~/.bundle/config
で global (ユーザ毎) に保持
これをどう実現しているかは実装を見た方が早い。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.settings
に Bundler::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::Configuration
は database_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_initialize
や before_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 はユーザに公開する「変更が大変な部分」なので、なるべく先を見越して設計しておきたい。 「この項目は本当にこんな名前で、こういう扱いでいいのか」ということを悩んだときには、ここで挙げたような先人の知恵が助けになりそうだ。
バージョン情報
- irb 0.9.6
- rubygems 2.2.2
- rack 1.5.2
- bundler 1.5.3
- rails (railties) 4.0.2
- sprockets-rails 2.0.1