Иногда вы хотите определить метод, который принимает необязательный аргумент, но программист может передать nil. И ваш код должен различать не предоставленное значение (значение по умолчанию) и nil. Как этого добиться?

Обычное решение для значения по умолчанию - определить их как nil или другие пустые / нулевые значения, которые имеют смысл, такие как 0 или строка, пустой массив и т. Д.

class Foo
  def bar(one, two: nil)
    # ...
  end
end

Но что, если вам нужно различать nil и отсутствие значения? Что делать, если вы хотите различать:

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 не обязательно, но мне нравится напоминать разработчикам Ruby, что мы можем использовать его целую вечность и что таким образом у нас могут быть частные классы.

Foo.new.bar(1)
1
not provided
Foo.new.bar(1, two: 2)
1
2

Вы можете использовать символ (:not_provided), число или что-нибудь еще, уникальное для Ruby, но в общих методах (например, assert_changes, описанных ниже) они могут быть допустимыми объектами, которые должны быть предоставлены в качестве аргумента. Итак, лучший способ решить эту проблему - использовать уникальный объект, который никто не может передать в качестве аргумента.

Вот как Rails использует его для реализации assert_changes:

### API ###
assert_changes :@object, from: nil, to: :foo do
  @object = :foo
end
assert_changes -> { object.counter }, from: 0, to: 1 do
  object.increment
end
### Implementation ###
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)

но я восхищаюсь реализацией assert_changes, в которой используется объект UNTRACKED.

Хотя это похоже на использование логических аргументов, которые часто указывают на необходимость определения двух отдельных методов. Поэтому вместо foo(1, true) и foo(1, false) часто утверждают, что лучше иметь просто foo(1) и bar(1), и я обычно согласен с этим правилом. Однако в случае assert_changes использование именованных аргументов и одноэлементного объекта мне кажется нормальным.

Хотели бы вы продолжить обучение?

Если вам понравилась эта история, подпишитесь на нашу рассылку новостей. Мы делимся своими повседневными проблемами и решениями по созданию поддерживаемых приложений на Rails, которые вас не удивят.

Возможно, вам понравится читать:

Также не забудьте ознакомиться с нашей последней книгой Domain-Driven Rails. Особенно, если вы работаете с большими и сложными приложениями на Rails.

изначально опубликовано в блоге Arkency