Пересекая границы: Расширения в rails

Анатомия плагина acts_as

Язык программирования Java™ уже давно является величайшим плавильным котлом технологий с богатейшими возможностями для интеграции - от контейнеров для подключения зависимых компонентов (dependency-injection containers) для интеграции с корпоративными библиотеками до технологии Enterprise Java Beans (EJB) и моделей компонентов для Eclipse. Java программисты, имея доступ к такому количеству идей и архитектур, открывают новые способы интеграции различных библиотек и компонентов ПО в единое целое. Однако они не обладают монополией на хорошие интеграционные технологии. Узнайте, как работают плагины для Ruby on Rails, на примере популярного плагина acts_as_state_machine.

Брюс Тэйт, президент, RapidRed

Брюс Тэйт (Bruce Tate) является отцом, горным байкером и байдарочником, проживающим в Austin, Texas. Он автор трех бестселлеров по языку Java, в том числе, победителя Jolt "Лучше, быстрее, легче Java". Недавно издал "За пределами Java"… Он работал 13 лет в IBM, а сейчас является основателем RapidRed consultancy, где специализируется на стратегиях и архитектурах облегченной разработки, основанных на технологии Java и Ruby on Rails.



15.11.2007

Когда пишется эта статья, Техас и Оклахома оттаивают после долгого ледяного ливня. Водители, опасаясь не гололеда, а разъезжающих по нему других лихих техасцев, начинали снова появляться на дорогах. Моя жизнь снова входит в нормальную колею после трех дней бездействия. Мне довелось испытать похожее чувство холода в другой ситуации - после перехода с Java на Ruby. Когда я работал с Java, то всегда мог найти библиотеку для Spring или компонент для Eclipse для решения какой-нибудь специальной проблемы. К счастью, это чувство тоже начало быстро таять благодаря эффективной архитектуре плагинов, которую тысячи разработчиков использовали для расширения возможностей Rails.

Об этой серии

В серии Пересекая границы Брюс Тейт продвигает идею, что сегодня Java программистам полезно изучать другие языки и подходы. Ситуация в области разработки ПО изменилась, и технология Java больше не является наилучшим выбором для всех проектов по разработке ПО. Другие framework'и (каркасы для разработки приложений) влияют на построение framework'ов для Java, и знания, которые можно получить из других языков, могут привнести свежие идеи в Java-программирование. Код, написанный вами на языке Python, Ruby, Smalltalk (продолжите перечень сами), может изменить ваш подход к написанию кода на языке Java.

Эта серия статей знакомит с понятиями и приемами, которые хотя и радикально отличаются от Java, но могут быть напрямую применены в Java программировании. В некоторых случаях необходимо интегрировать такую технологию, чтобы получить от нее пользу. В других можно непосредственно применить понятия, лежащие в основе технологии. Не так важны конкретные инструменты разработки, как идея, что другой язык и framework'и могут оказать влияние на Java сообщество: разработчиков, framework'и и даже фундаментальные подходы.

Если вы уже работали сколько-нибудь времени с Rails, то, несомненно, обратили внимания на команды acts_as в модели ActiveRecord. Хотя модель ActiveRecord занимается управлением и привязкой объектов к данным в СУБД (persistence), часто возникает желание добавить в классы более широкую функциональность, чем просто сохранение и загрузка из базы. Например, используя модуль acts_as_tree, можно добавить функциональность дерева к классу с атрибутом parent_id. Не используя ничего, кроме объявления модуля acts_as_tree в модели ActiveRecord, можно динамически добавлять методы для управления деревом, такие как методы для получения родителя или потомков данного элемента. За последний месяц мне удалось найти плагины для Rails для проведения голосования, управления версиями, AJAX, составных ключей и другой разнообразной функциональности, не поддерживаемой базовым Rails.

Модели расширения в Rails, построенные поверх возможностей языка Ruby, разительно отличаются от их аналогов в Java. В этой статье я исследую открытые плагины acts_as, чтобы показать модель расширения изнутри. Я представлю фрагмент реального рабочего приложения, вместо того чтобы создавать законченное игрушечное приложение, чтобы охватить больше областей и познакомить вас с реальными плагинами и тем, как они используются в коммерческих приложениях.

API для реализации конечных автоматов

