Rails Forum
Rails Work - the best place to post and find great Ruby on Rails jobs.
Username
Password

You are not logged in.

New Posts in this thread

#1 2006-09-24 22:56:51

ryanb
Moderator
Registered: 2006-06-14
Posts: 6323
Website

Refactoring on Rails: Move to Model

This is the first article in a series which will cover various refactoring techniques that are specific to Rails. Refactoring is used to improve the design of existing code without changing its functionality.

The first refactoring I will be discussing involves taking a bit of logic out of the view and placing it in the model. This can clean up the views quite nicely.

Let's start out with a very simple example. We have a Person model with first_name, last_name, and middle_initial attributes. In our view we have this code:


Code :  ruby - fold - unfold
  1. Full Name:
  2. <%= @person.first_name %>
  3. <% unless @person.middle_initial.blank? %>
  4.   <%= @person.middle_initial %>.
  5. <% end %>
  6. <%= @person.last_name %>
Not very pretty. Even worse, this code is repeated several times throughout the application. To remove this duplication and to clean up the views, we can define a method in the Person class to handle this logic.


Code :  ruby - fold - unfold
  1. class Person < ActiveRecord::Base
  2.   def full_name
  3.     result = first_name + ' '
  4.     unless middle_initial.blank?
  5.       result += middle_initial + '. '
  6.     end
  7.     result += last_name
  8.     result
  9.   end
  10. end 
This makes the view much nicer:


Code :  ruby - fold - unfold
  1. Full Name: <%= person.full_name %>
We successfully shoved the logic into the Person model, but I'm still not satisfied. I think we can clean up the full_name method, but before we do that, let's create some tests to make sure everything continues to work as we do the cleaning. Tests are very important when refactoring to make sure we don't break anything as we improve the code.


Code :  ruby - fold - unfold
  1. # in person_test.rb
  2. def setup
  3.   @person = Person.new(:first_name => 'John', :last_name => 'Smith')
  4. end
  5.  
  6. def test_full_name
  7.   assert_equal 'John Smith', @person.full_name
  8. end
  9.  
  10. def test_full_name_with_middle_initial
  11.   @person.middle_initial = 'A'
  12.   assert_equal 'John A. Smith', @person.full_name
  13. end 
Good, the tests are passing so now it's time to improve the full_name method. I would like to remove that temporary "result" variable and merge it all onto one line. I can use an array to help me out. Maybe something like this:


Code :  ruby - fold - unfold
  1. def full_name
  2.   [first_name, middle_initial, last_name].join(' ')
  3. end 
That looks nice and tidy, but unsurprisingly it fails both of the tests. It fails the second test because there's no period, so let's create a new method to return a middle initial with a period:


Code :  ruby - fold - unfold
  1. def middle_initial_with_period
  2.   middle_initial + '.' unless middle_initial.blank?
  3. end
  4.  
  5. def full_name
  6.   [first_name, middle_initial_with_period, last_name].join(' ')
  7. end 
The second test is working now, but the first test still fails because there's an extra space in there when no middle initial is defined. We can remove that by using array's "compact" method like this:


Code :  ruby - fold - unfold
  1. def full_name
  2.   [first_name, middle_initial_with_period, last_name].compact.join(' ')
  3. end 
Our tests are all passing now. Yay!

Wait one minute, I hear someone say, the compact method just removes nil values, what if the middle initial is an empty string? Well let's add a test and find out:


Code :  ruby - fold - unfold
  1. def test_full_name_with_empty_middle_initial
  2.   @person.middle_initial = ''
  3.   assert_equal 'John Smith', @person.full_name
  4. end 
Surprisingly this passes successfully without us changing anything. Why? Because the middle_initial_with_period method already takes care of empty strings and will return nothing (nil) if middle_initial is blank.

In this tutorial you learned how to refactor code from the view into the model, but this can just as easily be applied to code in the controller or any other part of the application. Your job is to look through your current rails projects for code that can be moved into a model. Usually if it can be moved into a model, it should, but just be careful to keep view/controller specific code out of the model. For example, don't move HTML code into the model.

If you found this useful, keep an eye out for the next Refactoring on Rails article. In the meantime, check out Martin Fowler's excellent book: Refactoring.

Check out the next article in this series: Multiple Scopes in Controller

Last edited by ryanb (2006-11-16 16:29:48)


Railscasts - Free Ruby on Rails Screencasts

Offline

 

#2 2006-09-25 16:52:42

Kelli
Illusionist
Registered: 2006-05-11
Posts: 218
Website

Re: Refactoring on Rails: Move to Model

Nicely done! I wish I'd seen this before I wrote my online billing system. I did end up doing this, but it was a lot more muddling through that I'd have otherwise had. wink

Offline

 

#3 2006-09-25 20:36:26

boyles
Ticketholder
From: Cape Town
Registered: 2006-09-24
Posts: 21

Re: Refactoring on Rails: Move to Model

RAILSforum is turning out to be a great resource for Tutorials.
Nice work 

Added this one as well to my growing (140+) collection below

Last edited by boyles (2006-09-25 22:07:55)

Offline

 

