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
июля 21, 2012 at 21:48 (#)
Стоит заметить, что код
, а поэтому и весь подход – не threadsafe.
Почему в методе ext не создавать объект класса MyArrayExt::Array и возвращать его?
июля 22, 2012 at 09:45 (#)
Леонид, немного не понял, касательно объекта MyArrayExt::Array. Этот класс наследуется от Array и его объект копирует старый массив давая ему новые методы? Я правильно понял? C тредами у меня действительно проблема, никогда с ними толком не работал. Можешь пояснить почему не threadsafe и если можно ссылки на статьи где описано как писать threadsafe код, а то я сам не нашел.
июля 23, 2012 at 02:26 (#)
Мда. Я бы не сказал, что все так уж «безболезнено».
У меня вот как-то так вышло,
но на деле я бы ограничился просто вызовом декоратора или просто наследника Array и не парился, со всеми этими фишками
кроме того, если я правильно понял задачу odd/even_elem выдать елементы с парными и непарными индексамы, то она решена, как минимум странно.
Зачем перобразование в Float после подсчета суммы?
Навеситься на extend класса, что-бы потом отевалить в нем instance метод?
Так инклудьте сразу модуль с готовым методом
ИМХО отличный «безболезненный» способ выстрелить себе в ногу черезе левое плечо :-)
июля 23, 2012 at 13:49 (#)
Спасибо Роман, как всегда подробно и твоя реализация мне больше нравится. Побольше бы таких комментариев.