Как вы, наверное, знаете, конечный автомат (state machine) - это математическое отображение состояния некоторой системы. Конечный автомат состоит из узлов, представляющих состояния, и переходов между ними. В каждый указанный момент времени у автомата есть одно активное состояние, также называемое текущим состоянием. События инициируют переходы между состояниями. Чтобы проиллюстрировать это понятие, я приведу пример из моей текущей работы: разработки и поддержки проекта ChangingThePresent.org (CTP) - онлайновой торговой площадки для некоммерческих организаций и жертвователей (см. дополнительные материалы). Проект CTP позволяет благотворительным организациям размещать информацию о себе и о типах поощрения жертвователей, таких как один час общения с исследователем рака или комплект книг для одного студента. Площадка позволяет дарителям с помощью простой программы для онлайновых покупок делать, благотворительные пожертвования в качестве подарка для другого человека. Сбор всей этой информации порождает проблемы, связанные с логистикой, поэтому я упрощаю процесс с помощью конечного автомата.

Для этого я использую плагин от стороннего разработчика Скотта Барона (Scott Baron), называющийся acts_as_state_machine (cм. дополнительные материалы). Подобно другим Rails-плагинам acts_as_state_machine на основе возможностей Ruby и эксклюзивной функциональности Rails создает не только библиотеку, но и специализированный язык (Domain Specific Language - DSL), расширяющий возможности пользователя.

Заказчик предоставляет свою информацию на сайт CTP (состояние "отправлено" - submitted). Администратор CTP получает эту информацию и может ее отредактировать (состояние "обработка" - processing). Если СТР вносит изменения, то некоммерческая организация должна просмотреть их (состояние "просматривается организацией" - nonprofit_reviewing). Когда СТР или организация одобряют информацию, то информация может быть показана на сайте CTP (состояние "одобрено" - accepted). На рисунке 1 показано графическое изображение конечного автомата.

Рисунок 1. Конечный автомат для CTP
Конечный автомат для CTP

С помощью плагина я могу напрямую представить мой класс в виде автомата, используя DSL, чтобы отобразить различные состояния, переходы между ними и события, вызывающие эти переходы. В листинге 1 приведена упрощенная версия конечного автомата, который я использовал для управления благотворительными организациями в CTP.

Листинг 1. Пример автомата
class Nonprofit < ActiveRecord::Base

  acts_as_state_machine :initial => :created, :column => 'status'
  
  # These are all of the states for the existing system. 
  state :submitted            
  state :processing           
  state :nonprofit_reviewing  
  state :accepted             
  
  event :accept do
    transitions :from => :processing, :to => :accepted
    transitions :from => :nonprofit_reviewing, :to => :accepted
  end
  
  event :receive do
    transitions :from => :submitted, :to => :processing
  end

  # either a CTP  or nonprofit user edits the entry, requiring a review
  event :send_for_review do   
    transitions :from => :processing, :to => :nonprofit_reviewing
    transitions :from => :nonprofit_reviewing, :to => :processing
    transitions :from => :accepted, :to => :nonprofit_reviewing
  end

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

Каждое выражение соответствует синтаксису Ruby. После определения класса следует acts_as_state_machine :initial => :created, :column => 'status'. Для Java разработчика может показаться странным, что приведен вызов метода вместо его определения. Ruby ссылается на вызовы этих методов, объявленных на уровне класса, как на макросы. Макросы часто используются в Ruby, чтобы добавлять функциональности классу в момент его загрузки. На самом деле определения методов def - это просто макросы Ruby.

Затем приведен набор состояний, например, state :submitted. Это вызовы методов, каждый из которых принимает на вход единственный параметр – символ (определенное пользователем имя). Команда event - это тоже вызов метода, принимающий символ, определяющий название события, и cодержанием, определяющим переходы для этого события.

Каждый переход это вызов метода, сопровождаемый хеш-таблицей. В Ruby хеш-таблица представляется в виде набора пар key => value , разделенных запятыми и помещенными в {}. Когда хеш-таблица используется в качестве последнего параметра при вызове функции, то скобки не обязательны. Можно видеть, что эти методы - состояния, переходы и события - в сочетании со своим содержимым и хэш-таблицами образуют полноценный DSL.

Чтобы использовать автомат, я могу создать объект типа Nonprofit и вызывать на нем методы для каждого события, завершая их символом !, как показано в листинге 2.

Листинг 2. Манипулирование автоматом.
>> np = Nonprofit.find(2)
=> ...
>> np.current_state
=> :submitted
>> np.receive!                              
=> true
>> np.accept!
=> true
>> np.current_state
=> :accepted

Символ ! используется в Ruby для методов, которые изменяют и сохраняют параметр за один прием. Так что требования к плагину для конечного автомата достаточно очевидны. Требуется:

  • удобное место для хранения кода автомата,
  • способ для определения в классах методов, которые потребуются мне для DSL,
  • способ подключить методы к классу Nonprofit или к любому другому классу.

