MForever78 Code Blog

见微知著,谈重构的正确方式

重构的优势是利用你现在知道、但当时的开发程序员并不知道——或并没有加以利用的信息。不断的简化代码,让它们更容易理解。不断的使它们在将来的变更变得更容易、更安全。

我相信大部分程序员都多多少少地动过重构代码的心思,但不是所有人都会被给予这样的机会。上周我对部门的签名系统进行了一次重构,过程实在是值得一记。

为什么要重构?

代码可读性差,不应是重构的惟一理由。如果这段代码可以正常工作,在整个系统中不十分重要,并且它所负责的逻辑很少需要变动,那么重构的理由就不太充分。如果冒然行动,就很可能进入到代码重构的七个阶段。那么怎么才算是重构的充分理由呢?

见微知著,就以这个签名系统为例来说明。

需求分析

这个系统可以根据当前登入的帐号自动从后台获取相关信息,填入到签名档的对应位置上,因为自动获取到的信息可能有误差,所以还支持用户手动修改。用户修改完成后可以生成一张图片保存下来,加入到邮件的签名档中。由于部门不同团队都希望使用自己的签名档,所以用户进入后可以点击切换生成不同部门的签名档。这个系统目前神盾局和技术保障部两个部门在使用。

生成的签名效果如下图:

神盾局

问题分析

原来系统最大的问题就是生成签名的逻辑不统一,不具有通用性。如上面图中花名、姓名、岗位三个字段是行内内联关系,而它们的整体和部门信息,个性签名又是块级元素并列关系。对于这个问题,原来系统是这样处理的:

这是配置文件:

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. 优化页面加载逻辑

对于字体文件过大导致的页面加载慢的问题,我们很自然地想到「异步按需加载」的解决方案,即用哪个载哪个。但具体的实现,我们需要把完整的页面加载逻辑梳理清楚。

flowchart

逻辑搞清楚后,代码就很容易实现了。至此,我们的重构基本上完成,很好地解决了上面提出的四个问题。

回顾

在重构完成后,我们来评估一下是否完成了原来的目标。对用户来说,进入新系统页面后并不需要加载所有的字体,速度提升。对维护者来说,添加或修改签名设计的时候,完全无需修改逻辑层的代码,只需要修改配置文件就可以完成。即使是添加一个全新的设计,从设计稿到页面也可以轻松地在 5 分钟之内完成,大大提升了可维护性。

最后,用师兄 @辰秋 的话来提醒自己和大家:「不是任何情况下你都有机会去重构自己的代码,因此当你下决心写烂代码的时候,要记住将来踩坑的人可能是自己,但更可能是后来的维护者」。虽然重构有时不可避免,但不要在设计之初就留下这条后路,尽量一次做到最好。