高效重构 C++ 代码

引言

Martin Fowler的《重构:改善既有代码的设计》一书从2003年问世至今已有十几年时间了,按照计算机领域日新月异的变化速度,重构已经算是一门陈旧的技术了。但是陈旧并不代表不重要,恰恰随着演进式设计被越来越广泛的使用,重构技术已经被认为是现代软件开发中的一项必备的基本技能!所以今天在任何软件开发团队中,你都会不时听到或看到和重构相关的代码活动。然而对于这样一种被认为应该是如同“软件开发中的空气和水”一样的技术,在现实中却比比皆见对重构的错误理解和应用。首先是不知道重构使用的正确场合,总是等到代码已经腐化到积重难返的时候才想起重构;其次面对一堆的代码坏味道没有选择标准、无从下手;接下来修改代码的过程中不懂得安全、小步的重构手法,总是大刀阔斧地将代码置于危险的境地,很难再收回来;最后要么构建、测试失败后无法恢复只能推到重来,或者最终结果只是将代码从一种坏味道修改到了另一种坏味道!

总结以上问题,一部分原因是因为没有正确的理解重构,不知道重构的起点和目标,对重构的对象和目标没有衡量和比较的标准;其次是因为没有掌握形式化的重构手法和步骤,重构过程往往只是跟着感觉走;最后实践重构的过程中,没有先理顺自己的开发、构建和测试环境,导致重构成本很高! 对于开发、构建和测试环境的问题,C/C++领域尤其严重,除了没有像Java领域那么好用的自动化重构工具,很多开发人员连一个好用的IDE都找不到,更不要说普遍认知的构建速度慢,自动化测试匮乏等等问题!

本文站在作者学习和实践重构的基础上,为大家梳理重构技术,带领大家重新认识重构的目标和起点,重构手法背后的原理以及实践方式。最后介绍在实践中高效实施C/C++重构的经验、技巧和工具。

什么是重构?

重构的定义

Martin Fowler在《重构:改善既有代码的设计》一书中给出了重构的两个定义.

第一个是名词形式:

Refactoring: 对软件内部结构的一种调整,目的是在不改变软件可观察行为的前提下,提高其可理解性,降低其修改成本.

第二个是动词形式:

Refactor: 使用一系列重构手法,在不改变软件可观察行为的前提下,调整其结构.

重构的目标

重构的目标是什么? 重构的目标绝不是将代码从别人的taste改成自己的taste,也不是将代码从一种坏味道改到另一种坏味道!

Matin Fowler利用上面两个定义,指出了重构的目标:

  • 不改变软件可观察行为
  • 提高软件可理解性
  • 降低软件修改成本

而对于上述目标,我们再深入一点分析,发现其实已经有更经典的定义. 那就是Kent Beck的简单设计四原则:

  • Pass All Test: 通过全部测试;
  • No Duplication: 没有重复(DRY)
  • Reveals Intent: 程序表达意图,易于理解
  • Has no superfluous parts: 没有冗余,或者YAGNI原则

上述四条的重要程度依次降低.

到目前为止,简单设计四原则是对”什么是好的软件设计”最好的定义!

简单设计四原则第一条定义好的软件首先应该通过所有测试,即正确满足所有功能需求.而重构的目标中最基本的就是”不改变软件的可观察行为”,也就是说:

1) 重构后的软件不能破坏原来所有测试!

Matin定义的重构的其它两条目标,对应了简单设计原则的第2和第3条:

2) 重构应该消除重复: 降低软件修改成本;

3) 重构应该让程序显示表达意图: 提高软件可理解性;

最后,我们把简单设计四原则的最后一条也加入重构的目标:

4) 重构应该消除冗余:降低软件不必要的复杂度.

所以以后当我们再来讨论重构的目标,或者评判重构有没有收益的时候,就用简单设计四原则来衡量它.

从哪里开始?

对于重构的目标达成一致后,我们回到起点:什么样的软件需要重构? 以及什么时候进行重构?