В оставшейся части статьи мы подробно разберем плагин. Если вы хотите исследовать код по ходу чтения, скачайте плагин acts_as_state_machine. В дополнительных материалах приведена ссылка на сайт Скотта Баррона (Scott Barron), где можно, следуя инструкциям, скачать плагин через Subversion. Перейдите в каталог trunk/lib, где находится файл act_as_state_machine.rb. Инициализирующий код находится в файле trunk/init.rb. Вам потребуются только эти два файла.


Плагины типа acts_as

В принципе все плагины типа acts_as работают одинаково. Для построения acts_as модуля выполните следующие действия:

  1. Создайте модуль. Название метода класса (инициализирующего макроса) должно начинаться с acts_as_.
  2. В инициализирующем коде откройте базовый класс ActiveRecord и добавьте свой модуль acts_as_.
  3. Расширьте поведение используемого класса в функции acts_as_, например, acts_as_state_machine.

Ознакомьтесь с инициализирующим кодом из файла init.rb, приведенным в листинге 3.

Листинг 3. Инициализирующий код для acts_as_state_machine
require 'acts_as_state_machine'

ActiveRecord::Base.class_eval do
  include ScottBarron::Acts::StateMachine
end

Этот код открывает базовый класс ActiveRecord (ActiveRecord::Base) и добавляет acts_as_state_machine. Метод class_eval открывает класс и запускает следующее замыкание в контексте указанного класса. И это все. На самом деле принцип крайне прост: код раскрывает базовый класс ActiveRecord и подмешивает в него модуль ScottBarron::Acts::StateMachine. В Ruby можно раскрыть любой класс и быстро его переопределить.

Эта возможность Ruby - одна из самых сильных его сторон, поскольку она значительно повышает его гибкость. Но эта же возможность может привести и к слабости. Слишком большая гибкость может привести к коду, который трудно понимать и поддерживать, так что будьте осторожны. Теперь откройте файл acts_as_state.rb, чтобы увидеть, какой код будет добавлен.


Инициализация модуля.

На этом этапе я отойду от деталей реализации автомата. Вместо этого я сконцентрируюсь на демонстрации интерфейса автомата через плагин. В листинге 4 приведено определение модуля и часть интерфейса собственно автомата.

Листинг 4. Структура модуля
module Acts                        #:nodoc:
  module StateMachine              #:nodoc:
    class InvalidState < Exception #:nodoc:
    end
    class NoInitialState < Exception #:nodoc:
    end
    
    def self.included(base)        #:nodoc:
      base.extend ActMacro
    end
    
    module SupportingClasses
      class State
        attr_reader :name
      
        def initialize
          ...
        end
        
        def entering
          ...
        end
        
        ...
      end
      
      class StateTransition
        attr_reader :from, :to, :opts
        
        def initialize
          ...
        end
        
        def perform
          ...
        end
        ...
      end
      class Event
      ...
        def fire
          ...
        end
        
        def transitions
          ...
        end
        ...
      end

Вверху листинга 4 можно увидеть вложенное объявление модуля. У модуля есть определения методов, но нет базовой иерархии наследования. Вместо этого модуль можно подключить к любому существующему классу Ruby. Если такой подход для вас в новинку, можете считать, что модуль - это интерфейс плюс реализация этого интерфейса. Преимущество модуля в том, что его функциональность можно подключить к любому существующему классу Ruby, и таких модулей можно подключить сколько угодно. Также можно усилить существующие возможности класса. Эта техника называется смешиванием (mixing). В C++ для получения подобного результата используется множественное наследование, но это порождает множество значительных осложнений. Разработчики Java отказались от множественного наследования, чтобы избавиться от этих усложнений. С помощью модулей можно реализовать некоторые преимущества множественного наследования без осложнений. Такие языки, как Smalltalk и Python, также поддерживают наследование путем смешивания.

В оставшейся части листинга 4 приведены детали, относящиеся к реализации автомата. Вам достаточно просто знать, что эти классы представляют автономную реализацию автомата. Остаток кода более интересен, так как он представляет интерфейс автомата клиентам плагинa.


Модули acts_as

Напомню, что разработчику плагина требуются три вещи: место, чтобы поместить реализацию, способ предоставить DSL (методы класса), и способ предоставить автомату доступ к методам экземпляра. Это включает в себя методы событий, которые вы видели в листинге 3. В листинге 4 представлено место для размещения реализации. Следующий фрагмент занимается обработкой DSL.

У архитектуры плагинов типа acts_as есть одна точка привязки: макрос acts_as. Клиенты acts_as плагинa вводят этот метод с вызовом метода в целевом классе. В моем случае, я вызываю acts_as в листинге 1 в классе Nonprofit в этом фрагменте кода:

