1. 将代码从 Controller 重构到 Model 放在 Controller 的代码一般来说比较难进行重用(re-use)和单元测试,可读性也较差,我们希望将更多代码放在 Model 里面。
方法:
善用 Model scope
,把 where 条件搬到 model 的 scope 宣告,这样就可以在 controller 使用已经定义好的 scope。可读性变好,而且可以在不同地方重复沿用这个 scope。
2. 善用 Model association 情境:新建数据时,想要关联建立的用户
重构前:1
2
3
4
5
6
7
class PostsController < ApplicationController
def create
@post = Post.new(params[:post ])
@post.user_id = current_user.id
@post.save
end
end
重构后:由于 User has_many posts
的关系,我们可以用 current_user.posts.build
来取代 @post.user_id = current_user.id
的作用。
1
2
3
4
5
6
class PostsController < ApplicationController
def create
@post = current_user.posts.build(params[:post ])
@post.save
end
end
1
2
3
class User < ActiveRecord::Base
has_many :posts
end
3. 有关联的权限检查 scope access 情境:读取数据时,想要检查用户有没有操作该数据的权限
重构前:需要检查 @post.user
等于 current_user
1
2
3
4
5
6
7
8
9
class PostsController < ApplicationController
def edit
@post = Post.find(params[:id )
if @post.user != current_user
flash[:warning ] = 'Access denied'
redirect_to posts_url
end
end
end
重构后:直接用 current_user.posts.find
就可以了。如果该 post 不属于该 user,就会找不到数据。不过,如果权限允许管理员的话,这招就不行了。1
2
3
4
5
6
7
class PostsController < ApplicationController
def edit
@post = current_user.posts.find(params[:id )
end
end
4. 使用 Model 虚拟属性 情境:在表单中,操作不是直接对应 Model 属性的字段。例如下述范例,假设 User model
里面有 first_name
和 last_name
字段,但是画面显示时,我们希望改用 full_name
来操作。这个 full_name
并不是数据库中真正的字段,而是 first_name
和 last_name
两个字段合在一起显示而已。
重构前:表单只能用原始的 text_field_tag
方法,并且在 action 中拆开 params[:full_name]
塞进 model 的 first_nam
e 和 last_name
字段
1
2
3
4
5
6
7
8
9
10
11
<% form_for @user do |f| %>
<%= text_filed_tag :full_name %>
<% end %>
class UsersController < ApplicationController
def create
@user = User.new(params[:user )
@user.first_name = params[:full_name ].split(' ' , 2 ).first
@user.last_name = params[:full_name ].split(' ' , 2 ).last
@user.save
end
end
重构后:在 model 中新增 full_name
和 full_name=
方法,这样在表单就可以把 full_name
当作一般的 model 字段使用。而 controller action 更是简化到不需要处理 full_name
。这个 full_name
并没有实际对应数据库的字段,因此称之虚拟属性
。
1
2
3
4
5
6
7
8
9
10
11
class User < ActiveRecord::Base
def full_name
[first_name, last_name].join(' ' )
end
def full_name= (name)
split = name.split(' ' , 2 )
self .first_name = split.first
self .last_name = split.last
end
end
1
2
3
<% form_for @user do |f| %>
<%= f.text_field :full_name %>
<% end %>
1
2
3
4
5
6
7
class UsersController < ApplicationController
def create
@user = User.create(params[:user )
end
end
5. 使用 Model 回呼(callback) 情境:新增文章时,有一个核选方块 auto_tagging
,如果打勾表示想要系统自动下标籤。
重构前:需要在 action 中检查 params[:auto_tagging]
,然后调用 model 方法产生标籤(这里假设有一个 AsiaSearch.generate_tags 方法可以自动下标籤)1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<% form_for @post do |f| %>
<%= f.text_field :content %>
<%= check_box_tag 'auto_tagging' %>
<% end %>
class PostController < ApplicationController
def create
@post = Post.new(params[:post ])
if params[:auto_tagging ] == '1'
@post.tags = AsiaSearch.generate_tags(@post.content)
else
@post.tags = ""
end
@post.save
end
end
重构后:新增一个虚拟属性 auto_tagging,以及一个 before_save 回呼来检查要不要自动下标籤。如此在 action 中就可以不必检查和处理 auto_tagging。这段过程会自动在 Post 存储前,自动调用 generate_taggings 方法进行处理。1
2
3
4
5
6
7
8
9
class Post < ActiveRecord::Base
attr_accessor :auto_tagging
before_save :generate_taggings
private
def generate_taggings
return unless auto_tagging == '1' self .tags = Asia.search(self .content)
end
end
1
2
3
4
<% form_for :note , ... do |f| %>
<%= f.text_field :content %>
<%= f.check_box :auto_tagging %>
<% end %>
1
2
3
4
5
6
class PostController < ApplicationController
def create
@post = Post.new(params[:post ])
@post.save
end
end
6. 将逻辑放到 Model 情境:有一个发布文章 publish 的 action 有很多操作的相关步骤,需要设定很多 model 字段。
重构前:所有操作都在 action 之中1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class PostController < ApplicationController
def publish
@post = Post.find(params[:id ])
@post.is_published = true
@post.approved_by = current_user
if @post.create_at > Time.now - 7 .days
@post.popular = 100
else
@post.popular = 0
end
@post.save
redirect_to post_url(@post)
end
end
重构后:把相关的操作全部搬到 Model 的自定义方法 publish!
,这样 action 中只需要调用 @post.publish!
即可,非常可读清楚。这个 publish!
方法也可以在其它各处使用。
1
2
3
4
5
6
7
8
9
10
11
12
class Post < ActiveRecord::Base
def publish! (user)
self .is_published = true
self .approved_by = user
if self .create_at > Time.now-7 .days
self .popular = 100
else
self .popular = 0
end
self .save!
end
end
1
2
3
4
5
6
7
8
class PostController < ApplicationController
def publish
@post = Post.find(params[:id ])
@post.publish!(current_user)
redirect_to post_url(@post)
end
end
7. 使用工厂方法(Factory Method)取代复杂的建构过程 情境:建立 model 需要复杂的建构过程,例如以下建构 Invoice 需要设定很多属性。
重构前:都在 create action 中完成
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class InvoiceController < ApplicationController
def create
@invoice = Invoice.new(params[:invoice ])
@invoice.address = current_user.address
@invoice.phone = current_user.phone
@invoice.vip = ( @invoice.amount > 1000 )
if Time.now.day > 15
@invoice.delivery_time = Time.now + 2 .month
else
@invoice.delivery_time = Time.now + 1 .month
end
@invoice.save
end
end
重构后:在 model 中新写一个类方法 new_by_user
,把建构的过程全部搬进来。这样 controller action 里面只需要调用这个方法即可。这一招跟上一节是一样的道理。这种建构用途的方法又叫做工厂方法
。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Invoice < ActiveRecord::Base
def self.new_by_user(params, user)
invoice = self.new (params)
invoice.address = user.address
invoice.phone = user.phone
invoice.vip = ( invoice.amount > 1000 )
if Time .now .day > 15
invoice.delivery_time = Time .now + 2. month
else
invoice.delivery_time = Time .now + 1. month
end
return invoice
end
end
1
2
3
4
5
6
class InvoiceController < ApplicationController
def create
@invoice = Invoice.new_by_user(params[:invoice ], current_user)
@invoice.save
end
end
8. 善用 Module 抽取相关代码(不太懂) 情境:如果将代码从 Controller 重构到 Model 做得不错了,接下来如何进一步重构 Model 代码?
重构前:在 Model 中有一些高度相关的代码,希望能够更清楚他们是一起的,或是希望能在不同 Model 中也能重复使用。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class User < ActiveRecord::Base
validates_presence_of :cellphone
before_save :parse_cellphone
def parse_cellphone
end
def self .foobar
end
end
重构后:善用 Ruby 的 module 语法,可以将这些相关代码抽取出来,放在 app/models/concerns
目录下,包括 model 的 validates 宣告、回呼宣告、关联宣告、对象方法、类方法等等都可抽取出来。
app/models/concerns/has_cellphone.rb 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
module HasCellphone
def self .included (base)
base.validates_presence_of :cellphone
base.before_save :parse_cellphone
base.extend ClassMethods
end
def parse_cellphone
end
module ClassMethods
def foobar
end
end
end
或是使用 Rails ActiveSupport::Concern 语法,可以更简洁一点:app/models/concerns/has_cellphone.rb 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
module HasCellphone
extend ActiveSupport::Concern
included do
validates_presence_of :cellphone
before_save :parse_cellphone
end
def parse_cellphone
end
class_methods do
def foobar
end
end
end
最后在 model 里面 include 即可。
1
2
3
class User < ActiveRecord::Base
include HasCellphone
end
其他 model 也可以沿用这段代码,只要 include 即可:
1
2
3
class Contact < ActiveRecord::Base
include HasCellphone
end
这一招其实 controller 也可以用,在 rails 中 app/controllers/concerns
目录就是拿来放 controller 的 module 档案
关于 View,最重要的守则就是在 template 中绝对没有商务逻辑 。
9. 将代码重构到 Model 情境:在 View 中需要一些条件判断 ,来决定要不要显示某些内容
重构前:需要检查有登入、该用户是该篇文章作者或是编辑员
1
2
3
4
<% if current_user && (current_user == @post.user ||
@post.editors.include ?(current_user) %>
<%= link_to 'Edit this post' , edit_post_url(@post) %>
<% end %>
重构后:这一整段条件判断,可以重构到 Model 的一个方法。这样 View 里面只需要调用 @post.editable_by?(current_user)
即可,代码干净又清楚。
1
2
3
4
5
6
7
8
<% if @post.editable_by?(current_user) %>
<%= link_to 'Edit this post' , edit_post_url(@post) %>
<% end %>
class Post < ActiveRecord::Base
def ediable_by? (user)
user && ( user == self .user || self .editors.include ?(user)
end
end
10.将代码重构到 Helper 情境:
HTML 与 Ruby 高度混杂
该段程式码有很多 if / else
该段程式码衣服穿很多层 simple_format(truncate(auto_link(@post.content), :length => 30) )
重构前:我们想要把文章内容自动加超连结、截断只留前30字前、保留换行等等:
<%= simple_format(truncate(auto_link(@post.content), :length => 30) ) %>
重构后:抽取出一个 pretty_content
helper 方法,这样在各处 template 都可以重复使用这个 helper,而且也比较清楚。1
2
3
4
5
6
app/helpers/posts_helper.rb
module PostsHelper
def pretty_content (content)
simple_format(truncate(auto_link(content), :length => 30 ) )
end
end
<%= pretty_content(@post.content) %>
那到底什么情境适合把代码重构到 Model? 什么时候用 Helper 呢?如果跟 HTML 画面显示无关,跟商务逻辑有关,则放到 Model 里面。如果跟 HTML 画面显示有关,则适合放在 Helper 里面。一般来说,在 Model 里面是不会处理 HTML 代码的,这是 Helper 的事情。
11. 将代码重构到 Partial 样板 情境:Helper 是 Ruby 代码 ,里面不适合放太多的 HTML。如果你有一整段的 HTML 代码想要抽取出来,应该用 Partial 样板。
重构前:1
2
3
4
5
6
7
def render_product_item(product )
content_tag(:div , :class => "col-md-3" ) do
content_tag(:div , link_to(product .title), product_path(product ) ) +
content_tag(:p , tag(:hr )) +
content_tag(:span , product.price + "元" )
end
end
<%= render_product_item(@product) %>
重构后:应该改用 partial 而不是 helper。
1
2
3
4
5
6
7
8
app/views/products/_item.html.erb
<div class ="col-md-3" >
<div > <%= link_to(product.title), product_path(product) %> </div >
<p >
<hr >
<span > <%= product.price %> 元</span >
</p >
</div >
<%= render :partial => "item", :locals => { :product => @product } %>
12.使用 Partial 尽量用区域变量取代对象变量 情境:在使用 Partial 时1
2
3
4
5
class Post < ApplicationController
def show
@post = Post.find(params[:id )
end
end
重构前:在 action 中宣告的对象变量例如 @post
,会穿透到这个 template 内所有使用的 partial。虽然很方便,但是如果 partial 内要用到的变量很多,就会搞不清楚到底要准备哪些变量才能使用这个 partial。
<%= render :partial => "sidebar" %>
_sidebar.html.erb
重构后:将 @post 传入这个 partial,这样在这个 partial 里面,就会变成区域变量 post
。这个好处是可以增加这个 partial 的可重复使用性,也比较清楚要传这些变量才能使用。
<%= render :partial => "sidebar", :locals => { :post => @post } %>
_sidebar.html.erb
13. 整理 Helper 档案 情境:每次 rails g controller XXX 时,Rails 都会自动新增对应的 XXX_helper.rb 档案
重构前:很多 Helper 档案打开来里面都是空的,没有用到
1
2
3
4
app/helpers/user_posts_helper.rb
app/helpers/author_posts_helper.rb
app/helpers/editor_posts_helper.rb
app/helpers/admin_posts_helper.rb
重构后:简化集中到少数的 Helper 档案即可。这些 Helper 档案跟 Controller 并没有对应的关系,所有 Helper 档案里面宣告的方法都是通用的 ,不会因为放在哪一个 Helper 档案而有差异。因此我们可以重新编排整理。
app/helpers/posts_helper.rb
14. 代码分析工具
15. 补充和推荐资源
当你仍不满足 Rails 的 concerns 机制时,你会需要更多面向对象的知识,在编程语言教程中有介绍到。关于 Rails 的部分推荐以下补充资料: