manner). Would you care to explain the rationale behind moving the
parameterization into the db?
There are a few reasons behind this. First, and foremost, was related
to ensuring that the database is normalized. Since the plugin stores
state changes, state deadlines, etc. I was uncomfortable with using
the state name in these state_change/state_deadline records. Instead,
I wanted those records to reference states stored in the database.
This also helps with simplifying queries and defining associations.
In addition, that state is also referenced in the model that has the
state, so again we’re normalizing the database quite a bit.
In addition to this, I took into consideration the fact that, as your
project progresses, states may get removed and become “inactive”, i.e.
they are no longer a part of the state machine. When this occurs, the
states should still be valid in any state_change/state_deadline/model
that references it, but it should no longer be declared in your
model’s code. By storing it in the database and forcing the developer
to define which states are active in the model, we can continue to
have associations working between these various models on inactive
states and still ensure referential integrity. If it were only
defined in the class, it would be difficult to track all of the states/
events that have been used over time and were, at one point or
another, valid. You would have to look through all the records in
your database and find names of events/states that were used at one
point. It doesn’t smell right to me.
There may be arguments both ways, but this is what I felt I was most
comfortable with.
In my rethinking about these things, I came to think that it would be
useful, but never found a single example where I would use it. I think
of it like they take all they need from their context… did you find
it useful?
I have found it very useful. I often want to perform an operation
when an event occurs, based on certain data that is being passed in.
For example, I had an InvoiceTransaction model that had an event call
capture. Capture took a single value, which would basically make a
partial charge on a payment account through a payment gateway. By
passing in the value, I was able to do several things:
- Define a guard which determines whether the invoice transaction
goes to completed (meaning no more captures can be made) or captured
(meaning more captures can be made) based on whether the value being
captured has reached the maximum total amount that can be captured.
- Define an on-invocation callback that makes the call to the payment
gateway
e.g.
class InvoiceTransaction < ActiveRecord::Base
event :capture do
transition_to :completed, :if => Proc.new {|t, value|
t.max_capture_value == (t.total_captured_so_far + value)}
transition_to :captured
end
def capture(value)
# send to gateway
true
end
end
transaction.capture(50.00)
That’s obviously not the exact code I use, but it’s one use I found
for passing in parameters.
I guess that’s what I called “composite states”. Is it? I mean, a
state can have substates, and you could declare something (say, a
transition or a property) in the parent which all its children would
have (I never implemented that). Plus, you can query for a superstate,
say :active, an get all the models which are in :published
or :another_one, but not in :closed…
Hmm, that’s an interesting idea, but I was actually referring to the
fact that states, events, and transitions defined in a class are only
valid and accessible by that class and all its subclasses. I believe
there are a few issues with how transitions are handled in subclasses
in acts_as_state_machine, though I could be wrong.
That’s good. I’ve been doing it separately (was about to turn it into
a plugin, or reuse acts_as_trackable). Do you have another model, say
“Event”, as a log?
There are three models in has_states: State, Event, and StateChange.
StateChange tracks the from_state, to_state, event, and when it
occurred. For the initial state change (the first state a model is
set to), the from_state and event are set to nil. Otherwise, all
fields are always set. In addition, it tracks all changes, regardless
of whether that same change has already occurred (e.g. loopbacks).
When accessing a state change, you can choose whether to access the
first, last, or all of the changes. For example,
car.parked_at(:first), car.parked_at(:last), and car.parked_at(:all).
- Dynamic initial states (e.g. has_states :initial => Proc.new {…})
Did you find an scenario which would use it? I can’t imagine right
now…
There is a scenario I’m using it for, although it may be invalid since
it may call for inheritance where the subclass overrides the
superclass’s initial state, but this is something I have to re-
evaluate. The scenario involves an InvoiceTransaction which starts
off either as a payment to an individual from a company or from an
individual to a company. This may simply be a flaw in-of-itself,
where there is a need for a class hierarchy that separates this
functionality. I decided to keep it in there for now since I already
had it implemented and thought I would let the plugin be used
throughout various projects and see if I ever find another need for
it. If it seems that no more scenarios come up, I’ll axe it out of
the implementation.
What’s the difference with :guard ? (or my :precondition
and :postcondition)
Callbacks canceling events are meant to parallel the structure used in
callbacks for ActiveRecord. For example, you would normally cancel a
save through the use of a validation. However, if any callback (e.g.
before_save, after_save, etc.) explicitly returns false, this will
cancel the save as well. As a result, in order to make it familiar to
ActiveRecord callbacks, state/event callbacks act the same way.
Currently, the scenario in which I make use of this has to do with
making changes to other models as a result of an event being invoked.
For example, using InvoiceTransaction as an example, I have guards
setup to determine whether the model transitions to :completed
from :captured. However, in addition to this, I defined a callback
which invokes a web service to actually send the capture to the
payment gateway. If that fails, I want to cancel the event. This
really couldn’t be done as a guard, because you only want to talk to
the gateway if all other guards have passed. You could possibly hack
this by making sure it’s the last guard, but I don’t personally find
that as clear.
- Support for state deadlines through has_state_deadlines plugin (i.e.
time at which a state must change or it’ll automatically change)
I’m interested! How do you do that? got a cron hook?
This is currently implemented via two methods. First, whenever the
record is accessed, the deadline for its current state is checked and
the deadline event invoked if it has been passed. This ensures that
whenever you access the record, it is always in the correct state.
Second, we have a backgroundrb worker checking the state deadlines
every few minutes and updating the appropriate records. That’s
probably not the best way to accomplish it, and I haven’t personally
put much thought into better methods, but it does the job for now.
Anyway, I hope this answers your questions!