14 июля 2009 в 12:00

Конечные автоматы в Ruby

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

Конечный автомат (Finite-state machine) — это такая штука, описывающая поведение объекта с конечным количеством состояний. Пути перехода из одного состояния в другое, условия этого перехода, действия выполняемые во время перехода или после. С теорией у меня всегда было плохо, поэтому больше вдаваться в нее не буду, вместо этого, для тех кто интересуется подробностями, могу порекомендовать посмотреть википедию (как же без нее) http://en.wikipedia.org/wiki/Finite-state_machine и http://ru.wikipedia.org/wiki/Конечный_автомат, а оттуда уже капать на сколько захочется. На практике это можно использовать много где, от парсинга строк (привет Ragel), до модели User в вашем веб-приложении.

Я же сейчас хочу поговорить про реализацию state machine в языке ruby. Есть такой замечательный сайт ruby-toolbox.com, по которому можно достаточно точно судить о том, что сейчас популярно в мире руби. В разделе State machines на первом месте мы видем gem aasm от rubyist. Кстати, так уж получилось, что о качестве руби-библиотеки можно почти всегда судить по ее популярности, во всяком случае, в сферах где есть конкуренция библиотек. Ну вот так вот получилось. aasm действительно хорош, в отличии от своего популярного предшественника (acts_as_state_machine) умеет работать не только (а для некоторых и не столько) с ActiveRecord, но и с любым ruby-объектом. Вот только документация к нему уж очень скудная, даже в западном вебе я не смог найти никакого более-менее полного описания этой библиотеки. Так что позволю себе, по-сути, написать к ней небольшой мануал.

Итак, начнем с примера из самой бибиотеки (это и есть вся документация).

class Conversation < ActiveRecord::Base
include AASM

aasm_initial_state :unread

aasm_state :unread
aasm_state :read
aasm_state :closed

aasm_event :view do
transitions :to => :read, :from => [:unread]
end

aasm_event :close do
transitions :to => :closed, :from => [:read, :unread]
end
end



Что же для нас теперь сгенерировалось:

conversation = Conversation.new

conversation.aasm_current_state => :unread

conversation.view # перейти в состояние :read

conversation.view! # перейти в состояние :read и вызвать aasm_write_state, если он определен
conversation.read? # true or false. Мы как бэ спрашиваем “текущее состояние read?”

conversation.closed # Генерируются named scopes для всех состояний, соответсвенно этот метод вернет нам scope для всех закрытых бесед.


Если объект наследуется от ActiveRecord::Base, то к нему подмешивается persistence-составляющая aams. Именно для нее в первую очередь актуальны bang-методы. conversation.view! не только переведет текущее состояние объекта, но и сохранит его в БД. Так же вам никто не мешает определить aasm_write_state для любого объекта и делать в нем все что душа пожелает (точно так же как и aasm_read_state).

Посмотрим на еще пару примеров.

