Entender las máquinas de estado, State Machine, AASM y Workflow


Ruby_on_Rails_logo (1)Hace poco que me encontré en la situación de utilizar en un proyecto “state machine” y en alguna que otra ocasión alguna discusión sobre si utilizar State machine o Workflow. Si voy a the Ruby ToolBox me encuentro que en el top de utilización está en orden de aparición State Machine, AASM y Workflow por lo que he montado un pequeño laboratorio de prácticas en las que pongo en prueba las tres y entender cómo funcionan.

State Machine

Vamos a ver primero en orden de aparición a State Machine y su enlace a GitHub por si tienes curiosidad, pero básicamente nos permiten manejar el comportamiento de una clase, que si lo piensas, podríamos hacerlo mediante atributos booleanos, pero podría complicarnos la vida del mantenimiento de dicha clase si aumentase su complejidad. Aquí es dónde aparecen las máquinas de estado.

Como primen paso para entender cómo funcionan veamos que primero necesitamos una tabla donde el comportamiento se quede reflejado y una clase. Nuestra tabla se llama orders donde se guardan los pedidos y los estados por los que pasa, ahora vemos cómo se pasan de un estado a otro. Pues bien, nos creamos un fichero de migraciones en db/migrate para crear nuestra tabla:

class CreateOrders < ActiveRecord::Migration
          def change
                   create_table : orders do |t|
                                 t.string :state
                                 t.timestamps
                   end
          end
end

Después tenemos que añadir en nuestro Gemfile la línea de utilización de la gema gem ‘state_machine’ y correr en modo comando bundle o bundle install para la instalación de la gema o gemas necesarias. Pasemos entonces al modelo que lo importante de todo el mecanismo. Para lo que nos creamos un modelo llamado order.rb en el directorio de model de la siguiente forma:

class Order < ActiveRecord::Base
state_machine initial: :incomplete do

        event :purchase do
transition :incomplete => : open
end

        event :cancel do
transition : open => :canceled
end

        event :resume do
transition :canceled => : open
end

        event :ship do
transition : open => :shipped
end

  end
end

Si nos vamos a la consola de rails con rails c ya estamos en predisposición de probar nuestra máquina de estados y lo primero que vamos a ejecutar ya que no tenemos nada en nuestra tabla de orders, es lo siguiente:

o = Order.create

con lo que nos va a crear nuestro primer registro y con nuestro primer estado

1.9.3-p327 :019 > o = Order.create
(0.1ms) begin transaction
SQL (0.5ms) INSERT INTO “orders” (“created_at”, “state”, “updated_at”) VALUES (?, ?, ?) [[“created_at”, Sat, 30 Mar 2013 14:53:58 UTC +00:00], [“state”, “incomplete”], [“updated_at”, Sat, 30 Mar 2013 14:53:58 UTC +00:00]]
(908.5ms) commit transaction
=> #<Order id: 2, state: “incomplete”, created_at: “2013-03-30 14:53:58”, updated_at: “2013-03-30 14:53:58”>

Ya te habrás fijado, que nuestro primer estado de nuestra primera orden de pedido es state : “incomplete” y ¿por qué este estado y no otro? pues para eso nos vamos a la declaración de nuestra clase, y vemos que le estamos diciendo de una manera coloquial, que cuando arranque nos ponga a incomplete su estado y después pasamos a decirle los diferentes estados por lo que puede pasar en un orden lógico. En nuestro caso  state_machine initial: :incomplete do.

Los siguientes estados por los que puede pasar los definimos como event :purchase do, event :cancel do, event :resume do y event :ship do, pero no todos ellos se pueden llamar en el orden que nos interese, por ejemplo si llamamos a o.purchase nos daría como resultado:

1.9.3-p327 :020 > o.purchase
(0.1ms) begin transaction
(0.4ms) UPDATE “orders” SET “state” = ‘open’, “updated_at” = ‘2013-03-30 15:41:51.217485’ WHERE “orders”.”id” = 2
(609.9ms) commit transaction
=> true

El cambio de estado ha sido correcto y nos lo a dejado a open, ¿pero por qué? fíjate en la declaración del evento:

        event :purchase do
                     transition :incomplete => : open
end

