{技术}代码的坏味道——你们的项目有这些问题吗?

232 views
Skip to first unread message

missdeer

unread,
Sep 21, 2009, 8:07:44 PM9/21/09
to TopLanguage
  最近一直在学习Martin Fowler的《重构》,并且对照我参与的一个已经投入至少15人年,历时3年,约20万行,目前仍然在继续开发维护
的项目,让我觉得触目惊心,其中的代码,到处充斥着Martin Fowler所谓的坏味道,而又困惑重重,不知道别的项目代码质量是如何的。
  下面就都只是随便举一下项目中的实际情况为例,项目是用MFC开发,使用了Codejock的Xtreme Toolkit Pro界面扩展库。
  重复代码。有3处计算MD5的实现,分别由3个开发人员完成,大概实在是这种实现的代码在网上太容易找到了。另外有一个特性,可以与另一个服务进行
文件的上传、下载、更新、同步,而文件因为类型不同,做这些操作时某些细节有细小的差别,但实现中却是为每一类文件具体而完整都实现了一遍这些操作。
  过长的函数。有的开发人员就是习惯性地写出长函数。整个项目中,圈复杂度超过100的有4个函数,超过20的不知道是几十还是上百个。
  过大的类。有一个类的cpp文件,是18000行,另外有一个类的cpp文件是10000行。还有CMainFrame类的cpp文件,用
Source Insight打开后,在列出函数列表的窗口中显示“Too complex to parse”。
  过长的函数列表。有一个cpp文件中共9个函数实现,每个函数的参数都超过7个,而且含义晦涩,自从原创人员两年前离职后,没人敢去动那块代码。
  发散式变化。前面提到的一个18000行的cpp文件,是一个视图的实现。如果要给该视图的右键菜单中新增加一个菜单项,并进行响应,需要修改不知
多少个函数,记得曾经有个开发人员,花了一周时间都在为了一个新增的菜单项。添加代码没花多少时间,时间花在添加后,因此引发的问题上。
  霰弹式修改。有两个模块都需要一个高亮显示语法关键字的编辑功能。有一个基本的控件封装类,但要修改一些代码时,总是要很小心地去从头检查一遍另一
模块的实现是否受影响。我的理解是,这个控件封装类的抽象不够通用,或者两个模块的相似度并不高。
  基本型别偏执。这样的代码在项目中不好找,不过有类似的。项目中使用MSXML操作xml数据,在各个模块的实现中,都直接聚合了一堆MSXML的
接口指针,操作xml的方法,和业务逻辑、界面响应完全混合在一起。
  Switch结构。很多处又大又长的switch结构。
  冗余累赘类。有两个(派生)类过于考虑以后的扩展性,而那种扩展性的需求至少在未来2、3年内是遇不上的。
  夸夸其谈未来性。有一个快捷键处理模块,从项目刚开始就已经实现完成,但后来一直没被用过。项目没有开始实际编码前,超过5个人,花了2个月制订了
各个模块需要暴露的COM接口,结果到现在3年了,真正实现的接口也才10个左右。
  中间转手人。CMainFrame类已经成了各个模块用来转发消息的场所。一个重要的原因是界面与业务逻辑耦合,很多业务处理需要
MainFrame转发到相应的界面实现类中进行处理。
  狎昵关系。无论是各个Pane还是MDIClient,都与CMainFrame存在着这种双向依赖关系。
  异曲同工的类。两个有交互的模块,居然各自定义了一组数据结构,用来描述现实世界中的同一种事物,中间又由CMainFrame来完成这两组数组结
构之间的转换。
  纯数据的类。很多时候,为了向线程函数传递一些数据(超过一个DWORD的量),就专门定义一个纯数据的类。
  被拒绝的遗赠。两个平行的模块,一个类是从另一个类继承过来的,而明明有很多那被继承的类的功能,在派生类中是不需要的。呃,被继承的就是那个
18000行的类。另外还有那两个需要编辑功能的模块,曾经居然也是一个类从另一个类直接继承,导致在派生类中变成不需要什么功能,就加些代码,把那部
分功能屏蔽掉。
  过多的注释,有一个开发人员,喜欢在自己编写的函数开头部分写上几十行注释,呃,全是算法描述和伪代码。
  在公司4年,我参与过的略有规模的项目,除了这个外,另外有一个,基本是独自一人完成,代码量最高峰是7万行,后来路过不断的重构,在仍然有新特性
