Статьи

Государственные машины в рубине

Virtual_finite_state_machine_executor_flow_chart Чтобы дать им свое воскресное имя, конечные автоматы (FSM) находятся вокруг нас, и если мы откроем глаза достаточно долго, вы сможете увидеть их в игре, когда вы покупаете банку газировки, ходите по магазинам через электронную торговлю, проходите через электронные превращать стили на вокзалах. Вернувшись в мою предыдущую жизнь в качестве инженера-электронщика, мы постоянно внедряли автоматические автоматы с использованием сети логических элементов спагетти. И это правда, в мире программного обеспечения мы будем спагетти наш код, если предложения, которые действительно должны быть очищены с использованием State State.

Рассечение конечного автомата

Нет ничего сложного в объяснении FSM. Подсказка действительно в названии, у нас есть система, модуль, класс, машина и т. Д. С известным числом состояний. В программном плане, когда они становятся важными, это когда какое-то внутреннее состояние имеет значение для поведения объекта. По сути, есть три основных понятия для FSM, States (duh), Transitions и Events.

В качестве примера давайте рассмотрим действительно простую газировочную машину. Хитрость, как я вижу, состоит в том, чтобы идентифицировать правильный объект как FSM. На первый взгляд у вас может возникнуть соблазн выбрать такие состояния, как бездействие, вставленные монеты, выпитый напиток обратно в режим ожидания. Это не хороший конечный автомат, хотя он описывает общее поведение, которое мы сделали, смешав поведение автомата с газировкой и транзакции с газировкой в ​​один объект.

Реализация конечного автомата

Лучшее место для начала с известной проблемой в мире Ruby — это, конечно, RubyGems. Беглый взгляд на Ruby Toolbox, и мы получаем сморгасборд о драгоценных камнях конечных автоматов. Мое личное предпочтение — метко названный драгоценный камень State Machine .

Моя причина выбора этого из всего остального — смесь истории (предыдущего опыта) и ее богатства интеграций. Хотя он и не привязан напрямую к Rails, он имеет интеграцию ActiveRecord и ActiveModel. Эти интеграции означают, что мы можем использовать полезные функции, такие как наблюдатели, валидация, а в мире ActiveRecord мы получаем именованные области и транзакции базы данных при переходах. Он также хорошо работает с другими уровнями базы данных, такими как DataMapper, Sequel и Mongoid.

Сейчас мы просто будем использовать способность gem state_machine для добавления поведения FSM к простым старым объектам Ruby.

Если вы предполагаете, что машина создаст новый экземпляр SodaTransaction , начальное состояние которого — SodaTransaction . Путь перехода будет следовать чему-то вроде: awaiting_selection , dispense_soda , complete .

 require 'rubygems' require 'state_machine' class SodaTransaction state_machine :state, initial: :awaiting_selection do end end 

Мы инстинктивно знаем, что состояние awaiting_selection должно реагировать на нажатие кнопки на передней панели. Итак, давайте идти вперед и реализовать это.

 require 'rubygems' require 'state_machine' class SodaTransaction state_machine :state, initial: :awaiting_selection do event :button_press do transition :awaiting_selection => :dispense_soda end end end 

На данный момент мы можем попробовать нашу транзакцию соды в IRB. Быстрый совет, который я нашел, — это запустить команду irb -I . из пути, в котором находится наш файл soda_transaction.rb. Это просто запускает сеанс irb с рабочим каталогом, установленным по текущему пути, что позволяет вам просто использовать require 'soda_transaction' без необходимости использования другого пути.

 require 'soda_transaction' sm = SodaTransaction.new puts sm.state #=> awaiting_selection sm.button_press puts sm.state #=> dispense_soda 

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

 class SodaTransaction attr_accessor :selection state_machine :state, initial: :awaiting_selection do event :button_press do transition :awaiting_selection => :dispense_soda, if: :in_stock? end end def in_stock? stock_levels[@selection] > 0 end def stock_levels { dr_pepper: 100, sprite: 10, root_beer: 0, cola: 8 } end end 

Здесь мы просто установили защиту для перехода: awaiting_selection to: dispense_soda. Используя простой взгляд на хеш, мы проверяем, что нужный напиток есть в наличии.

Очевидно, что в реальной реализации мы хотели бы обрабатывать уменьшение запаса и делегировать этот вид проверки в другом месте приложения. На данный момент простой поиск хеш-заглушки просто отлично. Но из примера видно, что выборка является переменной экземпляра, где этот набор? Конечно, в случае?

Лучший способ, который я нашел, чтобы эти дополнительные параметры были переданы событию, это переопределить событие как таковое:

 class SodaTransaction state_machine :state, initial: :awaiting_selection do event :button_press do transition :awaiting_selection => :dispense_soda, if: :in_stock? end end def button_press(selection) @selection = selection super end def in_stock? stock_levels[@selection] > 0 end def stock_levels { dr_pepper: 100, sprite: 10, root_beer: 0, cola: 8 } end end 

Из-за способа, которым методы объявляются в конечном автомате, мы можем, как только мы выполнили какие-либо пользовательские действия или новый метод, мы просто вызываем super чтобы передать вызов обратно по дереву исходному событию.

С содовой

