高效重构 (三):构建高效工程环境

高效实施重构

当我们熟练掌握了重构技术后,还不能就此说自己在实践中已经可以安全而高效地实施重构了! 因为落到真正的工程实践环境,安全和高效的重构过程还需要好用的IDE工具,成熟的自动化测试套件,快速高效的开发构建环境,以及良好的编码习惯来支撑.
本节以C/C++语言为例,总结了一些和高效重构相关的语法及工程实践方面的关注点,这些点同样可以供别的编程语言参考。

语言相关

不同编程语言的设计和发展历程不同,导致语法层面有些方面对重构非常友好,但也都有不利于开展重构的语法特性。例如C++,由于它语法相对复杂,导致业界对C++的自动化重构工具支持一直非常不理想,很多重构到目前为止只能手工进行。重构工具对编程语言的支持,其中一个重要的点在于对代码引用点分析查找的可靠性。 C++中有很多语法会干扰到这一分析,导致难以进行自动化重构.

C++中会让重构难以进行的语法包含以下:

  • 指针
  • 函数重载
  • 操作符重载
  • 默认参数
  • 类型强转
  • 隐式转换
  • 友元
  • 模板

还有一些不合理的编码规范,也会阻碍到重构的进行,例如

  • 匈牙利命名
  • 函数单一出口
  • 大而全的common头文件
  • ...

上述语法或者规范不仅会让自动化重构难以进行,也会干扰到手动重构. 对于不好的编码规范要及时摒弃! 对于会妨碍重构的语言元素要慎重使用,最好保持其影响在一定范围内,缩小重构时查找引用不确定性的范围.

当然C++语言中也有一些语法非常有利于重构,例如:

  • const
  • 访问限制修饰符privateprotected
  • 匿名namespace
  • 局部变量延迟定义

这些语言元素有些会让代码语义更明确,有些会限制代码元素的可见度,缩小引用查找范围. 对于这些语言元素在开发中尽可能多的去使用,可以让重构变得更加容易.

最后,保持好的编码习惯,例如一开始就不要将文件\类\函数搞得过大,尽可能多的对外隐藏细节,对复杂语法的使用保持慎重态度,这样每次重构的成本都会小很多.

自动化测试

重构的安全性依赖于快速的自验证测试用例。基本每种编程语言都有对应的xUnit测试套件,可以对软件进行自动化测试用例的开发和执行.

针对C++,可供选择的xUnit套件有GtestTestngppCppUnitCXXTest以及Boost Test等。目前最多人使用的应该是Gtest,由于使用广泛所以非常成熟,支持跨平台,方便简单容易上手,提供了丰富的断言机制和成熟的用例管理. 当然Gtest也有其问题,主要是测试注册机制不够优雅导致限制过多,测试用例运行期不能隔离导致互相影响,测试工程的组织不能模块化导致单元测试不能显示化物理依赖,无法低成本完成测试依赖文件的物理替换. 而TestngppGtest一样跨平台,断言丰富,用例管理成熟.。它最大的强大之处是支持沙盒模式,测试用例运行期间互相隔离,不会彼此影响. 另外,Testngpp支持模块化管理测试工程,支持方便的对测试依赖文件进行物理替换,这一特性对于没有虚接口情况下的mock非常实用. 但是由于Testngpp的强大,也导致了它某些情况下不如Gtest方便使用,受众相对小,文档也不全面。

在使用xUnit的同时再能结合一款合适的Mock框架,可以大大降低单元测试的成本。对于C++目前最好用Mock框架是Mockcpp。事实上mock技术在测试中经常容易被滥用,导致编写过多的面向行为的脆弱测试用例. 其实在测试中直接打桩进行状态验证成本并不高,所以现今我已经很少使用mock工具了。但是学会一款mock工具留着在适合的时候偶尔使用一下也是不错的.

关于C/C++语言中如何做好自动化测试以及挑选合适的测试框架,可以参考这篇文章:"C/C++如何做好单元测试"

最后,我们都知道重构需要自动化测试的支持! 但是并不是没有自动化测试,重构就不能开展! 有些历史遗留代码,一开始很难加进去测试,必须要先着手修改,才能逐渐添加测试. 这时起步需要的就是对安全小步的重构手法的熟练掌握,以及利用原有手工测试进行安全保证. 当对软件进行修改从而可以添加自动化测试后,重构慢慢就上轨道了.

