1. 总则
总则规定了一些大体原则,必须要作的、最需要注意的事项。也是面向目前我们的代码中亟需解决的一些问题:
(1)头文件、源文件布局混乱,直接影响编译效率
(2)有编程规范,但遵守的很差
(3)过长函数
(4)大量重复代码
1.1 源文件
源文件原则:
● 函数行数尽量不要超过50行,超过50行的目前阶段并非严格禁止,但需要说明理由
● 源文件长度尽量不要超过500行, 不同子功能、子模块的代码不要放在一个源文件中;理论上源文件分的越细越好。保证同一源文件中的代码“强内聚”。
● 无特殊情况,不允许使用全局变量,一律局部静态化,对外封装访问接口,对有并发可能的业务,
均需在访问接口中作并发控制。
● 函数拆分封装基本原则:明显的重复代码一律封装函数;何时该拆分较长函数、封装新函数?只要能提高代码的可读性、可复用性就该拆分:哪怕只有一行。
1.2 头文件
头文件总体原则:头文件应该越简单越好,包含的其他头文件越少越好
●头文件包含其他头文件的原则: 仅包含且必须包含那些头文件中所出现的类型需要的头文件(如,头文件A中出现的类型定义在头文件B中,则头文件A必须包含头文件B,除此以外的其他头文件不允许包含)。头文件应该包含哪些头文件应该仅决定于它自己,而不是那些包含此头文件的源文件(如,将源文件编译时需要用到的头文件B,这时源文件已经包含头文件A了,索性将头文件B包含在此头文件A中,这是错误的做法)。
●头文件划分原则:类型定义、宏定义应与函数声明相分离,分别位于不同的头文件中。内部函数声明头文件与外部函数声明头文件相分离,内部类型定义头文件,与外部类型定义头文件相分离。需要特别注意的是,外部函数声明头文件与外部宏、结构定义头文件一定要独立出来,这样可以保证外部调用者引用的头文件肯定是“最简化的”,不会包含外部调用者不需要的内部函数声明或者接口定义。
●头文件的语义层次化原则:头文件需要有语义层次,不同语义层次的类型定义不要放在一个头文件中,不同层次的函数声明不要放在一个头文件中。
●仅在一个源文件中用到的类型和宏定义,就放在此源文件中定义,不要放入任何头文件中。
●头文件的语义相关性原则:同一头文件中出现的类型定义、函数声明应该是语义相关的、有内部逻辑关系的,避免将风马牛不相及的定义和声明放在一个头文件中。
●对开放给其他模块使用的外部头文件尤其要注意其所包含的头文件的合理性,如包含太多不必要的其他头文件,将影响所有引用此头文件的模块,因此影响更严重。
1.3 编程规范
编程规范中的规则必须遵守,如有特殊情况不能遵守的,必须说明理由,编程规范见附件
1.4 过长函数拆分
说明见附录部分《函数设计》
函数行数尽量不要超过50行,超过50行的需要说明理由
1.5 代码复用
原则: Write once and only once
只写一次,需要作些灵活化处理,需要动脑子,而不是简单的拷贝粘贴
只写一次,则只需要修改一次,代码更容易维护与扩展
原则上超过3行的重复代码应该复用。
代码复用分成下面若干情况,复用的方法一个比一个更抽象,复用的难度也依次增加,下面依次描述。
● 库。 现成的通用数据结构及基于其上的通用算法、标准函数库、通用模板库 均可直接拿来使用,如C++的STL、BOOST库。业务程序员应该集中精力于上层业务模型构建,而不是数据存储、数据访问这些“做轮子”的重复性的、机械的、还很容易出错的基础劳作。C语言这方面的标准库非常欠缺,但非标准库并不缺少,这些库多半是由少数高级程序员开发,且在实践中久经考验,安全性、性能、可靠性方面可以说远超过大部分水平一般的代码,比如原来的TMS框架中就有相当多的基础库,ROS中也有一些。能用的就要直接使用。库复用的难度在于必须按照库规定的使用方式来正确使用,越是复杂的库使用方式与注意事项也就越复杂。
● 完全一样的代码, 直接封装函数予以复用,这是最简单的情况,没有任何难度,必须做到。
● 基本上完全一样的代码,只是其中用到的变量不一样,封装成函数,将不一样的变量作为函数的参数传入。
● 大部分一样的代码,只有其中少数几行代码不一样,封装一样的部分。(1)将不一样的部分封装成接口类型一致的函数,将函数作为函数指针参数传入可复用的函数,一般可以封装成一种接口的代码,我们称作可复用的是一种“代码结构”。(2) 如果不一样的地方差异比较大,无法封装成接口一致的函数,就需要考虑“要复用的东西倒底是什么”,也许你需要复用的不是“代码结构”,而是其他的东西,比如某种获取操作数据的方式。
● 代码模式类似的代码,抽象出相同的“模式”,对模式进行编码,将代码的抽象层次升级。
● 设计复用。 这是最高层次的复用,有现成的可行的优秀的解决方案,就采用之。 如面向对象领域已经蔚成风气的“设计模式”。设计模式即:在某种上下文环境适于采用的设计方案,用专用术语予以表达,减低沟通成本,形成一族“设计语汇”。
代码复用具体代码实例见附录部分example.c
2.细则
细则中针对目前我在代码中发现的其他一些常见问题进行详细说明
2.1 全局变量
全局变量的主要问题:
● 非常难对访问进行合法性检查。 全局变量的可见性是“整个工程”,工程中的所有文件可以直接对之进行读写,没有任何保护措施可以加以防范(如值域、下标合法性检查),靠客户代码去作检查是不可靠、代价也是不可容忍的(因为所有调用处都需要检查)的。
● 可扩展性问题。 也许当前的需求只是简单的对值进行读取、设置即可。如果以后需要增加复杂一点的访问逻辑如何增加? 当前用静态数组一次性开足空间直接用下标定位非常方便,但后来内存不够了需要替换成动态分配的链表,如何扩展?
● 可复用性问题。对复杂全局数据对象(链表等)如果没有封装访问接口以复用其访问代码,对其的访问将非常痛苦,可以想见会有通篇的拷贝粘贴代码。
● 并发性问题。全局变量(尤其是访问方式比较复杂的全局数据,如链表等)的直接访问是很难进行并发保护的。
● 语义单薄。通过变量名称所能传递的语义是非常单薄的,通过访问接口函数命令可以提供层次更丰富的业务语义信息。
全局变量的主要“好处”:
全局变量普遍被认可的好处当然是:
●使用方便, 既然非常方便,没有任何限制,就难免会有使用失误-没有控制的东西最危险。如何确保每次对此变量进行访问时都对值进行了合法性检查? 如何确保对一个复杂全局数据对象(如链表)进行操作时没有其他的并发操作正在进行? 因此,所谓的“好处”也许正是它的害处。
全局变量的替换性方法:
●定义静态变量,以减少其可见性。注意:可见性和生存期是两码事情。全局变量之所以有害主要在于其“可见性”太广,而非其生存期。局部静态变量生存期和全局变量是一样的。
●对静态变量封装访问接口。
●复杂数据对象访问接口可以根据索引、关键字(简单相等逻辑判断,通过 “==” 直接判断)、eqaul函数(复杂逻辑相等判断函数,可按函数指针参数方式传给访问接口)对之进行定位。
访问接口可以作到什么?
●前面提到的全局变量问题将全部得到解决。
原则: 数组、链表等复杂数据类型全局数据必须封装访问接口,作合法性检查。
2.2 平台业务代码中项目宏、业务宏的处理方式
业务宏开关相关的函数和代码应能尽量独立在某文件中最好,这样查找阅读更方便,项目相关的特色与通用框架部分应尽可能区分开来,各自独立变化,不要纠缠错节在一起。
这样做的附加好处是:
框架代码的可复用性、可扩展性、可读性得到提高。
要做到这一点,尤其是优美的用代码作到这一点是对框架代码的灵活性提出了更高的要求,对框架设计提出了更高的要求,设计质量也可因此得到升级。
框架代码和特性代码都更显眼了,彼此可以各自独立扩展,提高了框架代码的可复用性、可扩展性,因为特性代码已经被抽象语义化了,被赋予了显示框架语义。框架代码的抽象性更好,而不再被各类特性代码填塞,方便从更高级别、更抽象的的语义层次阅读框架代码,业务语义逻辑的可读性得到了提高。
在面向对象编程领域有成熟的语言特性可以支持这种设计,那就是“派生与多态”,在范型编程(面向类型编程)领域可以使用语言的模板特化萃取机制,对于C语言,使用类似范型的萃取机制更合适(用C语言写面向对象的程序是没问题的,只是要自己实现派生机制,这个不符合目前的代码现状,因此采用后者更合适)。
在C语言里对特性的一种“萃取”实现方式是可以采用利用函数指针向框架注册各个项目的特化接口,通过查找注册接口,很容易查找不同的项目在有区别的业务关注点上不同的处理策略,代码可读性更好,对重复的特性处理代码,也很方便予以复用。
如:
#if INSTALL_MSAN_SLAVESHELF_SUPPORT
for(shelfNo = 0; shelfNo < IGMP_MAX_SHELF_NUM; shelfNo ++)
#endif
可以演变为:
for(shelfNo = 0; shelfNo < IgmpFuncTbl.GetMaxShelfNo(); shelfNo ++)
IgmpFuncTbl为框架注册函数指针表,其中注册了各类项目、业务的特性处理接口函数,在注册时根据项目宏进行一次性注册,此处,由隐式项目特性处理代码转化为显示框架语义接口的是“GetMaxShelfNo”。
框架代码中可以不再出现项目宏嵌入的项目相关代码,成为纯粹的框架代码。反之亦然。
2.3 标识符命名
全局对象(全局变量、全局函数, 宏)应包含尽可能丰富的语义信息,以防对全局命名空间的污染(命名冲突)效应,局部对象(静态数据,静态函数)可以弱化语义信息(没有命名冲突问题),因为可以通过文件名信息提供这层附加语义。
标识符命名现在代码中的现状可谓一片混乱。无规则。随意而为。
具体命名原则可参考附录部分:标识符命名.ppt,及《代码大全》相应章节。
2.4 循环的使用
●何时应该使用while,何时应该使用for
while主要用于循环次数不一定的循环
for 用户循环次数固定的循环
对while循环, 应防止“死循环”(可以通过循环最大次数限制,如链表遍历,可通过链表最大理论长度限制遍历次数)。
● 对for 循环, 不应该在循环体内修改循环变量,否则,你需要的可能是一个while循环
● 减少循环的嵌套层次的方法
运用防卫子句及早break或continue,封装循环体为函数
2.5 使用宏,不要使用魔数
这是非常关键,非常实用,也非常容易做到的一条规则,但往往容易被忽视没有遵守好。
原则:
●一切有语义、会重复出现的数字常量都应该用宏替代
●宏不应该重名, 不应该被重定义
●必要时,使用enum组织一组语义相关的宏