函数规范

797 阅读7分钟

封装判断

好的函数应该是清晰易懂的,我们先从一个简单又实用的函数重构技法说起。如果没有上下文,if和while语句中的布尔逻辑就难以理解。如果把解释条件意图作为函数抽离出来,用函数名把判断条件的语义显性化地表达出来,就能立即提升代码的可读性和可理解性。

下面来看一个例子,在我们的CRM系统中,需要判断一个客户是否可以被业务员捡入自己的私海库。原来的代码是这样写的:

if(customer.getCrmUserId().equals(NIL_VALUE) && customer.getCustomerGroup() != CustomerGroup. CANCEL_ GROUP)
{
privateSea.pickUp(customer);
}

在上述代码中,if后面的判断条件令人十分费解,原因是缺少封装和合理的命名,我们可以用封装判断将其改写成:

if(canPickUpToPrivateSea())
{
    privateSea.pickUp(customer);
}

private boolean canPickUpToPrivateSea(){
    if(StringUtil.isBlank(this.getCrmUserId())){
        return false;
    }

    if(this.getCustomerGroup() == CustomerGroup.CANCEL_GROUP){
        return false;
    }
    return true;
}

不难发现,重构后的代码要更容易理解,因为通过封装判断,判断条件的业务语义被显性化地表达出来了,代码的可读性自然也好了很多。

函数参数

最理想的参数数量是零(零参数函数),其次是一(一元函数),再次是二(二元函数),应尽量避免三(三元函数)。有足够特殊的理由,才能用3个以上参数(多元函数)。当然凡事也不是绝对的,关键还是看场景,在程序设计中,一大忌讳就是教条。在某些场景下,两个参数可能比一个参数好。例如,Point p = new Piont(0 , 0);,两个参数就比一个参数要合理,坐标系中的点就应该有两个参数。如果看到newPoint(0),我们会倍感惊讶。

总体上来说,参数越少,越容易理解,函数也越容易使用和测试,因为各种参数的不同组合的测试用例是一个笛卡儿积。如果函数需要3个以上参数,就说明其中一些参数应该封装为类了。例如,要绘制一条直线,可以用如下函数声明:

Line makeLine(double startX, double startY, double endX, double endY);

上述代码中的X和Y是作为一组概念被共同传递的,我们应该为这一组概念提供一个新的抽象,叫作Point。这样将参数对象化之后,参数的个数减少了,表达上也更加清晰。

Line makeLine(Point start, Point end);

class Point{
    double x;
    double y;
}

短小的函数

有时保持代码的逻辑不变,只是把长方法改成多个短方法,代码的可读性就能提高很多。超长方法是典型的代码“坏味道”,对超长方法的结构化分解是提升代码可读性最有效的方式之一。

那么函数的代码行数多长才合适呢?

这没有一个绝对的量化标准,各团队可以有自己的标准,不同的开发语言可能会稍有不同。如果是Java语言,建议一个方法不要超过20行代码,如果这样执行的话,代码质量会得到显著的改善。

职责单一

按照行数规定函数的长度是定量的做法,实际上,我更喜欢另一种定性的衡量方法,即一个方法只做一件事情,也就是函数级别的单一职责原则(Single Responsibility Principle,SRP)。

遵循SRP不仅可以提升代码的可读性,还能提升代码的可复用性。因为职责越单一,功能越内聚,就越有可能被复用,这和代码的行数没有直接的关联性,但是有间接的关联性。

通常,长方法意味着肯定需要拆分,需要用多个子函数的组合来进行更好的表达。然而短小的函数并不一定就意味着就不需要拆分,只要不满足SRP,就值得进一步分解。哪怕分解后的子函数只有一行代码,只要有助于业务语义显性化的表达,就是值得的。

精简辅助代码

所谓的辅助代码(Assistant Code),是程序运行中必不可少的代码,但又不是处理业务逻辑的核心代码,比如判空、打印日志、鉴权、降级和缓存检查等。这些代码往往会在多个函数中重复冗余,减少辅助代码可以让代码显得更加干净整洁,易于维护。

如果辅助代码太多,会极大地干扰代码的可读性,读这种代码会让人抓狂,摸不着头脑。因此,我们应该尽量减少辅助代码对业务代码的干扰。让函数中的代码能直观地体现业务逻辑,而不是让业务代码淹没在辅助代码中。