增加的前提下,代码量缩减到4万多,现在回头看来,这个项目中代码的坏味道似乎少一些,但质量却也不行,崩溃经常发生,其他业务逻辑有问题的也不少。
  所以,我就很是困惑啊,别人的项目是怎么样的情况?

sagasw

unread,
Sep 21, 2009, 8:18:35 PM9/21/09
to pon...@googlegroups.com
都有类似的问题,哪怕是大师,也不是一下子就成为了大师,而是有个渐进的过程。

最重要的一点建议:

如果你觉得应该做,那么就从现在开始一点点的进行重构,看书学习只是开始,最重要的部分是动手。

另外不知道你们这个项目有没有自动测试和单元测试,这些都是开发人员的好帮手,不花费测试组多少时间又能检验修改质量。

2009/9/22 missdeer <miss...@gmail.com>

sagasw

unread,
Sep 21, 2009, 8:30:19 PM9/21/09
to pon...@googlegroups.com
对于这种代码我有一些办法也许你可以试试:

1,长函数分解,这种长函数往往是内部有重复代码或者功能很独立的代码,可以抽取或者合并成为一个新函数,小心一点做不会有什么问题。

2,函数参数合并,不需要修改太多,只要新建一个结构就可以,把那些参数都放进去。开始阶段先不要对函数个数、序列进行增减,只要让代码看着整洁就好,也是小心点做不会有什么问题。

3,新旧逻辑对比,某个比较独立的逻辑块可以把接口(不是interface,而是调用的entry)部分分成两块,先调用旧代码,然后调用你修改的代码,然后确保两块代码的返回值、修改值都是一样的,当然这是在这段代码不考虑性能问题的前提下。这是用旧代码检验新代码的一种方法,没有unit test的时候可以用它。

4,一直使用version control,保证可以时刻回溯代码修改。然后删掉不必要的函数、变量、参数、注释。

5,最为重要的一点,在修改之前应该征得你的team leader的同意和支持,否则出力不讨好。如果有比较熟悉代码而且愿意做代码重构的老同事,那就更好了,可以跟他们一起动手。


-------------------------------------------
孙秀楠宝宝博客 http://sunxiunan.com
-------------------------------------------

2009/9/22 sagasw <sag...@gmail.com>

Tiny fool

unread,
Sep 21, 2009, 8:40:16 PM9/21/09
to pon...@googlegroups.com
像极了我当年去XXX遇到的那个程序,呵呵


当然我们的重构是把项目重新设计了一遍,而不是一般意义的代码级的重构。因为我们很多工程问题是由于要给略有不同的同类硬件产品设计PC客户端造成的。以前的实现方式是,一个新硬件产品,就从老的PC客户端代码改起,最后有无数的差异不大的PC客户端代码,里面充斥着各种谁也不知道的宏定义。新的实现方式是,把所有产品共有的界面逻辑和数据模型,放在一个exe里面,为每一款新产品设计一个dll插件,升级和新产品的主要工作就是设计和分发新的dll插件。

2009/9/22 missdeer <miss...@gmail.com>



--
Tinyfool的开发日记 http://www.tinydust.net/dev/

sagasw

unread,
Sep 21, 2009, 9:00:53 PM9/21/09
to pon...@googlegroups.com
Tiny的情况应该和作者的不太一样,关键问题不是技术,而是人。

Tiny写的重构里面如何设计如何实现都是完全掌控的,客户方只是提出商业需求没有技术限制。

但是我看作者的描述,他在项目中应该只是一个程序员的身份,如何影响别人注意代码味道的问题,如何影响管理层让他们感觉到代码味道带来的后期维护代价,是比较有难度的。我的意见是不要改革要改良,针对他自己写的feature代码力争清晰简洁,修改其他bug时候相应的进行小范围代码重构,这样才能把风险降到最低。

代码坏味很难看,但是为了修改搞到自己饭碗危险那就不值了。

2009/9/22 Tiny fool <tiny...@gmail.com>

