Динамические свойства в Ruby

апреля 16, 2011  |  Published in Ruby, Основы  |  4 Comments

rubyЯ работал над приложением написанным на 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.

Tags: , ,

Responses

  1. mirosm says:

    апреля 17, 2011 at 11:55 (#)

    В статье всё не так. Метод миссинг отвергается (хотя он как раз для данной задачи хорошо подходит) без всяких разумных доводов (насчёт производительности — это очень слабый довод в контексте задачи автора). При этом предлагается решение имеющие сразу несколько серъёзных проблем:

    1. value декларируется как attr_accessor, то есть его можно изменить после создания объекта, но при этом методы доступа не будут перегенерированы.
    2. Самое главное: аксессоры навешиваются на весь класс целиком (в то время, как свойства конкретной джейсон строки специфичны для конкретного объекта). Аксессоры должны генерироваться как синглтон методы объекта.

  2. admin says:

    апреля 17, 2011 at 12:25 (#)

    mirosm, в чем-то с вами согласен. Незачем объявлять методы для класса, когда они специфичн для объекта и должны быть объявлены для объекта (его singleton класса). Еще здесь атрибуты объявляются при инициализации объекта, а потом добавить новые невозможно будет. Лично я бы не создавал бы специальные аксессоры для каждого поля, вместо этого использовал бы универсальный аксессор filed и параметром — символом соответствующим элементу fields. Это было бы не так удобно, но повысило бы производительность, если для автора это так критично.

    Статью перевел просто как пример использования #define_method.

  3. Rodger says:

    апреля 18, 2011 at 20:09 (#)

    пример просто отвратный!

    Какой смысл в создании методов для объекта основываясь на данных? все равно работая с объектом (в коде) ты будешь предполагать наличие методов, так не проще ли создать Struct и инициализировать его хешом атрибутов?

    Кстати конструктор принимающий на вход json-строку тоже выглядить ужасно. Лучше, я думаю, использовать метод-фабрику (например Track.from_json)

  4. abonec says:

    апреля 27, 2011 at 11:35 (#)

    В ActiveRecord для отражения полей БД на атрибуты класса используется имнно #method_missing. При первом обращении он проверяет, не идет ли обращение к приватному методу, а затем этот метод уже определяется. Таким образом второе обращение уже будет идти к методу, который был определен.

Leave a Response

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