Иногда вы хотите определить метод, который принимает необязательный аргумент, но программист может передать 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, которые вас не удивят.
Возможно, вам понравится читать:
- Составные сопоставители RSpec - как реализовать красивые сопоставители RSpec
expect(domain_event).to be_an_event(OrderPlaced).with_data(order_id: 42).strict
- Inject vs each_with_object - 2 очень популярных рубиновых метода, в чем разница между ними
- Объяснение оператора === (равенство регистров) в Ruby - познакомьтесь с мощью
===
operator - Относительное тестирование против абсолютного - 2 режима тестирования, между которыми вы можете переключаться, чтобы упростить написание тестов.
- Использование синтаксического анализатора ruby и дерева AST для поиска устаревшего синтаксиса - когда grep недостаточно для рефакторинга.
Также не забудьте ознакомиться с нашей последней книгой Domain-Driven Rails. Особенно, если вы работаете с большими и сложными приложениями на Rails.