-
-
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 |
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.
@hosh, can you clarify some statements for me?
It seems like you always want a valid model when entering the next state.
Example?