Tech Racho エンジニアの「?」を「!」に。
  • 開発

Ruby: シングルトンオブジェクトをデフォルト引数として使う(翻訳)

概要

原著者の許諾を得て翻訳・公開いたします。

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アプリを構築するための弊社のノウハウを毎日お送りします。

よろしければ以下もどうぞ(訳注: いずれも英語記事です)。

弊社の最新刊『Domain-Driven Rails』もぜひどうぞ。特に、巨大で複雑なRailsアプリを扱ってる方に有用です。

ツイートより

関連記事

Rails: モデルの外では名前付きスコープだけを使おう(翻訳)


CONTACT

TechRachoでは、パートナーシップをご検討いただける方からの
ご連絡をお待ちしております。ぜひお気軽にご意見・ご相談ください。