大多数学习编程的新手,都是从某个任务或项目开始的,目标就是做出某个实际的功能,可能是一个网页,实现几个 API,或直接在 Terminal 上打印出结果。刚开始时,只要能完成功能就会很有成就感,不过随着你的项目开始越来越大,开发过程中会渐渐发现几个问题:
-
觉得改动越来越困难,越改越心虚,不确定改完会不会有哪里出问题 -
过一阵子就忘了自己当时在写什么,为什么这样写 -
出现 bug 时找不到问题点,只能胡乱打印错误信息或者下断点慢慢调试
这其实代表着,你即将进入一个重要的阶段了。你们已经从功能面的追求,进阶到对代码品质的追求。而这个把代码品质变好的过程,通常就叫做重构,先让我们来看看重构比较明确的定义:
Refactoring is “the process of changing a software system in such a way that it does not alter the external behavior of the code yet improves its internal structure” by Martin Fowler
Martin Fowler 算是当今软件开发领域最有影响力的几位大师之一,它在 1999 年写过的一本书 Refactoring: Improving the Design of Existing Code ,就是专门在谈重构这件事,也是学习重构的圣经之一。这段对于重构的定义,白话说就是要在功能不变的情况下,提升代码本身的品质。所以我们并不是在增加程序的功能,也不是在改善程序的性能,而是让代码能更好的被理解与修改。
书中将许多重构的常见问题分门别类,也给出一系列的理论与方法去一一破解,但对新手来说可能不容易理解抽象的理论。所以接下来我会拿书中提到的前两个最常见的问题,配合实际的范例,让你在不用有太多软件工程知识的基础下,也能轻松理解,并大幅提升代码的品质。
重复的代码
在新手的代码中,时常可以看到各种重复的代码,小至参数的赋值,大至整片的代码复制贴上,而这通常是个可以简单被修正的问题。以下用 javascript 来写些简单的例子。
重复的变量使用
function getUserProfile(user) {
let profile = {};
profile.name = user.name;
profile.address = user.address;
profile.picture = user.picture;
return profile;
}
这边我们想从 user 这个对象里面,提取想要的几个字段构造成新的 profile 对象,但这边可以看到 profile 这个变量被不断的重复。以这个例子来看可能不是大问题,但当属性多起来问题就会被放大,所以我们可以很简单的让它出现一次就好。
function getUserProfile(user) {
let profile = {
name: user.name,
address: user.address,
picture: user.picture
};
return profile;
}
这样就避免了最简单的重复问题,当然这例子也可以选择直接省略掉 profile 这个变量只返回结果,但这是另一个层面的问题我们暂不讨论。再看下一个类似的例子
function getProduct(req) {
let product = {
title: req.body.title,
description: req.body.description,
price: req.body.price
}
return product;
}
现在应该不难看出来问题点了,就是 req.body 不断的重复出现,所以只要简单改成以下即可
function getProduct(req) {
let body = req.body;
let product = {
title: body.title,
description: body.description,
price: body.price
};
return product;
}
当然这样微小的例子,可能还感受不到太大的差异,以下我们再来看着差异更显着的例子,以下是一个简化版的用户登录 API。
function signIn(req, res) {
let user = req.body.user;
let result;
switch (user.signInType) {
case 'native':
let natName = user.name;
let natPassword = user.password;
let natPicture = user.picture;
result = addUser('native', natName, natPassword, natPicture);
break;
case 'weixin':
let wxName = user.name;
let wxPassword = user.password;
let wxPicture = user.picture;
result = addUser('weixin', wxName, wxPassword, wxPicture);
break;
}
res.send(result);
}
简单来说这段代码在做以下事情:
-
在用户登录的时候,判断它是普通的登录,还是经由 WX 登录 -
由另一个叫 addUser 的 function 去处理登录事项,并返回成功与否,在此假设 addUser 已于其它地方实现,我们不特别关心它的实现逻辑。 -
最后由 res 对象去呼叫一个 send method 把登录结果给传送出去
备注:一般正规的微信登录会有不太一样的认证机制,不过这边为了简化范例,假设直接使用 password 进行认证。另外 addUser 如果牵扯到数据库处理可能有非同步的问题,这边也可先忽略,假设所有程序都是同步执行。
仔细观察一下,会发现两种登录的逻辑几乎一模一样,这种状况我们通常会用一招叫做“提炼方法”的方式处理,大家可以先花点时间想一下有什么可以移除的重复部分,接下来我会示范如何一步一步系统的重构它。
首先第一步新建个 function ,命名为 signInHandler,并将可能重复的程序,选其中一个,先原封不动的复制过来。其中得把必要的变量(如 user)给传进新的 function,并将结果返回出来,让本来的程序功能保持不变。
function signInHandler(user) {
let natName = user.name;
let natPassword = user.password;
let natPicture = user.picture;
return addUser('native', natName, natPassword, natPicture);
}
function signIn(req, res) {
let user = req.body.user;
let result;
switch (user.signInType) {
case 'native':
result = signInHandler(user);
break;
case 'weixin':
let wxName = user.name;
let wxPassword = user.password;
let wxPicture = user.picture;
result = addUser('weixin', wxName, wxPassword, wxPicture);
break;
}
res.send(result);
}
接下来,观察一下新 function 内的变量,如果它从头到尾只出现在此 function 内部,我们就可以将原本非常特定用途的命名,改为更广泛一点的命名,也就是像类似 natName 这种变量,现在只要叫 name 即可,毕竟它只在此 function 内存在,不再拥有任何独特的含义了。
function signInHandler(user) {
let name = user.name;
let password = user.password;
let picture = user.picture;
return addUser('native', name, password, picture);
}
function signIn(req, res) {
let user = req.body.user;
let result;
switch (user.signInType) {
case 'native':
result = signInHandler(user);
break;
case 'weixin':
let wxName = user.name;
let wxPassword = user.password;
let wxPicture = user.picture;
result = addUser('weixin', wxName, wxPassword, wxPicture);
break;
}
res.send(result);
}
再来就是最关键的一步了,我们比较一下它跟另一个 weixin 登录的差异,找到关键的不同之处,把这个不同的部分放到新 function 的传入变量中。在这边就是这个叫 ‘native’ 的 string。
function signInHandler(type, user) {
let name = user.name;
let password = user.password;
let picture = user.picture;
return addUser(type, name, password, picture);
}
function signIn(req, res) {
let user = req.body.user;
let result;
switch (user.signInType) {
case 'native':
result = signInHandler('native', user);
break;
case 'weixin':
let wxName = user.name;
let wxPassword = user.password;
let wxPicture = user.picture;
result = addUser('weixin', wxName, wxPassword, wxPicture);
break;
}
res.send(result);
}
这时候我们就可以很开心的把 weixin 的登录的部分也直接用我们的新 function 处理了。
function signInHandler(type, user) {
let name = user.name;
let password = user.password;
let picture = user.picture;
return addUser(type, name, password, picture);
}
function signIn(req, res) {
let user = req.body.user;
let result;
switch (user.signInType) {
case 'native':
result = signInHandler('native', user);
break;
case 'weixin':
result = signInHandler('weixin', user);
break;
}
res.send(result);
}
至此,我们终于达到了去除重复的主要目的,可喜可贺。遵照这个步骤,你将可以很有系统的一步步优化你的程序。
不过单就这例子而言,我们还可以更进一步。观察力很敏锐的人可能会发现,我们 switch 的 case 判断的部分,刚好也就是传入signInHandler 的第一个变量,也就是 native
和 weixin
这两个 string 也被重复了,所以其实可以更进一步把整个 switch 拿掉,变成以下更简洁的形式。
function signInHandler(type, user) {
let name = user.name;
let password = user.password;
let picture = user.picture;
return addUser(type, name, password, picture);
}
function signIn(req, res) {
let user = req.body.user;
let result = signInHandler(user.signInType, user);
res.send(result);
}
最后,再往前推一小步,我们传入 signInHandler 的变量是不是都在 user 对象上,如此只要传一个 user 进去即可,不用重复两次,再者,signInHandler 里面的临时变量 name、password、picture 也都是 user 对象提取出来的,其实大可省略。最终就可以变成以下简洁的写法。
function signInHandler(user) {
return addUser(user.signInType, user.name, user.password, user.picture);
}
function signIn(req, res) {
let user = req.body.user;
let result = signInHandler(user);
res.send(result);
}
到这边,终于算是大功告成,我们的代码跟原版比起来,是不是变得非常简洁,而过程中的每一个步骤,原本功能都完全没有被改变到,这就是重构的根本精神。对新手来说,尽量去除掉重复出现的代码就是迈向品质提升的第一大步。
有些人可能会发现,上面例子中的最后版本,连 signInHandler 都显得多余了,毕竟它只是调用了另一个 addUser 方法而已,但这已经不是重复代码的问题了,让我们用下面的第二类问题,来探讨 function 的拆分吧。
过长的方法
这里要谈的是,一个过于复杂冗长的 function,该如何处理。基本准则就是职责分离,也就是一个 function 尽量只做一件事情。我们通常会用以下方式处理
-
在一个大 function 中,观察是否有一块一块做不同类型事情的代码段。 -
将这些代码段分别移到新的小 function 中,然后用小 function 的名称来解释你想做什么,并把复杂难懂的逻辑部分隐藏在小 function 里面。
让我们从实际的例子来理解,这是一个帮教授计算期末考试调整分数的 function,可以先试着读一下,看能不能搞懂这位教授打分的逻辑是什么。
function adjustScores(scores) {
let fullScore = 100;
for (score of scores) {
if (score < 0 || score > fullScore) {
return "error";
}
}
let maxScore = 0;
for (score of scores) {
if (score > maxScore) {
maxScore = score;
}
}
let diffFromFullScore = fullScore - maxScore;
let newScores = [];
for (score of scores) {
newScores.push(score + diffFromFullScore);
}
它的逻辑其实并不难,但你可能还是花了一点时间才理解了它到底想表达什么,现在让我们一起来照顺序看看它做了哪些事吧。
-
定义了一个变量 fullScore,设定满分是 100 分 -
检查一下传入的分数有没有不合理的数值,有的话直接返回 “error” -
找到最高分的那个人 -
看一下它和满分差距多少 -
将每个人的分数都加上 “最高分与满分的差距”,其实也就是简单的把最高分的人调到满分,其它人相应的调整上来的线性打分策略。
好了,其实逻辑并没有很难对吧,那来看看我们怎样用一个个小 function 来叙述这个逻辑,让它更贴近人类理解事情的方式。
function adjustScores(scores) {
let fullScore = 100;
if (!validateScores(scores, fullScore)) {
return "error";
}
let maxScore = findMaxScore(scores);
let diffFromFullScore = fullScore - maxScore;
let newScores = addScoreByNumber(scores, diffFromFullScore);
return newScores;
}
function validateScores(scores) {
for (score of scores) {
if (score < 0 || score > 100) {
return false;
}
}
return true;
}
function findMaxScore(scores) {
let maxScore = 0;
for (score of scores) {
if (score > maxScore) {
maxScore = score;
}
}
return maxScore;
}
这是重构完的结果,我们拆出了 validateScores
、findMaxScore
与 addScoreByNumber
,分别去处理特定的小任务,并在主 function adjustScores
中调用它们。
如此一来,再看一次我们原本的 adjustScores
,是不是感觉到它的每一行,几乎就与人类语言叙述的处理过程一样。这样读代码的时候,就能一目了然了,如果还想细看你到底实际上怎样做的,也只要去看对应的 function 逻辑即可,是不是轻松多了呢?
不过做这件事也要注意不要过度,如果你将 function 拆得太零碎,可能又会让代码变得多余,一个简易的衡量指标就是 function name 能不能让人理解它在做什么,并能靠阅读一个个 function name 去快速理解你的整体逻辑,如果做不到,它反而增加了阅读的负担。所以重点不仅是拆分,取名也是非常关键的环节。
其实对新手来说,处理好重复代码与冗长方法的问题,已经可以大幅提升代码的品质了,最后来聊点比较实际的问题,就是重构的好处到底是什么?毕竟重构了一整天,代码好像变漂亮了,自己好像也觉得舒服了些,但你的功能其实是没改变的,这时如果你老板跑来问你今天到底都做了什么,你该怎么回答呢?
关于代码品质的优化,这边介绍几个重要的特性:
-
Modifiability(可修改性) :当重复代码减少,未来你的代码需要修改时,就不用每个重复的地方都要去改一遍了,所以去除重复其就是节省了未来修改时所需的时间。 -
Readability(可读性) :当重复代码减少,而且 function 的职责也很清楚,命名也足够好,看你的程序就轻松多了,这就是可读性的增加。可读性好,会让团队合作时大家互相理解对方的效率大幅提高。 -
Reusability(可重复利用性) :当你 function 的职责能分得清楚,也就更容易让其它地方拿这个 function 去重复使用,开发会变得更快速,也间接降低了重复性的问题。以上述的例子中,例如像 findMaxScore 这个 function,我们其实可以把它的名字取得更普遍一点,例如 findMaxValue,那它可能就能在很多地方被重复使用了。 -
Testability(可测试性) :每个方法只执行一个特定的任务或实现一个独立的功能点,更容易针对这个功能点编写单元测试,验证其正确性和预期行为。
备注:以上谈到过大 function 的问题也同样会发生在 class 上,与 Long Method 对应的问题就是 Large Class。但处理的方式是一样的,只是分离时复杂度更高些,这里先不赘述。
简单说,重构其实是为了未来的修改铺路。你现在花在重构上的时间,就是帮公司,或帮未来的自己省下了更多开发时间。也只有达到这个效果,才是有意义的重构。
一旦你把这种对代码品质的追求变成为一种自我要求,你的技巧将会越来越纯熟,最终在第一时间就能写出考虑到这种种特性的代码了。而如果你是接手其它人没写好的代码,或因为需求导致原本的设计必须改变而必须重构,你也会知道该如何处理了。
原文始发于微信公众号(程序猿技术充电站):给新手程序员的代码重构建议
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/224846.html