Покажи свой Monkey Patch
У меня всегда есть файл config/initializers/_monkey_patching.rb
, в который я добавляю
необходимую низкоуровневую функциональность в рейлс и руби. Когда я плохо знал
возможности языка и фреймворка, этот файл был большой. Теперь я его регулярно
пересматриваю и по возможности переписываю код на стандартные механизмы.
Сегодня хочу поделиться с вами, что хранится у меня в этом файле сейчас.
_ASSERT - реал-тайм проверки
Когда проект разрастается программисты жертвуют компактностью и изяществом в пользу единообразия и стандартизации. После некоторого размера (я его оцениваю примерно в 10-20 KLOC для руби-подобных языков) один человек уже не в состоянии удержать в голове все нюансы программы одновременно. Возникает необходимость доверять коду. Обычно это достигается с помощью написания модулей, классов, систем с которыми можно взаимодейстовать как с черными ящиками с помощью формальных интерфейсов.
Мне очень нравится идея разработки программного обеспечения, которая называется "программирования по конкракту". В википедии написано много слов на эту тему, я предпочитаю программированием по контракту называть такой способ разработки методов и классов, что они обязуются правильно работать при правильных аргументах. То есть черные ящики должны быть крепко сколочены.
В С++ крепкость ящиков на нижнем уровне достигается строгой типизацией, вам нужно достаточно хорошо умаслить компилятор, чтобы он собрал ваш код, и ассертами - положительными утверждениями, которые бросают эксепшен если утверждение оказалось ложным.
Выглядит это примерно так:
// Возвращает корень числа `x`.
// Note: аргумент x должен быть неотрицательным.
float sqrt(float x) {
_ASSERT(x >= 0);
return Math::sqrt(x);
}
Eсли мы передадим неправильный аргумент, то программа разорвет с нами контракт и честно грохнется. По стеку гораздо легче искать ошибку, чем если бы программа попыталась сохранить статус-кво и продолжила бы работать с внутренними ошибками и противоречиями.
В руби рантайм-ошибки встречаются гораздо чаще (нет строгой типизации), частичную роль компиляции выполняют тесты. В качестве ассерт-механизма я обычно использовал стандартные эксепшены:
raise "Some msg" if something_wrong
однако недавно завел формальный метод _ASSERT, который теперь использую значительно чаще. Преимущество отдельного метода заключается в том, что не нужно каждый раз придумывать сообщение об ошибке. Кроме того контрактные предположения становятся заметными.
Метод совсем простой, так как его значимость не техническая, а идеологическая:
# Runtime Assert as in all other languages
def _ASSERT(condition, msg = nil)
unless condition
raise "Runtime Error at #{caller.first.sub(Rails.root.to_s, '<root>')}: #{msg}"
end
end
Each on steroids
Бывает нужно пройтись по коллекции и особо обработать первый и последние элементы. Если коллекция анонимная, то стандартными методами не получится отличить последний элемент от своих коллег. Исторически я использую следующий метод:
module Enumerable
class SteroidIter < Struct.new(:index, :first, :last)
def first?; first; end
def last?; last; end
def to_i; index; end
end
# Продвинутый #each_with_index с возможностью узнать является ли
# текущий элемент первым/последним в коллекции.
#
# ==== Example
#
# [1, 2, 3, 4].each_with_index do |elem, iter|
# if iter.first?
# puts "first"
# elsif iter.last?
# puts "last"
# else
# puts "#{iter.to_i}.#{elem}"
# end
# end
#
# # =>
# # first
# # 1.2
# # 2.3
# # last
#
def each_on_steroids(&block)
each_with_index do |elem, index|
yield elem, SteroidIter.new(index, index == 0, index == length - 1)
end
end
end
Дубликаты
Несколько раз я сталкивался с задачей найти дублирующиеся элементы в коллекции. Задача довольно
не тривиальная и за 10 секунд я ее решить не смогу (array - array.uniq
, который
мне всегда кажется, что должен решать эту задачу, к сожалению не работает). Поэтому родился
такой метод:
module Enumerable
# Returns all duplication in current collection
#
# ==== Example
#
# [1, 4, 1, 3, 4, 4, 4].duplications # => [1, 4]
#
def duplications
self.select{|e| self.count(e) > 1}.uniq
end
end
Assets Compilation Progress
У нас ассеты собираются 2 минуты. Это значение находится на границе психологического комфорта,
когда еще не хочется разбираться и оптимизировать, но уже хочется потыкать программу во время
деплоя палкой, чтобы проверить не зависла ли она. Поэтому я немного расширил sprockets
компилятор, заставив его отчитываться после каждых 5 собранных ассетов. Наблюдать за деплоем
теперь стало гораздо занимательнее:
# Делает компиляцию ассетов более итеративной
if defined?(Sprockets)
module Sprockets
class StaticCompiler
cattr_accessor :processed
self.processed = 0
def write_asset_with_logging(asset)
if (self.class.processed += 1)%5 == 0
puts "PROCESSED ASSETS: #{self.class.processed}"
end
write_asset_without_logging(asset)
end
alias_method_chain :write_asset, :logging
end
end
end
Вот такие монкей-патчи. Несколько совсем уж специфических хаков я выкинул, но все самое интересное показал. Спасибо за внимание.
Tweet