组合函数模式

组合函数模式(Composed Method Pattern)出自Kent Beck的Smalltalk Best Practice Patterns一书,是一个非常容易理解上手、实用,对代码可读性和可维护性起到立竿见影效果的编程原则。

SLAP

抽象层次一致性(Single Level of Abstration Principle,SLAP),是和组合函数密切相关的一个原则。组合函数要求将一个大函数拆成多个子函数的组合,而SLAP要求函数体中的内容必须在同一个抽象层次上。如果高层次抽象和底层细节杂糅在一起,就会显得凌乱,难以理解。

举个例子,假如有一个冲泡咖啡的原始需求,其制作咖啡的过程分 为3步。

(1)倒入咖啡粉。

(2)加入沸水。

(3)搅拌。

其伪代码(pseudo code)如下:

public void makeCoffee() {
    pourCoffeePowder();
    pourWater();
    stir();
}

如果要加入新的需求,比如需要允许选择不同的咖啡粉,以及选择不同的风味,那么代码就会变成这样:

public void makeCoffee(boolean isMilkCoffee, boolean isSweetTooth, CoffeeType type) {
    //选择咖啡粉
    if (type == CAPPUCCINO) {
        pourCappuccinoPowder();
    }
    else if (type == BLACK) {
        pourBlackPowder();
    }
    else if (type == MOCHA) {
        pourMochaPowder();
    }
    else if (type == LATTE) {
        pourLattePowder();
    }
    else if (type == ESPRESSO) {
        pourEspressoPowder();
    }

    //加入沸水
    pourWater();
    //选择口味
    if (isMilkCoffee) {
        pourMilk();
    }
    if (isSweetTooth) {
        addSugar();
    }
    //搅拌
    stir();
}

如果继续有更多的需求加入,那么代码会进一步恶化,最后变成一个谁也看不懂且难以维护的逻辑迷宫。

再回看上面的代码,新需求的引入当然是根本原因。但除此之外,另一个原因是新代码已经不再满足SLAP了。具体选择用什么样的咖啡粉是倒入咖啡粉这个步骤应该去考虑的实现细节,和主流程步骤不在一个抽象层次上。同理,加奶和加糖也是实现细节。

因此,在引入新需求以后,制作咖啡的主要步骤从原来的3步变成了4步。

(1)倒入咖啡粉,会有不同的选择。

(2)加入沸水。

(3)调味,根据需求加糖或加奶。

(4)搅拌。

按照组合函数和SLAP原则,我们要在入口函数中只显示业务处理的主要步骤。具体的实现细节通过私有方法进行封装,并通过抽象层次一致性来保证,一个函数中的抽象在同一个水平上,而不是高层抽象和实现细节混杂在一起。

根据SLAP原则,我们可以将代码重构为:

public void makeCoffee(boolean isMilkCoffee, boolean isSweetTooth, CoffeeType type) {
    //选择咖啡粉
    pourCoffeePowder(type);
    //加入沸水
    pourWater();
    //选择口味
    flavor(isMilkCoffee, isSweetTooth);
    //搅拌
    stir();
}

private void flavor(boolean isMilkCoffee, boolean isSweetTooth) {
    if (isMilkCoffee) {
        pourMilk();
    }
    if (isSweetTooth) {
        addSugar();
    }
}

private void pourCoffeePowder(CoffeeType type) {
    if (type == CAPPUCCINO) {
        pourCappuccinoPowder();
    }
    else if (type == BLACK) {
        pourBlackPowder();
    }
    else if (type == MOCHA) {
        pourMochaPowder();
    }
    else if (type == LATTE) {
        pourLattePowder();
    }
    else if (type == ESPRESSO) {
        pourEspressoPowder();
    }
}

重构后的makeCoffee()又重新变得整洁如初了,满足SLAP实际上是构筑了代码结构的金字塔。金字塔结构是一种自上而下的,符合人类思维逻辑的表达方式。

在构筑金字塔的过程中,要求金字塔的每一层要属于同一个逻辑范畴、同一个抽象层次。在这一点上,金字塔原理和SLAP是相通的,世界就是如此奇妙,很多道理在不同的领域同样适用。