Designing a software is just like designing a jewelry
本文属于软件工程系列笔记总结第一部分,如何编写高质量的代码。
编程过程与规范
此处详见1,关于注释多说几点,首先注释要解释为什么而不是怎么做,其次不要对明确知道功能的语句进行注释,应当以块作为划分,除非某一条语句特别复杂。应尽可能做到去文档化,使开发文档融合在注释中,最后利用一些工具自动生成开发文档。另外需要注意的是,对于不好的程序,不要试图修改,而是要直接重新写。
良好的编程实践
在编程实践中,我们要做到以下几点:
- 看:阅读优秀代码
- 问:以专业的方式提出问题,通过问题提升技能,这里给出一个网页,描述了应当如何提问2
- 练:大量的动手练习
开发软件的过程
开发软件的过程是一个自上而下然后自下而上的过程,我们首先根据指定的问题,将一个大的问题拆分为小的问题,然后针对小问题设计解决模块,最后将小模块进行组合,构成大型的软件系统。为了高质量的完成这个过程,我们需要三种编程实践
- 模块化设计
- 面向抽象编程
- 错误与异常处理
模块化设计
将大程序按照功能拆分成一系列小的模块,可以有效降低设计复杂性、提高可靠性、缩短周期、易于维护与功能扩展。常见模块划分原则包括水平、垂直;易变、不易变;基于单一职责。下面是分别使用水平和垂直原则对WEB应用进行的划分
所谓易变与不易变,是将代码中可能会经常改动的和不容易经常改动的分开,每次迭代只需改变易变部分;而单一职责原则即设计时一个模块只实现一个职责,注意单一职责不是指单一功能,而是说避免万能类和庞大函数。
案例:生命游戏的模块划分
生命游戏是一个很经典的细胞自动机算法,对于一个生命游戏,它包括棋盘、逻辑规则和计时器,所以我们可以将上述三个部分设置为三个不同的模块,并通过一定的接口相互调用。
模块名称 | 功能 | 备注 |
---|---|---|
地图模块 | 管理地图相关的初始化、获取与更新 | |
逻辑模块 | 控制完整游戏逻辑,根据地图模块的数据对地图模块进行更新 | |
时间模块 | 在适当的时间对地图进行更新 | |
UI模块 | 管理用户输入输出交互 |
面向抽象编程
在设计好模块的基础上,我们可以先设计出各个模块的骨架,或者说对各个模块进行抽象,定义它们之间的接口;模块间相互关联的接口在未来开发中应尽可能保持不变。
案例:生命游戏的接口设计
地图模块接口
1 | enum LifeState{ |
逻辑模块接口
1 | class GameControl{ |
时钟模块
1 | class Timer{ |
模块实现
在模块化分解之后,开发人员可以分别实现各个模块,根据函数单一职责原则,各模块内部可以定义更多函数,与此同时,模块测试的设计工作也可以开始。需要注意的是,前面设计的接口一旦设计好了,尽可能不要变动,否则可能造成连带性的一系列修改。
错误和异常处理
在实现过程中,我们还需要考虑常见的错误和异常应该如何处理,特别是涉及到用户输入的部分,同时,我们还需要针对是否使用异常机制进行一些斟酌。
代码静态检查
所谓代码静态检查,是指在代码未运行期间,通过对源代码的阅读和审查,保证软件的质量和规范性。为了规范化审查流程,本节给出一些静态检查工具以及静态检查Checklist,帮助我们更好地进行代码静态检查过程。
缺陷检查表
类别 | 常见缺陷 | 类别 | 常见缺陷 |
---|---|---|---|
编程规范 | 命名规则、注释、排版、声明初始化、语言格式等 | 程序流程 | 循环结束条件是否准确 |
面向对象设计 | 类的设计与抽象是否合适 | 是否避免了死循环的产生 | |
是否符合面向接口编程的思想 | 对循环处理是否合适,是否避免多层嵌套 | ||
是否使用合适的设计模式 | |||
性能方面 | 是否能够正确处理海量数据 | ||
是否选择了合适的数据结构并进行了设置 | |||
是否滥用数据结构 | |||
是否采用通用线程池或对象池等高速缓存技术 | |||
接口是否设计合理,内部是否尽可能没有类型转换 | |||
是否采用内存或硬盘缓冲机制提高效率 | |||
并发访问策略 | |||
IO方面是否使用合适的类或良好的方法提高性能,例如减少序列化,使用buffer类封装 | |||
同步方法是否正确使用而没有滥用 | |||
递归迭代深度是否合适 | |||
如果有阻塞,是否考虑了性能保证的措施 | |||
避免过度优化,对性能要求高的代码 | |||
资源管理 | 分配内存是否释放,错误发生时是否保证所有资源都释放掉,是否同一个对象多次释放 | ||
代码是否保存准确的引用计数 |
代码静态分析工具
这里给出一些常用的代码分析工具,具体的应用方式请参考
C/C++
代码性能分析及优化
优化的目的:结果相同,效率更高,根据2/8原则,实现程序重构、优化、扩展及文档相关内容一般会消耗80%工作量。
优化的要点及原则
要点
- 针对时间和空间两个方面进行优化
- 正确性、可靠性、健壮性、可读性是前提
- 全局效率为主,局部效率为辅
- 先找到代码中的效率瓶颈
- 先优化数据结构和算法,后优化执行代码
- 时间效率和空间效率可能是对立的,此时就需要进行权衡
- 从设计之初就开始考虑优化程序性能
- 测试数据很重要,要覆盖所有情况
- 永远不要在执行性能评估的情况下尝试对代码优化
原则
一般来说,优化是一个追求性价比的问题,要优先对优化性价比最高的部分进行优化,所谓性价比,是指投入的优化精力和得到的优化效果达到平衡的一个时刻。并不是说程序最耗时的部分就是最难优化的,因为可能这部分优化起来很困难。记住一个原则:进行最有性价比的优化。
优化的一般步骤
graph LR node["证明需要优化"] node1["找出优化关键部分"] node2["代码测试"] node3["进行优化"] node4["评测优化结果"] node-->node1 node1-->node2 node2-->node3 node3-->node4
案例:词频统计代码
使用Python读入一个文本文件,然后统计该文本文件中英文单词出现的频率,并输出词频最高的100个单词,单词指的是被空格和标点符号进行分割的字符串。Python的代码如下:
1 | def splitWords(InputFile) |
为了获得影响程序性能的部分,我们需要一定的工具对代码进行测试,确定性能瓶颈,python中提供了profile帮助我们自动确定软件的性能,一个简单的使用例子如下:
1 | import profile |
运行结果如下:
1 | 15 function calls in 0.016 seconds |
可以看到函数的调用次数以及运行时间,具体参数解释如下:
参数名 | 含义 |
---|---|
ncalls | 调用次数 |
tottime | 函数运行总时间,除去函数中调用的其他函数 |
percall | 一次时间 |
cumtime | 函数运行总时间,包括函数中调用的其他函数 |
filename:lineno(function) | 文件名,函数行号,函数名 |
结对编程
所谓结对编程,就是两人合作,同时针对一个功能进行开发,类似于开车过程中的领航员和驾驶员,合作可能得到更优的结果
参考文献
- 1.Google C++编程风格 ↩
- 2.如何正确提问 ↩