编写高质量的代码

Designing a software is just like designing a jewelry

本文属于软件工程系列笔记总结第一部分,如何编写高质量的代码。

编程过程与规范

此处详见1,关于注释多说几点,首先注释要解释为什么而不是怎么做,其次不要对明确知道功能的语句进行注释,应当以块作为划分,除非某一条语句特别复杂。应尽可能做到去文档化,使开发文档融合在注释中,最后利用一些工具自动生成开发文档。另外需要注意的是,对于不好的程序,不要试图修改,而是要直接重新写。

良好的编程实践

在编程实践中,我们要做到以下几点:

  • 看:阅读优秀代码
  • 问:以专业的方式提出问题,通过问题提升技能,这里给出一个网页,描述了应当如何提问2
  • 练:大量的动手练习

开发软件的过程

开发软件的过程是一个自上而下然后自下而上的过程,我们首先根据指定的问题,将一个大的问题拆分为小的问题,然后针对小问题设计解决模块,最后将小模块进行组合,构成大型的软件系统。为了高质量的完成这个过程,我们需要三种编程实践

  • 模块化设计
  • 面向抽象编程
  • 错误与异常处理

模块化设计

将大程序按照功能拆分成一系列小的模块,可以有效降低设计复杂性、提高可靠性、缩短周期、易于维护与功能扩展。常见模块划分原则包括水平、垂直;易变、不易变;基于单一职责。下面是分别使用水平和垂直原则对WEB应用进行的划分

图片名称

所谓易变与不易变,是将代码中可能会经常改动的和不容易经常改动的分开,每次迭代只需改变易变部分;而单一职责原则即设计时一个模块只实现一个职责,注意单一职责不是指单一功能,而是说避免万能类和庞大函数。

案例:生命游戏的模块划分

生命游戏是一个很经典的细胞自动机算法,对于一个生命游戏,它包括棋盘、逻辑规则和计时器,所以我们可以将上述三个部分设置为三个不同的模块,并通过一定的接口相互调用。

模块名称 功能 备注
地图模块 管理地图相关的初始化、获取与更新
逻辑模块 控制完整游戏逻辑,根据地图模块的数据对地图模块进行更新
时间模块 在适当的时间对地图进行更新
UI模块 管理用户输入输出交互

面向抽象编程

在设计好模块的基础上,我们可以先设计出各个模块的骨架,或者说对各个模块进行抽象,定义它们之间的接口;模块间相互关联的接口在未来开发中应尽可能保持不变。

案例:生命游戏的接口设计

地图模块接口
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
enum LifeState{
NOLIFE = 0,
ALIVE
DEAD
};

class GameMap{
public:
// 构造函数
GameMap(unsigned int _height = 10, unsigned int _width = 10);

// 重置地图并填充活细胞
void reset(float life_ratio);

// 获得指定位置周围的细胞数目
unsigned int getNeighborCount(unsigned int row, unsigned int col);

// 更新地图上某个方格的生命状态
void setLifeState(unsigned int row, unsigned int col, LifeState life_state);

// 获得地图上某个方格的生命状态
LifeState getLifeState(unsigned int row, unsigned int col);

// 获得地图的长和宽
unsigned int getHeight(){
return m_height;
}

unsigned int getWidth(){
return m_width;
}

// 设置地图的长和宽
void setHeight(unsigned int _height){
m_height = _height;
}

void setWidth(unsigned int _width){
m_width = _width;
}
private:
unsigned int m_height;
unsigned int m_width;
vector<vector<int>> m_board{};


};
逻辑模块接口
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class GameControl{
public:
GameControl();
~GameControl();
// 进行一次游戏循环,将更新一次地图并打印
void update();

// 结束游戏
void end();

// 在控制台上打印地图
void printMap();
private:
GameMap* map;
};
时钟模块
1
2
3
4
5
6
7
8
class Timer{
public:
// 设置时钟频率
Timer(unsigned int _interval, void fun);

// 开始时钟
void start();
};

模块实现

在模块化分解之后,开发人员可以分别实现各个模块,根据函数单一职责原则,各模块内部可以定义更多函数,与此同时,模块测试的设计工作也可以开始。需要注意的是,前面设计的接口一旦设计好了,尽可能不要变动,否则可能造成连带性的一系列修改

错误和异常处理

在实现过程中,我们还需要考虑常见的错误和异常应该如何处理,特别是涉及到用户输入的部分,同时,我们还需要针对是否使用异常机制进行一些斟酌。

代码静态检查

所谓代码静态检查,是指在代码未运行期间,通过对源代码的阅读和审查,保证软件的质量和规范性。为了规范化审查流程,本节给出一些静态检查工具以及静态检查Checklist,帮助我们更好地进行代码静态检查过程。

缺陷检查表

