Tips&Tricks — Безболезненное расширение классов ядра

Posted by Марк Мельник on July 21, 2012

Говорят, что расширение классов ядра (monkey-patching) — это зло. Тут еще некоторые ведут споры касательно того, что считать monkey-patching’ом — добавление новых методов или переопределение старых, но мы обойдем этот спор стороной и сконцентрируемся на том, как это делать безболезненно.

module MyArrayExt
  # ArrayMethods хранит методы для расширения Array
  module ArrayMethods
    def self.mid(array)
      if array.any? { |elem| !["Fixnum", "Float"].include?(elem.class.to_s) }
        raise NotNumericArrayError
      else
        array.inject(0) { |sum, elem| sum += elem }.to_f / array.size
      end
    end

    def self.odd_elems(array)
      indexes = []
      (array.size / 2).times { |n| indexes << n * 2 + 1}
      return indexes.map { |i| array[i] }
    end

    def self.even_elems(array)
      indexes = []
      (array.size / 2.0 + 0.5).to_i.times { |n| indexes << n * 2 }
      return indexes.map { |i| array[i] }
    end

    class NotNumericArrayError < StandardError
    end

  end

  # Delegator хранит ссылку на объект - массив
  # и делегирует вызов методов модулю ArrayMethods
  class Delegator
    attr_accessor :obj

    # Объявляем делегирующие методы, а можно было бы использовать
    # method_missing с динамической делигацией, но так быстрее
    ArrayMethods.singleton_methods.each do |method_name|
      define_method(method_name) { ArrayMethods.send(method_name, obj) }
    end
  end

  # Так мы создаем объект Delegator и
  # присваиваем ему ссылку на объект - массив
  def self.delegator_obj=(obj)
    @delegator ||= Delegator.new
    @delegator.obj = obj
  end

  def self.delegator
    @delegator
  end

  # коллбек срабатывающий при extend'e модуля MyArrayExt
  def self.extended(klass)
    klass.class_eval do
      # создаем метод Array#ext который возвращает объект
      # делегатора и через который вызываем методы расширения
      define_method :ext do
        MyArrayExt.delegator_obj = self
        MyArrayExt.delegator
      end
    end
  end

end

В действии:

Array.extend MyArrayExt

[1,2,3].ext.mid #=> 2.0
[1,2,3,6,9,100,678,345].ext.mid #=> 143.0
[1,2,3,6,9,100,678,345.1].ext.mid #=> 143.0125
[1,2,3,6,9,100,678,345.1].ext.odd_elems #=> [2, 6, 100, 345.1]
[1,2,3,6,9,100,678,345.1].ext.even_elems #=> [1, 3, 9, 678]
[1,2,3,6,9,"ololo",678,345.1].ext.mid
# => MyArrayExt::ArrayMethods::NotNumericArrayError: MyArrayExt::ArrayMethods::NotNumericArrayError