重构的重构 - 《重构》第二版导读

近20年过去了,Martin Fowler先生终于推出了新版的《重构》。本人有幸于ThoughtWorks技术雷达十周年峰会现场率先拿到了此书的国内发行版。

在这20年中,软件开发技术发生了很多重要的变化。新的编程语言不断涌现,老的编程语言也加快迭代。主流编程语言大都支持了多种编程范式,函数式编程和面向对象一样成了主流编程语言的标配。对并发的更好支持也已成为主流编程语言新的核心竞争力。于此同时各种软件开发工具也日益现代化,常用的编程IDE都具备了面向重构、测试甚至容器化发布的自动化工具和快捷键。

基于上,很多人都认为新版的重构会迎合时代的变化,焕然一新。然而当我用一整天时间读完全书后,却不禁如释重负。正如本书中文译者熊节先生所说“Flowler先生不仅没有拔高,反而把功夫做的更扎实了”。

确实,无论编程语言的语法如何变化、编程范型如何多元化、工具如何发展,软件设计的目标并没有变:那就在保证软件满足功能和非功能需求的前提下,如何更易应对变化以及更易让人理解和维护。由此所推导出来的软件设计原则也是几十年都没有变,如高内聚、低耦合,如SOLID原则等。甚至连GOF设计模式至今依然生命力旺盛,除了偶有在一些新的编程范型中出现的新模式以及对原有模式的更简单实现。此刻再回顾重构技术,它所传授的如何识别代码中的坏味道,以及如何采用小步安全的重构手法逐步将代码演化到更易理解、更易应对变化的状态,正是为了满足软件设计的核心诉求!所以重构应该和设计模式一样,是一项软件开发中历久而弥新的核心能力。

基于此,新版《重构》在主体内容上和第一版相似。首先从一个示例开始,先让读者从整体上体会重构的过程和效果。然后给出了重构的具体概念和原则。之后Martin老先生给读者列出了一份重要的代码坏味道清单并逐一诠释。随后用了一章篇幅来讲述如何搭建对重构来说至关重要的“自动化测试体系”。最后Martin用整本书近四分之三的篇章详细阐述了几十种关键的重构手法。

而在所谓与时俱进的方面,Martin则将更多的精力放在了对细节的持续优化上。首先直接可见的是新版删除了第一版中的最后几章:“大型重构”、“重构,复用与现实”,“重构工具”等,一方面是因为这几章中有些内容在今天看来已不是那么重要,其次所谓的“大型重构”其实仍旧是一系列小的重构手法的合理组合和持续应用。在第二版中Martin将重点放到了对重构手法的持续优化上:首先他将原本22种代码坏味道调整为24种,然后对所有重构手法进行了重新分类和排布,以便更加内聚和操作连贯。新版保留了第一版中大部分的手法,增加了一些更加具体和有用的手法,同时对所有的描述和示例都进行了更加精致的优化。

除这些之外,整本书的示例语言也从Java换成了JavaScript。

接下来我们具体看一下新版中的种种变化。

JavaScript

不少人认为第二版中将重构的示例语言由Java变为JavaScript,并不是一种妥当的选择。毕竟JavaScript作为一门动态脚本语言,并不适合较大规模的工业级软件产品。但我个人认为选择JavaScript是经过Martin深思熟虑的结果。

自从NodeJs将JavaScript带进了服务端开发,JavaScript就变成了一门前后端通吃的语言。此外,ES6标准将JavaScript变成了一门现代语言,语法上同时支持面向对象和函数式范式。最后JavaScript内置多种支持并发编程的特性,它的包管理工具和生态也都建设得非常好。相比其它语言,JavaScript的受众以及可应用的领域相对更加广泛一些。

JavaScript作为一门动态类型语言,IDE对其自动化重构的支持并没有Java那么好。正因如此,书中介绍的手动重构步骤才会显得更加有意义。而熟练掌握手动重构技巧,不仅能更好的理解重构的本质,对在其它一些主流编程语言下实施重构也非常有价值(例如C、C++、Python等)。

站在另一方面,借助JavaScript的多范式编程以及动态类型的语法特性,很多重构手法可以做的更加简单和灵活。例如JavaScript支持在函数内嵌套定义函数,所以应用Extract Method手法时,可以先把新提来出来的子函数定义在调用它的函数内部,这样借助函数的闭包性可以减少函数间传递的参数数量,等到真正需要复用该子函数的时候再把它挪到外面。全书中类似的例子还有很多。

在书中,Martin使用了ES6的语法,不仅因为ES6标准为JavaScript引入了类和继承的语法糖,而且在很多细节上也能让代码更加清晰。例如书中对constlet关键字的大量使用等。但同时Martin也尽量避免使用其他程序员不太熟悉的JavaScript编程风格,例如JS独特的对象模型以及generatorpromiseawait/async等语法。

因为使用JavaScript的缘故,新版对“构筑测试体系”一章做了较大修改。Martin演示了如何使用JavaScript的测试框架Mocha和断言库Chai构筑自动化测试系统,以支持重构的安全实施。

代码坏味道

学好重构的关键在于掌握三个点:起点、手法和目标(可以参考我之前写的一篇文章:《高效重构(一):正确理解重构》)。

代码的坏味道清单为我们指出了重构的起点。当发现代码中有类似清单中所列的坏味道时,就暗示着这是一个应该实施重构的地方。由此可见代码坏味道清单的重要性。

Martin在新版中对这份清单进行了重新梳理。下表对比了两版之间的差异。

第一版 第二版 变化概要
神秘命名
Mysterious Name
新版增加,突出了好的命名对于代码的重要性
重复代码
Duplicataed Code
重复代码
Duplicataed Code
新版删除了对”毫不相干的类中出现的重复“的描述,避免不合时宜的提取非本质重复
过长函数
Long Method
过长函数
Long Function
基本未变
过长参数列表
Long Parameter List
过长参数列表
Long Parameter List
新版提供了更多的重构解决方案
全局数据
Global Data
新版突出了全局数据对代码耦合性的恶劣影响
可变数据
Mutable Data
新版突出了适宜的不可变性对代码可维护性上的优势
发散式变化
Divergent Change
发散式变化
Divergent Change
新版提供了更多应对发散式变化的重构方案
霰弹式修改
Shotgun Surgery
霰弹式修改
Shotgun Surgery
新版提供了更多应对霰弹式修改的重构方案
依恋情结
Feature Envy
依恋情结
Feature Envy
新版对封装单元的表述不再仅仅针对类,扩展到函数和模块
数据泥团
Data Clumps
数据泥团
Data Clumps
基本未变
基本类型偏执
Primitive Obsession
基本类型偏执
Primitive Obsession
新版增加了”类字符类型变量“的危害,并精简了描述
Switch表达式
Switch Statement
重复的Switch
Repleated Switch
新版承认第一版有些“矫枉过正”!这项重构的核心是消除重复的switch
循环语句
Loops
凸显了在函数式下,循环有了更多的更具语义性的表达方式
平行继承体系
Parallel Inheritance Hierarchies
平行继承体系其实是散弹式修改的特殊形式。为了不再突出面向对象,新版删去
冗赘类
Lazy Class
冗赘的元素
Lazy Element
新版中体现了冗余的未必只是类,可能是函数、模块等
夸夸其谈通用性
Speculative Generality
夸夸其谈通用性
Speculative Generality
基本未变
临时字段
Temporary Field
临时字段
Temporary Field
删除”为了特殊算法引入临时字段“的情况
过长的消息链
Message Chains
过长的消息链
Message Chains
基本未变
中间人
Middle Man
中间人
Middle Man
基本未变
狎昵关系
Inappropriate Intimacy
内幕交易
Insider Trading
新版中不再仅限于类之间的不恰当耦合,将描述范围扩大到模块
过大的类
Large Class
过大的类
Large Class
旧版中描述的”GUI大类“已经有些过时了,所以删除了
异曲同工的类
Alternative Classes with Different Interfaces
异曲同工的类
Alternative Classes with Different Interfaces
新版对所谓类的"异曲同工"解释的更加清晰
不完美的类库
Incomplete Library Class
类库属于可扩展不可修改的代码,新版中把对类库的坏味道辨别和重构删掉了
纯数据类
Data Class
纯数据类
Data Class
新版提出了例外情况:当纯数据类不可修改且仅用于传递信息时
被拒绝的遗赠
Refused Bequest
被拒绝的遗赠
Refused Bequest
基本未变
注释
Comments
注释
Comments
基本未变

