16 熊节:什么代码应该被重构?
你好,我是郑晔。
代码坏味道的说法源自《重构》这本书,坏味道和重构这两个概念几乎是如影随形。提及《重构》这本书,在国内谁还能比《重构》两版的译者熊节更了解它呢?所以,这一讲,我就请来了我的老朋友熊节,谈谈在他眼中看到的重构和坏味道。有请熊节老师!
你好,我是熊节。
自从翻译了《重构》以后,很多公司找我去做重构的培训,光是华为一家,这个主题在各个不同的部门就培训过好些次。每次讲这个主题,我都觉得挺为难的:重构这事有什么可培训的呢,不就是一个无脑模式匹配的事吗!然而跟各家公司的读者们一交流,我就发现事情并没有那么简单。
很多人一说到重构,就聊到虚无缥缈的事上了,像什么架构啦、文化啦,等等。我不得不先把他们拉住仔细问问,他们是怎么读《重构》这本书的?这一问我就发现,原来很多读者(恐怕是绝大多数读者),还没弄明白这本书到底应该怎么读。
什么代码应该被重构?
《重构》这本书,以及重构这门手艺,提纲挈领的部分,都在一个关键的问题上:什么代码应该被重构。
你可能会说,质量不好的代码需要被重构。没错,可是代码的质量到底应该如何评判呢?
首先我们要明确的是,代码的好与坏不应当用个人好恶、“含混的代码美学”来表达,因为这会带来两个困难:
- 第一,每个人对于“好”或“美”的观念可能相当不同;
- 第二,对于坏代码缺乏明确的“症状”判断,也就很难提出明确的改进措施。
即便是一些经典的程序设计原则,也有同样的问题。例如“高内聚低耦合”,尽管这是所有人都赞同的设计原则,但究竟什么样的代码呈现了“低内聚”、什么样的代码呈现了“高耦合” 、“低内聚”与“高耦合”是否总是同时出现、应该以何种办法提高内聚降低耦合……这些问题仍然是悬而未决的。
因此,对于真正在一线工作的人来说,“高内聚低耦合”很多时候就成了一句咒语,念完咒语后,呼唤出的其实还是每个人原本的编程习惯与风格,并不真正指导任何行为的改变。
而当我们去观察“低内聚高耦合”带来的问题时,事情就变得明朗了。比如,当我们仔细阅读《重构》第三章时,我们会发现,“低内聚”会直接引发的现象是“霰弹式修改(Shotgun Surgery)”:
每当需要对某个事情做出修改时,你都必须在许多不同的类内做出许多小修改,那么就可以确定,你所面临的坏味道是霰弹式修改。当需要修改的代码分散在多处,你不但很难找到它们,也很容易忘记某个重要的修改。
而“高耦合”直接引发的现象则是有某种相似性、但又表现不同的“发散式变化(Divergent Change)”:
如果某个类经常因为不同的原因在不同的方向上发生变化,发散式变化就出现了。
我再举另一个设计原则的例子。“迪米特原则”也是常被提及的面向对象设计原则之一,然而知道这个名称是一回事,知道如何识别不符合迪米特原则的代码,则又需要更多的个人经验。《重构》第三章则把这个原则表述为两个非常直观的症状:“过长的消息链(Message Chains)”和“中间人(Middle Man)”。
如果你看到用户向一个对象请求另一个对象,而后者再次请求另一个对象,然后再请求另一个对象……这就是消息链。在实际代码中,你看到的可能是一长串取值函数,或者一长串临时变量。
人们可能过度运用委托。你也许会看到某个类接口有一半的函数都委托给其他类,这样就是过度运用。
你发现没?对于开始我们提到的“什么代码应该被重构”这个关键问题,虽然《重构》作者Martin Fowler和Kent Beck非常客气地声称:“并不试图给你一个何时必须重构的精确衡量标准”,实际上,《重构》给出的24项“坏味道”(在《重构》第一版中是22项)已经形成了一个非常明确的代码质量检查清单。
尽管这本书从未声称这是一份完备的坏味道清单,但在实际工作中,还不用说完全识别并消除这份列表中的全部坏味道,只要能做到命名合理、没有重复、各个代码单元(类、函数等)体量适当、各个代码单元有明确且单一的职责、各个代码单元之间有恰当的交互,这就已经是质量相当高的代码了。
更重要的是,伴随着对具体症状的了解,对症的解决办法也变得明确。在《重构》第三章里非常明确地讲到:
- 对于“霰弹式修改”,解决的办法是使用“搬移函数”和“搬移字段”,把所有需要修改的代码放进同一个模块;
- 对于“发散式变化”,解决的办法是首先用“提炼函数”将不同用途的逻辑分开,然后用“搬移函数”将它们分别搬移到合适的模块;
- 对于“过长的消息链”,你应该使用“隐藏委托关系”;
- 对于“中间人”,对症的疗法则是“移除中间人”,甚至直接“内联函数”。
这就是我前面所说的“无脑模式匹配”。
讲到这里,你也就明白了,对于绝大多数程序员而言,阅读和使用《重构》这本书的正确方法就应该是:
- 打开任意一段代码(可以是自己刚写完的或者马上要动手修改的);
-
翻开《重构》第三章,遍历其中的每个坏味道:
-
识别这段代码中是否存在上述坏味道;
- 如有,则遵循该坏味道所列的重构手法,对该段代码进行重构;
- 如无,则继续遍历代码。
上述过程不需要玄妙的理论和含混的代码美学,只需要机械的重复和简单的模式匹配。正因为此,重构才是一项完完全全具备可操作性、能够在任何遗留代码库上实践的技术。
培养对“坏味道”的判断力
当然,每位实践者仍然“必须培养出自己的判断力,学会判断一个类内有多少实例变量算是太大、一个函数内有多少行代码才算太长”。
就在最近,我看到某大厂的一位“代码委员会理事”在文章里说,某段代码“挺好的,长度没超过80行,逻辑比较清晰”。而在我看来,一个函数超过7行就已经是“太长”(这还是在考虑到Java语法比较啰嗦的前提下)。这就是不同实践者“自己的判断力”所体现的差异。
尽管从来没有明确指定对每个函数或类的代码行数要求,但“对象健身操”这篇文章(见于《ThoughtWorks文集》)提出的9项规则已经有非常明确的指向:
- 方法中只允许使用一级缩进;
- 不允许使用else关键字;
- 封装所有的原生类型和字符串;
- ……
在这样的规则约束下,写出一个超过10行的函数将是相当困难的(实际上在“规则6:保持实体对象简单清晰”中已经明确提出,每个类的长度不能超过50行)。
正如“对象健身操”这篇文章的作者Jeff Bay自己所说,这套“健身操”的意义在于:“在一个简单的项目里尝试一些比以前严格得多的编码标准……会迫使你更为严格地以面向对象的风格编写代码”,从而“以一种全新的方式思考你的代码”。
不过这得需要你刻意练习。正所谓“台上一分钟,台下十年功”,缺乏在受控环境下的刻意练习,很难通过工作中的自然积累提升判断力。
另外,对正确的代码构造足够熟悉,也是很重要的一个基本功,这个观点最早是Kent Beck的《实现模式》这本书中提到的。什么意思呢?
传说旧时民间古董店的学徒需要先在仓库里看真货,看得多了,见到假货时就会本能地提起警觉。对于代码也是一样:程序员需要熟悉正确的代码构造,在看到有问题的代码构造时才会本能地提起警觉。并且,“正确的代码构造”并非无穷无尽,实际上在单线程编程中,几十个常见的模式已经几乎能够完全覆盖所有场景。
Kent Beck在前言里说“这是一本关于‘如何编写别人能懂的代码’的书”,尽管他还谦虚地说这本书“不是模式书籍”,但实际上《实现模式》充分地展现了“模式”的本意:它提供了一整套“用代码表述意图”的模式语言,这套语言能让程序员在最短的时间内学会如何写出具有表现力的代码,并且自然而然地远离坏味道。
从一开始就以合理的方式编程,从而使坏味道不要出现,我想这才是负责任的程序员应该采取的工作方式。
当然,极限编程的各种实践,尤其是工程技术实践彼此紧密相关。例如自动化测试、持续集成、集体代码所有制的缺失,都会导致代码的坏味道更容易堆积。而从另一个角度来看,这些实践从任何一个切入,又都会自然地引导出其他相关的实践。
一位“知行合一”的程序员最终会发现,极限编程是唯一合理且有效的软件开发方法。最终,只有采用以可工作的软件为核心的软件开发方法,才能得到高质量的可工作的软件,这就是《敏捷宣言》第二句关于坏味道的终极答案。
郑老师说
好了,熊节老师的分享就到这里,我是郑晔。
熊节老师对于问题的分析总是这么一针见血。重构就是一个模式匹配的过程,识别出坏味道,运用对应的重构手法解决问题。坏味道是一切重构的起点,而识别坏味道不是靠个人审美,而要依赖通用的标准。
我的这个专栏就是把一些坏味道用更直接的代码形式展现在你面前,让你可以日常的工作中,不断地锻炼自己的代码嗅觉。
思考题
经过熊节老师的讲解,你是不是对重构和坏味道有了新的认识呢?欢迎在留言区分享你更新过的想法。
感谢阅读,我们下一讲再见。
- Edison Zhou 👍(29) 💬(4)
就我经历过的团队(传统企业信息中心)来看,不论是初级还是中级的同事,总是喜欢学习和研究分布式架构相关的知识点,而不喜欢阅读如《重构》、《代码整洁之道》一类的提高程序员最本质的手艺-写代码。然而,分布式的东西对我所经历的团队来说并不重要,因为实际上能用上的并不多。然而,每次Code Review总会有一些让人摸不着头脑要讲半天的代码,虽然也加了静态代码复杂度检查之类的,但是还是层出不穷。或许是这个浮躁的信息爆炸时代让架构师课程随手可得,却让人忘了自己去追求所谓的整洁代码之美,其实个人觉得一堆可维护的代码就是取悦同事取悦领导最佳的方式。作为TL,只能尽可能多培养识别坏味道的嗅觉,帮助团队成员指出坏味道并改正,先独善其身,才能有机会兼济天下,能指导和培养一个是一个。知易行难,学习的最高层次就是输出,找一两个小时,团队坐在一起Code Review也是一个输出和输入的过程,发挥集体智慧,走向重构。
2021-02-05 - NiceBen 👍(14) 💬(4)
这种极限拆分,对于很多业务代码,拆分成很多方法,更加零散一些,而且有些拆出来的方法几乎不会被复用。这样还有必要拆分么?
2021-02-04 - Geek_3b1096 👍(7) 💬(2)
专栏更贴近真实场景
2021-02-05 - adang 👍(6) 💬(1)
读书不等于掌握;掌握了不等于正在执行;执行了不等于一直在践行;识别出坏味道并践行重构的能力就像武功一样,不用就会慢慢荒废。难的不是掌握这个能力,最难的是坚持践行合一。
2021-02-04 - Jxin 👍(4) 💬(1)
喜欢郑老师的课程。每次都能get到好的新主题,进而课后可以做专项的主题学习。课程内容反而是附赠的。 本章: 1.get: 缺乏在受控环境下的刻意练习,很难通过工作中的自然积累提升判断力。 原本认知: 一直认为对象健身操过于苛刻,不适合在工作中应用,只是理想化的东西。但如果把它做为训练手段,用于提高自己对代码坏味道的觉察心,就直接豁然开朗了。下来可以时常写些小玩意,对照着苛刻下。 2.get: 《实现模式》 书已买,拜读下。
2021-02-05 - 秋天的透明雨🌧️ 👍(3) 💬(1)
学习了老师的三个专栏,收获颇多,感谢分享。请问老师,除了专栏中提到的书籍和示例代码等一些代码片段,哪里可以找到更多符合高质量代码标准的完整的工程/代码示例?感觉通过学习了专栏之后,再结合优秀优秀代码会加深理解。目前github上代码太多,没办法逐个甄别,如果老师能够帮忙推荐一些最好了,感激不尽。
2021-04-22 - LA 👍(3) 💬(1)
静态代码扫描,其实也不治根,规约然后说了,但是就是存在,还是得从意识上去唤醒这种细节点,毕竟只能要求自己,团队太难驱动
2021-02-04 - 6点无痛早起学习的和尚 👍(2) 💬(1)
这个专栏值得反复来阅读,尤其是那些坏味道的代码,值得记住,当自己 coding 的时候就多看看写的代码里是否有这些坏味道的代码
2021-04-11 - topsion 👍(2) 💬(2)
读到这章内容才知道原来:熊节是人名。我还以为是表示很厉害的章节
2021-04-07 - qinsi 👍(1) 💬(4)
这种机械和简单的模式匹配最好能交给机器去做
2021-02-04 - 晴天了 👍(0) 💬(1)
被调用者角度: 高内聚: 把属于自己职责的逻辑 收回来. 低耦合: 把不属于自己的职责, 归还给职责所属人. 请教郑大, 不知这么理解对不对.
2022-08-03 - 杯莫停 👍(0) 💬(2)
其实很多公司都不重视这些,老板只在乎你是否安计划时间完成了工作,只要能实现功能,只要能卖钱。才不管你代码质量怎么样。所以往往这个时候就需要自己对自己要求严格。后疫情时代,对互联网行业的影响很严重,导致各个公司都在缩减资源,如何保持竞争力,我想只有努力学习,精进业务了。
2022-07-02 - maxq 👍(3) 💬(2)
我觉得有时候用 if...else 结构的代码反而更易理解。 > 而在我看来,一个函数超过 7 行就已经是“太长” 语不惊人死不休啊……
2022-03-10 - 6点无痛早起学习的和尚 👍(1) 💬(0)
2023年11月24日08:30:50 来留言,最近把郑大的:代码之丑、软件设计之美、程序员的测试课、10X 关联知识一起看了,非常有感受,并且也在工作中实际起来发现坏味道,但也遇到一些困惑吧 昨晚还在跟同组同事 battle,spring 中通过构造函数注入和字段注入的选择,我表达了自己的看法:Spring 官方文档、郑大的内容、一些书都比较推荐构造函数注入(原因:可测试性、符合设计原则等等),同事持有观点:认可构造函数的优点,但手动维护构造函数有人工成本(我当时很气,我聊 A 他给我聊 B,并且告诉他,idea 自带插件快捷操作不需要人工维护成本)(就对应这些实践从任何一个切入,又都会自然地引导出其他相关的实践) 回到这个问题上,因为我就是个团队的大头兵,我只做我自己的实践推荐,不做让大家强制这样,当面临到同事这类 battle,我觉得很没意思,哈哈哈,我说我自己用构造函数,你可以用字段注入。
2023-11-24 - Geek_66158e 👍(0) 💬(0)
不寻常的结论需要不寻常的证据,talk is cheap, show me the code。希望熊节可以在github 开源出他写的任何一个超过10000行代码的项目,大家都想看看一个方法超过7行就太长的代码是什么样子的。
2023-08-10