类别 常见缺陷 类别 常见缺陷
编程规范 命名规则、注释、排版、声明初始化、语言格式等 程序流程 循环结束条件是否准确
面向对象设计 类的设计与抽象是否合适 是否避免了死循环的产生
是否符合面向接口编程的思想 对循环处理是否合适,是否避免多层嵌套
是否使用合适的设计模式
性能方面 是否能够正确处理海量数据
是否选择了合适的数据结构并进行了设置
是否滥用数据结构
是否采用通用线程池或对象池等高速缓存技术
接口是否设计合理,内部是否尽可能没有类型转换
是否采用内存或硬盘缓冲机制提高效率
并发访问策略
IO方面是否使用合适的类或良好的方法提高性能,例如减少序列化,使用buffer类封装
同步方法是否正确使用而没有滥用
递归迭代深度是否合适
如果有阻塞,是否考虑了性能保证的措施
避免过度优化,对性能要求高的代码
资源管理 分配内存是否释放,错误发生时是否保证所有资源都释放掉,是否同一个对象多次释放
代码是否保存准确的引用计数

代码静态分析工具

这里给出一些常用的代码分析工具,具体的应用方式请参考

C/C++

cppcheck:一个C++代码静态检查工具

代码性能分析及优化

优化的目的:结果相同,效率更高,根据2/8原则,实现程序重构、优化、扩展及文档相关内容一般会消耗80%工作量。

优化的要点及原则

要点

  • 针对时间和空间两个方面进行优化
  • 正确性、可靠性、健壮性、可读性是前提
  • 全局效率为主,局部效率为辅
  • 先找到代码中的效率瓶颈
  • 先优化数据结构和算法,后优化执行代码
  • 时间效率和空间效率可能是对立的,此时就需要进行权衡
  • 从设计之初就开始考虑优化程序性能
  • 测试数据很重要,要覆盖所有情况
  • 永远不要在执行性能评估的情况下尝试对代码优化

原则

一般来说,优化是一个追求性价比的问题,要优先对优化性价比最高的部分进行优化,所谓性价比,是指投入的优化精力和得到的优化效果达到平衡的一个时刻。并不是说程序最耗时的部分就是最难优化的,因为可能这部分优化起来很困难。记住一个原则:进行最有性价比的优化。

优化的一般步骤

graph LR
    node["证明需要优化"]
    node1["找出优化关键部分"]
    node2["代码测试"]
    node3["进行优化"]
    node4["评测优化结果"]

    node-->node1 
    node1-->node2
    node2-->node3
    node3-->node4

案例:词频统计代码

使用Python读入一个文本文件,然后统计该文本文件中英文单词出现的频率,并输出词频最高的100个单词,单词指的是被空格和标点符号进行分割的字符串。Python的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
def splitWords(InputFile)
# 文件IO
try:
alltext = fileobject.read() # 文件IO通常比较慢
finally:
fileobject.close()

# 分割单词
words = re.split('[^a-zA-Z]+', alltext) # 使用正则表达式分词

# 统计单词词频
dic = {}
for word in words:
if word in dic.keys(): # 词频统计
dic[word] += 1
else:
dic[word] = 1

# 排序
result = sorted(dic.items(), key = lambda dic:dic[1], reverse = True) # 排序,使用内置算法

为了获得影响程序性能的部分,我们需要一定的工具对代码进行测试,确定性能瓶颈,python中提供了profile帮助我们自动确定软件的性能,一个简单的使用例子如下:

1
2
3
4
5
6
7
8
9
10
11
12
import profile

def profileTest():
Total = 1;
for i in range(10):
Total = Total*(i+1)
print(Total)
return Total


if __name__ == "__main__":
profile.run("profileTest()")

运行结果如下:

1
2
3
4
5
6
7
8
9
10
11
      15 function calls in 0.016 seconds

Ordered by: standard name
ncalls tottime percall cumtime percall filename:lineno(function)
1 0.000 0.000 0.000 0.000 :0(exec)
10 0.000 0.000 0.000 0.000 :0(print)
1 0.016 0.016 0.016 0.016 :0(setprofile)
1 0.000 0.000 0.000 0.000 <string>:1(<module>)
1 0.000 0.000 0.016 0.016 profile:0(profileTest())
0 0.000 0.000 profile:0(profiler)
1 0.000 0.000 0.000 0.000 profileTest.py:3(profileTest)

可以看到函数的调用次数以及运行时间,具体参数解释如下:

参数名 含义
ncalls 调用次数
tottime 函数运行总时间,除去函数中调用的其他函数
percall 一次时间
cumtime 函数运行总时间,包括函数中调用的其他函数
filename:lineno(function) 文件名,函数行号,函数名

结对编程

所谓结对编程,就是两人合作,同时针对一个功能进行开发,类似于开车过程中的领航员和驾驶员,合作可能得到更优的结果

参考文献

0%