-
-
Save tekwiz/379033 to your computer and use it in GitHub Desktop.
state_machine User do | |
state :new | |
state :normal | |
state :locked | |
end | |
state_machine Membership do | |
state :new | |
state :active | |
state :inactive | |
end | |
state_machine Account, User do | |
# interesting idea... thoughts on this? | |
end |
class User | |
state_machine do | |
state :new | |
state :normal | |
state :locked | |
end | |
end |
hosh
commented
Apr 26, 2010
Here are the reasoning:
- MVC -> Models, Views, Controllers. Where do "workflows" fit in?
- Models -> "How to manipulate data"
- Controllers -> "What data to manipulate"
- Workflow -> kinda "what", kinda "how"?
- All ActiveRecord state machine suck.
- No one has come up with a good one, which is why you are deriving your Apollo off of the workflow stuff
- They don't suck because of features, they feel like something is wrong because the state machine is inside the model itself
- Managing state now require you to manage side-effects
- Side-effects suddenly makes the whole thing more difficult to test
- You have to keep constructing these valid model objects just to mess around with the state transitions
- What are workflows really?
- They are an event pattern!
- Evented code works by taking in data objects, matching them against an event, process real quick, and return
- Workflows get more complicated, but fortunately, latency isn't as big of an issue
- Easy to test: send in a mock object that complies with #save and #state/#status and maybe .transaction
- Easy to extract into a separate runner: runner code can load the workflows and autoload the models
- OpenWFEru takes workflow out of the models
- Unfortunately, its Java pedigree shows through
- It really live in app/workflows and work with ActiveModel-compliant models
@tekwiz This idea is still gestating. I would like smart people like you tell me why you think this is a bad idea. (For one thing, this appears to violate Single-Authority Principle)
If it's just 1 model, it's a simple state machine with actions. And if it's 2 models (and sometimes more than 2), it's still a simple state machine on the relation model between them (e.g. User-Account state machine would go on the UserAccount or Membership model) with possible implications from the User and Account models which I think would, by definition, be evaluated always before the relationship's state (e.g. user.disabled? or account.inactive?).
So in those cases, it seems like a separate Workflow model would be unnecessary at best and confusing at worst. However, if we can find a case where:
- the state of 3 or more models must be maintained and
- the relationship of the 3 models is not inherent or cannot/should not be defined or persisted
then we might have found the case where this works.
@hosh, can you clarify some statements for me?
You have to keep constructing these valid model objects just to mess around with the state transitions
It seems like you always want a valid model when entering the next state.
Managing state now require you to manage side-effects
Side-effects suddenly makes the whole thing more difficult to test
Example?
So I'm coming from having worked more with functional programming and immutable data objects. Side-effects, then, are anything that goes beyond doing a single thing in a single function, and are generally anything that goes beyond manipulating those immutable data objects. We don't have immutable data objects with Rails, and the whole data persistence layer is backed by a DB. However, what we do have is RESTful architecture where we're using four HTTP verbs and seven actions to describe the interaction with Rails and the data object. From that point of view, any code that does more than index, show, new, create, edit, update, delete are side-effects
Examples:
- In the Rackspace Cloud API, we have a RESTful resource called servers where we manage the data representing a server. This includes a convenience handle, the instance id, its flavor (RAM size), etc. However, actually initiating a build or a reboot are not directly manipulating the RESTful data, even though they are important. These are side-effects, and in some ways, we care more about the side-effect of build, reboot, shutdown, etc. The pattern in this case is that it is twiddling an external build process that is not under the control of the Rails app. When you put that logic into the model, then you're effectively putting controller-like logic (what to do, as opposed to how to do).
- User signup activation manipulates the user signup. However, what we really care about is the side-effect of doing an email confirmation or an invitation. The "external device" in this case is the human being that you are confirming or inviting.
- Comment moderation looks simple at the beginning. You either approve it or deny it. What happens when you start adding in spam filtering? Now you're going to feed the spam through an external code that returns either yea or nay.
These are all side-effects. I'm pushing around the idea that these don't really fit in the RESTful way of doing things. Rackspace Cloud Servers, for example, have you do a POST/PUT on an action endpoint to initiate those side effects, to distinguish them from purely RESTful stuff, such as manipulating the metadata.
When testing this stuff, you have the same problem you have when testing controllers. To test the controllers, now you have to create a whole bunch of valid objects and mock objects. In most controllers protected by user signup, you now have to set up a fake user just to see if it responds to CRUD actions; we're not trying to test the validity of the object, yet here we are, doing that. It gets worse if you add model logic that should be in model logic into the controllers. At that point, it is harder to run those particular logic in its own thread, or from the command line, because now you have to create a fake request just to manipulate it.
The same is true when you add controller-like logic into model logic, even in the case of manipulating a single model (such as single-model user activation).
In the above example I gave, the test would look like this:
describe UserActivation do
describe "when sending email confirmation" do
before(:each) do
@user = mock(:user)
@user.stub(:email) = "[email protected]"
end
it "should send a confirmation to the user's email" do
UserActivation.activate!(@user) ...
end
end
end
(OK, I admit, this part is vague for me and needs more thought. For one thing, it occurred to me that the email notification itself should be declared inside the UserActivation and not in the controller).
I was talking to Sam Schenken-Moore about it. He said, he thinks those state machines only solve half of the problem, because you usually put together a state machine of some sort to handle workflow and lifecycles.
This circles around the same problem domain and comes up with a different solution from a different approach: http://www.infoq.com/news/2009/11/restfulie-hypermedia-services
The states are kept in the model, but now we're linking it to the controller and adding some actions via hyperlinking into the resources directly.