- PVSM.RU - https://www.pvsm.ru -
Наверняка вы сталкивались с ситуацией, когда есть достаточно жирный метод, и вам приходится вынести часть его кода в отдельный метод и ваш класс/модуль переполняется методами, которые относятся к одному единственному методу и нигде более не используется. Ужасный каламбур, правда?
Если вы просто хотите ознакомиться с реализацией класса, то эти самые вспомогательные методы очень сильно мозолят глаза, приходится прыгать по коду туда-сюда. Да, конечно, можно разнести их по отдельным модулям, но я считаю, что зачастую это слишком избыточно (я, например, не хочу создавать модуль, который, по сути, определяет только один метод, декомпозированный на n частей). Особенно неприятно, когда эти вспомогательные функции состоят из одной строки (например, метод, который выдергивает определенный элемент из распарсенного JSON).
И раз уж я заговорил о парсинге, то давайте приведу несколько синтетический пример.
Сгенерировать Hash с курсом разных валют по отношению к рублю. Примерно такой:
{ 'USD' => 30.0,
'EUR' => 50.0,
... }
На сайте Центробанка есть такая страница: http://www.cbr.ru/scripts/XML_daily.asp [1]
Собственно, все можно сделать вот так:
require 'open-uri'
require 'active_support/core_ext/hash' # for Hash#from_xml
def rate_hash
uri = URI.parse('http://www.cbr.ru/scripts/XML_daily.asp')
xml_with_currencies = uri.read
rates = Hash.from_xml(xml_with_currencies)['ValCurs']['Valute']
rates.map(&method(:rate_hash_element)).to_h
end
def rate_hash_element(rate)
[rate['CharCode'], rubles_per_unit(rate)]
end
def rubles_per_unit(rate)
rate['Value'].to_f / rate['Nominal'].to_f
end
Либо классом:
require 'open-uri'
require 'active_support/core_ext/hash' # for Hash#from_xml
class CentralBankExchangeRate
def rubles_per(char_code)
rate_hash_from_cbr[char_code] || fail('I dunno :C')
end
#
# other public methods for other currencies
#
private
# Gets daily rates from Central Bank of Russia
def rate_hash_from_cbr
uri = URI.parse('http://www.cbr.ru/scripts/XML_daily.asp')
xml_with_currencies = uri.read
rates = Hash.from_xml(xml_with_currencies)['ValCurs']['Valute']
rates.map(&method(:rate_hash_element)).to_h
end
# helper method for #rate_hash_from_cbr
def rate_hash_element(rate)
[rate['CharCode'], rubles_per_unit[rate]]
end
# helper method for #rate_hash_element
def rubles_per_unit(rate)
rate['Value'].to_f / rate['Nominal'].to_f
end
#
# other private methods
#
end
Не будем рассуждать о том, какие библиотеки стоило использовать, будем считать, что у нас есть рельсы и поэтому воспользуемся Hash#from_xml
оттуда.
Собственно, нашу задачу решает метод #rate_hash
, в то время как оставшиеся два метода являются вспомогательными для него. Согласитесь, что их присутствие очень сильно отвлекает.
Обратите внимание на переменную xml_with_currencies
: ее значение используется всего-лишь один раз, а это значит, что ее наличие совсем необязательно и можно было написать Hash.from_xml(uri.read)['ValCurs']['Valute']
, однако, как мне кажется, ее использование чуть-чуть улучшает читаемость кода. Собственно, появление вспомогательных методов — это тот же самый прием, но для кусков кода.
Как вы уже, наверное, догадались по заголовку, я предлагаю использовать для таких вспомогательных методов лямбы.
require 'open-uri'
require 'active_support/core_ext/hash' # for Hash#from_xml
def rate_hash
uri = URI.parse('http://www.cbr.ru/scripts/XML_daily.asp')
xml_with_currencies = uri.read
rates = Hash.from_xml(xml_with_currencies)['ValCurs']['Valute']
rubles_per_unit = -> (r) { r['Value'].to_f / r['Nominal'].to_f }
rate_hash_element = -> (r) { [r['CharCode'], rubles_per_unit[r]] }
rates.map(&rate_hash_element).to_h
end
Либо классом:
require 'open-uri'
require 'active_support/core_ext/hash' # for Hash#from_xml
class CentralBankExchangeRate
def rubles_per(char_code)
rate_hash_from_cbr[char_code] || fail('I dunno :C')
end
#
# other public methods for other currencies
#
private
# Gets daily rates from Central Bank of Russia
def rate_hash_from_cbr
uri = URI.parse('http://www.cbr.ru/scripts/XML_daily.asp')
xml_with_currencies = uri.read
rates = Hash.from_xml(xml_with_currencies)['ValCurs']['Valute']
rubles_per_unit = ->(r) { r['Value'].to_f / r['Nominal'].to_f }
rate_hash_element = ->(r) { [r['CharCode'], rubles_per_unit[r]] }
rates.map(&rate_hash_element).to_h
end
#
# other private methods
#
end
Теперь визуально сразу видно, что у нас есть один метод, пригодный для использования. А если мы захотим погрузиться в его реализацию, то проблем с чтением также возникнуть не должно, поскольку объявления лямбд достаточно броские и понятные (спасибо синтаксическому сахару).
Насколько я знаю, в JavaScript справедливо является плохой практикой вкладывание функций друг в друга:
function foo() {
return bar();
function bar() {
return 'bar';
}
}
Справедливо, потому что каждый раз при вызове foo()
мы создаем функцию bar, а затем уничтожаем ее. Более того, параллельное выполнение нескольких foo()
создаст 3 одинаковых функции, что еще и тратит память.
Но насколько критичен вопрос потребления лишних долей секунды для нашего метода? Лично я не вижу смысла ради выигрыша в полсекунды отказываться от разнообразных удобных конструкций. Например:
some_list.each(&:method)
Медлительнее, чем
some_list.each { |e| e.method }
Потому что в первом случае используется неявное приведение к Proc
.
К тому же, Ruby все-таки работает на сервере, а не клиенте, так что скорости там намного выше (хотя тут тоже можно поспорить, ведь сервер обслуживает множество людей, и потеря даже доли секунды в глобальном масштабе увеличивается до минут/часов/дней)
Давайте проведем отдаленный от реальности эксперимент.
using_lambda.rb:
N = 10_000_000
def method(x)
sqr = ->(x) { x * x }
sqr[x]
end
t = Time.now
N.times { |i| method(i) }
puts "Lambda: #{Time.now - t}"
using_method.rb:
N = 10_000_000
def method(x)
sqr(x)
end
def sqr(x)
x * x
end
t = Time.now
N.times { |i| method(i) }
puts "Method: #{Time.now - t}"
Запуск:
~/ruby-test $ alias test-speed='ruby using_lambda.rb; ruby using_method.rb'
~/ruby-test $ rvm use 2.1.2; test-speed; rvm use 2.2.1; test-speed; rvm use 2.3.0; test-speed
Using /Users/nondv/.rvm/gems/ruby-2.1.2
Lambda: 11.564349
Method: 1.523036
Using /Users/nondv/.rvm/gems/ruby-2.2.1
Lambda: 9.270079
Method: 1.523763
Using /Users/nondv/.rvm/gems/ruby-2.3.0
Lambda: 9.254366
Method: 1.333142
Т.е. использование лямбы примерно в 7 раз медленнее аналогичного кода, использующего метод.
Данный пост был написан в надежде узнать, что думает хабросообщество по поводу использования данного "приема".
Если, например, скорость не является настолько критичной, что нужно бороться за каждые милисекунды, а сам метод не вызывается по миллиону раз в секунду, можно ли пожертвовать скоростью в данном случае? А может, вы вообще считаете, что это не улучшает читаемость?
К сожалению, приведенный пример не иллюстрирует наглядно смысл такого странного использования лямбд. Смысл появляется, когда есть класс с достаточно большим количеством приватных методов, большая часть которых используется в других приватных методах, причем, только единожды. Это по задумке должно облегчить понимание реализации работы отдельных методов класса, т. к. нет кучи def
и end
, а есть достаточно простые однострочные функции (-> (x) { ... }
)
Спасибо, за уделенное время!
UPD.
Некоторые люди, с которыми я общался по этому поводу, не совсем правильно поняли идею.
UPD2.
Кстати, в первом примере два вспомогательных метода можно объединить в один:
def rate_hash_element(rate)
rubles_per_unit = rate['Value'].to_f / rate['Nominal'].to_f
[rate['CharCode'], rubles_per_unit]
end
Автор: Sna1L
Источник [2]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/ruby/137814
Ссылки в тексте:
[1] http://www.cbr.ru/scripts/XML_daily.asp: http://www.cbr.ru/scripts/XML_daily.asp
[2] Источник: https://habrahabr.ru/post/303594/?utm_source=habrahabr&utm_medium=rss&utm_campaign=best
Нажмите здесь для печати.