We'll respond shortly.
Better Rails Code through
…ActiveRecords with no public methods that have side-effects–other than Create, Update, and Destroy (CUD).
In a typical web application, some “triggered actions” or “side-effects” occur in response to various events. Examples: A confirmation email is sent when a User registers. A ping is sent to Technorati when a BlogArticle is published. Cookies are set when a user logs in. Two Friendship objects are created between two users when one approves the other’s FriendshipRequest.
Often these side-effects are modeled with public methods on an ActiveRecord. For instance,
class FriendshipRequest < ActiveRecord::Base def accept! self.status = ACCEPTED save! Friendship.create!(:from => from, :to => to) Friendship.create!(:to => from, :from => to) end end
This is code is dangerous, as shown below. The principal of CUDly models is eliminate all public methods on your ActiveRecord that have any side effects. CUDly models are much safer. Let’s replace the dangerous code with the equivalent, more friendly, CUDly code:
class FriendshipRequest after_update :create_mutual_friendship private def send_email_on_accept if status == ACCEPTED Friendship.create!(:from => from, :to => to) Friendship.create!(:to => from, :from => to) end end end
The above CUDly code exploits the richness of ActiveRecord’s lifecycle callbacks to trigger the side-effect. This illustrates a general principle: in a perfectly CUDly world, constrain the interface to your models such that no methods have side effects other than Create, Update, and Destroy.
There are several virtues to CUDly models: encapsulation, transactions, consistent interface, and lifecycle power.
Your side-effects can be hidden behind the standard ActiveRecord interface. And your Controllers will be skinny as can be! Compare:
class FriendshipRequestsController def update friendship_request = FriendshipRequest.find(params[:id]) if params[:friendship_request][:state] == ACCEPTED friendship_request.accept! else friendship_request.reject! end end end
Instead, you can write an equivalent, Formulaic Controller:
class FriendshipRequestsController def update friendship_request = FriendshipRequest.find(params[:id]) friendship_request.attributes = params[:friendship_request] friendship_request.save end end
Thanks to ActiveRecord, the CUDly approach has rich transactional semantics. In the CUDly implementation, if any of the Friendship.create! invocations fails, the entire transaction is rolled back, meaning the you cannot put the world in an incoherent state (where Tom and Dick are only half-friends). The equivalent non-CUDly code is the onerous and obese:
class FriendshipRequest < ActiveRecord::Base def accept! self.status = ACCEPTED self.class.transaction do save! Friendship.create!(:from => from, :to => to) Friendship.create!(:to => from, :from => to) end end end
Who wants to cuddle with code like that?!
ActiveRecord already has a wonderful pattern: build, then test. It’s so simple, and yet so powerful. Why not re-use it?
f = Friendship.new if f.save ...
#Save returns true or false, and it sets errors on the model to be displayed by the user. Using CUDly code, you continue to do that:
class FriendshipRequestsController def update friendship_request = FriendshipRequest.find(params[:id]) friendship_request.attributes = params[:friendship_request] if friendship_request.save flash[:notice] = ... else flash[:error] = ... render :action => :edit end end end
Try doing that with unCUDly #accept and #reject methods! Surely it will be unCUDly and ungodly!
A fundamental limitation of public, side-effecting methods is that they can be called at any time for any reason. Suppose we had something like this:
class MyModel < ... def foo=(bar) send_an_email! end
This could be called as part of #attributes=, triggering the email deilvery regardless of whether the model was valid and could be saved! In the CUDly implementation, on the other hand:
class MyModel after_save :send_an_email def send_an_email ...
Thanks to the flexibility of
#after_find, etc., you can ensure that your triggered action only happens after successful validation, or regardless of validation, or only on update, or only on destroy–you name it! Try enforcing that with a public method!
There is a beautiful symmetry in having all side-effecting methods “funneled” through the three “dangerous” methods (create, update, and destroy). It appeals to my sense of elegance and order. I’ve used this design strategy 100% for the last few months and it’s been a smashing success! It truly is the way ActiveRecord was meant to be used. So give it a try!