对于第一个问题,由于我们重构的目标是使软件满足简单设计四原则,那么任何违反简单设计四原则的代码都应该是我们重构的目标.例如1)代码很容易出现bug,导致测试失败! 或者 2)代码存在知识重复使得不易修改! 或者 3)代码写的晦涩非常难以理解! 或者 4)代码存在过度设计,存在冗余导致复杂!

现实中可能有一堆的代码问题等待我们解决,而时间、成本、人力是有限的,所以我们需要从最有价值,最没有争议的部分开始重构. 由于简单设计四原则的重要程度是依次降低的,对于四条原则的判定从上往下也是逐渐主观化,所以我们选择重构的代码的优先级顺序也是按照它们破坏简单四原则的顺序依次降低! 如果一坨代码存在很多重复,另外一坨代码不易理解,那么我们优先选择去解决重复代码的问题,因为按照简单四原则消除重复更重要,也更容易被客观评价.

在《重构》一书中Martin为了避免引起所谓编程美学的含混争辩,总结了代码的22条坏味道. 在实践中我们一般都是从某一代码坏味道着手重构的,但是对于优先重构哪个坏味道,我们遵守上面描述的原则.

对于进行重构的时机,Matin给出:

  • 重复地做某一件事情的时候 (三次法则)
  • 添加新功能的时候
  • 修改Bug的时候
  • Code Review的时候

事实上在我的工作过程中,重构是随时随地进行的. 尤其对于采用演进式设计方法论,重构和代码开发是紧密结合难以分割的,甚至很多时候只有依托重构才能完成代码的开发.

重构的手法

明白了起点和目标,下来最重要的就是掌握完成这一过程的手段! 而重构的手法则是带领我们正确到达目标的工具.

很多人认为学习重构只要掌握背后的思想就足够了,其详细繁琐的操作手法并不重要.于是乎现实中我们看到很多人在实际操作重构的过程中章法全无,一旦开始半天停不下来,代码很多时候处于不可编译或者测试不能通过的状态,有时改的出错了很难再使代码回到初始状态,只能推倒重来! 实际上重构是一项非常实践性的技术,能够正确合理地使用重构操作,安全地,小步地,高效地完成代码修改,是评价重构能力的核心标准.

那么什么才是正确的重构手法?

Martin对重构的第二个定义中提到使用一系列的重构手法,但是对这一系列的重构手法却没有概括.

而William Opdyke在他的论文”Refactoring Objected-Oriented Frameworks”里面对重构给出了如下定义:

重构:行为保持(Behavior Preservation)的程序重建和程序变换.

在论文里面将重构手法定义为一些程序重建或者程序变换的操作,这些操作满足行为保持(Behavior Preservation)的要求. 论文里面对行为保持的定义如下:

Behavior Preservation : For the same set of input values,the resulting set of output values should be the same before and after the refactoring.

也就是说存在一系列代码变换的操作,应用这些操作之后,在相同的输入条件下,软件的输出不会发生变化. 我们把满足上述要求的代码操作称之为代码等价变换操作. 在William Opdyke的论文中针对C++提出了26种低层次的代码等价变换操作(例如: 重命名变量,为函数增加一个参数,删除一个不被引用的类…). 按照一定设计好的顺序组合上述低层次的代码等价变换操作,我们可以完成一次安全的代码重构,保证代码重构前后的行为保持要求.

这里代码等价变换的过程. 类似于初等数学中的多项式变换.例如对于如下公式变化:

每一步我们运用一次多项式等价变换公式,一步一步地对多项式进行化简,每次变换前后多项式保持等价关系.

在多项式化简的这个例子中,承载简化过程的是已经被数学证明过的多项式等价变换的公式. 同理承载重构的则是被证明过的一个个代表代码等价变换操作的重构手法.

另外,由于完成一项重构需要使用一系列的重构手法,这些手法的使用顺序也是至关重要的!

我们学习重构,就是要来学习每种场景下所使用的小步安全的重构手法及其使用顺序,并不断加以练习! 能够灵活而流畅地使用一系列重构手法完成一项重构,是衡量重构能力的一个非常重要的指标.

而本文后面的一个重点就是对常用的重构手法以及运用顺序进行提炼,降低大家的学习难度.

