Introducción (Opcional).
Cuando me encontraba estudiando la universidad tuve la oportunidad de cursar la materia de Automatas, materia en la cual trabajamos con máquinas de estados. Regularmente cuando trabajamos con este tipo de temas, temas teóricos, el primer paso era la explicación, posteriormente realizamos algo práctico. Si no mal recuerdo programe una máquina de estados en Java, proyecto que al día de hoy me daría mucha verguenza mostrar, principalmente por mi código espantoso. Después de finalizar el tema, nunca más volví a trabajar con una máquina de estados, hasta que me incorporé al equipo de códigofacilito, fue aquí donde me percate que las máquinas de estados pueden ser implementadas en todas partes, en cajeros automáticos, al momento de pagar el mercado en el super, al manejar un auto, etc... muy probablemente alguno de tus procesos puede ser automatizado con una máquina de estados, es por ello que en esta ocasión aprenderemos a crear máquinas de estados con Ruby on Rails.
Para este post estaré trabajando con la versión 5 de Ruby on Rails.
Máquinas de estados finita.
Una máquina de estados finita no es más que la forma en la cual podemos modelar el comportamiento de una sistema. Por ejemplo, el proceso de publicar un libro puediese ser el siguiente.
En este pequeño diagrama podemos encontrar cuatro estado : en borrador, verificado, rechazado y publicado. El libro comenzará en un estado por default, en este caso en borrador.
Con estos cuatro estados podemos definir las siguientes reglas:
- Un libro podrá ser publicado si anteriormente fue verificado.
- Un libro podrá ser rechazado si anteriormente fue verificado.
- Un libro en borrador podrá ser verificado.
Para que nuestro sistema pueda pasar de un estado a otro necesitamos una transición. Una transición la representaremos por una línea, la cual finalizará con una flecha.
Bien, ahora, representemos esta maquina de estados en Rails.
Modelos
Lo primero que haremos será crear nuestro modelo Book.
rails g model Book state:string
Los estados los almacenaremos en un atributo de tipo string. Con esto tenemos la flexibilidad que el día de mañana podamos agregar más estados al modelo.
Es posible trabar la máquina de estos con enums.
Para este post estaremos trabajando con la gema assm (Act as State Machine).
#Gemfile
gem 'aasm'
bundle install
Dentro de nuestro modelo definimos nuestra máquina de estados.
class Book < ApplicationRecord
include AASM
#Columna con la cual manejaremos los estados!
aasm column: 'state' do
state :draft, initial: true
state :verified
state :published
state :rejected
end
end
Una vez hemos colocamos todos los estados e indicado cuál será el estado inicial, el siguiente paso es definir todas las transiciones. En este caso nuestra máquina de estados poseerá tres transiciones.
- De borrador a verificado.
- De verificado a publicado.
- De verificado a rechazado.
aasm column: 'state' do
...
event :verify do
transitions from: :draft, to: :verified
end
event :reject do
transitions from: :verified, to: :rejected
end
event :publish do
transitions from: :verified, to: :published
end
end
Los eventos serán los acontecimientos que modifiquen el estado del sistema. Todo cambio de estado deberá hacerse a través de un evento. Los eventos serán representados mediante métodos.
Bien, estos serían los pasos mínimos para definir nuestra máquina de estados.
Ahora es momento de probar. En nuestra consola creamos un nuevo libro.
book = Book.create
book.state
=> "draft"
Cómo observamos el estado inicial de nuestro libro será draft, tal y como lo hemos indicado en nuestro modelo.
Ahora hagamos un cambio de estado.
book = Book.last
book.verify
=> true
book.state
=> "verified"
Si queremos persistir el cambio de estado en nuestra base de datos, finalizamos los eventos(métodos) con el signo de admiración !.
book = Book.last
book.verify!
(1.2ms) begin transaction
SQL (1.9ms) UPDATE "books" SET "state" = ?, "updated_at" = ? WHERE "books"."id" = ? [["state", "verified"], ["updated_at", "2018-09-01 21:55:47.314368"], ["id", 1]]
(1.0ms) commit transaction
Una vez el libro se encuentra verificado ya es posible publicarlo o rechazarlo.
Si queremos conocer si nuestro objeto se encuentra en un estado en especifico, únicamente realizamos la pregunta.
book.draft?
=> false
book.verified?
=> true
book.rejected?
=> false
book.published?
=> false
Callbacks
Muy probablemente en cada cambio de estado necesitemos ejecutar ciertas acciones, por ejemplo, si un libro fue publicado, que mejor que notificar al autor de la buena noticia. En estos caso necesitaremos hacer uso de los callbacks.
Los callbacks los podemos implementar a nivel máquina de estado o a nivel de transición.
A nivel máquina de estado usaremos los métodos:
- before_all_transactions
- after_all_transactions
A nivel maquina de transición usaremos los métodos:
- before_transaction
- after_transaction
- after_commit (Después de persistir los cambios)
aasm column: 'state' do
state :draft, initial: true
state :verified
state :published
state :rejected
after_all_transactions :after_transactions
...
event :publish,
before_transaction: :before_transaction,
after_commit: :send_congratulations_mail do
transitions from: :verified, to: :published
end
end
def before_transaction
puts "Antes de la transacción!!"
end
def after_transactions
puts "Después de la transacción!!"
end
def send_congratulations_mail
puts "Aquí debemos de enviar nuestro mail!"
end
Validaciones
Realizar validaciones en nuestros modelos es de suma importancia, con las máquinas de estados no es la excepción. Aunque por si sola la máquina de estados ya nos provee de ciertas validaciones, por ejemplo, no permitir pasar de un estado a otro sin una transición valida, habrá ocasiones en las que necesitemos validar más que solo la transición, en esos casos haremos huzo de los guards.
Podemos validar una transición a partir del resultado de un método. El cambio de estado se hará, siempre y cuando el método regrese verdadero.
aasm column: 'state' do
...
event :verify, guard: :is_valid_to_verify? do
transitions from: :draft, to: :verified
end
...
end
...
def is_valid_to_verify?
true
end
Si nosotros queremos asegurarnos que es posible realizar una transición, nos apoyaremos de los métodos "may". Estos métodos tendrán la siguiente estructura.
may_ + evento + ?
book.may_publish?
=> true
book.may_reject?
=> true
book.may_verify?
=> false
Conclusión
Las máquinas de estados nos permiten crear modelos muchos más robustos, de tal forma que tengamos un control sobre todos los posibles cambios que puedan suscitarse a lo largo de algún proceso. Con las máquinas de estado podemos definir de forma concreta qué hacer en caso de 😃.