acts_as_state_machine :initial => :created, :column => 'status'

Теперь разберем листинг 5, в котором объявляется макрос модуль ActMacro для плагинa acts_as_state_machine. Этот класс обрабатывает параметры для модуля и предоставляет различные методы класса и экземпляра.

Листинг 5. Добавление acts_as
module ActMacro
  # Configuration options are
  #
  # * +column+ - specifies the column name to use for keeping the state (default: state)
  # * +initial+ - specifies an initial state for newly created objects (required)
  def acts_as_state_machine(opts)
    self.extend(ClassMethods)
    raise NoInitialState unless opts[:initial]
    
    write_inheritable_attribute :states, {}
    write_inheritable_attribute :initial_state, opts[:initial]
    write_inheritable_attribute :transition_table, {}
    write_inheritable_attribute :event_table, {}
    write_inheritable_attribute :state_column, opts[:column] || 'state'
    
    class_inheritable_reader    :initial_state
    class_inheritable_reader    :state_column
    class_inheritable_reader    :transition_table
    class_inheritable_reader    :event_table
    
    self.send(:include, ScottBarron::Acts::StateMachine::InstanceMethods)

    before_create               :set_initial_state
    after_create                :run_initial_state_actions
  end
end

В модуле из листинга 5 есть единственный метод: acts_as_state_machine. Это метод выполняет пять действий:

  • объявляет методы класса,
  • обрабатывает исключительные ситуации, сбрасываемые автоматом,
  • управляет параметрами,
  • объявляет методы экземпляра,
  • управляет фильтрами, срабатывающими до и после метода.

Метод acts_as_state_machine сначала объявляет методы класса. Полное содержание этих методов можно посмотреть в листинге 6. Затем этот метод обрабатывает исключения. В данном случае единственным исключением является ситуация, когда клиент не указывает начальное состояние. Кратко пройдемся по унаследованным атрибутам - подробно мы разберем их позже. Метод self.send объявляет методы экземпляра. Они приведены в листинге 7. Наконец, фильтры before (до) и after (после) - это макросы ActiveRecord, которые вызывают set_initial_state и run_initial_state_actions до и после того, как ActiveRecord создает запись.

Вернемся к макросам write_inheritable_attribute и class_inheritable_reader. ВВы можете задаться вопросом, почему модуль не использует простое наследование. Причина очень проста - модуль хранит иерархию наследования в себе. Эти макросы позволяют модулю проецировать свои атрибуты на целевой класс - в данном случае, Nonprofit. Наиболее важные параметры - это state_column и набор таблиц переходов, содержащих состояния, события и переходы. Теперь пришло время добавить методы класса, которые образуют DSL.


Добавление методов класса и методов экземпляра

В листинге 6 наконец, можно увидеть реализацию DSL.

Листинг 6. Методы класса модуля act_as_state_machine
module ClassMethods
  def states
    read_inheritable_attribute(:states).keys
  end
  
  def event(event, opts={}, &block)
    tt = read_inheritable_attribute(:transition_table)
    
    et = read_inheritable_attribute(:event_table)
    e = et[event.to_sym] = SupportingClasses::Event.new(event, opts, tt, &block)
    define_method("#{event.to_s}!") { e.fire(self) }
  end
  
  def state(name, opts={})
    state = SupportingClasses::State.new(name.to_sym, opts)
    read_inheritable_attribute(:states)[name.to_sym] = state
  
    define_method("#{state.name}?") { current_state == state.name }
  end
  ...

Макросы state и event, как и обещалось, оказались простыми методами, объявленными в модуле, называемом ClassMethods. Метод event считывает параметр из таблицы переходов, а потом параметр из таблицы событий. Метод добавляет событие в таблицу событий, затем динамически определяет метод для события, подключая новый метод к методу fire на событии event.

После метода event модуль определяет метод state. Этот метод считывает таблицу состояний и добавляет новое состояние. Таким способом в целевой класс добавляется удобный метод, возвращающий true, если объект находится в текущем состоянии. Например, код nonprofit.submitted вернет true, если флаг состояния будет submitted. Теперь DSL поддерживается полностью.

Методы экземпляра работают точно также как методы класса. Эти методы приведены в листинге 7.

Листинг 7. Методы экземпляра для модуля acts_as_state_machine
module InstanceMethods
  def set_initial_state
    write_attribute self.class.state_column, self.class.initial_state.to_s
  end

  ...
  
  def current_state
    self.send(self.class.state_column).to_sym
  end
  
  ...
