前言
最近拜读了下《重构 改善既有代码的设计第二版》书里大概可以分为4部分,前半部分主要讲了 什么是重构、为什么要重构、什么时候重构;后半部分则是作者总结的重构的手法和一些相应的小范例,读完之后深有体会。
什么是重构
不同于技术换型等专门抽出时间对代码进行的大型重构。书中提到的重构是对代码结构的一种调整,在不改变代码外在行为的前提下,对代码做出修改,提高其可理解性。
重构的关键在于运用大量微小且保证软件行为的步骤,将重构贯穿在项目的整个迭代开发中。每个单独的重构要么很小,要么由若干小步骤组成。因此代码很少进入不可工作的状态,即使重构未完成,代码也时刻可以运行。
为什么要重构
代码设计不是在一开始完成的,有些代码在一开始设计的时候可能设计的很漂亮,很干净。但随着时间的推移、项目的不断迭代、需求的不断增多等,我们不断的在调整代码,开始的设计可能不再能够满足现有的需求,然后代码开始变得臃肿。我们可以借助重构,不断改进设计,将它重新加工成设计良好的代码。
什么时候重构
💡 时时勤拂拭,勿使惹尘埃
佛家有句著名的话:时时勤拂拭,勿使惹尘埃。意思是时时刻刻不忘勤加拂拭,莫让身心沾染世俗的尘埃。在作者看来,重构应该是贯穿在项目的整个迭代开发中,而不是单独抽出一段时间来进行重构。在日常开发的时候,勤加拂拭你的代码,说白了就是平时多在意点。就像打扫房间一样,看见哪脏了就打扫打扫,而不是一直累积到最后受不了才选择重新粉刷。
代码中的臭味道
对于如何'删除一个变量'或'提炼一段代码'很容易,比较难的是'该在什么时候做这些动作'。作者用味道来形容重构的时机,当你觉得你的代码开始散发出臭味道时候,就是重构它的时候。下面是几个常见的臭味道的代码场景。
神秘命名
命名是编程中最难的两件事之一,也正因为如此,改名可能是最常见的重构手法包括:给函数改名,给变量改名、给字段属性改名等。在项目的维护中,把一些力气花在起名字或者改名字上的收益还是很大的,一个好的命名可以很好的解释你的代码在干什么。当你觉得某个名字与对应的代码不再契合时,我们应该给他起个更好的名字。
重复代码
如果你在一个以上的地方看到相同的代码结构,那么可以肯定,设法将它们合二为一,程序会变得更好,一旦有重复代码存在,阅读这些代码就需要格外仔细,留意其间细微的差异。如果要修改重复代码,你还需要找到所有的副本来同步修改。
过长函数
小函数有着更好的诠释力,更易于分享。函数越长,就越难理解,在早期,子函数调用需要额外的开销,使人们不太乐意使用小函数。但在现代编程中,几乎免除了进程函数调用的开销。小的函数固然会给阅读者带来一些心智负担,需要来回切换上下文,才能明白函数在做什么,但是现代的开发环境可以让我们在函数的调用处与声明处之间快速跳转,或是同时看到两处。当然,一个好的小函数的关键还是在于良好的命名,如果能给函数起一个好的名字,阅读代码的人可以通过名字了解函数的作用,根本不必去看其中写了什么。可以遵循这样一个原则, 每当感觉需要注释来说明点什么的时候,我们就可以把需要说明的代码提炼成一个新函数。
过长参数列表
同理,过长的参数列表也会大大降低代码的可读性,如果可以向某个参数发起查询从而获取另一个参数的值,那么就通过查询取代参数;如果有几项参数总是同时出现,可以将其合并成一个对象当入参;如果某个参数被用作区分函数行为的标记(flag)可以将其移除等。
注释
人们常常把注释当作"除臭剂"使用,有些注释之所以存在是因为代码太糟糕。当你感觉需要撰写注释的时候,先尝试重构,试着让注释变得多余。在你不知道该做什么的时候,才是注释的良好运用时机。
一些重构的手法
提炼函数
提炼函数是常见的重构手法,浏览一段代码,理解其作用,然后将其提炼到独立的函数中,并以这段代码的用途为这个函数命名。
对于"何时应该把代码放进独立的函数",有人认为从代码长度考虑,有人认为从代码复用性考虑,但作者认为,应该将意图与实现分开,如果你需要花时间浏览一段代码才能弄清楚它到底在干嘛,那么就应该将其提炼到一个函数里。并根据他所做的事情命名,之后再阅读主函数的时候 ,我们一眼就能看到函数的用途,大多数情况我们无需关心子函数如何达成其用途(这是子函数内部干的事)。
提炼函数的关键在于起个好名字,你应该在命名上多花点心思,可以给这段代码写上注释说明这段代码是干什么的,而这样的注释可以给我们提供一个好名字 ,如果暂时想不出一个好的名字,可以使用当下觉得最好的那个,并在之后想到更好的名称的时候替换掉它。函数名字的长度不重要,你需要做的是当阅读者看到你的函数调用时,不查看函数实现就能理解他做了什么。
也许你会担心短函数会造成大量函数调用,因而影响性能,在以前会有这种问题,但如今这种情况已经非常罕见了,所以,遵循性能优化的一般指导方针,不要过早的担心性能问题。
提炼变量
表达式有可能非常复杂而难以阅读。这种情况下,局部变量可以帮助我们将表达式分解为比较容易管理的形式。
vbnet
return (
order.quantity * order.itemPrice -
Math.max(0, order.quantity - 500) * order.itemPrice * 0.05 +
Math.min(order.quantity * order.itemPrice * 0.1, 100)
);
// ---------------------------
const basePrice = order.quantity * order.itemPrice;
const quantityDiscount =
Math.max(0, order.quantity - 500) * order.itemPrice * 0.05;
const shipping = Math.min(basePrice * 0.1, 100);
return basePrice - quantityDiscount + shipping;
拆分阶段
遵循单一职责原则,每当看见一段代码在同时处理两件不同的事时,我们应该把它拆分成成各自独立的模块。这样到了需要修改的时候,就可以单独处理每个主题,而不必同时在脑子里考虑两个不同的主题,甚至我们只需要修改其中一个模块,完全不用回忆另一个模块的诸般细节。
最简洁的拆分方式就是把一大段行为分成顺序执行的多个阶段,每一步都有边界明确的范围,我可以聚焦思考其中一步,而不用理解其他步骤的细节。
kotlin
const renderContent = (data) => {
// ...
// 处理data格式;
// ...
// ...
// 根据处理后的data渲染数据
// ...
}
//-----------------------------
const renderContent = (data) => {
const formatData = handleData(data);
return renderData(formatData);
}
const handleData = (data) => {
// ...
// 处理data格式;
// ...
}
const renderData = (data) => {
// ...
// 根据处理后的data渲染数据
// ...
}
......
剩下的技巧还有很多,我还没看完,你们有兴趣的话可以去看一看
总结
技巧
这本书相对比较偏向于新人,书中的一些重构技巧大多比较简单,有一些只看名字就能懂了是咋回事。尽管如此,作者总结了许多适用于重构的小技巧,并提供了示例,使得这本书也可作为一个小手册,时不时的简单翻一下。这些技巧不止能用在重构上,还可以提升我们的编程水平。
不足的是有很多案例技巧是基于面向对象编程的。对于现在的函数式编程来说可能有些过时。
思想
书中强调的思想是以不断的小重构替代大重构,强调重构日常化,在平时做需求的时候,看哪不顺眼就顺便重构一下。这样工作才会越来越轻松,而不是在屎山上一层一层往上加。到最后迫不得已重构。
参考资料
《重构 改善既有代码的设计第二版》