rails代码的重构

此随笔来源于 https://kerzzi.github.io/2017/10/22/2017-10-22-rails-code-refactoring/#1-%E5%B0%86%E4%BB%A3%E7%A0%81%E4%BB%8E-Controller-%E9%87%8D%E6%9E%84%E5%88%B0-Model

转载只为了做笔记。方便查阅。有问题可以联系。

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_name 和 last_name 字段,但是画面显示时,我们希望改用 full_name 来操作。这个 full_name 并不是数据库中真正的字段,而是 first_name 和 last_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_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
# 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. 补充和推荐资源

  • 重构 这本书是软件开发领域的经典之作,原作使用 Java 代码,但是仍然值得一读。另有 Ruby 版本。
  • how DHH organizes his rails controllers Rails 发明人 DHH 如何组织 Controller 代码,坚持 RESTful
  • Put chubby models on a diet with concerns DHH 对于 concerns 用法的看法

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

  • 7 Patterns to Refactor Fat ActiveRecord Models
  • Object Oriented Rails – Writing better controllers
  • Slimming Down Your Models and Controllers with Concerns, Service Objects, and Tableless Models
  • Growing Rails Applications in Practice

你可能感兴趣的:(rails代码的重构)