 | Уровень сложности: средний Брюс Тэйт, президент, RapidRed
15.11.2007 Язык программирования Java™ уже давно является величайшим плавильным котлом технологий с богатейшими возможностями для интеграции - от контейнеров для подключения зависимых компонентов (dependency-injection containers) для интеграции с корпоративными библиотеками до технологии Enterprise Java Beans (EJB) и моделей компонентов для Eclipse. Java программисты, имея доступ к такому количеству идей и архитектур, открывают новые способы интеграции различных библиотек и компонентов ПО в единое целое. Однако они не обладают монополией на хорошие интеграционные технологии. Узнайте, как работают плагины для Ruby on Rails, на примере популярного плагина acts_as_state_machine.
Когда пишется эта статья, Техас и Оклахома оттаивают после долгого ледяного ливня. Водители, опасаясь не гололеда, а разъезжающих по нему других лихих техасцев, начинали снова появляться на дорогах. Моя жизнь снова входит в нормальную колею после трех дней бездействия. Мне довелось испытать похожее чувство холода в другой ситуации - после перехода с 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
С помощью плагина я могу напрямую представить мой класс в виде автомата, используя 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 модуля выполните следующие действия:
- Создайте модуль. Название метода класса (инициализирующего макроса) должно начинаться с
acts_as_.
- В инициализирующем коде откройте базовый класс
ActiveRecord и добавьте свой модуль acts_as_.
- Расширьте поведение используемого класса в функции
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-программирования.
Обсудить
Об авторе  | |  | Брюс Тэйт (Bruce Tate) является отцом, горным байкером и байдарочником, проживающим в Austin, Texas. Он автор трех бестселлеров по языку Java, в том числе, победителя Jolt "Лучше, быстрее, легче Java". Недавно издал "За пределами Java"… Он работал 13 лет в IBM, а сейчас является основателем RapidRed consultancy, где специализируется на стратегиях и архитектурах облегченной разработки, основанных на технологии Java и Ruby on Rails. |
Выскажите мнение об этой странице
|  |