总结一下,新版在”代码的坏味道“这一章中除了一些语言上的调整外,主要的变化如下:

  • 更加注重对细节的打磨。新增一些小的明确的坏味道(如Mysterious NameGlobal Data),去除了一些大的、稍显模糊或者可被分解的坏味道(如Parallel Inheritance HierarchiesInappropriate Intimacy);
  • 修正了一些之前不准确的描述。例如将Switch Statement更改为Repleated Switch

    这个修改非常有意思,Martin在书中承认当年有些矫枉过正了!那时多少人看完重构,致力于消除代码中的每一处if-elseswtich!殊不知对于缺乏反射的静态语言,即使采用多态替换了条件分支,但是最后在工厂方法里拼装对象的时候还是会存在一个条件分支。这次总算是拨乱反正了!

  • 弱化了面向对象和类的分量,更多使用兼顾通用化的定义(例如使用模块一词替代)。

    关于这点,Martin Fowler在博客中曾如是说:
    "在写这本书第一版的时候,把视为是构建代码机制的主要结构已经成为主流。然而,现在我们看到其它结构发挥了更大的作用。在我看来,类仍然很有价值,但是重构已经比较少地以类为中心;我们要意识到,随着代码不断被重构,类也是可以变化的。"

  • 在某些局部的点上,优先推荐了一些函数式的解决方案,例如把loops替换为管道

    对于在新版中直接将loops定义为一种坏味道,个人认为是Martin新的一次矫枉过正。虽然管道和流在某些场景下比loops语义更清晰,但是不可否认在一些简单的场景下loops更加符合一般程序员的思维习惯。所以这得看具体的场景,一刀切有些不妥!不知道下一版中Martin会不会承认这类似第一版中的Switch Statement一样,是一次故意的“矫枉过正”。

重构手法

正确的实施重构手法是安全高效的达成重构目标的保障。

很多人认为学习重构只要掌握背后的思想就足够了,其详细繁琐的操作手法并不重要。于是乎现实中很多人在实际操作重构的过程中章法全无,一旦开始半天停不下来,代码很多时候处于不可编译或者测试不能通过的状态。有时改的出错了很难再使代码回到正确状态,只能推倒重来!

实际上重构是一项非常注重操作过程的技术,能够正确合理地使用重构手法,安全、小步、高效地完成代码修改,是评价重构能力的核心标准。否则Martin老先生也不值得花费多达四分之三的篇章对重构手法浓墨重彩了。

