#101
Apr 14, 2008

Refactoring Out Helper Object

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.
Download (17.4 MB, 7:25)
alternative download for iPod & Apple TV (10.7 MB, 7:25)

I failed to mention in the screencast, the 3 methods which aren’t called externally for the object should be made private. See the code below for an example.

# application_helper.rb
def render_stars(rating)
  StarsRenderer.new(rating, self).render_stars
end

# helpers/stars_renderer.rb
class StarsRenderer
  def initialize(rating, template)
    @rating = rating
    @template = template
  end
  
  def render_stars
    content_tag :div, star_images, :class => 'stars'
  end

  private
  
  def star_images
    (0...5).map do |position|
      star_image(((@rating-position)*2).round)
    end.join
  end
  
  def star_image(value)
    image_tag "/images/#{star_type(value)}_star.gif", :size => '15x15'
  end
  
  def star_type(value)
    if value <= 0
      'empty'
    elsif value == 1
      'half'
    else
      'full'
    end
  end
  
  def method_missing(*args, &block)
    @template.send(*args, &block)
  end
end

RSS Feed for Episode Comments 23 comments

1. Kieran Apr 14, 2008 at 01:01

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 ^_^


2. Artūras Šlajus Apr 14, 2008 at 01:06

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?


3. Benny Apr 14, 2008 at 01:53

@Artūras Šlajus:
you may cache the results. so rails won't initialize the StarsRenderer each time.


4. Jorge Calás Apr 14, 2008 at 02:44

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.


5. peanut Apr 14, 2008 at 05:15

Pretty cool! Thanks!


6. Mig Apr 14, 2008 at 05:33

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!


7. bryce Apr 14, 2008 at 06:40

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...


8. Inside Apr 14, 2008 at 08:43

Pretty good =]

I'm working on my 5 tips too lol

[]'s


9. Hartwig De Colle Apr 14, 2008 at 10:23

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.


10. Ale Apr 14, 2008 at 10:28

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?


11. Joran Jessurun Apr 15, 2008 at 00:56

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.


12. Anthony Underwood Apr 15, 2008 at 01:58

This cast does not seem to have made its way onto the iTunes Podcast RSS Feeds. Is it because you've passed the magic 100?


13. Joshua Peek Apr 15, 2008 at 07:49

Check out the stencil plugin. Its basically a subclass for these types of helpers.

http://rubyforge.org/projects/stencil/


14. James Burka Apr 16, 2008 at 11:22

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


15. Carl Apr 16, 2008 at 13:28

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?


16. Ryan Bates Apr 18, 2008 at 20:04

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


17. Cheba May 05, 2008 at 13:23

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.


18. supaspoida May 14, 2008 at 16:42

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?


19. Russ Jones May 19, 2008 at 14:33

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!


20. Uzytkownik Jun 02, 2008 at 12:27

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.


21. Matt Aug 01, 2008 at 06:25

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"}


22. Andy Ferra Aug 16, 2008 at 23:27

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.


23. Nick Skriloff Sep 04, 2008 at 13:48

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.

Add your comment:

(SKIP THIS ONE)

(required)

(not shown)


(use pastie or gist for code)

sponsored by:
if you want to help:
required:
Get Quicktime Player