IDE

为了提高重构效率,我们需要一款智能的IDE。它需要支持基本的自动化重构,能够高效准确的搜索到代码的引用点,支持良好的面向对象风格代码浏览,高效流畅地快捷键...

对于Java来说无论eclipse或者IntelliJ IDEA都相当不错,而对于类似C/C++这样相对复杂的静态语言,或者动态脚本语言,都相对难找到一款高效好用的全面支持自动化重构的IDE。

对于C++ IDE,这里做以下对比和推荐:

  • Visual studio C++
    自身的重构工具并不完善,但是加上番茄小助手就很不错了.支持自动重命名,自动提取函数,重命名类还会自动把类的头文件和所有的#include点的文件名都做一修改.最大的缺点是不能跨平台,而且耗费机器资源较多。据说从Visual studio 2015以后已经内置了很多重构功能,不过本人没有试过.
  • clion
    Jetbrains公司出品的跨平台C/C++ IDE,号称要做最强大的C++ IDE. 目前是收费的,免费的只能试用30天。工程构建只支持CMake,另外只能在64位机器上使用。 Clion内置的自动化重构菜单是最强大(支持rename,inline,extract,Pull member up/down ...)。本人试用过1.0版本,比较耗性能,当时用户也还不是特别多。
  • eclipse-cdt
    个人使用频率比较高的C++ IDE。 最大的好处是支持跨平台. 用eclipse在类的继承引用关系之间跳转非常流畅.。支持简单的重命名,提炼函数,提炼常量等自动化重构.。从luna版本开始支持重命名文件或者移动文件的时候会自动替换所有#include中的路径名. 个人目前最满意的C++ IDE.

关于如何为C/C++语言挑选好用的IDE,具体可以参考"C++ IDE口味谈""Effective Eclipse CDT"系列文章。

物理重构

物理设计往往容易被人们忽视,但其实它对软件的易理解性和可维护性也非常重要! 好的物理设计不仅可以减少物理依赖,还会有利于软件的构建和发布。所以程序员除了掌握一般的代码元素级别的重构,还需要熟悉常见的物理级别的重构,包括文件重命名,文件提炼,文件移动,或者目录结构调整等等。对于C/C++语言,物理重构比较繁琐的地方在于经常需要同步修改makefile或者头文件的#include引用点。

以下以C/C++举例,看看一些和物理重构相关的经验技巧(make相关的见下节):

  • 每个文件只包含一个类,并且文件名和类名相同. 这样方便重命名类的时候IDE自动对文件进行重命名.
  • 利用IDE提供的自动化重命名或者移动菜单进行文件/目录的重命名或者移动,以便IDE可以对涉及文件的所有#include引用点进行路径自动替换.
  • 使用IDE提供的自动化头文件添加功能来添加物理依赖,例如在eclipse中是ctrl+shift+n
  • 配置IDE的头文件模板,让自动生成头文件的Include Guard使用Unique Identifier,避免每次头文件重命名后还得要修改Include Guard(见下). (最新的eclipse mars版本中在Windows -> Preferences -> C/C++ -> Code Style -> Name Style -> Code -> Include Guard 中选择 Unique Identifier,较老版本需要在workspace中修改配置文件,可以自行google修改办法).
    // Runtime.h
    #ifndef HDEA41619_5212_4A92_8A09_3989000E6BAE
    #define HDEA41619_5212_4A92_8A09_3989000E6BAE
    
    struct Runtime
    {
        void run();
    };
    
    #endif
    

编译构建

不好的编译构建工程不仅干扰到物理重构,更重要的可能导致编译效率底下从而让重构无法正常开展. 没人愿意在每次构建都要几十分钟到数小时的工程上进行频繁重构. 尤其是对于大型的C/C++工程,需要对其编译构建过程进行持续地分析优化,不断提高版本的编译构建速度。