最后,既然重构中使用的是安全小步的代码等价变换手法,为什么我们还需要测试? 首先是因为我们是人,我们总会犯错! 另外由于编程语言的复杂性导致所谓的等价变换是受上下文约束的,例如在C++中为一个存在继承关系的类的成员方法重命名,有可能导致新的方法名和它某一父类中有默认实现的虚方法重名,而即使编译器也不能发现该错误.

高效地重构

虽然我们了解了如何/何时开始,目标,以及重构的手法,但是如果我们有了下面这些因素的辅助,会让我们更加安全和高效.

  • 覆盖良好高效的自动化测试
  • 合适的IDE,最好提供基本的自动化重构菜单
  • 良好的工程设置
  • 高效的构建环境
  • 良好的编码习惯

对于上面这些,不同语言面临的现状不同,针对C++语言我们后面会专门总结.

哪些不是重构?

针对上面的讨论,我们站在严格的重构定义上来看看下面这些反模式:

  • “我把bug重构掉了!”
  • “Debug一下刚才的重构那里出错了”
  • “昨晚重构出来的Bug到现在还没有查出来”
  • “先把代码重构好,再看测试为啥不过”
  • “我把软件架构由集中式重构成分布式了”

想想上面的场景哪里存在问题?

在实际的开发过程中,我们还经常面临另外一种场景,那就是对某一已经开发完成的软件模块进行整体重构. 在这样的过程中,虽然也存在频繁地使用重构手法对原有模块代码进行修改,但是更多的是进行大量的架构和设计方案上的修改.为了与我们要讨论的重构进行区分,对于这样的过程,我们称其为reengineering(软件重建).

软件重建一般是站在之前开发、测试的基础上,伴随着对软件要解决的问题和解决方式本身有了更深入的理解,通过修改软件把这些学习成果反映到软件的结构中去,使得软件可以更好、更精炼的解决业务问题。站在DDD(领域驱动设计)的角度,软件重建一般是对领域模型的进一步精练,使得软件更加贴合业务的本质!虽然成功的软件重建往往能对组织带来较大的收益,但是由于软件重建的开销普遍较大,而软件开发又是一项商业活动,所以需要对软件重建谨慎评估其成本收益比以及过程风险后才能决定是否启动。而本文中的重构技术,则只是一项日常编码中频繁使用的安全、高效的代码修改技术,被普遍认为是现代软件开发技术中必备的一项基本技能,是演进式软件设计或者软件重建目标达成的一项必要手段!

关于本文

我们总结一下,重构有三个要点,见下图:

  1. 你要有一个敏感的鼻子,能够嗅出代码中的坏味道; 一般只要发现不符合简单设计四原则的Code,就是我们需要重构的目标对象. 而Martin总结的22条代码坏味道给我们一个很好的实践起点.
  2. 你要知道重构的目标,就是让代码逐渐靠近简单设计四原则.
  3. 需要掌握小的安全的重构手法,以及在不同场景下合理的使用顺序,以便安全高效地承载重构目标的达成.

由于重构手法和实施顺序是学习重构的关键,所以本文后面会重点讲述这个主题. 另外,在实践中如何高效和安全的进行重构,和具体使用的编程语言及其开发、构建、测试环境关系也很密切.本文最后会针对C++语言总结这方面相关问题.

时间: 2024-08-11 23:45:02

高效重构 C++ 代码的相关文章

高效的CSS代码(2)

——阅读笔记,欢迎纠错^_^ 内容比较零散..... 1.让浮动元素的父容器根据元素的高度而自适应高度的方法: <div class="clearfix"><div class="fl"></div></div> /*意思就是最外面的一层<div></div>是父容器,给他加上清除浮动的功能,clearfix的代码再高效的CSS代码(1)中有*/ 2.再写代码前不是要先分析一下页面的模块吗,模块

Node.js(十三)——Promise重构爬虫代码

在重构代码之前,先要了解下什么是https? https协议:基于ssl/tls的http协议,所有的数据都是在 ssl/tls协议的封装之上传输的,也就是说https协议是在http协议基础上 添加了ssl/tls握手以及数据加密传输,因此这就是两者之间最大的区别. https模块专门处理加密访问的,区别在于搭建https服务器的时候需要有ssl证书. 模拟搭建https服务器 var https = require('https') var fs = require('fs')//文件系统模

