见微知著,谈重构的正确方式
01 Aug 2015重构的优势是利用你现在知道、但当时的开发程序员并不知道——或并没有加以利用的信息。不断的简化代码,让它们更容易理解。不断的使它们在将来的变更变得更容易、更安全。
我相信大部分程序员都多多少少地动过重构代码的心思,但不是所有人都会被给予这样的机会。上周我对部门的签名系统进行了一次重构,过程实在是值得一记。
为什么要重构?
代码可读性差,不应是重构的惟一理由。如果这段代码可以正常工作,在整个系统中不十分重要,并且它所负责的逻辑很少需要变动,那么重构的理由就不太充分。如果冒然行动,就很可能进入到代码重构的七个阶段。那么怎么才算是重构的充分理由呢?
见微知著,就以这个签名系统为例来说明。
需求分析
这个系统可以根据当前登入的帐号自动从后台获取相关信息,填入到签名档的对应位置上,因为自动获取到的信息可能有误差,所以还支持用户手动修改。用户修改完成后可以生成一张图片保存下来,加入到邮件的签名档中。由于部门不同团队都希望使用自己的签名档,所以用户进入后可以点击切换生成不同部门的签名档。这个系统目前神盾局和技术保障部两个部门在使用。
生成的签名效果如下图:
问题分析
原来系统最大的问题就是生成签名的逻辑不统一,不具有通用性。如上面图中花名、姓名、岗位三个字段是行内内联关系,而它们的整体和部门信息,个性签名又是块级元素并列关系。对于这个问题,原来系统是这样处理的:
这是配置文件:
var config = {
background: {
// 背景相关设置
},
avatar: {
// 头像相关设置
},
name: {
// 花名、姓名、职位
},
content: {
// 个性签名
}
};
这是逻辑处理:
if (key === 'name') {
// 对于数据中的 name 字段作特殊处理
// 生成 3 个内联元素 span ,分别填充花名,姓名和岗位
} else {
// 生成一个块级元素 div ,填充对应内容
}
而这个时候,假如部门信息这部分内容也需要做一定的调整才能填充,逻辑就变成了:
if (key === 'name') {
// 对于数据中的 name 字段作特殊处理
// 生成 3 个内联元素 span ,分别填充花名,姓名和岗位
} else if (key === 'depDesc') {
// 对数据中的 depDesc 字段作特殊处理
} else {
// 生成一个块级元素 div ,填充对应内容
}
可以看到,当设计稿需要发生变化的时候,这里的逻辑需要加上一层一层的 if ... else ...
语句,这是很难维护的。
第二个问题也非常致命。不同的签名使用了不同的字体,这些字体加起来有近 10M 的大小。原来的系统是一次性加载所有字体文件,等全部加载完成后再去除页面上的加载图标,显示内容。10M 的大小即使是在内网的速度下也是无法忍受的。
第三个问题有关事件绑定。比如神盾局签名的部门信息这里需要对换行操作进行特殊处理,我们想在 keyup
事件发生后执行一些逻辑。现有系统的做法是在 DOM 加载完毕加入一段绑定事件的代码。如果有新的类似需求,我们只能在这段后面再加一个绑定。可想而知,这种做法是不方便也不容易维护的。
最后一个问题与表现层相关。我们可以看到签名信息的不同字段间的样式几乎都是不同的,这导致我们写的 CSS 类几乎无法重用,而是要给每个字段指定自己独特的类,这就失去 CSS 「类」的意义了。而且如果想增加或删除一个字段,就需要修改对应的配置文件和样式表两个地方,对于维护者也是不方便的。
根据以上分析,这个系统虽然现阶段可用,但如果要增加新的签名,或者是要对已有的签名设计做一些修改就十分困难了。而不难想到,这样的变动很可能经常发生。因此,我们谨慎地作出决定,我们要重构。
重构方案
1. 更改数据结构
我们在代码中做大量的 if ... else ...
的特殊判断才能达到目标的主要原因就是抽象层级不够。如果我们不需要关心此时在处理的是什么信息,而把这些特殊处理都放到配置文件中去的话,这个问题可能就会解决。
我们重新梳理页面上的信息:
除了背景和头像之外,其他的都可以归结为 block
,只不过有一些是多个字段组成的 block
,还有些是一个字段独立成 block
。因为这些字段间的 CSS 几乎不可重用,我们干脆直接给他们分别指定各自的内联样式。这样,我们只要在每个 block
的配置项中写好内容、偏移、样式等信息,就可以交给逻辑层去生成了。
修改后的配置文件的写法变成了:
var config = {
background: {
// 背景相关设置
},
avatar: {
// 头像相关设置
},
blocks: [
{
// 花名、姓名、岗位
name: "name",
top: 30,
left: 300,
data: [
{
content: user.cname,
style: "font-size: 22px"
},
{
content: user.lastname,
style: "font-size: 12px; margin-left: 5px;"
},
{
content: user.jobDesc,
style: "font-size: 12px; margin-left: 5px;"
}
},
{
// 部门
name: "department",
top: 59,
left: 302,
data: {
content: user.depDesc,
style: "font-size: 11px; line-height: 28px;"
}
}
]
};
而逻辑就可以直接方便地写出来:
for (var block in blocks) {
if (Object.prototype.toString.call(block.data) === '[object Array]') {
for (var span in block.data) {
// 在 DOM 树中生成 span
}
} else {
// 生成 div
}
}
对于需要加工的字段,我们只要允许用户在配置文件里加入自定义函数,这个问题也就迎刃而解了。
blocks: [
{
name: "lastname",
top: 30,
left: 300,
data: {
content: function() {
return "(" + user.lastname + ")";
},
style: "font-size: 12px; margin-left: 5px;"
}
}
]
在逻辑层中加入相应的处理:
if (Object.prototype.toString(data.content) === "[object Function]") {
data.content = data.content();
}
还有一个需要消除的特例就是事件绑定,同样给用户在配置文件中自行添加事件的途径:
blocks: [
{
name: "department",
top: 59,
left: 302,
data: {
content: user.depDesc,
style: "font-size: 11px; line-height: 28px;"
},
events: {
"keyup": function() {
// 处理换行
}
}
}
]
逻辑层添加事件绑定:
if (block.events) {
var $target = $('#' + data.name + '-' + block.name);
for (var event in events) {
$target.on(event, events[event]);
};
}
至此,通过更改配置文件数据结构的方法,我们就解决了第一、三、四个问题。
2. 优化页面加载逻辑
对于字体文件过大导致的页面加载慢的问题,我们很自然地想到「异步按需加载」的解决方案,即用哪个载哪个。但具体的实现,我们需要把完整的页面加载逻辑梳理清楚。
逻辑搞清楚后,代码就很容易实现了。至此,我们的重构基本上完成,很好地解决了上面提出的四个问题。
回顾
在重构完成后,我们来评估一下是否完成了原来的目标。对用户来说,进入新系统页面后并不需要加载所有的字体,速度提升。对维护者来说,添加或修改签名设计的时候,完全无需修改逻辑层的代码,只需要修改配置文件就可以完成。即使是添加一个全新的设计,从设计稿到页面也可以轻松地在 5 分钟之内完成,大大提升了可维护性。
最后,用师兄 @辰秋 的话来提醒自己和大家:「不是任何情况下你都有机会去重构自己的代码,因此当你下决心写烂代码的时候,要记住将来踩坑的人可能是自己,但更可能是后来的维护者」。虽然重构有时不可避免,但不要在设计之初就留下这条后路,尽量一次做到最好。