Ruby on Rails

индекс
81,97

Улучшенные идиомы для плагинов

Этот пост был опубликован 12 ноября 2009 года, но я думаю он не потерял своей актуальности, потому что плагины к Rails (и не только) все так же актуальны.

Последние дни мы с Карлом работали над системой плагинов. В частности, мы прошлись по Rails Plugin Guide. Читая гайд, мы заметили много излишеств в представленных там идиомах.

Я не упрекаю автора гайда; представленные идиомы в точности повторяют те, которые использовались с самых ранних дней Rails. С другой стороны, глядя на них я вспоминал те дни, когда при виде такого кода мне казалось, что Ruby полон магических заклинаний и относительно простые вещи требуют проведения каких-то особых церемоний (вроде танца с бубном. — Прим. перев.).

Вот пример:
Copy Source | Copy HTML
module Yaffle
  def self.included(base)
    base.send :extend, ClassMethods
  end
 
  module ClassMethods
    # любой метод здесь будет применяться классом, например, Hickwall
    def acts_as_something
      send :include, InstanceMethods
    end
  end
 
  module InstanceMethods
    # любой метод тут будет применяться экземпляром, например, @hickwall
  end
end

Начнем с того, что send вообще не нужен. Метод acts_as_something будет вызываться в самом классе, что даст ему доступ к приватному методу include.


Этот код будет использоваться следующим образом:
Copy Source | Copy HTML
class ActiveRecord::Base
  include Yaffle
end
 
class Article < ActiveRecord::Base
  acts_as_yaffle
end

Этот код
  1. Регистрирует хук для того, чтобы при инклуде модуля класс расширялся методами из ClassMethods
  2. В нем (в ClassMethods) объявляет метод, который инклудит InstanceMethods
  3. Чтобы вы могли использовать acts_as_something в своем коде
Ненормальная вещь во всем этом — что это заново изобретает систему модулей, которая и так есть в Ruby. Это было бы совершенно идентично:

Copy Source | Copy HTML
module Yaffle
  # любой метод здесь будет применяться классом, например, Hickwall
  def acts_as_something
    send :include, InstanceMethods
  end
 
  module InstanceMethods
    # любой метод тут будет применяться экземпляром, например, @hickwall
  end
end

Чтобы потом использовать в:
Copy Source | Copy HTML
class ActiveRecord::Base
  extend Yaffle
end
 
class Article < ActiveRecord::Base
  acts_as_yaffle
end

В двух словах, нет смысла оверрайдить include, чтобы он вел себя как extend, если в Ruby есть они оба!

Вы можете сделать:
Copy Source | Copy HTML
module Yaffle
  # любой метод здесь будет доступен экземплярам, например, @hickwall, 
  # потому что это то, как работают модули!
end
 

Чтобы потом использовать в:
Copy Source | Copy HTML
class Article < ActiveRecord::Base
  include Yaffle
end
 

На самом деле, начальный код (оверрайд хука для инклуда, чтобы расширить класс через extend, который дальше инклудит модуль) — это два слоя абстракции вокруг простого инклуда в Ruby!

Давайте рассмотрим еще несколько примеров:
Copy Source | Copy HTML
module Yaffle
  def self.included(base)
    base.send :extend, ClassMethods
  end
 
  module ClassMethods
    def acts_as_yaffle(options = {})
      cattr_accessor :yaffle_text_field
      self.yaffle_text_field = (options[:yaffle_text_field] || :last_squawk).to_s
    end
  end
end
 
ActiveRecord::Base.send :include, Yaffle

Снова, идиома оверрайда include, чтобы он вел себя как extend (вместо простого вызова extend!).

Решение получше:
Copy Source | Copy HTML
module Yaffle
  def acts_as_yaffle(options = {})
    cattr_accessor :yaffle_text_field
    self.yaffle_text_field = options[:yaffle_text_field].to_s || "last_squawk"
  end
end
 
ActiveRecord::Base.extend Yaffle

В этом случае следует использовать acts_as_yaffle, так как мы предлагаем дополнительные опции, которые не могли быть инкапсулированы с помощью нормального extend. (Загадочная фраза. В оригинале: In this case, it’s appropriate to use an acts_as_yaffle, since you’re providing additional options which could not be encapsulated using the normal Ruby extend.Прим. перев.)

Еще один «более продвинутый» случай:

Copy Source | Copy HTML
module Yaffle
  def self.included(base)
    base.send :extend, ClassMethods
  end
 
  module ClassMethods
    def acts_as_yaffle(options = {})
      cattr_accessor :yaffle_text_field
      self.yaffle_text_field = (options[:yaffle_text_field] || :last_squawk).to_s
      send :include, InstanceMethods
    end
  end
 
  module InstanceMethods
    def squawk(string)
      write_attribute(self.class.yaffle_text_field, string.to_squawk)
    end
  end
end
 
ActiveRecord::Base.send :include, Yaffle

Снова include оверрайдят для выполнения extend, и вызывают send, хотя это не нужно. Идентичная функциональность:
Copy Source | Copy HTML
module Yaffle
  def acts_as_yaffle(options = {})
    cattr_accessor :yaffle_text_field
    self.yaffle_text_field = (options[:yaffle_text_field] || :last_squawk).to_s
    include InstanceMethods
  end
 
  module InstanceMethods
    def squawk(string)
      write_attribute(self.class.yaffle_text_field, string.to_squawk)
    end
  end
end
 
ActiveRecord::Base.extend Yaffle

Конечно, можно сделать и так:
Copy Source | Copy HTML
module Yaffle
  def squawk(string)
    write_attribute(self.class.yaffle_text_field, string.to_squawk)
  end
end
 
class ActiveRecord::Base
  def self.acts_as_yaffle(options = {})
    cattr_accessor :yaffle_text_field
    self.yaffle_text_field = (options[:yaffle_text_field] || :last_squawk).to_s
    include Yaffle
  end
end

Так как модули всегда инклудятся в ActiveRecord::Base, предыдущий код с дополнительными модулями и использованием extend не хуже простого переоткрытия класса и добавления метода acts_as_yaffle напрямую. Теперь можно положить метод squawk прямо внутрь модуля Yaffle, откуда он легко заинклудится.

Может это и не сильно важно, но так заметно уменьшается количество обманчивой магии в шаблоне написания плагинов, делая его более доступным пользователю. В добавок, новый пользователь имеет возможность быстро вникнуть в работу include и extend без ложного впечатления необходимости магических заклинаний, использования send и специальных модулей типа ClassMethods для того, чтобы плагины заработали.

Чтобы было ясно, я не говорю, что эти идиомы не нужны в некоторых специальных, продвинутых случаях. С другой стороны, я говорю, что в наиболее распространенных случаях они сильно загромождают код, что скрывает реальную функциональность и вводит пользователя в тупик.
+4
16 марта 2010, 02:57
8

комментарии (3)

0
woto #
Ничего статья, только задумался, понял бы я её, если бы не прочитал сначала это guides.rubyonrails.org/plugins.html и будучи не поковырявшись в исходниках authlogic'а
0
maratk #
Да, у этого парня все такое — для «поковырявшихся» :) Поэтому я и люблю его читать :)
0
Rakoth #
По-моему, самым прозрачным вариантом подключения плагина будет

module PluginName
  def self.included(base)
    base.extend(ClassMethods)
    base.class_eval do
      include InstanceMethods
    end
  end

  module ClassMethods
  end

  module InstanceMethods
  end
end

При этом не нужно дергать AR::Base и подключение методов экземпляра происходит до вызова acts_as_something

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.