如何编写高效的Android代码

时间是很宝贵的东西,在编写Android代码的时候尽可能的编写出高效的Android代码可以节省你很多的时间,让你有时间去泡妞.去陪女朋友.去陪基友,去吃饭.去娱乐.去睡觉!毕竟,谁都不想整夜整夜的加班,然后还乱吼:时间都去哪了?!下面就整理了怎样提高效率,编写高效的Android代码!看到这,偷笑了吧,表掩饰,我已看到! 对于如何判断一个系统的不合理,这里有两个基本的原则: 一.不要做不必要做的事情. 二.尽可能的节省内存的使用优化链接: http://c.tieba.baidu.com/p/

AIDE支持实时错误检查、代码重构、代码智能导航、生成APK

AIDE是一个Android Java集成开发环境,可以在Android系统内进行Android软件和游戏的开发.它不仅仅是一个编辑器,而是支持编写-编译-调试运行整个周期,开发人员可以在Android手机或者平板机上创建新的项目,借助功能丰富的编辑器进行代码编写,支持实时错误检查.代码重构.代码智能导航.生成APK,然后直接安装进行测试.

编写高效的Android代码

为什么在这就意味着没有多少剩余空间给你去浪费了,因此,在你写Androi编写Android程序时要时刻考虑执行的效率,这些系统不是想象中的那么快,并且你还要考虑它电池的续航能力.写程序的时候,要尽可能的使你的代码优化而提高效率. 对于如何判断一个系统的不合理,这里有两个基本的原则: 1.不要做不必要做的事情. 2.尽可能的节省内存的使用. 下面是常用的几点优化建议: 1.尽可能避免创建对象(Object) 因为对象的创建并不是没有代价的,如果你在一个用户界面的循环中分配一个对象,你不得不强制的进

利用|,&amp;,^,~,&lt;&lt;,&gt;&gt;&gt;写出高效艺术的代码

简介: 大家在阅读源码的时候经常会看到一些比如下面这样特别难理解的代码. cancelEvent.setAction(MotionEvent.ACTION_CANCEL | (motionEvent.getActionIndex() << MotionEvent.ACTION_POINTER_INDEX_SHIFT)); order = ((order) >> (INDEX_OFFSET -1) + 1) << INDEX_OFFSET; 类似与这种"高大上&

重构改善代码的既有设计

最近在学习重构改善代码的即有设计,虽然在平时的工作学习中有尝试进行重构,但没有清晰的思路往往就是随性而为,以个人的编码风格为准,我们往往知道这样会更好,但是面对编程风格的挑战时,我们往往拿不出准确专业的理论去说服别人遵循这项准则,而我们的想法最终也无疾而终,还是沦落成为个人英雄主义. 此博客不是为了阐述,仅因为还未完全熟练,需时时查看,但是在工作中往往书不在身边,无法翻阅时作为参考: 重新组织函数: Extract Method(提炼函数) Inline Method(内连函数) Inline

A1128 | 逻辑想象能力、简洁高效美观的代码、memset的使用情景

写了三遍才AC,这真是对智商极大的侮辱 C++代码: #include <stdio.h> #include <memory.h> #include <math.h> #include <string> #include <vector> #include <set> #include <stack> #include <queue> #include <algorithm> #include &l

JAVAEE——BOS物流项目05:OCUpload、POI、pinyin4J、重构分页代码、分区添加、combobox

1 学习计划 1.实现区域导入功能 n OCUpload一键上传插件使用 n 将文件上传到Action n POI简介 n 使用POI解析Excel文件 n 完成数据库操作 n 使用pinyin4J生成简码和城市编码 2.区域分页查询 n 页面调整 n 服务端实现 3.重构分页代码 n BaseAction n 子类Action 4.分区添加功能 n 什么是分区 n 页面调整(combobox使用) n 服务端实现 2 实现区域导入功能 2.1 jquery OCUpload一键上传插件使用 O