aasm_state :waiting, :enter => :start_timer
aasm_state :selecting_cards
aasm_state :made_turn, :exit => lambda { unseletcted_cards.each { |c| c.destroy }

aasm_event :go do
transitions :to => :selecting_cards, :from => [:ready], :guard => :attacking?
transitions :to => :waiting, :from => [:ready], :guard => :defending?
end

aasm_event :make_turn, :success => :after_make_turn do
transitions :to => :made_turn, :from => [:selecting_cards], :on_transition => :do_make_turn
end


Что мы видим. Во-первых callbacks.
У transition это :guard и: on_transition. Если :guard true, то переход выполнится, если нет, то нет. :on_transition выполняеться во время перехода. Например, это означает, что нельзя делать переход к следующему стейту в этом коллбэке.
У event — :success, выполняющийся после успешного завершения перехода.
У state — :enter и :exit, выполняются, соответсвенно, при входе и выходе из стейта (неважно через какой ивент и через какой переход).
Любой из этих коллбэков может быть или Symbol или Proc, в общем-то как везде.

У самого объекта — aasm_event_fired и aasm_event_failed. Если кто-то из них определен у объекта, то aasm_event_failed будет вызван с одним параметром (названием ивента), а aasm_event_fired с двумя (названием ивента и названием стейта, в которых перешел объект)

Из этого примера мы так же видим, что у ивента может быть определено сколько угодно переходов. Выполнен будет тот, у кого :from соответсвует текущему состоянию, а :guard возвращает true.

Вот в общем-то и все. Перед нами пример маленькой, но очень гибкой и расширяемой библиотеки на руби. Ну а под конец немножко самодеятельности.

http://github.com/preprocessor/aasm

Реализован механизм хранения стейтов в БД в виде integers. Перфоманс и все такое. Использовать просто:

aasm_state :unread, :integer => 0
aasm_state :read, :integer => 1
aasm_state :closed, :integer => 2

Conversation.aasm_integers[:read] => 1


Named scopes продолжают работать как надо.

http://github.com/preprocessor/railroad_xing

Форк форка (господа руби-разработчики, давайте-ка держать на гитхабе свои проекты хоть в каком-нибудь виде. Тренд как-никак). Добавляет поддержу aasm. В итоге получаем:



Зачем это нужно? С такой схемкой очень часто значительно проще понять и обсуждать код. Однако ее рисование займет 5-10 минут. А если моделей 10 и частенько меняются? Естественно их никто не рисует. А вот если все автоматически и удобно, то почему бы и нет.

Удачи.

Upd. Мой форк railroad_xing теперь смерджен с оригиналом. Так что можно следить и использовать github.com/royw/railroad_xing/tree/master

iv_s @iv_s
карма
39,7
рейтинг 0,0
Похожие публикации
Самое читаемое Разработка

Комментарии (15)

  • 0
    поправьте плиз ссылку на картинку
    • 0
      поправьте еще плиз ссылку ttp//ru.wikipedia.org/....
    • 0
      А что с ней? В сафари нормально показывается.
      • 0
        FF3: Изображение «http://files.getdropbox.com/u/533240/full_models.png» не может быть показано, так как содержит ошибки.
      • 0
        К сожелению в Windows нет 8BPS кодека… Про Linux не скажу…
        • 0
          в убунту тоже нет картинки
          • 0
            А теперь?
            • 0
              Теперь в убунту есть картинка.
        • 0
          Фига что бывает оказывается… А сейчас нормальный кодек, который везде есть? :)
  • 0
    Кроме AASM есть еще неплохая библиотека Workflow, которая иногда позволяет более простые state-machine создавать. Ну и насчет поддержки int'ов в базе — можно использовать поле типа ENUM в mysql, и по производительности будет примерно то же самое.
    • +1
      Спасибо. По мне так aasm все же более выразителен. Но у Workflow есть все таки пара фишек, типа доступа к мета-информации.

      Ну а ENUM это все-таки не рэйлс вэй. Любая привязка к какой-то особенности СУБД (а в том же постгресе ENUM совсем по-другому определяются) снижает универсальность (а ведь мы хотим в один прекрасный момент перевести проект на Drizzle, правда? :) ). А в данном случае в этой привязке необходимости нет.
  • 0
    Материал интересный, мне понравился.
    Но исправьте, пожалуйста, ошибки.

    а оттуда уже капать на сколько захочется.


    И если уж где-то пишете event, а где-то ивент, то выберите что-то одно. А то трудно читать.
  • НЛО прилетело и опубликовало эту надпись здесь
    • 0
      Долго ждал этого комментария :)
  • 0
    Лично мне больше нравится когда к статьям прикреплены сырцы. Странная тенденция у рельсовиков, обычно никто не выкладывает исходники. Видимо по-умолчанию подразумевается что это так просто… Кстати полная противоположность jQuery плагинам, все с демками идут (ну может из-за серверной части).

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

Интересные публикации