Покажи свой Monkey Patch

November 16, 2012

У меня всегда есть файл 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

Вот такие монкей-патчи. Несколько совсем уж специфических хаков я выкинул, но все самое интересное показал. Спасибо за внимание.

comments powered by Disqus