yzzrn

unread,
Sep 21, 2009, 9:13:37 PM9/21/09
to TopLanguage
同臭,慢慢改,每天改一点,让生活更美好.

On 9月22日, 上午8时07分, missdeer <missd...@gmail.com> wrote:

Tiny fool

unread,
Sep 21, 2009, 9:14:22 PM9/21/09
to pon...@googlegroups.com
呵呵,恰恰相反,我跟作者的情况是完全一样的。遇到那个重构问题的时候,我是刚到那家公司的新程序员,普通程序员,连项目经理都不是,呵呵。

产品用户一直呼唤这么一个产品,其实我们部门也想过做这么一个产品,但是一直以来只是一个想法而已。

我到了那个公司的时候,跟我的boss一起,把这些需求整理,直到构建团队,我们把两个硬件团队的人也抽调到了这个team里来,这是这个部门一直也没有做过的。程序开发总共7个人,两个做插件,或者叫驱动,一个人做主界面,我做整体架构和产品设计,一个人做所有的原生程序的数据解析,等等。

这些都是建立在无数的沟通之下的,等着大boss自上而下的布置你应该重构了,呵呵,这简直是天方夜谭。

2009/9/22 sagasw <sag...@gmail.com>



--
Tinyfool的开发日记 http://www.tinydust.net/dev/

Shuo Chen

unread,
Sep 21, 2009, 9:25:41 PM9/21/09
to TopLanguage
100行的函数不算长吧?20行更算是短函数了。
特意把函数写短不是难事,无非是把参数传来传去,或者把状态保存在成员变量(甚至全局变量)里。这样每个函数都能写到5行以下,但是这就是我们的目的
吗?
短的函数,就其个体而言,确实比较容易理解。不过就整体程序而言,就不一定比长函数更容易理解了。因为读起来跳来跳去的,还得记住调用栈和上下文,不见
得比读长函数省劲。
我觉得200行以下的函数都可以接受,我们项目里最长的函数大概150行。

肖海彤

unread,
Sep 21, 2009, 10:48:38 PM9/21/09
to TopLanguage
这个世界上, 好的代码和差的代码都有, 但是只要可以实现其经济价值, 并且在用户手里可以正常稳定工作, 就是好软件.

如果你能将这20万行代码重构成结构良好的代码, 比起重新做出来20万行结构好的代码, 对你的锻炼会更大 :)

我最近重构了 12万行代码 C/C++ (上面跑着25万行脚本), 其中部分代码良好, 部分代码较差, 现在重构基本做完了, 觉得还是挺挑战
的: 要在无法写出单元测试, 覆盖测试代码的基础上, 分离耦合, 提取模块, 同时让上面的脚本一直保持可以正确运行.

Tiny fool

unread,
Sep 21, 2009, 10:55:40 PM9/21/09
to pon...@googlegroups.com
无法写出单元测试的情况下重构,确实可以表现你的功底,但是为了质量还是最好在单元测试的前提下重构吧

2009/9/22 肖海彤 <red...@gmail.com>



--
Tinyfool的开发日记 http://www.tinydust.net/dev/

jinhu wang

unread,
Sep 21, 2009, 11:00:16 PM9/21/09
to pon...@googlegroups.com
是啊,我觉得重构一行代码都有可能带来风险,一下重构12w行,这魄力!!


 
2009/9/22 Tiny fool <tiny...@gmail.com>

肖海彤

unread,
Sep 21, 2009, 11:36:08 PM9/21/09
to TopLanguage
问题是, 原来有一半的代码是通过全局变量来耦合的, 这些 .cpp 几乎引用所有的 .h; 完成同样的功能的代码, 每个地方各写一份; 超
过 800行的函数有不少个, 我是想不出可以怎么写测试代码了 :)

我只能当所有的 .cpp 是一个单元, 所有的上层脚本是我的测试代码了.

On Sep 22, 10:55 am, Tiny fool <tinyf...@gmail.com> wrote:
> 无法写出单元测试的情况下重构,确实可以表现你的功底,但是为了质量还是最好在单元测试的前提下重构吧

up duan

