这里使用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 () print ("Opening fridge door" ) function moveElephantToDoor () print ("Moving elephant to fridge door" ) function pushElephantIntoFridge () print ("Pushing elephant into fridge" ) function closeFridgeDoor () print ("Closing fridge door" ) function done () 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 2 StringBuffer transform (StringBuffer in) ;void transform (StringBuffer in, StringBuffer out) ;
传入True/False说明函数不止干了一件事。
如果参数太多可将其组合成结构体或类
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等机器人行业常用技术资料。