好的代码需要坚持哪些原则?

277 阅读5分钟

前言

这是一篇《代码精进之路》-技艺篇的读后感,知道这本书是在知乎偶尔看到了作者,阿里高级技术专家张建飞的帖子《一文教会你如何写复杂业务代码》,深有感触,于是买了本他的书回来看看。以下为读书笔记。

函数原则

1. 组合函数模式(Composed Method Pattern)

组合函数要求所有的公有(public)函数读起来像一系列执行步骤的概要,而这些步骤的真正实现细节是在私有函数里面。阅读这样的代码就像「在看一本书」,公有函数是目录,目录的内容指向各自的私有函数,而具体内容是在私有函数中实现的。

举个栗子,用JDBC连接数据库,拿到对象,最后加入列表。

public void populate() throws Exception  {
    Connection c = null;
    try {
        Class.forName(DRIVER_CLASS);
        c = DriverManager.getConnection(DB_URL, USER, PASSWORD);
        Statement stmt = c.createStatement();
        ResultSet rs = stmt.executeQuery(SQL_SELECT_PARTS);
        while (rs.next()) {
            Part p = new Part();
            p.setName(rs.getString("name"));
            p.setBrand(rs.getString("brand"));
            p.setRetailPrice(rs.getDouble("retail_price"));
            partList.add(p);
        }    
    } finally {
        c.close();
    }
}


这段代码并不复杂,只有十几行,但理解这段代码也需要花一定时间。那如果代码是几百行呢?那需要花多少时间理解?理解的不到位改出了bug最后背锅的还是你呀。

以下是我们通过组合函数模式进行重构,理解起来是不是容易了很多,它将连接数据库,获取对象,将对象加入列表,三件事封装成了三个函数。一个函数只做一件事情。代码语义非常清晰,出了bug也容易定位。

public void populate() throws Exception {
    Connection c = null;
    try {
        c = getDatabaseConnection();
        ResultSet rs = createResultSet(c);
        while (rs.next())
            addPartToListFromResultSet(rs);
    } finally {
        c.close();
    }
}
 
private ResultSet createResultSet(Connection c)
        throws SQLException {
    return c.createStatement().
            executeQuery(SQL_SELECT_PARTS);
}
 
private Connection getDatabaseConnection()
        throws ClassNotFoundException, SQLException {
    Connection c;
    Class.forName(DRIVER_CLASS);
    c = DriverManager.getConnection(DB_URL,
            "webuser", "webpass");
    return c;
}
 
private void addPartToListFromResultSet(ResultSet rs)
        throws SQLException {
    Part p = new Part();
    p.setName(rs.getString("name"));
    p.setBrand(rs.getString("brand"));
    p.setRetailPrice(rs.getDouble("retail_price"));
    partList.add(p);
}


2. SLAP 抽象层次一致性原则

SLAP要求函数体中的内容必须在同一个抽象层次上。如果高层次抽象和底层细节杂糅在一起,就会显得凌乱,难以理解。

举个栗子,制作咖啡分为3步,倒入咖啡粉,加入沸水,搅拌。

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


如果加入新的业务需求,允许选择不同的咖啡粉,是否加奶,代码就会变成这样。

public void makeCoffee(CoffeeTypeEnum coffeeType, boolean isMilkCoffee) {
        //选择咖啡粉类型
        if (coffeeType == CoffeeTypeEnum.LATTE) {
            pourLattePowder();
        } else if (coffeeType == CoffeeTypeEnum.MOCHA) {
            pourMochaPowder();
        }
        //加入沸水
        pourWater();
        //是否加奶
        if (isMilkCoffee) {
            pourMilk();
        }
        //搅拌
        stir();
    }


如果有更多需求,代码就会进一步恶化,变成难以维护的逻辑迷宫。问题的根源在于新的代码不再满足SLAP,选择咖啡粉与是否加奶和主流程步骤不在一个抽象层次上。按照组合函数和SLAP原则,我们在公有函数只显示业务处理的主要步骤,具体细节封装在私有方法中。一个函数的抽象只在同一水平。

public void makeCoffee(CoffeeTypeEnum coffeeType, boolean isMilkCoffee) {
        pourCoffeePowder(coffeeType);
        pourWater();
        flavor(isMilkCoffee);
        stir();
    }
    
    public void pourCoffeePowder(CoffeeTypeEnum coffeeType) {
        if (coffeeType == CoffeeTypeEnum.LATTE) {
            pourLattePowder();
        } else if (coffeeType == CoffeeTypeEnum.MOCHA) {
            pourMochaPowder();
        }
    }
    
    public void flavor(boolean isMilkCoffee){
        if (isMilkCoffee) {
            pourMilk();
        }
    }


重构后函数整洁如初,满足SLAP实际上是构建了代码结构的「金字塔」。在构筑金字塔的过程中,要求金字塔每一层都属于同一逻辑范畴,抽象层次。


3. Java语言一个函数不要超过20行代码

把长方法改成多个短方法,代码可读性会提高很多。超长方法是典型的代码坏味道。(我记得饿了吗有个核心系统Python主函数超过了1000行,最终导致无人敢动,直到被阿里收购后用Java重写)。


4. 函数参数数量应限制在3个以内

参数越少,越容易理解,函数也越容易测试,因为各种参数的不同组合的测试用例是一个笛卡尔积。如果函数需要3个以上的参数,就说明其中一些参数应该封装为类了。除非有足够特殊的理由,参数才能超过3个。


5. 函数单一职责

一个方法只做一件事。职责越单一,功能越内聚,就越有可能被复用。


命名原则

  • 当我们不能给一个模块,一个对象,一个函数,甚至一个变量找到合适名称的时候,往往说明我们对问题的理解还不够透彻,需要重新去挖掘问题的本质,对问题域进行重新分析和抽象。
  • 如果你无法想出一个合适的名字,很可能意味着代码的「坏味道」设计有问题,例如一个方法里是不是实现了太多的功能。
  • 变量名应该是名词,能够正确描述业务,有表达力。例如魔术数86400应该用常量SECONDS_PER_DAY表达。
  • 函数名要具体,要体现业务语义。空泛的命名没有意义例如processData()。好的命名例如validateUserCredentials()。
  • 小心注释。如果注释是为了阐述代码背后的意图,那么这个注释是有用的。如果注释只是复述代码功能,那就要小心,说明代码的表达能力不足。


其他

三次原则。当某个功能第三次出现时,就有必要着手「抽象化」,写出通用的解决方案。

统一的异常处理。坏的异常处理会使代码中到处充斥着异常捕获的 try/catch 的代码,搞乱了代码结构,把错误处理和正常流程混为一谈,严重影响了代码的可读性。

防止破窗。在软件工程中,破窗效应可谓屡见不鲜。面对一个混乱的系统和一段杂乱无章的代码,后人往往加入更多的垃圾代码。我们不要做「打破第一扇破窗」的人。其次,发现由破窗需要及时修复,不要让事情进一步恶化。


欢迎关注微信公众号《深入浅出Java源码》