unread,
Sep 21, 2009, 11:50:25 PM9/21/09
to pon...@googlegroups.com
就我的经验,简简单单的rename重构,就能极大地改善很多代码的质量,尤其是有一个良好的命名规范的时候。而且风险奇小。
重复代码的合并风险还是挺大的,反倒是长函数分拆风险很小。
取消全局变量一般来说都是个大动作,很有可能导致内部接口连锁式修改。

2009/9/22 肖海彤 <red...@gmail.com>

肖海彤

unread,
Sep 22, 2009, 12:07:08 AM9/22/09
to TopLanguage
除了rename 重构在这个项目中远远不够之外, 其他几个方面, 我都同意你的看法.

这个项目中, 面临的问题是, 程序结构太复杂, 已经理解不了了, 不但新功能很难加, 旧 bug 也很难调试了. 所以不得不大动干戈.

我的方法是分步走, 全局变量先弄成类静态变量, ok 了, 再转成普通成员变量, ok 了, 再看看要不要加访问控制. 改一点就编译跑一
跑, 好在现在的 cpu 比前几年快不少.

up duan

unread,
Sep 22, 2009, 12:23:50 AM9/22/09
to pon...@googlegroups.com


2009/9/22 肖海彤 <red...@gmail.com>

除了rename 重构在这个项目中远远不够之外, 其他几个方面, 我都同意你的看法.

这个项目中, 面临的问题是, 程序结构太复杂, 已经理解不了了, 不但新功能很难加, 旧 bug 也很难调试了. 所以不得不大动干戈.

我的方法是分步走, 全局变量先弄成类静态变量, ok 了, 再转成普通成员变量,  ok 了, 再看看要不要加访问控制.  改一点就编译跑一
跑, 好在现在的 cpu  比前几年快不少.


呵呵,主要是我不太适合复杂的场合【可以认为是脑容量有点小:)】,所以我觉得rename以后,能极大的提升我的理解复杂方案的能力,因为改的都是我自己的风格,对我来说统一、一致。比如:Pre* Post* Begin* End* Start* Stop* Open* Close* Active* Deactive* Parse ToString Draw ……等,让我能够花费很小的脑力就能知道它在干什么。
是的,大多数情况下,单纯的rename是不够的,但是我认为单纯的rename已经能解决很多问题了,更何况我还有金山词霸在手边帮我找一个自认为最且贴的名字:)。
嗯,对我来说,重构也是一种帮助我理解原有代码的手段,一段代码被我重构了以后,我也就大致理解了它的结构。
最痛苦的事情莫过于C++的重构,很难有比较好的工具支持【顺便鼓吹一下SlickEdit,它支持C++重构】,而没有工具支持,纯手工重构不但效率低,还容易出错。据说由于C++的语法太丑陋和难于解析了,所以很难写出一个C++的重构工具。

肖海彤

unread,
Sep 22, 2009, 12:32:26 AM9/22/09
to TopLanguage
> 最痛苦的事情莫过于C++的重构,很难有比较好的工具支持【顺便鼓吹一下SlickEdit,它支持C++重构】,而没有工具支持,纯手工重构不但效率低,还容易出错。据说由于C++的语法太丑陋和难于解析了,所以很难写出一个C++的重构工具。

我用 eclipse, 它的 c++ refactor 能力, 有一个是可靠的, 就是函数参数和变量的改名, 其余的功能都不可靠 :)

SlickEdit 的功能可靠吗 ?

up duan

unread,
Sep 22, 2009, 2:15:30 AM9/22/09
to pon...@googlegroups.com


2009/9/22 肖海彤 <red...@gmail.com>

> 最痛苦的事情莫过于C++的重构,很难有比较好的工具支持【顺便鼓吹一下SlickEdit,它支持C++重构】,而没有工具支持,纯手工重构不但效率低,还容易出错。据说由于C++的语法太丑陋和难于解析了,所以很难写出一个C++的重构工具。

我用 eclipse, 它的 c++ refactor 能力, 有一个是可靠的, 就是函数参数和变量的改名, 其余的功能都不可靠 :)

SlickEdit 的功能可靠吗 ?

SlickEdit也是改名可靠,别的用的少,不过到没有发现啥错误。会给一个预览界面让你看看重构是否正常。

肖海彤