После этого небольшого обхода пользовательского опыта нам все еще нужно завершить транзакцию для содовой. Для этого нам понадобится еще одно событие, которое сработает, когда сода может упасть с лотка. Проще говоря, мы настраиваем это событие, переводя его из состояния dispense_soda в `complete:

 class SodaTransaction state_machine :state, initial: :awaiting_selection do event :button_press do transition :awaiting_selection => :dispense_soda, if: :in_stock? end event :soda_dropped do transition :dispense_soda => :complete end end def button_press(selection) @selection = selection super end def in_stock? stock_levels[@selection] > 0 end def stock_levels { dr_pepper: 100, sprite: 10, root_beer: 0, cola: 8 } end end 

Выполнение нескольких строк в irb показывает, что мы успешно сбросили газировку для нашего пользователя.

 require 'soda_state_machine' sm = SodaTransaction.new puts sm.state #=> awaiting_selection sm.button_press(:cola) puts sm.state #=> dispense_soda sm.soda_dropped puts sm.state #=> complete 

Теперь остается только одно, что нас беспокоит, и это управление запасами нашего содового автомата. Из приведенного выше примера мы теперь можем меньше колы.

Наблюдатели и обратные вызовы

К счастью, управлять этим было бы тривиально с использованием гема state_machine. Он предоставляет несколько обратных вызовов перехода, которые работают аналогично фильтрам Rails.

Мы можем установить обратный вызов для события :soda_dropped следующим образом.

 class SodaTransaction state_machine :state, initial: :awaiting_selection do after_transition :on => :soda_dropped, :do => :manage_stock event :button_press do transition :awaiting_selection => :dispense_soda, if: :in_stock? end event :soda_dropped do transition :dispense_soda => :complete end end def button_press(selection) @selection = selection super end def manage_stock puts "Removing 1 from the #{@selection} count" end def in_stock? stock_levels[@selection] > 0 end def stock_levels { dr_pepper: 100, sprite: 10, root_beer: 0, cola: 8 } end end 

Поскольку я использую простой хеш для управления запасами, я просто распечатываю, что объект SodaTransaction отправляет какое-то сообщение, чтобы удалить 1 из текущего подсчета запасов.

Это не совсем правильно, хотя с точки зрения SodaTransaction теперь управляет запасами содового автомата. Моя интуиция говорит мне, что мы можем захотеть взглянуть на это иначе. Не поймите меня неправильно в простой машине, которую мы реализовали, то, что мы дали, прекрасно, на мой взгляд.

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

 class SodaStockObserver def self.manage_stock(transaction, transition) puts "Removing 1 from the #{transaction.selection} count" end def self.after_transition(transaction, transition) puts "#{transition.attribute} was: #{transition.from}, #{transition.attribute} is: #{transition.to}" end end class SodaTransaction attr_reader :selection state_machine :state, initial: :awaiting_selection do after_transition :on => :soda_dropped, :do => SodaStockObserver.method(:manage_stock) after_transition SodaStockObserver.method(:after_transition) event :button_press do transition :awaiting_selection => :dispense_soda, if: :in_stock? end event :soda_dropped do transition :dispense_soda => :complete end end def button_press(selection) @selection = selection super end def in_stock? stock_levels[@selection] > 0 end def stock_levels { dr_pepper: 100, sprite: 10, root_beer: 0, cola: 8 } end end 

Как вы можете видеть, использование наблюдателя на обратном вызове перехода может быть очень мощным, если не будет больших усилий. Примечание о коде наблюдателя является использование
transition.attribute в методе after_transition . Это просто представляет, какой ключ мы дали нашему конечному автомату, в данном случае «состояние».

Как упоминалось ранее, в геме state_machine есть несколько замечательных интеграций с популярными слоями базы данных, такими как ActiveRecord. Частью этих интеграций являются «автоматические» обратные вызовы. Например, если объект SodaTransaction унаследован от ActiveRecord, тогда простое определение SodaTransactionObserver позаботится о подключении для нас.

 class SodaTransaction < ActiveRecord::Base ... end class SodaTransactionObserver < ActiveRecord::Observer def after_soda_dropped(transaction, transition) # Remove 1 from selection qty. end def after_transition(transaction, transition) # Do whatever logging we need end end 

Практическое использование

Что касается государственных машин в дикой природе, то их много. Проекты, которые приходят на ум, — это Spree , RestfulAuthentication и EmberJS без Ruby.

Spree использует FSM в процессе оформления заказа, переходя от получения различных платежных реквизитов к завершению заказа. Вы даже можете подключиться к FSM и ввести свои собственные состояния для дальнейшей настройки процесса оформления заказа.

RestfulAuthentication (кто-нибудь использует это после Devise ?) Реализует конечный автомат ( acts_as_state_machine ). Он управляет учетной записью пользователя, «ожидающий», «активный», «приостановленный» и т. Д.

Ember, с другой стороны, хотя он явно не написан на Ruby, использует конечный автомат в качестве принципа маршрутизации. Когда пользователь перемещается по сайту, маршрутизатор использует URI для определения состояния приложений.

Был также момент, когда конечный автомат некоторое время был интегрирован в ActiveRecord, но был удален до версии 3.0 .

По моему опыту, они являются настоящим асом в дыре для разработчиков, и я был удивлен тем, как мало они на самом деле используются. Тем не менее, автоматы могут быть реализованы в неправильных ситуациях, что делает жизнь в целом несчастной. Я полагаю, что совет на вынос будет, во всяком случае, начинаться с использования массива состояний и переменной экземпляра state на ваших объектах. Но никогда не бойтесь внедрять более полное решение FSM, когда первое становится слишком громоздким.