新版将重构手法从原来的68种调整到61种。由于具体的重构手法数量较多,这里就不一一对比了,只介绍一些重要的变化点。

  • 为了让重构手法间衔接得更加顺畅,新版中重新调整了重构手法的编排顺序;

    新版中将基本的常用重构放到了一章,起名叫做”第一组重构“,然后将其余的手法按照”封装“、”搬迁“、”数据“、”逻辑“、”API“以及”继承关系“进行了分类和排序;

  • 为了保持概念的内聚和一致,对一些重构手法进行了重命名或者合并;

    例如将引入Null对象(Introduce Null Object改为引入特例(Introduce Special Case)
    函数改名(Rename Method)添加参数(Add Parameter)移除参数(Remove Parameter)统一合并为改名函数声明(Change Function Declaration)

  • 通过调整原有的重构命名或者新增新的重构手法,让重要的重构同时存在正向和反向手法,以便可以在实践时更加灵活地决定重构的方向;

    例如将原来的内联临时变量(Inline Temp)引入解释性变量(Introduce Explaining Variable)改名为内联变量(Inline Variable)提炼变量(Extract Variable)
    以函数取代参数(Replace Parameter with Methods)修改为以查询取代参数(Replace Parameter with Query),并为其引入反向重构以参数取代查询(Replace Query with Parameter)

  • 为了凸显重构之间的关联关系,每种手法的介绍里面增加了曾用名反向重构示意图

    • 曾用名指出该重构在第一版中的原有命名;
    • 反向重构指出该重构对应的的反向操作的手法名称,为重构之间建立起互逆关系;
    • 示意图是Martin为每个重构手法画的一副小图,形象化的展现每个重构的效果;
    • 另外,建议在阅读时,除了关注每个重构手法的做法范例外,最好能把动机部分也好好阅读下。动机部分介绍了”为什么要做“以及”什么时候不该做“这项重构,里面的很多思考都具有启发意义。
  • 考虑到编程范式的多元化,新版的描述中弱化了面向对象中的类的概念,同时引入了和函数式相关的一些重构;

    在很多描述中,不再以作为封装的主要手段,取而代之以模块作为主要称呼。
    增加以管道取代循环(Replace Loop with Pipeline)的重构;

  • 考虑到”组合优于继承“,调整删除了一些和"重构到继承"相关的重构手法;

    例如删除了提炼子类(Extract Subclass)以继承取代委托(Replace Delegation with Inheritance)

  • 由于JavaScript鸭子类型的语法特性,删除了一些不再适用的重构手法;

    例如删除了提炼接口(Extract Interface)塑造模板函数(Form Template Method)封装向下转型(Encapsulate Downcast)

  • 在某些重构的操作步骤里,利用了JavaScript的语法特性简化重构的操作过程;

    书中经常使用两类JavaScript的语法特性:
    - 函数式。正如Martin Fowler在博客中所说:”JavaScript将函数作为一等公民无疑是最正确的决策“。JavaScript允许将函数作为参数和返回值,允许在函数里面嵌套定义函数以及支持匿名函数和闭包。所以在提炼函数(Extract Function)里面,先将提炼出来的新函数定义在调用它的函数的内部,借助函数的闭包性可以减少函数间传递的参数数量,等到真正需要复用该子函数的时候再将它挪出;而以管道取代循环(Replace Loop with Pipeline)重构,则完全依赖于匿名函数和闭包性。
    - 动态类型。JavaScript支持灵活的对象模型,新版中很多重构手法会在操作过程中往对象里临时加入新的属性,以方便后面的重构过程。例如在函数组合成变换(Combine Functions into Transform)引入特例(Introduce Special Case)中,都会看到引入了类似enrichXXX名称的函数对原有的对象进行动态扩充;

对于重构手法这部分,Martin进行了精心的调整,修改和增加了很多描述和代码示例。上述只是其中一些比较明显的变动,更多细致的变化还请君仔细品读原书。

最后要说的是,书中介绍了多达六十多种重构手法,普通人很难把所有的重构步骤都牢记于心。此书可以当做一本重构名录,在使用时再反复翻书查阅。相信经过不断阅读和实践,慢慢会掌握重构手法背后的普遍原理和规律,逐渐做到”手中无剑,心中有剑“。我曾把所有的重构手法归结为四类基本手法,最后浓缩成了两个核心操作,并给出了在重构时合理编排操作步骤的方法规律以及指出如何利用锚点来简化重构操作。具体见《高效重构(二):掌握重构手法》

重构示例

给初学重构的朋友一个建议,那就是在读完全书后,最好能把第一章"重构,第一个示例"中的例子再自行做一遍。这个例子规模适中,代码很具样板性,非常适合用来演练重构。

在第二版中,Martin考虑到很多新时代人类完全没有在影片出租店租影片的体验,所以将例子改为”为戏剧演出的客户打印账单“的场景。这个例子和第一版中”影片出租店打印小票“的结构基本类似。大家在练习的时候,可以仔细品味以下方面:

  • 出现新的变化方向驱动重构的展开;
  • 将大的重构目标分解为一个个针对小的对代码坏味道的重构过程;
  • 在重构过程中执行安全小步的代码调整动作;
  • 依赖自动化测试保护重构的安全性;
  • 在重构的最后,代码面向新的变化方向重新变得满足开放封闭性

最后再提一点:在书中,面对”账单不同打印格式“的变化方向,重构到最后产生了两个函数renderPlainTextrenderHtml,它们分别用来计算普通文本和HTML格式的账单字符串。对于像我这样一个对重复代码”吹毛求疵“的程序员,往往会进一步消除这两个函数中出现的模式重复。这两个函数有相同的打印算法,都是先输出账单标题,然后逐一输出每个演出的具体细则,最后输出账单的汇总信息,而且每一步中所依赖的数据是完全一致的。基于此我们可以做进一步的重构:将打印算法的重复和打印格式的不同进行分离。完成这个重构后,代码将会变成一个策略模式:由通用的打印算法组合不同的打印策略。现实中这个重构是否值得做,完全在于在不同的账单格式下”打印算法相同“这个假设是否足够稳定。

感兴趣的同学可以在https://github.com/MagicBowen/refactoring/tree/master/code/js中找到上述重构的代码示例.

写在最后

本文简单地介绍了第二版《重构》的一些变化,更多的精彩细节还是推荐大家阅读原书。这次中文版本还是由熊节先生主译,所以书的翻译质量依然很高。第二版由异步图书出版,书质还是蛮不错的,又变成了硬皮封面,自然价格也有不小的上涨。但是相信对于想要提高编码能力的读者来说,此书绝对是物超所值的。

不过,还是有点小小的遗憾在这里吐槽一下。作为一个对代码整洁度要求较高的人,书中偶尔出现的代码缩进不对齐的问题会让我觉得有些刺眼。如果说有些缩进不对齐只关乎美感的话,那么如下的不对齐就会让人困惑于函数的定义位置了(没法一眼看出函数playFor是定义在顶层,还是在enrichPerformance的内部)。

最后我们以一个小小的测验结束本文吧。Martin先生为每个重构手法都绘制了一副示意图,尝试猜猜下面每幅图对应的重构手法名称,以及将对应的正反手法连接起来吧:)


最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,132评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,802评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,566评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,858评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,867评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,695评论 1 282
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,064评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,705评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,915评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,677评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,796评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,432评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,041评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,992评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,223评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,185评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,535评论 2 343