end

Макрос ActMacro открывает класс и добавляет эти методы. Мне не нужно проходить через макрос read_inheritable_attribute, чтобы использовать параметры, так как существуют переменные экземпляра класса, определенные ActiveRecord. Я привел только методы, которые устанавливают начальное и возвращают текущее состояние. Остальные работают аналогично.

Первый метод в листинге 7 устанавливает начальное состояние, обновляя существующий столбец в объекте ActiveRecord. Напомню, что я задаю название столбца при вызове ActMacro. Метод current_state просто возвращает значение переменной экземпляра. Метод send вызывает метод по имени единственного символьного параметра, в данном случае это название state_column.


Заключение

Можно подумать, что было бы проще построить автомат и использовать его как библиотеку. Однако плагин acts_as гораздо мощнее. Он позволяет фактически добавить столбец для автомата в базу данных. Другие плагины позволяют управлять версиями, строить истории аудита, обрабатывать изображения и выполнять сотни других простых задач так, как если бы эти задачи были прозрачной интеграцией между средой исполнения Rails и базой данных.

Возможно, вам приходилось использовать язык Java для интеграции плагинов Eclipse, задач Ant или библиотек Spring c вашим кодом или для подключения EJB-компонентов. Множество идей из Java-сообщества изменили представление разработчиков о расширении функциональности. Это краткое знакомство в плагинами Ruby типа acts_as показывает новый способ решения подобных задач. Гибкость языка Ruby изменила мой взгляд на расширение функциональности. Плагин acts_as позволяет новому поколению разработчиков попробовать себя в написании расширений. Результатом будет новая волна расширений для Ruby. Многие из этих приемов также доступны и Java-разработчикам через аспектно-ориентированное программирование или усовершенствование байт-кода.

В следующий раз я завершу эту серию статей подробным сравнением подходов к решению одной трудной задачи с использованием Ruby и моего опыта применения Java платформы. Пока же - продолжайте пересекать границы.

Ресурсы

Научиться

  • Оригинал статьи (EN).
  • Java To Ruby: Things Every Manager Should Know (EN) (Pragmatic Bookshelf, 2006): книга автора статьи о том, когда и где имеет смысл переключиться с языка программирования Java на Ruby on Rails и как это сделать.
  • Beyond Java (EN) (O'Reilly, 2005): книга автора статьи о развитии и зрелости языка Java и о технологиях, которые могут соперничать с платформой Java в некоторых областях.
  • Plugins in Ruby on Rails: ознакомьтесь с документацией по архитектуре плагинов для Rails.
  • acts_as_state_machine: плагин для Rails, который позволяет модели ActiveRecord действовать как конечный автомат.
  • Changing The Present: сайт для некоммерческих организаций, построенный на Ruby on Rails, с которого был взят пример для этой статьи.
  • Class and instance variables in Ruby (EN) (John Nunemaker, RailsTips.org, ноябрь 2006): работа с переменными класса и экземпляра, когда вы используете множественное наследование, может показаться трудной. В этой статье предлагается прием, называемый наследуемыми атрибутами.
  • Раздел Технология Java: сотни статей по всем аспектам Java-программирования.

Обсудить

Комментарии

developerWorks: Войти

Обязательные поля отмечены звездочкой (*).


Нужен IBM ID?
Забыли Ваш IBM ID?


Забыли Ваш пароль?
Изменить пароль

Нажимая Отправить, Вы принимаете Условия использования developerWorks.

 


Профиль создается, когда вы первый раз заходите в developerWorks. Информация в вашем профиле (имя, страна / регион, название компании) отображается для всех пользователей и будет сопровождать любой опубликованный вами контент пока вы специально не укажите скрыть название вашей компании. Вы можете обновить ваш IBM аккаунт в любое время.

Вся введенная информация защищена.

Выберите имя, которое будет отображаться на экране



При первом входе в developerWorks для Вас будет создан профиль и Вам нужно будет выбрать Отображаемое имя. Оно будет выводиться рядом с контентом, опубликованным Вами в developerWorks.

Отображаемое имя должно иметь длину от 3 символов до 31 символа. Ваше Имя в системе должно быть уникальным. В качестве имени по соображениям приватности нельзя использовать контактный e-mail.

Обязательные поля отмечены звездочкой (*).

(Отображаемое имя должно иметь длину от 3 символов до 31 символа.)

Нажимая Отправить, Вы принимаете Условия использования developerWorks.

 


Вся введенная информация защищена.


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=40
Zone=Технология Java
ArticleID=269719
ArticleTitle=Пересекая границы: Расширения в rails
publish-date=11152007