记 tower.im 的一次重构
原文in here: http://outofmemory.cn/wr?u=http%3A%2F%2Fblog.mycolorway.com%2F2013%2F05%2F01%2Ftower-refactor%2F
Tower.im上线已经快半年了,这半年来我们团队小步快跑,为 tower 增加了许多新的功能,使它渐渐完善起来,在这个过程中,tower 的代码量也逐渐增加,随着时间的流逝,系统中积淀的糟糕的代码渐渐增多,于是趁着节假日的到来,花了些时间对代码做了部分重构,在这里记录下来,和大家分享。
我们知道,重构代码,目的是为了对内让代码变得更精简,提高阅读性和可维护性,而对外不改变旧有的功能,所以首先应该分拆重构任务,一步步执行,不要想一次到位,其次,是通过测试用例保护重构过程,确保重构以后的代码仍然能正常工作。
明确了目标和方法后,我们就可以进入具体的重构阶段了。Tower.im 使用 Ruby on Rails 这个 MVC 的 Web 框架开发,在代码的组织形式上,Rails 社区比较推崇 ‘Skinny Controller, Fat Model’,因此,这次的代码重构主要在 Controller 层和 Model 层进行。
我们来看一个具体的例子。在 Tower 中,像任务、讨论、文档、文件这样的基础资源,都对应着自己的 Controller,比如任务对应着 todos_controller,讨论对应 messages_controller,这些 controllers 里面定义着一些接口方法,比如对这些基础资源进行增删改等等。在旧有代码中,最复杂的一个方法是为这些基础资源添加评论。这个方法的名称叫 comment,让我们看看老的代码长什么样:
def comment
@message = @project.messages.active.find_by_guid(params[:muid])
return render_404 if @message.blank? content = params[:comment_content]
if 1 == paras['_wysihtml5_mode'].to_i
content.gsub! '</div>', '<br/></div>'
content = Sanitize.clean(content, Sanitize::Config::BASIC)
else
content = Sanitize.clean(content).gsub(/[\n\r]{1,2}/, '')
end if 20000 < content.bytesize
render json: { errors: [{msg: '内容最多支持 5000 字,太长了保存不了哦',
target: :comment_content}]}, status: 500
return
end attach_guids = params[:attach_guids].present? ? \
params[:attach_guids].split(',') : [] if content.blank?
if attach_guids.empty?
render json: {erros: [{msg: '没有输入内容哦',
target: :comment_content}]}, status: 500
return
else
pic_count = 0
filenames = attach_guids.map do |guid|
ta = current_member.tmp_attachments.find_by_guid(guid)
pic_count += 1 if ta.content_type.starts_with?('image/')
ta.name
end content = ((0 < pic_count ? "[#{pic_count}P]" : "") + \
filenames.join(', ')).truncate(200)
end
end @message.cc_members = params[:cc_guids].respond_to?(:split) ?\
params[:cc_guids].split(',') : []
@message.last_commentator = current_member
@message.last_comment_at = DateTime.now
@message.last_conn_guid = live_conn_guid
@message.save! comment = @message.comments.create(content: content,
creator: current_member, last_conn_guid: live_conn_guid) if attach_guids.any?
tmp_file_dir = "#{::Rails.root.join('files', 'tmp')}"
attach_guids.each_with_index do |guid, index|
ta = current_member.tmp_attachments.find_by_guid(guid)
tmp_file_path = File.join(tmp_file_dir, "#{ta.guid}")
tmp_file = File.open(tmp_file_path, 'r') comment.attachments.create(team: current_team, attachtarget: @project,
creator: current_member, byte_size: ta.byte_size,
content_type: ta.content_type, name: ta.name, file: tmp_file, position: index)
end
end comment.reload
comment.create_event_add(current_member)
comment.send_new_comment_emails(current_member) @message.reload render json: {
guid: comment.guid,
html: render_to_string(
partial: 'comments/comment',
locals: {comment: comment}
)
}
end
是不是快崩溃了?这么长的代码,既难以阅读,也难以维护,必须重构。先让我们看看重构以后的结果:
def comment
return render_404 unless @message.is_active? content, cc_members, attach_guids = comment_params
comment = @message.create_comment(content, current_member,
live_conn_guid, attach_guids, cc_members) render json: {
guid: comment.guid,
html: render_to_string(
partial: 'comments/comment',
locals: { comment: comment }
)
}
end
是不是好多了?我理解的重构的核心口诀是“拆”,把复杂代码中的逻辑进一步细分、拆散,放到它应该属于的地方。在拆的过程中,应该考虑到复用,那些拆出来的逻辑应该最大程度的和其它代码共享,达到 DRY 的目的。具体对于comment 这个接口来说,我们做了如下的几个重构的步骤:
1. 在 controller 中使用 before_filter 注入全局对象
在 comment 方法的第一行,可以看到我们通过 find_by_guid 方法获取了 @message 对象,虽然只是一行代码,但是考虑到整个 messages_controller 里面,绝大多数方法都会复用它,所以可以在 controller 里面定义一个 before_filter 方法, 来为需要的接口注入 @message 对象:
before_filter :load_message_instance, only: [:comment]
only 后面可以指定这个 controller 里面有哪些方法是通过 load_message_instance 来注入 @message 变量,接下来,只需要在 messages_controller 里面定义一个 load_message_instance 的 private method 就可以了:
private
def load_message_instance
@message = @project.messages.find_by_guid! params.fetch :muid
end
这样首先通过 before_filter,DRY 掉了 controller 里面绝大多数方法重复的一行代码,虽然看上去微不足道,但是随着 controller 的扩展,积少成多,能让你的代码保持精简。
2. 使用 params helpers 封装评论所需参数
我们知道,controller 中的方法很多时候只是做一个“中转”:接收 http 请求,获取 GET/POST 参数,对参数进行处理后,交由 model 层对数据进行操作,最后将数据经过某种方式的 render 后,返还给浏览器。而在这个中转过程中,我们有大量的操作是在对请求参数做处理,比如在旧版本的 comment 方法中,我们对评论内容进行了过滤,对抄送的成员和附件进行了参数组装,这些操作其实可以放在一个 helper 方法里面来完成,而且考虑到除了 messages_controller 外,其它资源对象的 comment 方法其实在最开始都会对参数进行同样的处理,所以我们可以直接把对 comment 方法的 params 处理封装成一个 application_helper 方法:
def comment_params
cc_members = params[:cc_members].respond_to? (:split) ? \
params[:cc_guids].split(',') : []
attach_guids = params[:attach_guids].present? ? \
params[:attach_guids].split(',') :[]
content = get_safe_content(params[:comment_content],
1 == params['_wysihtml5_mode'].to_i,
attach_guids)
[content, cc_members, attach_guids]
end
可以看到,这个在 application_helper 中的方法,实际上只是对三个 POST 参数进行了相应的处理,最后以数组的形式返回,而这个 comment_params 方法中,又有一次小的重构,即通过 get_safe_content 方法对复杂的 content 进行处理,实际上这里仍然可以继续优化下去,不过就目前为止,已经足够了。这样,在messages_controller 里面,我们通过数组的连续赋值特性,就可以一行代码获取数据层实际上需要的数据了:
content, cc_members, attach_guids = comment_params
哇,这样又精简掉了一大堆丑陋的代码。
3. 将模型验证放进模型中
细心的童鞋应该会发现,重构后的代码里面没有任何的错误处理,是我们去掉了这部分代码么?实际上并没有,在 Rails 里面,对数据的合法性验证往往是放在 model 层去处理的,controller 只需要“中转”,没必要去判断。Rails 的 model 类往往会使用一系列的 validators 来对数据进行合法性验证,比如旧有代码里面,我们需要对 content 的长度和是否为空进行判断,其实只需要在 comment 的 model 里,用一个 validates 语句就能搞定:
validates :content,
presence: { message: "没有输入内容哦" },
length: { maximum: 20000,
too_long: "内容最多支持 5000 字,太长了保存不了哦" }
这样,当 content 本身为空,或者超过长度限制的时候,model 会直接 raise 错误信息,当然,更进一步的,你还可以把错误信息本身放到 I18n 里面,全站复用。
这个改动涉及到 Rails 框架本身的习惯,你当然也可以在 controller 层去做数据验证,但是使用 model 层自己的 validates,才是在正确的地点,做正确的事情。
4. 使用Concerns抽象模型结构
最后,是这次优化的重头戏。Rails 社区虽然提倡 ‘Skinny Controller, Fat Model’,但在实际开发中,太 fat 的 model 层也是很让人头疼的,所以我们引入了Concerns 机制来对模型层进一步做抽象。关于 Concerns ,大家可以参考DHH的这篇文章:Put chubby models on a diet with concerns。
对于 tower 来说,基础资源都可以被评论,这个意思是指,每个基础资源的模型都 has_many comments,每个基础资源的模型都有一个 cc_members 字段保存着当前的抄送成员的 id 数组,在 model 中也有一系列的方法来实现对 cc_members 的操作,这些代码其实大部分都可以重用,而且它们都从侧面反映出一个实时,就是基础资源模型本身,应该是一个 commentable 的抽象。因此,我们可以使用 concerns 实现这个抽象:
module Extensions
module Commentable
extend ActiveSupport::Concern included do
has_many: comments, as: :commentable, dependent: :delete_all
belongs_to :last_commentator, class_name: :Member
end def create_comment(content, creator, live_conn_guid, attach_guids, cc_members)
# 具体代码省略
end def cc_members= (guids)
# 具体代码省略
end def cc_members
# 具体代码省略
end def cc_member_add(member)
# 具体代码省略
end
end
end
可以看到,在这个 Commentable 里面,我们抽出了所有 Commentable 对象所应该具有的关系,并且创建了一系列的方法供基础资源使用,这些方法不是接口方法,它们不仅定义了方法名称和参数,还实际提供相应的功能,在完成了这个 concern 以后,我们就可以通过 Module Mixin 的方法,把这个 commentable concern 混入所有基础资源模型里面了:
class Message < ActiveRecord::Base
include Extensions::Commentable
end
这样,我们只需要在 @message 这个资源对象上调用 create_comment 方法,传入合适的参数,就能完成创建评论的操作了,核心还是,让 model 层去做关于数据的处理,controller 只需要做中间的中转即可。
Concerns 非常强大,在 Tower 中,像 回收站(trashable)、星标(starable) 等很多功能,都能通过 Concerns 来做抽象,让代码保持简洁。在 Rails4 中,Concerns 也被默认加载,算是众望所归吧。
总结
以上就是对 Tower.im 的一次简单的代码优化,当然,可以做的还有很多,不过目前这个 comment 方法已经可以让人轻松理解,我们的重构目的达到。
再总结一下优化过程:在 Tower 里面拆分任务,小步改进(把复杂的代码先从方法中剥离出来,放到它应该放在的地方),跑通测试,然后循环往复。
最后推荐大家一个网站:Code Climate,如果你的项目也使用Rails框架,可以把代码放到上面跑一跑,看看最后是个什么级别的呢?
记 tower.im 的一次重构的更多相关文章
- 记一次.NET代码重构
好久没写代码了,终于好不容易接到了开发任务,一看时间还挺充足的,我就慢慢整吧,若是遇上赶进度,基本上直接是功能优先,完全不考虑设计.你可以认为我完全没有追求,当身后有鞭子使劲赶的时候,神马设计都是浮云 ...
- 记一次java简单的if语句使用多态重构
场景描述: 一个controller中,部门领导有布置任务,查看任务整体情况,查看部门成员,查看部门成员完成情况,导出任务详情,如下: @RestController @RequestMapping( ...
- 记一次 node 项目重构改进
摘要:经常听到有祖传的代码一说,就是一些项目经过了很长时间的维护,经过了很多人之手,业务逻辑堆叠的越来越多,然后就变成了一个越来越难以维护. 经常听到有祖传的代码一说,就是一些项目经过了很长时间的维护 ...
- 记一次百万行WPF项目代码的重构记录
此前带领小组成员主导过一个百万行代码上位机项目的重构工作,分析项目中存在的问题做了些针对性的优化,整个重构工作持续了一年半之久. 主要针对以下问题: 1.产品型号太多导致代码工程的分支太多,维护时会产 ...
- NET代码重构
记一次.NET代码重构 好久没写代码了,终于好不容易接到了开发任务,一看时间还挺充足的,我就慢慢整吧,若是遇上赶进度,基本上直接是功能优先,完全不考虑设计.你可以认为我完全没有追求,当身后有鞭子使 ...
- 学习笔记——Maven实战(二)POM重构之增还是删
重构是广大开发者再熟悉不过的技术,在Martin Fowler的<重构——改善既有代码的设计>一书中,其定义为“重构(名词):对软件内部结构的一种调整,目的是在不改变软件之可察行为前提下, ...
- 公司ERP系统重构那些事
记一次会议,我提出插件化的想法,有支持也有反对,其中"系统架构师"表示插件化后的项目没什么意义,今天来讨论项目是否需要插件化的一些观点. 项目背景 公司内部"ERP&qu ...
- 【转】Xcode重构功能怎么用我全告诉你
原文网址:http://www.cocoachina.com/ios/20160127/15097.html 你会经常需要重构你的代码,让它有更好的结构,可读性或者提高可维护性.Xcode作为IDE其 ...
- 【BZOJ 1233】 [Usaco2009Open]干草堆tower (单调队列优化DP)
1233: [Usaco2009Open]干草堆tower Description 奶牛们讨厌黑暗. 为了调整牛棚顶的电灯的亮度,Bessie必须建一座干草堆使得她能够爬上去够到灯泡 .一共有N大包的 ...
随机推荐
- Hibernate一 入门
一 简介1.什么是ORMObject/Relation Mapping,即对象/关系映射.可以将其理解为一种规范,具体的ORM框架可以作为应用程序和数据库的桥梁.面向对象程序设计语言与关系数据库发展不 ...
- Solr使用solr4J操作索引库
Solrj是Solr搜索服务器的一个比较基础的客户端工具,可以非常方便地与Solr搜索服务器进行交互.最基本的功能就是管理Solr索引,包括添加.更新.删除和查询等.对于一些比较基础的应用,用Solj ...
- 用urllib2实现一个下载器的思路
下载器的构造 用urllib2实现下载器时从以下几个层面实现功能和灵活性: handler redirect, cookie, proxy 动作 timeout 构造请求 headers: ua, c ...
- 两种动态载入修改后的python模块的方法
方案一:循环导入/删除模块 a.py import sys, time while True: from b import test test() del sys.modules(b) time.sl ...
- 关于C++函数思考1(缺省的六大函数)
我们知道大神们在设计C++时候就给C++有六个默认的函数,所谓默认就是,无需我们这些程序猿们动手去写,仅仅要你在将类实例化.即创建一个对象,在利用对象进行数据操作时候,就会编译器自己主动调用默认的函数 ...
- 在Quick-cocos2dx中使用云风pbc解析Protocol Buffers,支持win、mac、ios、android
本例主要介绍 如何将 pbc 集成到quick-cocos2dx框架中,让我们的cocos2dx客户端Lua拥有编解码Protocol Buffers能力. 参考: 云风pbc的用法: http:// ...
- Linux的进程优先级-邹立巍
http://liwei.life/2016/04/07/linux%E7%9A%84%E8%BF%9B%E7%A8%8B%E4%BC%98%E5%85%88%E7%BA%A7/
- hook研究结果备忘
hook研究结果: 最近一周时间仔细研究了一下hook,也许不能称之为研究吧.顶多是让别人的思想拿过来抄袭一遍而已,写点结果也算对得起自己的这几天的苦心了. 1,首先从同事旁边听到了hook,然后看的 ...
- jQuery代码优化 事件委托篇
<转自 http://www.jb51.net/article/28770.htm> 参考文章: 解密jQuery事件核心 - 绑定设计(一) 参考文章: 解密jQuery事件核心 - ...
- 关于xml作为模板的配置服务系统开发
最近在做一个后台配置系统,其实之前也接触过,所谓的配置系统就是指,将你的网站布局抽象成一个xml模板,里面包括你自定义的节点,然后将变化的部分作为配置项,通过服务将配置选项与模板组装成一个js(这个服 ...