首页
社区
课程
招聘
[转帖]朴实的C++设计
发表于: 2011-8-29 18:06 2165

[转帖]朴实的C++设计

2011-8-29 18:06
2165
感觉特别好,就转过来了。
作者:Solstice
地址:http://blog.csdn.net/Solstice

朴实的C++设计

(这篇文章写于 2008 年底,“去年”指的是 2007 年。)

去年8月入职,培训了4个月,12月进入现在这个部门,到现在工作正好一年了。工作内容是软件开发,具体地说,用C++开发一个网络应用(TCP not Web),这是我们的外汇交易系统的一个部件。这半年来,和一两位同事合作把原有的一个C++程序重写了一遍,并增加了很多新功能,重写后的代码不长,不到15000行,代码质量与性能大大提高。实际上,重写只花了三个月,9月我们交付了第一个版本,实现了原来的主要功能,吞吐量提高4倍。后面这三个月我们在增加新功能,并准备交付第二个版本。这个项目让我对C++的使用有了新的体会,那就是“实用当头,朴实为贵,好用才是王道”。

C++是一门(最)复杂的编程语言,语言虽复杂,不代表一定要用复杂的方式来使用它。对于一个金融交易系统,正确性是首要的,价格/数量/交割日期弄错了就会赔钱。在编写代码时,我们特别注意把代码写得尽量简单直白,让人一看就懂。为了控制代码的复杂度,我们采用了基于对象的风格,也就是具体类加全局函数,把C++程序写得如C语言一般清晰,同时使用一些C++特性和库来减少代码。

项目中基本没有用到面向对象,或者说没有用到继承和多态的那种面向对象,不一定非得有基类和派生类的设计才是好设计。引入基类和派生类,或许能带来灵活性,但是代码就不如原来透彻了。在不需要这种灵活性的场合,干嘛要付出这样的代价呢?我宁愿花一天时间把几千行 C 代码弄懂,也不愿在几十个类组成的继承体系里绕来绕去浪费脑力。定义并使用清晰一致的接口很重要,但“接口”不一定非得是抽象基类,一个类的成员函数就是它的接口。如果看头文件就能明白这个类在干什么、该怎么用固然很好,如果不明白,打开实现文件,东西都在那儿摆着呢,一望而知。没必要非得用个抽象的接口类把使用者和实现隔开,再把实现隐藏起来,这除了让查找并理解代码变麻烦之外没有任何好处。一个进程内部的解耦意义不大,相反,函数调用是最直接有效的通信方式。或许采用接口类/实现类的一个可能的好处是依赖注入,便于单元测试。经过权衡比较,我们发现针对各个类写测试的意义不大。另外,如果用白盒测试,那么功能代码和测试代码就得同步更新,会增加不少工作量,碍手碍脚。

程序里边有一处用到了继承,因为它能简化设计。这是一个strategy,涉及一个基类和3、4个派生类,所有的类都没有数据成员,只有虚函数。这几个类的代码加起来不到200行。这个设计不是一开始就有的,而是在项目进行了一大半的时候,我们发现代码里有若干处针对请求类型的switch/case,于是我们提炼出了一个strategy,把好几处switch/case替换为了strategy对象的虚函数调用,从而简化了代码。这里我们纯粹把OO当做函数指针表来用的。

程序里还有几处用了模板,甚至是type traits,这都是为了简化代码,少敲键盘。这些代码都藏在一个角落里,对外只暴露出一个全局函数的接口,使用者不会被其困扰。

项目里,我们惟一仰赖的C++特性是确定性析构,即一个对象在离开其作用域之后会保证调用析构函数。我们利用这点大大简化了代码,并确保资源和内存的回收。在我看来,确定性析构是C++区别其他主流开发语言(Java/C#/C/动态脚本语言)的最主要特性。

为了确保正确性,我们另外用Java写了一个测试夹具(test harness)来测试我们这个C++程序。这个测试夹具模拟了所有与我们这个C++程序打交道的其他程序,能够测试各种正常或异常的情况。基本上任何代码改动和bug修复都在这个夹具中有体现。如果要新加一个功能,会有对应的测试用例来验证其行为。如果发现了一个bug,先往夹具里加一个或几个能复现bug的测试用例,然后修复代码,让测试通过。我们积累了几百个测试用例,这些用例表示了我们对程序行为的预期,是一份可以运行的文档。每次代码改动提交之前,我们都会执行一遍测试,以防低级错误发生。

我们让每个类有明确的职责范围,一个类代表一个概念,不能像个杂货铺一样什么都装。在增加或修改功能的时候,仔细考虑在哪儿下手才最合理。必要时可以动大手脚,而不是每次都选择最简单的修补方式,那样只会使代码越来越臭,积重难返,重蹈上一个版本的覆辙。有时我们会提炼出一个新的类,把原来分散在多个类里的代码集中到一起,从而优化结构。我们有测试夹具保障,并不担心修改会破坏什么。

设计不是一开始就形成的,而是随着项目进展逐步演化出来。我们的设计是基于类的,而不是基于类的继承体系。我们是在写应用,不是在写框架,在C++里用那么多继承对我们没好处。一开始我们只有三四个类,实现了基本的报价功能,然后增加了一个类,实现了下单功能。这时我们把报价和下单的共同数据结构提炼成一个新的类,作为原来两个类的成员(而不是基类!),并把解析客户输入的代码移到这个类里。我们的原则是,可以有特别简单的类,但不宜有特别复杂的类,更不能有大怪兽。一个类太大,我们就看看能不能把它拆成两个,把责任分开。两个类有共同的代码逻辑,我们会考虑提炼出一个工具类来用,输入数据的验证就是这么提炼出来的一个类。勿以善小而不为,所以始终能让代码保持清晰易懂。

让代码保持清晰,给我们带来了显而易见的好处。错误更容易暴露,在发布前每多修复一个错误,发布后就少一次半夜被从被窝里叫醒查错的机会:)

不要因为某个技术流行而去用它,除非它确实能降低程序的复杂性。毕竟,软件开发的首要技术使命是控制复杂度,防止脑袋爆掉。对于继承要特别小心,这条贼船上去就下不来,除非你是继承boost::noncopyable 讲解面向对象的书里,总会举一些用继承的精巧的例子,比如矩形、正方形、圆形继承自形状,飞机和麻雀继承自“能飞的”,这不意味着继承处处适用。我认为在C++这样需要自己管理内存和对象生命期的语言里,大规模使用面向对象、继承、多态多是自讨苦吃。还不如用C语言的思路来设计,在局部用一用继承来代替函数指针表。而GoF的《设计模式》与其说是常见问题的解决方案,不如说是绕过(work around)C++语言限制的技巧。当然,也是一些人挂在嘴边用来忽悠别人或麻痹自己的灵丹妙药。

[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课

收藏
免费 0
支持
分享
最新回复 (2)
雪    币: 166
活跃值: (25)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
2
实践出真知,适合自己才是真!
2011-8-29 19:36
0
雪    币: 156
活跃值: (27)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
3
其实我觉得很有道理
2011-8-29 22:27
0
游客
登录 | 注册 方可回帖
返回
//