Skip to content

Instantly share code, notes, and snippets.

@tekwiz
Created April 26, 2010 06:12
Show Gist options
  • Save tekwiz/379033 to your computer and use it in GitHub Desktop.
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
Copy link

hosh commented Apr 26, 2010

class UserActivation < Flow
  state_machine_for User, :status
  state :new
  state :normal
  state :locked

  # State is automatically saved, but this adds an additional hook
  transition :activate, :new => normal do |user|
    user.increment_loyalty_points(50, "50 points for signup!")
  end

end

class User
end

class UserController < ActionController
  def update
    @user = User.find(parms[:user])
    UserActivation.activate!(@user).on_success { |user|
      UserMailer.deliver_sign_bonus_points(user)
    }.on_failure { |user|
      AdminNotifier.deliver_error(user, "Something went horribly wrong")
    }    
  end
end

@hosh
Copy link

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

@hosh
Copy link

hosh commented Apr 26, 2010

@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)

@tekwiz
Copy link
Author

tekwiz commented Apr 26, 2010

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:

  1. the state of 3 or more models must be maintained and
  2. 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.

@tekwiz
Copy link
Author

tekwiz commented Apr 26, 2010

@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?

@hosh
Copy link

hosh commented Apr 27, 2010

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.

@hosh
Copy link

hosh commented Apr 27, 2010

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment