Pull to refresh

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

Reading time 4 min
Views 9.2K
Статья за авторством хабраюзера 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

Tags:
Hubs:
+20
Comments 15
Comments Comments 15

Articles