Динамические свойства в Ruby
апреля 16, 2011 | Published in Ruby, Основы | 4 Comments
Я работал над приложением написанным на Rails 3, в котором пользователи могли объявлять собственные свойства экземпляра модели, которые могли им понадобится. Проблема тогда состояла в том, как динамически создавать свойства объекта?
У меня был следущий класс:
class Track attr_accessor :value def initialize(value) @value = value end end
Поле value определяется приложением и оно всегда хранит данные в JSON формате.
После того, как экземпляр класса Track инициализирован, я хотел бы иметь возможность вызывать аксессоры #distance и #running для доступа к соответствующим свойствам экземпляра класса Track. Ниже приведен код RSpec спецификаций описывающий то, что мне нужно:
describe Track do it "should add distance and running as read-only properties" do track = Track.new('{"distance":2,"what":"running in the park"}') track.distance.should == 2 track.what.should == 'running in the park' end end
Вопрос состоит в том, как мне это реализовать?
Первым делом я подумал об использовании method_missing в экземпляре Track. Эта моя затея удалась, но я не был доволен решением. Это решение показалось мне неуклюжим и оно несколько занижало производительность.
Товарищ Google мне подсказал следущее решение: define_method.
Мне нужно было распарсить данные в формате JSON, что очень удобно сделать при помощи .
require 'rubygems' require 'json' data = '{"distance":2,"what":"running"}' parsed_data = JSON.parse(data) puts parsed_data["distance"] # => 2
Как только я узнал как распарсить JSON строку, добавление вызова #define_method в инициализатор было очень простым. Ниже приведено окончательное решение:
require 'rubygems' require 'json' require 'spec' class Track attr_accessor :value def initialize(value) @value = value parsed_values = JSON.parse(value) fields = parsed_values.keys.inject([]) do |result, element| result << element.to_sym end fields.each do |field| self.class.send(:define_method, field) do parsed_values[field.to_s] end end end end describe Track do it "should add distance and running as read-only properties" do track = Track.new('{"distance":2,"what":"running in the park"}') track.distance.should == 2 track.what.should == 'running in the park' end end
Оригинал статьи на английском:
Примечание от переводчика:
Решение с #method_missing считается неуклюжим, поскольку объявление методов как таковых не происходит, вместо этого обрабатывается ошибка NoMethodError, что означает, что при каждом повторном использовании неопределенного метода, происходит его глубокий поиск, после чего происходит вызов #method_missing. При использовании #define_method мы создаем метод, который далее действительно существует в приложении и повторных обращений к #define_method и глубокого поиска метода не происходит. Преимущества #define_method ярко проявляются в повышении производительности, тогда, когда нам необходимо вызывать определяемый в такой способ метод несколько раз, а также при тестировании. Недостатком #define_method является то, что чаще всего его использование имеет смысл либо в инициализаторе объекта, либо внутри #method_missing.
апреля 17, 2011 at 11:55 (#)
В статье всё не так. Метод миссинг отвергается (хотя он как раз для данной задачи хорошо подходит) без всяких разумных доводов (насчёт производительности — это очень слабый довод в контексте задачи автора). При этом предлагается решение имеющие сразу несколько серъёзных проблем:
1. value декларируется как attr_accessor, то есть его можно изменить после создания объекта, но при этом методы доступа не будут перегенерированы.
2. Самое главное: аксессоры навешиваются на весь класс целиком (в то время, как свойства конкретной джейсон строки специфичны для конкретного объекта). Аксессоры должны генерироваться как синглтон методы объекта.
апреля 17, 2011 at 12:25 (#)
mirosm, в чем-то с вами согласен. Незачем объявлять методы для класса, когда они специфичн для объекта и должны быть объявлены для объекта (его singleton класса). Еще здесь атрибуты объявляются при инициализации объекта, а потом добавить новые невозможно будет. Лично я бы не создавал бы специальные аксессоры для каждого поля, вместо этого использовал бы универсальный аксессор filed и параметром — символом соответствующим элементу fields. Это было бы не так удобно, но повысило бы производительность, если для автора это так критично.
Статью перевел просто как пример использования #define_method.
апреля 18, 2011 at 20:09 (#)
пример просто отвратный!
Какой смысл в создании методов для объекта основываясь на данных? все равно работая с объектом (в коде) ты будешь предполагать наличие методов, так не проще ли создать Struct и инициализировать его хешом атрибутов?
Кстати конструктор принимающий на вход json-строку тоже выглядить ужасно. Лучше, я думаю, использовать метод-фабрику (например Track.from_json)
апреля 27, 2011 at 11:35 (#)
В ActiveRecord для отражения полей БД на атрибуты класса используется имнно #method_missing. При первом обращении он проверяет, не идет ли обращение к приватному методу, а затем этот метод уже определяется. Таким образом второе обращение уже будет идти к методу, который был определен.