函数都写不好,确实有些丢人。如何把函数写的整洁呢?看了会书深有启发。

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

函数书写的原则

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

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

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

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函数,实现了相同的功能。这样做的好处是可以减少代码量,提高代码的可读性和可维护性。

#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;
}

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

只做一件事

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

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")

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

每个函数一个抽象层级

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

function cookTomatoEgg() {
    // 第一层函数调用,准备工作
    prepareIngredients();

    // 开始烹饪
    startCooking();

    // 上菜
    serveDish();
}

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

    // 开始热锅
    heatPan();

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

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

    // 加调味料
    addSeasoning();

    // 完成烹饪
    finishCooking();
}

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

    // 加油热锅
    addOil();

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

switch语句

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

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

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语句放在较低抽象层级

#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. 单参数函数

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

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

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

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

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

无副作用

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

分隔指令与查询

指令和查询混淆的示例:

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

指令和查询分开的示例:

if(attributeExists("username"))
{
    setAttribute("username", "unclebob");
}

使用异常替代返回错误码

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

使用异常替代返回错误码

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

例如:

enum Error{
    OK,
    INVALID,
    NO_SUCH,
    LOCKED,
    OUT_OF_RESOURCES
};

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

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

抽离try/catch代码块

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

错误处理就是一件事

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

别重复自己

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

如何写出这样的函数

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

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

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

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

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

以上总结自 《代码整洁之道》第三章—函数。

By the way, 示例代码使用ChatGPT生成。哈哈哈!


觉得有用就点个赞吧!

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

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