Friday, June 27, 2008

What are Rails Helpers for?

I was reading Dan Mange's Smart Model, Dumb Controller post, which has a very interesting comment from Greg Willits in which he proposes an additional logic layer between Controllers and Models. He and Dan go back a forth a bit on this - check it out.

This prompted me to think about what I use Helpers for. The exchange above brought up Helpers in their canonical sense, as being a sort of glue between the Controller and the View. My colleague Jim Lindley and I certainly use Helpers in that fashion, witness


module PreceptorsHelper
def self.select_list
Preceptor.find(:all).map { |p| [p.dropdown_name, p.id] }
end
end


Pretty typical. We have a Preceptor model, and our PreceptorsHelper has a select_list method that DRYs up how all of our Preceptor-related views get their select lists for dropdown menus.

Additionally, we have a dropdown_name, which differs from the plain old name field of the Preceptor, but neither the PreceptorsHelper nor the views know about that - it's properly encapsulated within the Preceptor model itself. This is basic OO design that provides tools for managing multiple levels of complexity and abstractions to handle that complexity. No surprises so far.

However, this all led me to reflect on my own use of Helpers that may not be textbook Controller<->View glue activity. I also use Helpers for logic that pertains to a given model (or more likely a collection of those models) without being a characteristic of any one of those model instances. This sounds a bit like Greg's additional controllers idea. Here's an example from another of our helpers:


module EnrollmentsHelper
def self.filter_by_readiness(enrollments, params)
ready_for_review = params[:ready_for_review]
return enrollments unless ready_for_review
enrollments.select { |e| e.ready_for_review? }
end
end


Something akin to this could also be accomplished via named_scope nowadays, I realize. However, let's abstract a bit. This is an operation on a set of things - Enrollments in this case. We certainly don't want this in the EnrollmentsController. We could make the case that it should be a class method of Enrollment. But making it a function that takes the Enrollments as a parameter makes this much easier to test. It's also more of a functional style, which I freely cop to preferring.

I guess I don't even think about Helpers as necessarily needing to provide utility for views so much as providing utility that is "about" a given topic without being a characteristic of any one of those instances.

Note that the topic doesn't even need to be a model. Our current app has several Models presenting different types of humans, and some of our clients care about gender. Presto:


module GenderHelper

VALID_GENDERS = %w[m f M F]
VALID_OPTIONS = {
:in => VALID_GENDERS,
:allow_blank => true,
:allow_nil => true,
:message => MessageHelper::MESSAGE[:invalid_gender]
}

MALE_VARIANTS = %w[BOY MAN MALE]
FEMALE_VARIANTS = %w[GIRL WOMAN FEMALE]

def self.distill_gender!(gender)
gender.andand.upcase!
gender = %q[M] if MALE_VARIANTS.include?(gender)
gender = %q[F] if FEMALE_VARIANTS.include?(gender)
gender
end

end


(Note also the similar delegation to MessageHelper). This allows us to DRYly do something like this in our Student and similar models:


validates_inclusion_of :gender, GenderHelper::VALID_OPTIONS


Nothing view-related there.

Much of this coding style sprang from a desire to simplify controllers. After doing this, I noted many comparatively large model files. One of them, Lottery, had a lot of activity that dealt with processing collections of Enrollments, which I've blogged about before. Why not move this into something more directly related to Enrollments?, I thought.

I like the results.

What do other people think about Helpers used in this fashion? Should there be two different ypes of them: view-related and non-view-related? More? I'm still sorting out my thoughts on this, and welcome new ideas to think about (or old ideas to consider anew).

No comments: