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

июля 21, 2012  |  Published in Ruby, Tips&Tricks, Расширения  |  4 Comments

Говорят, что расширение классов ядра (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
Tags: , , , ,

Responses

  1. says:

    июля 21, 2012 at 21:48 (#)

    Стоит заметить, что код

    MyArrayExt.delegator_obj = self
    MyArrayExt.delegator
    

    , а поэтому и весь подход – не threadsafe.

    Почему в методе ext не создавать объект класса MyArrayExt::Array и возвращать его?

  2. admin says:

    июля 22, 2012 at 09:45 (#)

    Леонид, немного не понял, касательно объекта MyArrayExt::Array. Этот класс наследуется от Array и его объект копирует старый массив давая ему новые методы? Я правильно понял? C тредами у меня действительно проблема, никогда с ними толком не работал. Можешь пояснить почему не threadsafe и если можно ссылки на статьи где описано как писать threadsafe код, а то я сам не нашел.

  3. says:

    июля 23, 2012 at 02:26 (#)

    Мда. Я бы не сказал, что все так уж «безболезнено».

    У меня вот как-то так вышло,

    class ArrayExt
      attr_reader :target
    
      def initialize(target)
        @target = target
      end
    
      def with_odd_index
        target.select.with_index { |e, index| index.odd? }
      end
    
      def with_even_index
        target.select.with_index { |e, index| index.even? }
      end
    
      def mid
        target.inject(0) { |e, sum| sum += e } / target.size
      end
    end
    
    module ArrayExtProxy
      module InstanceMethods
        def ext
          @ext ||= ArrayExt.new(self)
        end    
      end
    end
    
    Array.send :include, ArrayExtProxy::InstanceMethods
    

    но на деле я бы ограничился просто вызовом декоратора или просто наследника Array и не парился, со всеми этими фишками

    ArrayExt.new([1,2,3]).mid => 2
    

    кроме того, если я правильно понял задачу odd/even_elem выдать елементы с парными и непарными индексамы, то она решена, как минимум странно.
    Зачем перобразование в Float после подсчета суммы?

    Навеситься на extend класса, что-бы потом отевалить в нем instance метод?
    Так инклудьте сразу модуль с готовым методом
    ИМХО отличный «безболезненный» способ выстрелить себе в ногу черезе левое плечо :-)

  4. admin says:

    июля 23, 2012 at 13:49 (#)

    Спасибо Роман, как всегда подробно и твоя реализация мне больше нравится. Побольше бы таких комментариев.

Leave a Response

Для подсветки кода используйте BB - коды: [language]...[/language], где language может быть: ruby, javascript, css, html.