Rails Best Practices 2: Move logic to model

Today we continue our analysis of the Rails Best Practices.
In the previous post we saw Named Scope, Model Association and
Following the same direction, in today’s post we’ll examine the use of Callback Model and Virtual Attribute.

1. Virtual Attribute

Suppose we have a customers list table defined as follows

1
2
3
4
5
6
create_table "clients", :force => true do |t|  
  t.string   "first_name"  
  t.string   "last_name"  
  t.string   "street"  
  t.string   "city"  
end

Suppose we have a customers list table defined as follows but we want to define an input mask where street and city are grouped into one field called “address”.
The form with the “address” field will be defined as follows:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<h1>Create Client</h1>  
  <% form_for @client do |f| %>  
    <ol class="formList">  
      <li>  
        <%= f.label :first_name, 'First Name' %>  
        <%= f.text_field :first_name %>  
      </li>
      <li>  
        <%= f.label :last_name, 'Last Name' %>  
        <%= f.text_field :last_name %>  
      </li>    
      <li>  
        <%= f.label :address, 'Address' %>  
        <%= text_field_tag :address %>  
      </li>    
   </ol>  
 <% end %>

Now in the create method we’ll have something like:

1
2
3
4
5
6
7
8
9
10
11
12
class ClientsController < ApplicationController

  def create
    @client = Client.new(params[:client])
    @client.street = params[:address].split(' ', 2).first
    @client.city = params[:address].split(' ', 2).last
    @client.save
  end
 
...

end

We can now improve this method by defining the address field as a virtual attribute of the Client model.

1
2
3
4
5
6
7
8
9
10
11
12
class Client < ActiveRecord::Base

  def address
    [street, city].join(' ')
  end
 
  def address=(addr)
    split = addr.split(' ', 2)
    self.street = split.first
    self.city = split.last
  end
end

In the form we can now define the address field as f.field_tag instead of as text_field_tag :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<h1>Create Client</h1>  
  <% form_for @client do |f| %>  
    <ol class="formList">  
      <li>  
        <%= f.label :first_name, 'First Name' %>  
        <%= f.text_field :first_name %>  
      </li>
      <li>  
        <%= f.label :last_name, 'Last Name' %>  
        <%= f.text_field :last_name %>  
      </li>    
      <li>  
        <%= f.label :address, 'Address' %>  
        <%= f.text_field :address %>  
      </li>    
   </ol>  
 <% end %>

Finally, the controller will become simpler:

1
2
3
4
5
6
7
8
9
10
class ClientsController < ApplicationController

  def create
    @client = Client.new(params[:client])
    @client.save
  end
 
...

end

2. Model Callback
Now we see a method, called Model Callback that allows us, as with the virtual attribute just seen, to simplify the form and to move the controller’s logic inside the model.

Suppose we have to implement a feature that automatically associates a set of tags to a post.
This feature, called calculate_tags return a list of tags based on the most frequent words contained in the post.
We don’t see the code of this function and instead see how and when to invoke the automatic generation of tags.

A first implementation can be as follows. Given the following form:

1
2
3
4
<% form_for @post do |f| %>
  <%= f.text_field :content %>
  <%= check_box_tag 'calculate_tags' %>
<% end %>

the corresponding create method will be:

1
2
3
4
5
6
7
8
9
10
class PostController < ApplicationController
  def create
    @post = Post.new(params[:post])
    if params[:calculate_tags] == '1'
      @post.tags = TagGenerator.generate(@post.content)
    else
      @post.tags = ""
    end
    @post.save
  end

Editing the Post model, let’s see how to improve the code we’ve just wrote.
First we introduce in our model an attribute, “calculate_tags”, and a filter “generate_tags”, of type before_save

1
2
3
4
5
6
7
8
9
10
class Post < ActiveRecord::Base
  attr_accessor :calculate_tags
  before_save :generate_tags
 
  private
  def generate_tags
    return unless calculate_tags == '1'
    self.tags = TagGenerator.generate(self.content)
  end
end

Returning to the form we can now redefine the field calculate_tags as f.check_box

1
2
3
4
<% form_for @post do |f| %>
  <%= f.text_field :content %>
  <%= f.check_box 'calculate_tags' %>
<% end %>

Finally, our create method in the controller will return to its simplest form.

1
2
3
4
5
6
 class PostController < ApplicationController
  def create
    @post = Post.new(params[:post])
    @post.save
  end
end

Tags: , , ,


About Claudio

Claudio Marai is a co-founder of DevInterface.

After graduating in Computer Science has contributed to develop complex web applications based on Java/J2EE and desktop applications with the. NET framework for the Ministry of Justice and ultimately for the banking ambit.

The passion for web in recent years has led him to be interested in more modern frameworks such as Ruby on Rails and Django, and to a development approach based on agile methodologies such as eXtreme Programming and SCRUM.

About DevInterface

We are an information and communication technology agency. Our mission is to provide web application development, design services and communication strategies. We specialize in building web applications with modern and efficient frameworks.

Related Post

4 Responses to “Rails Best Practices 2: Move logic to model”

  1. [...] This post was mentioned on Twitter by Ruby Reflector. Ruby Reflector said: Top Ruby Article: Rails Best Practices 2: Move logic to model: http://bit.ly/amLWC2 [...]

  2. james says:

    Great post. I need to do better at this! One quick question, instead of: self.street = split.first; self.city = split.last, could you just write: street = split.first; city = split.last? This is one area of Ruby that confuses me.

  3. Fadhli says:

    James, if you’re saving a value to your database. Then you must explicitly call the self method e.g self.city = split.last.

    As a rule of thumb, self must always be explicit for setting attributes, but not for retrieving them.

  4. Millisami says:

    Nice Read. Keep them coming.

Leave a Reply

Insert code beetween <code lang="ruby"> and </code>

Copyright 2012 DevInterface s.n.c.

DevInterface Blog is proudly powered by WordPress