文章目录
  1. 1. 1. 将代码从 Controller 重构到 Model
  2. 2. 2. 善用 Model association
  3. 3. 3. 有关联的权限检查 scope access
  4. 4. 4. 使用 Model 虚拟属性
  5. 5. 5. 使用 Model 回呼(callback)
  6. 6. 6. 将逻辑放到 Model
  7. 7. 7. 使用工厂方法(Factory Method)取代复杂的建构过程
  8. 8. 8. 善用 Module 抽取相关代码(不太懂)
  9. 9. 9. 将代码重构到 Model
  10. 10. 10.将代码重构到 Helper
  11. 11. 11. 将代码重构到 Partial 样板
  12. 12. 12.使用 Partial 尽量用区域变量取代对象变量
  13. 13. 13. 整理 Helper 档案
  14. 14. 14. 代码分析工具
  15. 15. 15. 补充和推荐资源

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
# raise RecordNotFound exception (404 error) if not found
@post = current_user.posts.find(params[:id)
end
end

4. 使用 Model 虚拟属性

情境:在表单中,操作不是直接对应 Model 属性的字段。例如下述范例,假设 User model 里面有 first_namelast_name 字段,但是画面显示时,我们希望改用 full_name 来操作。这个 full_name 并不是数据库中真正的字段,而是 first_namelast_name 两个字段合在一起显示而已。

重构前:表单只能用原始的 text_field_tag 方法,并且在 action 中拆开 params[:full_name] 塞进 model 的 first_name 和 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_namefull_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
# do something
end
def self.foobar
# do something
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
# do something
end
module ClassMethods
def foobar
# do something
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
# do something
end
class_methods do
def foobar
# do something
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
1
<%= @post.title %>

重构后:将 @post 传入这个 partial,这样在这个 partial 里面,就会变成区域变量 post。这个好处是可以增加这个 partial 的可重复使用性,也比较清楚要传这些变量才能使用。

<%= render :partial => "sidebar", :locals => { :post => @post } %>

_sidebar.html.erb
1
<%= post.title %>

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. 代码分析工具

  • Rubocop 是一个 gem 可以分析 Rails 代码,建议一些可以重构的地方
  • CodeClimate 是一个线上的工具,可以为项目评分,并建议哪里需要修改。

15. 补充和推荐资源

当你仍不满足 Rails 的 concerns 机制时,你会需要更多面向对象的知识,在编程语言教程中有介绍到。关于 Rails 的部分推荐以下补充资料:

文章目录
  1. 1. 1. 将代码从 Controller 重构到 Model
  2. 2. 2. 善用 Model association
  3. 3. 3. 有关联的权限检查 scope access
  4. 4. 4. 使用 Model 虚拟属性
  5. 5. 5. 使用 Model 回呼(callback)
  6. 6. 6. 将逻辑放到 Model
  7. 7. 7. 使用工厂方法(Factory Method)取代复杂的建构过程
  8. 8. 8. 善用 Module 抽取相关代码(不太懂)
  9. 9. 9. 将代码重构到 Model
  10. 10. 10.将代码重构到 Helper
  11. 11. 11. 将代码重构到 Partial 样板
  12. 12. 12.使用 Partial 尽量用区域变量取代对象变量
  13. 13. 13. 整理 Helper 档案
  14. 14. 14. 代码分析工具
  15. 15. 15. 补充和推荐资源