Конечные автоматы в 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

    Поделиться публикацией
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама
    Комментарии 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 плагинам, все с демками идут (ну может из-за серверной части).

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