#101 Refactoring Out Helper Object
Apr 14, 2008 | 7 minutes | Views, Refactoring
If you have complex view logic, this can easily lead to helper methods which call each other. See how to refactor this out into another object in this episode. This is also known as the presenter pattern.
Another excellent episode (I see you've moved the sponsor ads to the beginning of the shows now too :( ).
I found Railscasts yesterday, and am catching up on all 101 episodes ASAP ^_^ Keep up the great work.
Have you considered a series of screencasts where you make an actual application from scratch, rather than edit an existing one. I know that for some people like myself, it would help get a grasp of which foot to start off on ;)
I know a guestbook was made in 10 minutes (there was a demo somewhere). How about building a forum ? :P I don't think there is one out there atm ^_^
Haven't rails default pagination helpers used this model?
Considering you (for example) need to render a rating for like... 100 products it would take 100 objects with their initializations, isn't that a bit too slow?
@Artūras Šlajus:
you may cache the results. so rails won't initialize the StarsRenderer each time.
Hello Ryan, thanks for this great episode. I was wondering why this StarRenderer class cannot live in lib/ directory. I use to put all my custom code in there.
Haven't tried yet but, there is a reason?
Thanks again for your time and dedication.
Ryan, this is your finest episode to date.
More episodes like this one! Where you get into refactoring and start touching on design patterns.
Good job kid!
Thanks for another great episode Ryan. Custom classes are still a little scary for me, but this exposure helps a lot. Thanks!
One question: rather than adding the method:
def method_missing(*args, &block)
@template.send(*args, &block)
end
could you also have the new class inheret the application controller? I.e.
class StarsRenderer < ApplicationController
I'm working on my 5 tips...
Hello Ryan your screencast are really great. They helped me to get a better rails coder :-).
Keep up the great work. Have you considerd a episode of the plugin SuperInPlaceControls for inline Edition or just some more about Ajax stuff?
Thanks for all episodes i watched them all.
It should be easier to share this widgets among different rails apps...
(I mean controller, images, views)
Do you know a more plug'n'play way to share these piece of code? I dunno embedded_actions, plugins, cells?
Is this not something for which you would want to use a component. I know using rails components is considered bad. But I think this problem could very well be solved with the concept of components. It would be great if rails would get a better and fast implementation for components.
Check out the stencil plugin. Its basically a subclass for these types of helpers.
http://rubyforge.org/projects/stencil/
I have a question about the method_missing part. Is it best practice to design your code around exception being thrown? The code in the method_missing part isn't unknown at the time the class is written (like the find_by methods). It seems an expensive way to get content_tag instead of @template.content_tag.
Thanks for the episodes they have been really helpful
Guys, I'd like to suggest going here www.rubyheroes.com and nominating someone (hint, hint) for his outstanding tutorials, delivered weekly, about Ruby on Rails. Six people will be selected to win an award on stage next month at RailsConf (which, while in my home town, I won't be able to attend this year, boo hoo!) If anyone deserves an award, it would be Ryan, don't you think?
@Artūras, a new object is created for each rating, yes. However, I don't think this will be a problem as each object is displayed on the screen. From my understanding, the problem with the old paginator is that each page was an object, so if you have thousands of pages you end up with thousands of objects even though not every one needs to be displayed. You will likely not need to display thousands of ratings at once.
@Jorge, good point! I considered placing this in the lib directory but decided not to because of the similarity these objects have with helpers. Either way I think it's best to make a renderer directory (which I really should have) and add it there.
@bryce, the renderer isn't a controller, so I'm not sure how inheriting from ApplicationController will benefit you. It does have some similarities in how it can render views, but the controller does so much more that this should not be concerned about. Also it's best to keep view code out of the controllers, but I feel okay to put view code in a renderer.
@Ale, I recommend moving this into a plugin if you want to share it between apps. However I find it's easy to get carried away with this and make the code more abstract than it really needs to be. Keep it specific to your application until you really see duplication with another app and then refactor that into a plugin.
@Joran, for me components often have too much overhead. I'm also not sure what benefit it has over this technique.
@James, I generally try to stay away from method_missing, especially in more complex objects when dealing with inheritance. However, the renderer seems simple enough where the downsides of method_missing aren't as apparent. In the code example I gave it's not really necessary, but if you're calling helper methods frequently in a renderer it can be very convenient.
I don't see how it became less complex than it was. It becomes even more complex when you're introducing template passing to renderer initializer. I thing this is the one of worst implementations of Presenter.
Further more you're using method missing where it's absolutely not needed. That's how Rails becomes slow.
Ryan, I really like this technique. I too have some concerns about performance, but for now this has already opened my eyes to many things that ruby & rails are capable of.
I am wondering though, how would you write specs for something like this? I am guessing they would be unit tests rather than helper tests? Do you have to do anything special to set them up?
Thanks for this episode Ryan, I used your approach soon after watching this to make myself a JQuery UI Tabs Helper :)
http://www.codeofficer.com/blog/entry/ruby_on_rails_jquery_ui_and_tabsrenderer/
looking forward to the next episode!
Why not:
def render_starts(rating)
content_tag :div, (0...5).map do |position|
image_tag begin
if rating >= position + 1
"/images/full_star.gif"
elsif rating <= position
"/images/empty_star.gif"
else
"/images/half_star.gif"
end
end
end.join, :class => 'stars'
end
It has only 11 lines and seems to be simpler then separate object.
Rearange of if's (full, half, empty) can increase the simplicyty.
This could be shortened with code similar to:
a = Array.new(rating.floor){"full"}
(rating.ceil-rating.floor).times{a<<"half"}
(5-rating.ceil).times{a<<"empty"}
Hi Ryan,
The method missing definition really helps clean things up.
I just implemented something similar to this on a project and I noticed that some of that somethings weren't making it to the view.
After fiddling with it for a bit I realized that the methods I was calling (for example: puts) already existed in the scope of the object so it wasn't sending them to @template and so the output wasn't making it to the browser.
This was easy to correct my explicitly calling @template.puts but, I'm just wondering if you know a cleaner way. :)
Thanks.
Ryan,
Why not just move the helper methods that were public into private static methods at the bottom of your file. Then it is clearer to the coder and they do not have to go look at another file.
Let me first say that I find these screencasts extremely useful, even (in fact, particularly) when one partially disagrees with a particular solution proposed by Ryan; he justs puts your neurons in motion.
Here, I question the need of a new class, based on the assertion that "with a class, we can introduce instance variables, while instead with the module we need to pass arguments..".
This is not completely correct; modules can define instance variables (if their name does not clash with existing ones).
And if we created a StarRenderer module:
a) we could define (if needed) instance variables (adding perhaps a prefix sr_) in the module's 'entry point' method.
b) the helper module needing its services would just need to do 'include StarRenderer'.
c) the need of passing 'self' and handling 'method_missing' would simply disappear.
But what if we needed to keep track of 'star results' for different products in the same view (ie, for some reason, they would not be consumed after each calculation)?
Answer: store each result in a virtual attribute in the model (this is 'the class').
Of course, the solution is less elegant than a class; it is also quite lighter, and it would address the 2 problems raised:
a) avoid littering the helper modules with bizarre methods (problem tackled by Ryan).
b) avoid making Rails slower than what already is (see comments above).
Thanks for this excellent and simple way to use presenter objects.
However, in one application I have a problem with the method_missing.
It seems not to work for the select helper method that I call.
(Maybe the select method that I call is already known for the object as another select method). However, it works if I use self.select instead.
Mybe your solution only works for missing methods and that dublicate methods demands the use of self.
Hi.
I'm trying to test the renderer class but I can't figure out how what to send as "template" when I initiate an instance in my unit test.
For example one of my helpers has:
BoxesRenderer.new( @box, self )
with works perfectly.
But what to substitute "self" with in my unit tests?
Here is my code: http://pastie.org/478963
Hi
I am very new to rails, any ideas why I am getting this error when I try and apply this helper method?
"You have a nil object when you didn't expect it!
You might have expected an instance of Array.
The error occurred while evaluating nil.-"
Thanks
@Nicklas, I just had the same question and passing ActionView::Base.new as the template worked for me!
Good luck!
@Nicklas, a better way to test your helper object would be to create a mock object instead of an actual ActionView::Base object, and test that the mock is called properly. No need to test ActionView--it's got a rich set of tests of its own!
I have been watching RailsCasts for roughly 2 years now devotedly, and I can't believe it took me this long to get to #101, and boy is this Rails 101!! Another excellent production. I will be using this immediately. Thanks Ryan.