Lo que le estamos diciendo es que si llamamos a :purchase la transición es de incomplete a open, si estamos en cualquier otro estado de partida que no sea incomplete nos devuelve false. En nuestro caso hemos partido de incomplete al crear nuestra primera orden de pedido y nos dejo en dicho estado, después hemos llamado a purchase y el mismo ha comprobado si corresponde el estado anterior antes de pasarlo al nuevo estado. Si ahora intentamos pasar del estado en el que nos encontramos a por ejemplo resume nos devuelve false ya que no nos encontramos en un estado :canceled para pasar al siguiente estado.

1.9.3-p327 :021 > o.resume
=> false

1.9.3-p327 :028 > o.state_events
=> [:purchase]

En cualquier momento podemos decirle qué podemos hacer como siguiente paso con la orden de o.state_events y nos devuelve el siguiente paso, en esta caso => [:purchase].

AASM

Vamos a ver ahora como decía anteriormente en orden de aparición a AASM y su enlace a GitHub por si quieres echarle un vistazo. Primero necesitamos como teníamos antes en State Machine, una tabla donde este comportamiento se quede reflejado y una clase. Nuestra tabla se llama igual orders donde se guardan los pedidos y los estados por los que pasa, ahora vemos cómo se pasan de un estado a otro en AASM. Pues bien, nos creamos un fichero de migraciones en db/migrate para crear nuestra tabla:

class CreateOrders < ActiveRecord::Migration
           def change
                    create_table : orders do |t|
                                t.string :aasm_state
                                t.timestamps
                   end
          end
end

Después tenemos que añadir en nuestro Gemfile la línea de utilización de la gema gem ‘aasm’ y correr en modo comando bundle o bundle install para la instalación de la gema o gemas necesarias. Pasemos entonces al modelo que es la parte importante. Para lo que nos creamos un modelo llamado order.rb en el directorio de model de la siguiente forma:

class Order < ActiveRecord::Base
include AASM

scope : open_orders, -> { where(aasm_state: “open”) }
attr_accessor :invalid_payment

aasm do
             state :incomplete, initial: true
             state : open
            state :canceled
           state :shipped

          event :purchase, before: :process_purchase do
transitions from: :incomplete, to: : open, guard: :valid_payment?
end

         event :cancel do
                    transitions from: : open, to: :canceled
          end

          event :resume do
                   transitions from: :canceled, to: : open
           end

          event :ship do
                   transitions from: : open, to: :shipped
           end
end

def process_purchase
# process order …
end

def valid_payment?
!invalid_payment
end
end

Fíjate ahora que, primero necesitamos hacer un include de AASM y para la declaración de los estados un bloque aasm do. Si queremos preguntar en que estado se encuentran tenemos la primera declaración:

state :incomplete, initial: true Le estamos indicando que es el primero
state : open
state :canceled
state :shipped

Cuando hagamos una orden nueva desde consola de Rails:

1.9.3-p327 :020 > p = Order.create
(0.1ms) begin transaction
SQL (0.6ms) INSERT INTO “orders” (“aasm_state”, “created_at”, “updated_at”) VALUES (?, ?, ?) [[“aasm_state”, “incomplete”], [“created_at”, Sat, 30 Mar 2013 20:26:47 UTC +00:00], [“updated_at”, Sat, 30 Mar 2013 20:26:47 UTC +00:00]]
(480.8ms) commit transaction
=> #<Order id: 5, aasm_state: “incomplete”, created_at: “2013-03-30 20:26:47”, updated_at: “2013-03-30 20:26:47”>

y queremos preguntar los los estados podemos hacer las consultas de:

1.9.3-p327 :032 > p.shipped?
=> false
1.9.3-p327 :033 > p.canceled?
=> false
1.9.3-p327 :034 > p.open?
=> true
1.9.3-p327 :035 > p.incomplete?
=> false

Cuando necesitamos pasar de un estado a otro, tenemos que utilizar p.purchase que corresponde a las declaraciones de event

WorkFlow

Vamos a ver ahora finalmente y siguiendo el order a WorkFlow y su enlace a GitHub por si quieres echarle un vistazo. Primero necesitamos como teníamos antes en State Machine, una tabla donde este comportamiento se quede reflejado y una clase. Nuestra tabla se llama igual orders donde se guardan los pedidos y los estados por los que pasa, ahora vemos cómo se pasan de un estado a otro en AASM. Pues bien, nos creamos un fichero de migraciones en db/migrate para crear nuestra tabla:

class CreateOrders < ActiveRecord::Migration
           def change
                    create_table : orders do |t|
                                  t.string :workflow_state
                                  t.timestamps
                    end
          end
end

Después tenemos que añadir en nuestro Gemfile la línea de utilización de la gema gem ‘workflow’ y correr en modo comando bundle o bundle install para la instalación de la gema o gemas necesarias. Pasemos al último modelo. Para lo que nos creamos un modelo llamado order.rb en el directorio de model de la siguiente forma:

class Order < ActiveRecord::Base
           include Workflow

           scope : open_orders, -> { where(workflow_state: “open”) }

          workflow do
                  state :incomplete do
                            event :purchase, transition_to: : open
                end
                state : open do
                          event :cancel, transition_to: :canceled
                          event :ship, transition_to: :shipped
                 end
                state :canceled do
                         event :resume, transition_to: : open
                end
                state :shipped
                end

          def purchase(valid_payment = true)
                   # process purchase …
                  halt unless valid_payment
           end
end

Nos creamos una order nueva:

1.9.3-p327 :004 > order = Order.create
(0.0ms) begin transaction
SQL (3.5ms) INSERT INTO “orders” (“created_at”, “updated_at”, “workflow_state”) VALUES (?, ?, ?) [[“created_at”, Sat, 30 Mar 2013 20:48:51 UTC +00:00], [“updated_at”, Sat, 30 Mar 2013 20:48:51 UTC +00:00], [“workflow_state”, “incomplete”]]
(612.7ms) commit transaction
=> #<Order id: 1, workflow_state: “incomplete”, created_at: “2013-03-30 20:48:51”, updated_at: “2013-03-30 20:48:51”>

para preguntar por cada uno de los estados order.open? order.canceled?..

Y para pasar finalmente de un estado a otro, debemos seguir el orden lógico de cada declaración de estado para ejecutar los eventos:

1.9.3-p327 :038 > order = Order.create
(0.1ms) begin transaction
SQL (0.6ms) INSERT INTO “orders” (“created_at”, “updated_at”, “workflow_state”) VALUES (?, ?, ?) [[“created_at”, Sat, 30 Mar 2013 21:02:50 UTC +00:00], [“updated_at”, Sat, 30 Mar 2013 21:02:50 UTC +00:00], [“workflow_state”, “incomplete”]]
(518.8ms) commit transaction
=> #<Order id: 3, workflow_state: “incomplete”, created_at: “2013-03-30 21:02:50”, updated_at: “2013-03-30 21:02:50”>
1.9.3-p327 :039 > order.purchase!
(0.1ms) begin transaction
(0.4ms) UPDATE “orders” SET “workflow_state” = ‘open’, “updated_at” = ‘2013-03-30 21:03:01.204319’ WHERE “orders”.”id” = 3
(1306.7ms) commit transaction
=> true
1.9.3-p327 :040 > order.cancel!
(0.1ms) begin transaction
(0.4ms) UPDATE “orders” SET “workflow_state” = ‘canceled’, “updated_at” = ‘2013-03-30 21:03:41.888602’ WHERE “orders”.”id” = 3
(557.8ms) commit transaction
=> true
1.9.3-p327 :041 > order.resume!
(0.1ms) begin transaction
(0.3ms) UPDATE “orders” SET “workflow_state” = ‘open’, “updated_at” = ‘2013-03-30 21:03:51.269845’ WHERE “orders”.”id” = 3
(1213.0ms) commit transaction
=> true

1.9.3-p327 :042 > order.ship!
(0.1ms) begin transaction
(0.3ms) UPDATE “orders” SET “workflow_state” = ‘shipped’, “updated_at” = ‘2013-03-30 21:07:12.216573’ WHERE “orders”.”id” = 3
(577.7ms) commit transaction
=> true

Ahora lo mejor es que pruebes dentro del proyecto cual te va mejor incorporar.

Un comentario en “Entender las máquinas de estado, State Machine, AASM y Workflow

Responder

Introduce tus datos o haz clic en un icono para iniciar sesión:

Logo de WordPress.com

Estás comentando usando tu cuenta de WordPress.com. Cerrar sesión / Cambiar )

Imagen de Twitter

Estás comentando usando tu cuenta de Twitter. Cerrar sesión / Cambiar )

Foto de Facebook

Estás comentando usando tu cuenta de Facebook. Cerrar sesión / Cambiar )

Google+ photo

Estás comentando usando tu cuenta de Google+. Cerrar sesión / Cambiar )

Conectando a %s