unread,
Sep 22, 2009, 2:40:31 AM9/22/09
to TopLanguage
用 Eclipse 的话, 如果程序中有几个局部类是重名的, 改名的话, 有可能会一起都改掉. 当然, 它会说可能有冲突, 也有
review, 但毕竟是不可靠.

>
> SlickEdit也是改名可靠,别的用的少,不过到没有发现啥错误。会给一个预览界面让你看看重构是否正常。

missdeer

unread,
Sep 22, 2009, 6:26:10 AM9/22/09
to TopLanguage
几乎没有自动测试和单元测试......

missdeer

unread,
Sep 22, 2009, 6:29:00 AM9/22/09
to TopLanguage
重构的手法在《重构》中也学了一些
SVN倒是一直用着,偶尔确实有那么一点点作用,可以回溯到出问题前的一个版本
PL倒是一直说要重构要重构,说了一年了,结果是有几个模块重写了-_-b

On Sep 22, 8:30 am, sagasw <sag...@gmail.com> wrote:
> 对于这种代码我有一些办法也许你可以试试:
>
> 1,长函数分解,这种长函数往往是内部有重复代码或者功能很独立的代码,可以抽取或者合并成为一个新函数,小心一点做不会有什么问题。
>
> 2,函数参数合并,不需要修改太多,只要新建一个结构就可以,把那些参数都放进去。开始阶段先不要对函数个数、序列进行增减,只要让代码看着整洁就好,也是小心点做不会有什么问题。
>
> 3,新旧逻辑对比,某个比较独立的逻辑块可以把接口(不是interface,而是调用的entry)部分分成两块,先调用旧代码,然后调用你修改的代码,然后确保两块代码的返回值、修改值都是一样的,当然这是在这段代码不考虑性能问题的前提下。这是用旧代码检验新代码的一种方法,没有unit
> test的时候可以用它。
>
> 4,一直使用version control,保证可以时刻回溯代码修改。然后删掉不必要的函数、变量、参数、注释。
>
> 5,最为重要的一点,在修改之前应该征得你的team
> leader的同意和支持,否则出力不讨好。如果有比较熟悉代码而且愿意做代码重构的老同事,那就更好了,可以跟他们一起动手。
>
> -------------------------------------------

> 孙秀楠宝宝博客http://sunxiunan.com


> -------------------------------------------
>
> 2009/9/22 sagasw <sag...@gmail.com>
>
> > 都有类似的问题,哪怕是大师,也不是一下子就成为了大师,而是有个渐进的过程。
>
> > 最重要的一点建议:
>
> > 如果你觉得应该做,那么就从现在开始一点点的进行重构,看书学习只是开始,最重要的部分是动手。
>
> > 另外不知道你们这个项目有没有自动测试和单元测试,这些都是开发人员的好帮手,不花费测试组多少时间又能检验修改质量。
>

> > 2009/9/22 missdeer <missd...@gmail.com>

missdeer

unread,
Sep 22, 2009, 6:31:05 AM9/22/09
to TopLanguage
呃,我们这的情况是,PL赞成要重构,普通开发人员对重构的了解不多,基本上停留在"重构≈重写"的认识上,于是PM就反对所谓的"重构",怕把程序弄
得更不稳定-_-b

On Sep 22, 9:00 am, sagasw <sag...@gmail.com> wrote:
> Tiny的情况应该和作者的不太一样,关键问题不是技术,而是人。
>
> Tiny写的重构里面如何设计如何实现都是完全掌控的,客户方只是提出商业需求没有技术限制。
>
> 但是我看作者的描述,他在项目中应该只是一个程序员的身份,如何影响别人注意代码味道的问题,如何影响管理层让他们感觉到代码味道带来的后期维护代价,是比较有难度的。我的意见是不要改革要改良,针对他自己写的feature代码力争清晰简洁,修改其他bug时候相应的进行小范围代码重构,这样才能把风险降到最低。
>
> 代码坏味很难看,但是为了修改搞到自己饭碗危险那就不值了。
>