#4 2006-11-15 01:53:16

jwcooper
Ticketholder
Registered: 2006-10-25
Posts: 16

Re: Refactoring on Rails: Move to Model

Excellent tutorial, thank you Ryan.  I was starting to wonder what the Model's were for. wink

I should really get a rails book...

Offline

 

#5 2006-11-15 14:16:47

cathyb
Passenger
From: Stockport, UK
Registered: 2006-10-30
Posts: 25
Website

Re: Refactoring on Rails: Move to Model

This refactoring is great and I'm looking forward to the rest of this series of tutorials.

I've done something similar in my person model to create a full_address method from all the individual address fields. I'm sure this could be DRYied up somewhat.


Code :   - fold - unfold
  1.   def full_address
  2.     result = ''
  3.     unless address_line_1.blank?
  4.       result += address_line_1
  5.     end
  6.     unless address_line_2.blank?
  7.       if result == ''
  8.         result += address_line_2
  9.       else
  10.         result += ', ' + address_line_2
  11.       end
  12.     end
  13.     unless address_line_3.blank?
  14.       if result == ''
  15.         result += address_line_3
  16.       else
  17.         result += ', ' + address_line_3
  18.       end
  19.     end
  20.     unless address_line_4.blank?
  21.       if result == ''
  22.         result += address_line_4
  23.       else
  24.         result += ', ' + address_line_4
  25.       end
  26.     end
  27.     unless town.blank?
  28.       if result == ''
  29.         result += town
  30.       else
  31.         result += ', ' + town
  32.       end
  33.     end
  34.     unless county.blank?
  35.       if result == ''
  36.         result += county
  37.       else
  38.         result += ', ' + county
  39.       end
  40.     end
  41.     unless postcode.blank?
  42.       if result == ''
  43.         result += postcode
  44.       else
  45.         result += ', ' + postcode
  46.       end
  47.     end
  48.     result
  49.   end
The above puts whatever parts of the address that exist all on one line with commas after each bit. However in a different view on my app I want to display the address differently with each part on a separate line with <br> in between instead of commas. Would I be best creating a new method full_address_multi_line or can I adapt my existing method to do both?

Also when I am displaying the person's profile details in the view, where a field is not completed I might want to display "Not supplied" or "N/A". Is this best handled in the view or could it be moved to the model too? At the minute I've got a lot of stuff like this in my view:


Code :   - fold - unfold
  1. <%= if @person.job_title.blank?
  2.       "Not supplied"
  3.     else
  4.       @person.job_title
  5.     end %>

Offline

 

#6 2006-11-15 18:48:49

ryanb
Moderator
Registered: 2006-06-14
Posts: 6323
Website

Re: Refactoring on Rails: Move to Model

Very good questions Cathy!

The full_address method can be greatly simplified by using a technique similar to what I did in the article - which is placing the address sections in an array and joining that.


Code :  ruby - fold - unfold
  1. def full_address(join_str = ', ')
  2.   sections = [address_line_1, address_line_2, address_line_3] # etc...
  3.   sections.delete_if(&:blank?) # removes any blank entries from the array
  4.   sections.join(join_str)
  5. end 
Notice I passed the join string as a parameter, this way you can change it when calling the method.

You can go one step further to clean this up a bit more:


Code :  ruby - fold - unfold
  1. def full_address(join_str = ', ')
  2.   address_sections.delete_if(&:blank?).join(join_str)
  3. end
  4.  
  5. def address_sections
  6.   [address_line_1, address_line_2, address_line_3]
  7. end 
As for your second question, I would hesitate to move this "Not Supplied" logic into the model. First of all you will need to handle this for many different attributes and models. Second, if you override the original attribute it will probably cause problems at the model/controller level. Lastly you may want to handle this differently in different views.

Certainly keeping this in the view isn't optimal either. What I would do is create a helper method that handles this for you. You can then input any value you want and it will return "Not Supplied" automatically when blank. For example:


Code :  ruby - fold - unfold
  1. # in view
  2. <%= not_supplied_if_blank @person.job_title %>
  3.  
  4. # in application_helper
  5. def not_supplied_if_blank(value)
  6.   value.blank? ? "Not Supplied" : value
  7. end 
You may want to rename that method.

Last edited by ryanb (2006-11-15 18:51:41)


Railscasts - Free Ruby on Rails Screencasts

Offline

 

#7 2006-11-16 11:46:41

cathyb
Passenger
From: Stockport, UK
Registered: 2006-10-30
Posts: 25
Website

Re: Refactoring on Rails: Move to Model

Thanks Ryan for the lovely code - you've certainly made my full_address method a lot more elegant and useful. cool

After I had posted I found some examples online of using application helpers and ended up writing pretty much what you wrote for showing "Not supplied" when something was blank. I've not really used helpers a great deal yet but am starting to. I guess when things get repetative in the view and don't seem to fit in the model then helpers are a good solution.

Both my view and model are now looking a lot better. Thanks again for the great advice. BTW I think you should link to the other refactoring tutorials at the top of this one.

Offline

 

Board footer

Powered by PunBB
© Copyright 2002–2005 Rickard Andersson