以下是一些针对C/C++工程的编译构建实践经验:

  • makefile中尽可能使用模式规则.自动搜索文件.不要显示使用源文件名,否则每次重命名文件后都得要修改makefile.
  • makefile中为预编译目标文件设置规则,不仅有利于解决一些宏展开的问题,更重要的可以快速解决一些头文件包含上的编译难题.
    # makefile example
    ...
    SRCS += $(abspath($(shell find $(SOURCE_HOME) -name "*.cpp")))
    OBJS = $(subst $(SOURCE_HOME),$(TARGET_HOME),$(SRCS:.cpp=.o))
    ...
    $(TARGET_HOME)%.i : $(SOURCE_HOME)%.cpp
        $(CXX) -E -o $@ -c $<
    $(TARGET_HOME)%.o : $(SOURCE_HOME)%.cpp
        $(CXX) -o $@ -c $<
    $(TARGET):$(OBJS)
        @$(generate-cmd)
    
  • makefile采用自底向上组织,保证每个源代码文件单独可编译,每个模块单独可编译. 最终产品版本的构建调用每个模块的makefile生成编译中间产物后进行链接. 不要采用自顶向下传递make参数的makefile工程管理模式,否则每次编译任何一个文件或者模块都要全编译所有代码.
  • 尽可能使用并行编译. 在Visual Studio下可以使用IncrediBuild分布式编译工具. 对于make使用-j选项,并且可以使用distcc来进行分布式编译.另外可以使用ccache来做缓存加速编译。
  • 将测试工程的编译构建和真实产品版本的构建分离,测试工程的编译构建可以采用更好的工具:例如cmake,rake等.
  • 保证增量编译可用,并且是可靠的.

很多项目虽然有增量编译,但是都不够可靠,尤其是当有头文件删除的时候! 另外每次无论是否有依赖变化,makefile都要重新生成加载.d文件,效率也很低下. 所以大多数时候增量编译功能是关闭的!
《GNU Make项目管理》一书中提供了很多大型工程中make组织的优秀实践,其中有一段对增量编译可靠高效的makefile片段,我将其提炼成了一段make函数,见下面,大家可以使用.

# make_depend.mak
# this file implement the makefile function for dependencies rules

# $(call make-depend,source_file,object-file,depend-file)
define make-depend
    mkdir -p $(dir $3);
    $(CC) $(CFLAGS) $(CPPFLAGS) -MM $(INCLUDE) $(TARGET_ARCH) $1 > $3.tmp1;
    $(SED) 's,\($(notdir $*)\)\.o[ :]*,$2 $3 : ,g' < $3.tmp1 > $3.tmp
    $(SED)  -e 's/#.*//'                                           \
    -e 's/^[^:]*: *//'                                             \
    -e 's/ *\\$$//'                                                \
    -e '/^$$/ d'                                                   \
    -e 's/$$/ :/' $3.tmp >> $3.tmp
    $(MV) $3.tmp $3
    $(RM) $3.tmp1
endef

# should use the make-depend as follow
# %.o: %.c
#   $(call make-depend,$<,$@,$(subst .o,.d,$@))
#   $(CC) -o $@ $<

后记

作为一名软件技术咨询师,过去些年指导过许多大型软件系统的重构工作。 这篇文章基本上是自己多年工作实践的一些心得,包括对如何高效实施重构的一些精炼和总结。

对于大型的软件重建,无论你的目标架构多么漂亮,最终还必须是一行一行的代码修改。如何安全可靠并且高效地修改代码,必然是落地的基本技能!

对于日常开发也是如此, 保持代码符合简单设计是一项日常行为,重构是达成它的唯一方式. 对重构的使用应该融入到代码开发的每时每刻,到最后不必强行区分是在开发还是重构,就像《重构》的序言中所说"变得像空气和水一样普通". 希望每个学习重构的同学都能体会到这种感觉!

千里之行,始于足下!


高效重构(一):正确理解重构
高效重构(二):掌握重构手法

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

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,749评论 25 707
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,629评论 18 139
  • 如何实施重构 稍微复杂的重构过程,都是由一系列的基本重构手法组成. 《重构》一书中针对各种重构场景,给出了大量的重...
    MagicBowen阅读 3,971评论 0 3
  • Spring Boot 参考指南 介绍 转载自:https://www.gitbook.com/book/qbgb...
    毛宇鹏阅读 46,773评论 6 342
  • 天色已晚,秋风萧瑟。 一个五六岁的女孩躺在床上,睁着眼,直直的看着上方,一动不动。 这就是我,百里家的三小姐,百里...
    木子北阅读 255评论 0 0