> 2009/9/22 Tiny fool <tinyf...@gmail.com>
>
> > 像极了我当年去XXX遇到的那个程序,呵呵
> > 这个故事详见:一个具体项目的重构(一)<http://www.tinydust.net/prog/diary/2004/09/blog-post_27.html>
> > ,一个具体项目的重构(二) <http://www.tinydust.net/prog/diary/2005/10/blog-post.html>
> > ,一个具体项目的重构(三)<http://www.tinydust.net/prog/diary/2005/10/blog-post_30.html>


> > 。
>
> > 当然我们的重构是把项目重新设计了一遍,而不是一般意义的代码级的重构。因为我们很多工程问题是由于要给略有不同的同类硬件产品设计PC客户端造成的。以前的实现方式是,一个新硬件产品,就从老的PC客户端代码改起,最后有无数的差异不大的PC客户端代码,里面充斥着各种谁也不知道的宏定义。新的实现方式是,把所有产品共有的界面逻辑和数据模型,放在一个exe里面,为每一款新产品设计一个dll插件,升级和新产品的主要工作就是设计和分发新的dll插件。
>

> > 2009/9/22 missdeer <missd...@gmail.com>

missdeer

unread,
Sep 22, 2009, 6:32:07 AM9/22/09
to TopLanguage
100、20不是代码行数,是函数的圈复杂度(《代码大全》里有讲什么是圈复杂度),我们用SourceMonitor度量出来的,好像VC2008也
有这个功能。

missdeer

unread,
Sep 22, 2009, 6:33:25 AM9/22/09
to TopLanguage
On Sep 22, 10:48 am, 肖海彤 <red...@gmail.com> wrote:
> 如果你能将这20万行代码重构成结构良好的代码, 比起重新做出来20万行结构好的代码, 对你的锻炼会更大 :)

严重同意这句话啊,不过没机会了。自从去年PL去听了个微软的人讲,Bug过多的模块在微软就是推倒重来的,他就信仰这个了。

missdeer

unread,
Sep 22, 2009, 6:35:09 AM9/22/09
to TopLanguage
这个基本同意,不过大前提是你得能取出一个好名字来。尽管所有开发人员都过了大学英语4、6级,但实际上那点英文水平似乎还不足以支持这个行动来。

On Sep 22, 11:50 am, up duan <fixo...@gmail.com> wrote:

missdeer

unread,
Sep 22, 2009, 6:36:03 AM9/22/09
to TopLanguage
我用Visaul Assist,基本可以忍受......

yzzrn

unread,
Sep 22, 2009, 9:22:40 PM9/22/09
to TopLanguage
SlickEdit的确强大一些,比VA可靠,不过VA与VC结合得比较好,而项目中大家一直在用VA.
很难说服大家用一个新的SlickEdit,所以我们也是VA凑和着用.一般Rename和Extract Method也就够用了.

On 9月22日, 下午6时36分, missdeer <missd...@gmail.com> wrote:
> 我用Visaul Assist,基本可以忍受......

yzzrn

unread,
Sep 22, 2009, 9:29:12 PM9/22/09
to TopLanguage
SlickEdit的确强大一些,比VA可靠,不过VA与VC结合得比较好,而项目中大家一直在用VA.
很难说服大家用一个新的SlickEdit,所以我们也是VA凑和着用.一般Rename和Extract Method也就够用了.

On 9月22日, 下午6时36分, missdeer <missd...@gmail.com> wrote:

yzzrn

unread,
Sep 22, 2009, 9:38:59 PM9/22/09
to TopLanguage
SlickEdit的确强大一些,比VA可靠,不过VA与VC结合得比较好,而项目中大家一直在用VA.
很难说服大家用一个新的SlickEdit,所以我们也是VA凑和着用.一般Rename和Extract Method也就够用了.

On 9月22日, 下午6时36分, missdeer <missd...@gmail.com> wrote:

Jay

unread,
Sep 24, 2009, 3:05:05 AM9/24/09
to TopLanguage
合作项目必须要有规约,一个功能可以用很多种方法实现,不加约定的话,到后期就无法控制了。
设计才是最重要的,小范围重构作用不大。

akirya(坏[其实我不是什么所谓的坏人])

unread,
Sep 25, 2009, 10:46:55 AM9/25/09
to TopLanguage
总结的不错,留着这个帖子做反面教材.
Reply all
Reply to author
Forward
0 new messages