こんにちは、hachi8833です。今回はBPSのSlackでのやりとりを元に記事にいたしました。
私も軽々しい方なので、メソッド定義でoptions = {}
が使いたくなったらよく考えることにします。
= {}
引数の使い方と歴史
今回の記事化にあたり、morimorihogeさんから= {}
引数について詳しく教えていただきました。
本題に入る前に、ここでは= {}
引数について必要な部分のみをまとめ、Rubyの引数そのものについては別記事といたします。
はじめに: 「オプション」とは
まずおさえておきたいのは、ITの文脈でオプション(option)という言葉を使ったら、その項目は原則として「必須ではない」「あってもなくても動く」という共通理解が求められるということです。つまり、「このオプションを指定しないと動かない」ような必須項目は、オプションとは呼びません。
英語辞書に載っているoptionの意味には「選択肢」「選択科目」「(金融用語の)オプション」などがありますが、ITで使われる「オプション」には原則として「指定しなくてもよい」という項目も選択肢に含めておくべきです。自分がたとえそう思わなくても、「オプション」という言葉を使えば相手はそう考えると思っておく必要があります。
もちろん現実には必ずしも「オプション」という言葉がそのように使われていないことは多々あります。
「OSをアップグレードするか、後にするか」ダイアログのオプションに「OSをアップグレードしない」という選択肢はないこともよくありますし、選択肢全般を口頭でカジュアルに「オプション」と呼んでしまうこともよくあるでしょう。
= {}
について
Rubyでは、メソッド定義の最後(ブロックを除く)にオプション名 = {}
のような引数を置くことで、可変長のハッシュを受け取る引数オプション的な挙動を実現できます。この引数の名前はもちろんoptions以外でも何でも構いません。
def hoge(one, two, options = {})
end
上のようなメソッド定義に対して、hoge('1st', '2nd', opt1: true, opt2: true, opt3: 'hugahuga')
のような多数の引数を持つ呼び出しを行えます。opt1: true, opt2: true, opt3: 'hugahuga'
のハッシュ部分は可変長なのでいくらでも長くできます。
= {}
がメソッド定義に複数ある場合
= {}
は可変長のため、引数で= {}
を複数使いたい場合には一工夫必要です(その是非は今は考えないことにします)。
- 呼び出し側で1つ目のオプション引数を
{ }
で囲まないと、1つ目のopt1 = {}
にすべて吸い込まれてしまい、2つ目のopt2 = {}
が空になってしまいます。 - かといって2つ目の
opt1 = {}
に渡す引数だけを{ }
で囲むと、今度はエラーになってしまいます。
def hoge(one, opt1 = {}, opt2 = {})
pp one, opt1, opt2
end
# 期待どおりにならない
hoge :lion, lion: :female, tiger: :male, jibanyan: :cat
#=> [:lion, {:lion=>:female, :tiger=>:male, :jibanyan=>:cat}, {}]
# エラー
hoge :lion, lion: :female, tiger: :male, {jibanyan: :cat}
#=> SyntaxError: unexpected '\n', expecting =>
# 期待どおり
hoge :lion, {lion: :female, tiger: :male}, jibanyan: :cat
#=> [:lion, {:lion=>:female, :tiger=>:male}, {:jibanyan=>:cat}]
メソッド定義に= {}
引数が複数ある場合、メソッド呼び出しで{ }
を省略できるのは最後の={}
だけです。
参考: Railsコードでオプション引数を渡すときに{ }
が必要な例
ビューのForm系メソッドなどで、オプション引数を{ }
で囲んで呼び出さないとうまく動かないものがときどきあります。
例: ActionView::Helpers::FormOptionsHelper#select
この#select
メソッド呼び出しでhtml_options = {}
にclass: 'select2'
というオプションを渡したい場合、以下のように明示的に{ }
で囲まないと1つめのoptions = {}
に全部吸い込まれてしまいます。これが地味に面倒です。
# ちゃんと動かない
select(obj, :obj_code, ['a', 'b', 'c'], include_blank: true, class: 'select2')
# 動く
select(obj, :obj_code, ['a', 'b', 'c'], {include_blank: true}, {class: 'select2'})
Rubyの記法ではブロックとハッシュの両方で{ }
が使われますが、ブロックではないハッシュを呼び出しでじかに{ }
で囲まないといけないというのもちょっと気持ち悪いですね。
options = {}
の歴史
2.0以前のRubyにはキーワード引数がありませんでした。当時のRailsを含むさまざまなRubyプログラムでは、キーワード引数的なものを使うためにoptions = {}
でハッシュのkeyとvalueでそれっぽいものをこしらえて使っていました。
キーワード引数が使える今なら、何でもかんでも= {}
引数で受け取る必要はだいぶ下がってきました。
ただ= {}
で受け取ったハッシュにたっぷり含まれているkey-valueペアをサブメソッドにそのまま投げられる(委譲)という特性があるので、そうした目的のために= {}
が使われることはまだあるかもしれません。
Railsフレームワークで多用される「 options = {}
引数」について
ここから本題に戻ります。
Railsフレームワークのコードでは、メソッド定義の引数にこのoptions = {}
という名前のオプション引数が多用されています。キーワード引数がなかった時代の名残であるものが多いのではないかと思います。
# actionpack-4.2.8/lib/action_controller/metal/rendering.rb 31行目より
def render_to_body(options = {}) # <= コレ
super || _render_in_priorities(options) || ' '
end
メソッド定義でoptions = {}
引数を使うメリット
主にフレームワークやライブラリのメンテナが恩恵を受けます。メソッドを最初に書く人と、次回あたりにオプションを拡張する人がラクできます。
APIを最初に設計した時点では、将来どんなオプションが欲しくなるかを見通すのは簡単ではありません。しかしそこでじっくり考えているといつまでたっても実装が進みませんので、引数にoptions = {}
を使ってoptions
で何でも受けられるようにしておくという手法があります。
将来オプションの数が増えたときもメソッドの引数部分を更新せずにすみますので、オプションが(変更ではなく)追加である限り既存のAPI利用者に影響を与えずに済みます。
前述のとおりoption
の引数は簡単に別メソッドに委譲できるので、メソッドに渡せばkeyで取り出せます。API利用者が与えたkeyが間違っていれば誰も処理しないだけなので何も起こらず、自然とオプションらしい動作になります。
メソッド定義でoptions = {}
引数を使うデメリット
options
引数のオプションが1つ2つ程度そのメソッド内だけで使われている限り、ほとんど問題にはなりません。
怖いのはそのメソッドが育って大きくなり、メソッド呼び出しのネストが深くなったときです。そのあおりをうけるのは、主にアプリ開発者と遠い将来のAPIメンテナです。特にアプリのデバッグがつらくなります。
options
引数が上述のような便利な性質を備えているために、APIの成長に伴って、メソッド内の別メソッド呼び出しで引数にoption
引数がそのまま中継されてしまい、しかもそれが繰り返しネストされてしまうことがしばしば発生します。なお、アンダースコア_
で始まるメソッド名はprivate
メソッドであることを示すのによく使われます。
# actionpack-4.2.8/lib/action_controller/metal/rendering.rb 63行目より
def _normalize_options(options)
_normalize_text(options) # <= 上位から受け取ったoptionsをそのまま中継している
#(略)
end
さらにネストの途中で、option
引数を読んだり書き換えたりするメソッドがあるかと思うとoption
を使わずにさらに下に中継するだけのメソッドもある、というように、options
引数の使い方がバラバラになると深刻です。以下の例ではA〜Kがメソッドで、階層はメソッド呼び出しを表し、すべての呼び出しでoptions
を受け渡しているとします。
ネストの奥底あたりのメソッドでオプションの利用法が他のメソッドとたすきがけで依存していたり、本来必須ではないはずのオプション項目が深いところで必須扱いになり、大元の呼び出しオプションまで必須になってしまっているとイラッと来たりします。
このようにoption
引数の利用場所が分散してしまうと、元のメソッドのoptions
引数のすべてのオプションがどうなっているのかを見通せなくなってしまいます。知りたければ呼び出しのネストを一番深いところまですべて追い、オプションの処理をひとつひとつ拾い上げて理解して、自分でAPIドキュメントみたいなものをこしらえるはめになってしまいます。
あるいは、デバッガでたまたまこうしたoptions
ネストに迷い込んでしまい、options
の全体像がわからなくてハマったりします。
options
という名前が広範囲にわたって使われていると、ひどいときにはIDEのサジェスチョンがループすることすらあるそうです。
babaさんいわく、options = {}
引数を使うなら、将来オプションが30個ぐらいにふくれあがる覚悟が必要とのことです。
要点: 最適なコードは立場や状況によって変わることがある
babaさんとmorimorihogeさんの冒頭のやりとりを元に、私なりにまとめました。
重要なのは、フレームワーク開発者(またはライブラリの開発者)の多くは options = {}
引数にともなうこうした問題を理解したうえで、開発速度や拡張性、API利用者の利便性などとのトレードオフを経て導入しているはずだということです。また、時代とともに手法も移り変わります。最適なコードは立場や状況や時期によって違うことがあると考えるのが、少なくとも私にとっては自然です。
むしろ注意したいのは、「フレームワークで使われている手法だから大丈夫」という理由だけでアプリの開発に options = {}
引数を安易に導入してしまうことであると理解しました。options = {}
引数は確かに便利です。だからこそチームによるアプリ開発でもこうした潜在的な問題について理解しておきたいと思いました。
私を含め、多くの人が「これさえやっておけば大丈夫」というような定番の手法をつい探してしまいがちなのですが、無条件のベストプラクティスというものは実はかなり限られているので、やはりほとんどの手法は限られた時間の中でトレードオフを繰り返しながらその都度選択するしかありません。今後Railsのソースを読むときにはフレームワーク開発者の立場や状況にも注意したいと思います。
options = {}
引数をどうするか
options = {}
引数には、まとまったオプションを楽に委譲できるというメリットが確かにあります。メソッド定義の引数でのオプション追加・変更・削除は大変ですし、定義の引数が10数個にもなると見た目も悪く、可読性も落ちます。
メソッド定義の引数を縦にアラインすることで見た目の問題は解消できますが、オプションをひとつひとつサブメソッドに渡すのはやはりつらいので、何らかの方法でまとめたいという需要は引き続き残ります。その一方、オプションの情報や処理の拡散も何とかしたいところです。
もちろん、書き方としてはoptions = {}
のほかに*
を使った可変長引数や**
を使ったオプション引数などもありますが、プロジェクトのスタイルや要件に応じて考慮しましょう。
状況にもよりますが、必須の引数は原則としてキーワード引数にするのがよいと思います。
options = {}
で拡散した情報をどうにかできるか
メソッド呼び出しの階層をクロールして、options = {}
のオプションを自動ですべてかき集められればよいかもしれませんが、メソッドごとの使い方が一定してない以上現実には無理そうです。
呼び出しの階層が進むといつしかメソッドがprivateになっていることがよくありますが、privateメソッドはその性質上コメントがまったくないか非常に少ないことが多いので、呼び出し階層の深いところでオプションの意味を理解するにはコードやコミットメッセージを頼りにするしかなくなることが多くなります。
最初にoptions
を受けたメソッドに、がんばってすべての情報をコメントで残す方法も現実的ではなさそうです。そういうコメントはたいていメンテされなくなるのがオチなので。
しかし少なくとも、引数で受け取ったoptions
をそのまま投げ合うのは避けたいですね。
他の工夫
それよりは、受け取ったoptions
をメソッド間で直接渡さないようにする方法と、情報の拡散を防ぐ方法を早い段階で検討する方がよさそうです。
方法はいろいろ考えられますが、思いつく範囲で書いてみます。
1. Struct#new
にオプションを保存してから渡す
RubyのStruct#new
を使って構造体のクラスを作り、最初にoptions = {}
引数を受け取ったメソッドがオプションを構造体のインスタンスに保存するというシンプルな方法です。
Webチームの元じゃばーkazzさんによると、これは引数オブジェクトと呼ばれるリファクタリング技法に相当するそうです。
Struct#new
で定義したattributeはすべてリードライト可能になりますので、外部から受け取ったオプションにも使えますが、どちらかというと画面上の座標のような動的な値の受け渡しに向いていそうです。attributeごとのリードライトの細かい制御ができないので、なるべくオプションを小規模に保っておこうと思います。
# 構造体用の軽いクラスを作成
MyOpts = Struct.new(
:address, # メールサーバーのアドレス(デフォルト: localhost)
:port, # メールサーバーのポート番号(デフォルト: 25)
:domain, # HELOのドメインを指定
:authentication # 認証方式(`:plain`, `:login`, `:cram_md5`)
:user_name # 認証のユーザー名
:password # 認証のパスワード
)
# 最初に構造体にオプションを保存する
def mymy_method(options = {})
mymy_opts = Struct.new(
options[:address],
options[:port],
options[:domain],
options[:authentication],
options[:user_name],
options[:password]
)
sub_method(mymy_opts)
sub_method2(mymy_opts)
sub_method3(mymy_opts)
...
end
# サブメソッドは構造体経由でオプションを利用する
def sub_method(mymy_opts)
if mymy_opts.address.nil?
#(略)
end
sub_sub_method(mymy_opts) # さらに別メソッドに投げる
end
attributeは文字列でも書けますが、シンボルで書くのが定番ですし便利です。
一手間かけることで、オプションの定義や用途を一箇所にまとめられますし、チーム開発で誰かがサブクラスにオプションを追加するときも自然にStruct#new
の定義とインスタンス生成をメンテするので、情報が古くなりにくいというメリットがあると思います。
構造体なので、Struct#new
のインスタンス生成時の値代入ではキーを指定しなくてよい(順序のみ守ればよい)のも地味に助かります。また、attributeにドット記法.
でアクセスできるのも個人的にはうれしいです(タイプ量が減るので)。
grepしやすい(かつオプションとわかる)名前をインスタンスにつけておけば、ソース内で拡散しまくった同名のoptions
のように途方に暮れなくて済みそうです。
構造体にないkeyをオプションに与えられても、options[]
から構造体に代入されないだけなので、オプションらしさを維持できます。
オプションの増加に伴い、オプションを書き換え禁止にしたいとか、型チェックしたいとか、初期化処理を加えたいといった要件が加わったら、そのときにStruct#new
を普通のクラス定義に書き換えればよいでしょう。
「そこが面倒なんだよ」という方は、次をご覧ください。
2. 初期化を支援するgem
この間のRailsウォッチ(20170317)でmorimorihogeさんに教わったばかりですが、Struct#new
よりもっと複雑な初期化を支援するgemがあります。これも引数オブジェクトとして活用できそうです。
両gemの機能や目的は似通っていて、require
してクラスにinclude
することで使えます。virtusはRuby全般、active_attrはその名のとおりRailsのActiveRecordを意識しているようです。
# Virtusの使用例
module MyModule
require 'virtus'
class Nw
include Virtus.model
attribute :str_a, String
attribute :str_b, String
attribute :dim_a, Integer
attribute :dim_b, Integer
attribute :mod_a, String
attribute :mod_b, String
attribute :f_ary, Array
end
class Bt
include Virtus.model
attribute :x, Integer
attribute :y, Integer
attribute :diag, Integer
attribute :up, Integer
attribute :left, Integer
end
def my_method
str_a, str_b = 'hoge', 'huga'
nw = Nw.new(
str_a: str_a,
str_b: str_b,
dim_a: str_a.length,
dim_b: str_b.length,
mod_a: "", mod_b: "",
f_ary: Array.new(str_a.length + 1) {Array.new(str_b.length + 1) {0}}
)
sub_method(nw)
end
def sub_method(nw)
bt = Bt.new(
x: nw.str_a.length - 1,
y: nw.str_b.length - 1,
diag: 0, up: 0, left: 0
)
pp bt.x, bt.y
end
end
初期化での型チェック、バリデーション、初期値の設定など、多くの機能があります。私はまだvirtusを少し触ってみた程度なので、別途記事にしてみたいと思います。
追記(2018/11/19)
Rubocopで、オプション引数よりもキーワード引数が推奨されるようになりました。