概要
原著者の許諾を得て翻訳・公開いたします。
- 英語記事: Using singleton objects as default arguments in Ruby | Arkency Blog
- 原文公開日: 2018/04/23
- 著者: Robert Pankowecki
- サイト: Arkency Blog
Ruby: シングルトンオブジェクトをデフォルト引数として使う(翻訳)
オプション引数を1つ取るメソッドを定義したくなることがときどきあるかと思いますが、プログラマーがこのメソッドにnil
を渡す可能性がないわけではありません。かつコードの側では、値が提供されない(つまりデフォルト値にする)場合とnil
の場合を区別する必要があるとします。どうやったらこれをうまくさばけるでしょうか?
こういう場合によく使われるのは、nil
やその他の空値やゼロ値をデフォルト値としてメソッドを定義するというものです。0
や文字列、空の配列などの場合であれば、この方法は理にかなっています。
class Foo
def bar(one, two: nil)
# ...
end
end
しかし、nil
と、値が指定されていない場合を区別する必要がある場合はどうしたらよいのでしょうか。具体的には、次の2つを区別する方法です。
foo.bar(:something, two: nil)
foo.bar(:something)
こんな場合に使えるのは、単一かつ一意のオブジェクトを定義して、それをデフォルトに用いる方法です。そして、渡された引数がnil
かどうかをチェックするのではなく、シングルトンオブジェクトであるかどうかをチェックします。
class Foo
NOT_PROVIDED = Object.new
def bar(one, two: NOT_PROVIDED)
puts one.inspect
if two == NOT_PROVIDED
puts "not provided"
else
puts two.inspect
end
end
private_constant :NOT_PROVIDED
end
private_constant
は無理して使う必要はありませんが、私はこのメソッドを引数に使えることと、privateクラスでも同じようなことができることをRuby開発者に思い出していただきたいのです。
Foo.new.bar(1)
1
not provided
Foo.new.bar(1, two: 2)
1
2
:not_provided
のようにシンボルを使ってもよいですし、数値などRuby内部で一意であるものなら何でも構いません。しかし後述のassert_changes
のような一般的なメソッドでは、引数に有効なオブジェクトが渡される可能性もあります。最善の方法は、引数として受け渡せない一意のオブジェクトを使うことです。
以下は、Railsでこれを用いてassert_changes
を実装する方法です。
assert_changes :@object, from: nil, to: :foo do
@object = :foo
end
assert_changes -> { object.counter }, from: 0, to: 1 do
object.increment
end
UNTRACKED = Object.new
def assert_changes(expression, message = nil, from: UNTRACKED, to: UNTRACKED, &block)
exp = if expression.respond_to?(:call)
expression
else
-> { eval(expression.to_s, block.binding) }
end
before = exp.call
retval = yield
unless from == UNTRACKED
error = "#{expression.inspect} isn't #{from.inspect}"
error = "#{message}.\n#{error}" if message
assert from === before, error
end
after = exp.call
if to == UNTRACKED
error = "#{expression.inspect} didn't changed"
error = "#{message}.\n#{error}" if message
assert_not_equal before, after, error
else
error = "#{expression.inspect} didn't change to #{to}"
error = "#{message}.\n#{error}" if message
assert to === after, error
end
retval
end
私はどうやら、RSpecのこのアプローチが好みです。
expect do
object.increment
end.to change{ object.counter }.from(0).to(1)
しかしその一方で、UNTRACKED
オブジェクトを用いるassert_changes
実装もありだと思います。
しかしこれは、論理値(boolean)引数にどこか似ています。これは、2つの異なるオブジェクトで定義されるべきフラグとしてよく用いられます。そこで、foo(1, true)
やfoo(1, false)
よりも、単にfoo(1)
やbar(1)
とする方がよいとされることが多く、私も普通はこのガイドラインに従います。ただしassert_changes
の場合であれば、名前付き引数とシングルトンオブジェクトの組み合わせはよいのではないかと思います。
お知らせ: もっと知りたい方へ
本記事を気に入っていただけた方は、Arkencyのニュースレターの購読をお願いします。開発者を悪い意味で驚かせないRailsアプリを構築するための弊社のノウハウを毎日お送りします。
よろしければ以下もどうぞ(訳注: いずれも英語記事です)。
- コンポジション可能なRSpecマッチャー - RSpecの
expect(domain_event).to be_an_event(OrderPlaced).with_data(order_id: 42).strict
マッチャーのうまい実装方法 inject
とeach_with_object
はどちらがよいか - 非常によく知られている2つのRubyメソッドについて違いを解説- Rubyの
===
(case等号)演算子を詳しく理解する -===
演算子の底力を知る - 相対テストと絶対テスト - 2つのテストモードを切り替えてテストを楽に書く
- RubyパーサーとASTツリーで非推奨化した構文を検出 - リファクタリング時にgrepだけでは足りないあなたに
弊社の最新刊『Domain-Driven Rails』もぜひどうぞ。特に、巨大で複雑なRailsアプリを扱ってる方に有用です。
ツイートより
これ私も使ったことあって有用…なんだけど、これ「シングルトン」オブジェクトって呼んじゃうことに違和感がある。オブジェクト指向の文脈ではGoFのシングルトンパターンかな? って思ってしまうけど、シングルトンパターンとは言えないからなあ https://t.co/0Oh2QuC8bg
— Satoshi Kojima (@skoji) June 15, 2018