Pull to refresh

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

Reading time6 min
Views2K
Original author: Yehuda Katz
Этот пост был опубликован 12 ноября 2009 года, но я думаю он не потерял своей актуальности, потому что плагины к Rails (и не только) все так же актуальны.

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

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

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

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


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

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

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

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

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

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

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

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

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

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

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

В этом случае следует использовать 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<br/>module Yaffle<br/>  def self.included(base)<br/>    base.send :extend, ClassMethods<br/>  end<br/> <br/>  module ClassMethods<br/>    def acts_as_yaffle(options = {})<br/>      cattr_accessor :yaffle_text_field<br/>      self.yaffle_text_field = (options[:yaffle_text_field] || :last_squawk).to_s<br/>      send :include, InstanceMethods<br/>    end<br/>  end<br/> <br/>  module InstanceMethods<br/>    def squawk(string)<br/>      write_attribute(self.class.yaffle_text_field, string.to_squawk)<br/>    end<br/>  end<br/>end<br/> <br/>ActiveRecord::Base.send :include, Yaffle <br/>

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

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

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

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

Чтобы было ясно, я не говорю, что эти идиомы не нужны в некоторых специальных, продвинутых случаях. С другой стороны, я говорю, что в наиболее распространенных случаях они сильно загромождают код, что скрывает реальную функциональность и вводит пользователя в тупик.
Tags:
Hubs:
Total votes 14: ↑9 and ↓5+4
Comments3

Articles