0%

如何把函数写的整洁

这里使用C++语言来作为示例,但对其他语言的函数书写也有借鉴意义。

函数书写的原则

写函数的第一规则是要短小。第二条规则是还要更短小。

下面是两段功能一致的代码,分别展示了不同的实现方式:

第一段代码将所有的实现都写在了一个函数里,实现了一个简单的计算平均值的功能:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
include <iostream>
#include <vector>

using namespace std;

double average(vector<double> v) {
double sum = 0;
for (int i = 0; i < v.size(); i++) {
sum += v[i];
}
return sum / v.size();
}

int main() {
vector<double> v{1, 2, 3, 4, 5};
double avg = average(v);
cout << "The average is: " << avg << endl;
return 0;
}

第二段代码对第一段代码进行了抽象,将复用的代码抽取成了一个共用的函数sum,然后在average函数中调用sum函数,实现了相同的功能。这样做的好处是可以减少代码量,提高代码的可读性和可维护性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <iostream>
#include <vector>

using namespace std;

double sum(vector<double> v) {
double s = 0;
for (int i = 0; i < v.size(); i++) {
s += v[i];
}
return s;
}

double average(vector<double> v) {
return sum(v) / v.size();
}

int main() {
vector<double> v{1, 2, 3, 4, 5};
double avg = average(v);
cout << "The average is: " << avg << endl;
return 0;
}

这两段代码实现的功能是一致的,但是第二段代码通过抽象函数,使代码更加简洁易懂,也更加容易维护。

只做一件事

下面的示例描述了把大象放进冰箱的过程:

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
function putElephantInFridge()
openFridgeDoor()
moveElephantToDoor()
pushElephantIntoFridge()
closeFridgeDoor()
done()

function openFridgeDoor()
// code to open fridge door
print("Opening fridge door")

function moveElephantToDoor()
// code to move elephant to fridge door
print("Moving elephant to fridge door")

function pushElephantIntoFridge()
// code to push elephant into fridge
print("Pushing elephant into fridge")

function closeFridgeDoor()
// code to close fridge door
print("Closing fridge door")

function done()
// code to signal completion
print("Elephant is now in the fridge")

注意,判断函数是否只做了一件事,就是看其是否能再拆出一个函数。

每个函数一个抽象层级

要确保函数只做一件事,函数中的语句都要在同一抽象层级上。下面用番茄炒蛋的伪代码来演示这一思想。

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
function cookTomatoEgg() {
// 第一层函数调用,准备工作
prepareIngredients();

// 开始烹饪
startCooking();

// 上菜
serveDish();
}

// 第二层函数调用,开始烹饪
function startCooking() {

// 开始热锅
heatPan();

// 加入蛋液煎炒
fryEgg();

// 加入西红柿煮炒
fryTomato();

// 加调味料
addSeasoning();

// 完成烹饪
finishCooking();
}

// 第三层函数调用,开始热锅
function heatPan() {
// 设置炉灶温度
setStoveTemperature(6);

// 加油热锅
addOil();

// 等待锅变热
waitPanHeat();
}

switch语句

确保每个switch语句都埋藏在较低的抽象层级,而且永远不重复。

什么是把switch语句放在较高抽象层级

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
Money calculatePay(unsigned int type)
{
if(type > 3) {std::cout << "invalid employee type!" << std::endl;}
switch (type)
{
case COMMISSIONED:
return calculateCommissionedPay(type);
case HOURLY:
return calculateHourlyPay(type);
case SALARIED:
return calculateSalariedPay(type);
default:
throw std::invalid_argument("Invalid employee type.");
}
}

void deliverPay(unsigned int type)
{
if(type > 3) {std::cout << "invalid employee type!" << std::endl;}
switch (type)
{
case COMMISSIONED:
return deliverCommissionedPay(type);
case HOURLY:
return deliverHourlyPay(type);
case SALARIED:
return deliverSalariedPay(type);
default:
throw std::invalid_argument("Invalid employee type.");
}
}

什么是把switch语句放在较低抽象层级

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
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
#include <iostream>
#include <string>

using namespace std;

class Employee {
public:
virtual bool isPayday() = 0;
virtual double calculatePay() = 0;
virtual void deliverPay(double pay) = 0;
};

class CommissionEmployee : public Employee {
public:
bool isPayday() override {
// 判断是否为发薪日
return true;
}

double calculatePay() override {
// 计算薪资
return 1000.0;
}

void deliverPay(double pay) override {
// 发放薪资
cout << "Commission Employee has been paid " << pay << " dollars." << endl;
}
};

class HourlyEmployee : public Employee {
public:
bool isPayday() override {
// 判断是否为发薪日
return true;
}

double calculatePay() override {
// 计算薪资
return 500.0;
}

void deliverPay(double pay) override {
// 发放薪资
cout << "Hourly Employee has been paid " << pay << " dollars." << endl;
}
};

