记 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大包的 ...
随机推荐
- MAVEN 工程打包resources目录外的更多资源文件
首先,来看下MAVENx项目标准的目录结构: 一般情况下,我们用到的资源文件(各种xml,properites,xsd文件等)都放在src/main/resources下面,利用maven打包时,ma ...
- php 利用第三方软件进行网页快照
网页快照有很多方法,具体的大家可以百度下.这里我复制一位别人的. 这里我只说下利用第三方软件(Web2Pic_Pro)实现. (1). 下载web2pic_pro软件.下载地址 http://isha ...
- AbpZero--4.不使用谷歌字体,提升加载速度
jtable控件样式中会使用到谷歌字体,每次访问都特别慢 1.打开jtable.css文件 [..\MyCompanyName.AbpZeroTemplate.Web\libs\jquery-jtab ...
- webservice使用基本技巧
一,webService基本概念 webService也叫XMLWeb SerVice WebService是一种可以接收从Internet或者Intranet上的其它系统中传递过来的请求,轻量级的独 ...
- python-用户登录小程序
算是第一篇博客吧~哈哈哈 虽然说是为了完成作业,不过以后估计会常来分享.首先说一下下边这个程序的基本功能.毕竟是第一次写python程序还是有点小激动和满满的成就感的,下边这个程序: 1.输入不存在的 ...
- 如何理解oracle 11g scan ip
如何理解oracle 11g scan ip 在11.2之前,client链接数据库的时候要用vip,假如你的cluster有4个节点,那么客户端的tnsnames.ora中就对应有四个主机vip ...
- 【安卓】给ViewFlipper加指示器,相似ViewPagerIndicator库提供的那种、!
思路: 1.viewPager有setOnPageChangeListener能够监听切换动作,但viewFlipper却死活没类似的东西.! 此处有一个变种思路,基于animation,animat ...
- 物联网MQTT协议分析和开源Mosquitto部署验证
在<物联网核心协议—消息推送技术演进>一文中已向读者介绍了多种消息推送技术的情况,包括HTTP单向通信.Ajax轮询.Websocket.MQTT.CoAP等,其中MQTT协议为IBM制定 ...
- dedecms网站文章标题与简标题的调用问题
使用dedecms调用标签的时候,既然有,咱们就合理利用,如果没有,咱也可以自己去添加.以下介绍dedecms网站文章标题调用的一些技巧,希望大家能够合理运用. dedecms网站文章标题与简标题的调 ...
- C#中的线程二(BeginInvoke和Invoke)
近日,被Control的Invoke和BeginInvoke搞的头大,就查了些相关的资料,整理如下.感谢这篇文章对我的理解Invoke和BeginInvoke的真正含义 . (一)Control的In ...