class SalariedEmployee : public Employee {
public:
bool isPayday() override {
// 判断是否为发薪日
return true;
}

double calculatePay() override {
// 计算薪资
return 800.0;
}

void deliverPay(double pay) override {
// 发放薪资
cout << "Salaried Employee has been paid " << pay << " dollars." << endl;
}
};

class EmployeeFactory {
public:
Employee* createEmployee(string employeeType) {
Employee* employee = nullptr;
switch (employeeType) {
case "CommissionEmployee":
employee = new CommissionEmployee();
break;
case "HourlyEmployee":
employee = new HourlyEmployee();
break;
case "SalariedEmployee":
employee = new SalariedEmployee();
break;
default:
throw std::invalid_argument("Invalid employee type.");
}

};

int main() {
EmployeeFactory* employeeFactory = new EmployeeFactory();

Employee* employee1 = employeeFactory->createEmployee("CommissionEmployee");
if (employee1->isPayday()) {
double pay = employee1->calculatePay();
employee1->deliverPay(pay);
}
delete employee1;

Employee* employee2 = employeeFactory->createEmployee("HourlyEmployee");
if (employee2->isPayday()) {
double pay = employee2->calculatePay();
employee2->deliverPay(pay);
}
delete employee2;

Employee* employee3 = employeeFactory->createEmployee("SalariedEmployee");
if (employee3->isPayday()) {
double pay = employee3->calculatePay();
employee3->deliverPay(pay);
}
delete employee3;

delete employeeFactory;
return 0;
}

使用描述性的名称

函数名称需要较好地描述函数做的事情。

函数越短小,功能越集中,就越便于取个好名字。

别害怕长名称。

命名方式要保持一致。使用与模块名一脉相承的短语、名词和动词给函数命名。

函数参数

最理想的参数数量是零,其次是一个,再次是两个,应尽量避免多参数函数。

多参数函数难以编写测试用例。

  1. 单参数函数

    有输入参数无输出参数。程序将函数看作是一个事件,使用该参数修改系统状态。

    有输入参数有输出参数。如果函数要对输入参数进行转换操作,转换结果要体现为返回值。

    1
    2
    StringBuffer transform(StringBuffer in);
    void transform(StringBuffer in, StringBuffer out);
  2. 传入True/False说明函数不止干了一件事。

  3. 如果参数太多可将其组合成结构体或类

    1
    2
    Circle makeCircle(double x, double y, double radius);
    Circle makeCircle(Point center, double radius);

无副作用

函数内只做函数名描述的事情,不要隐藏地做其他事情。

分隔指令与查询

指令和查询混淆的示例:

1
2
bool set(string attribute, string value);
if(set("username", "unclebob"));

指令和查询分开的示例:

1
2
3
4
if(attributeExists("username"))
{
setAttribute("username", "unclebob");
}

使用异常替代返回错误码

在if语句中把指令当作表达式使用的示例(这种方式违背了分隔指令与查询思想):

使用异常替代返回错误码

错误码可能由一个共有的类型去管理。当新增错误码时需要重新编译所有依赖该错误码类型的文件。

例如:

1
2
3
4
5
6
7
enum Error{
OK,
INVALID,
NO_SUCH,
LOCKED,
OUT_OF_RESOURCES
};

使用异常替代错误码,新异常就可以从异常类派生出来,编译时不影响其他文件。

1
throw std::runtime_error("invalid parameters");

抽离try/catch代码块

将错误处理代码抽象成单个函数,避免try/catch中写过多的语句。

错误处理就是一件事

错误处理就是一件事,这意味着可以实现一个专门处理错误的函数。这个函数里只有try/catch结构。

别重复自己

将重复的代码抽象到公有函数或基类中,从而避免冗余。

如何写出这样的函数

写代码和写别的东西很像。在写论文或文章时,你先想什么就写什么,然后再打磨它。 初稿也许粗陋无序,你就斟酌推敲,直至达到你心目中的样子。

我写函数时,一开始都冗长而复杂。有太多缩进和嵌套循环。有过长的参数列表。名称 是随意取的,也会有重复的代码。不过我会配上一套单元测试,覆盖每行丑陋的代码。

然后我打磨这些代码,分解函数、修改名称、消除重复。我缩短和重新安置方法。有时 我还拆散类。同时保持测试通过。

最后,遵循本章列出的规则,我组装好这些函数。

我并不从一开始就按照规则写函数。我想没人做得到。

以上来自于 《代码整洁之道》第三章–函数。


觉得有用就点个赞吧!

我是首飞,喜欢做一些有趣的事情,拿出来分享。

另外在公众号《首飞》内回复“机器人”获取精心推荐的C/C++,Python,Docker,Qt,ROS1/2等机器人行业常用技术资料。

公众号