likes
comments
collection
share

代码重构之路:编写方法时最易忽视的"问题"

作者站长头像
站长
· 阅读数 19

本系列文章皆在从记录日常重构项目代码中发现的一些"丑陋的代码",同时分享记录开发中容易忽视的问题和错误,带你规避Java开发中的各种"坑"。

思考,输出,沉淀。用通俗的语言陈述技术,让自己和他人都有所收获。 作者:毅航😜


前言

所以,下次再为变量命名时不妨问一问自己:这样的命名方式,是否真的能够体现业务所需含义。

事实上,为变量选取一个“合适”的名字是我们写出"可维护"代码的第一步。接下来,我们将分析通过变量构建方法时容易忽视的一些“问题”。

长参数列表

参数不仅仅是不同方法之间传递信息的方式,更是向方法提供输入数据的方式。换言之,参数的传递可以使得方法能够执行许多复杂的处理操作。

但当你编写方法时不知道有没有这样的感觉,有时为了快速完成需求,就简单的将所需的变量放在方法参数列表中。而后期随着项目的不断迭代,导致参数列表中的数据越变越多,有时甚至都需要换行来看。此处,我们以Mybaits中的 MapperBuilderAssistant类的addMappedStatement方法为例子来看看究竟什么是“长参数列表”:

MapperBuilderAssistant

public MappedStatement addMappedStatement(
    String id,
    SqlSource sqlSource,
    StatementType statementType,
    SqlCommandType sqlCommandType,
    Integer fetchSize,
    Integer timeout,
    String parameterMap,
    Class<?> parameterType,
    String resultMap,
    Class<?> resultType,
    ResultSetType resultSetType,
    boolean flushCache,
    boolean useCache,
    boolean resultOrdered,
    KeyGenerator keyGenerator,
    String keyProperty,
    String keyColumn,
    String databaseId,
    LanguageDriver lang,
    String resultSets) {
    
    // ... 省略其他无关逻辑
    }

如果你细数的话,上述方法有足足十二个参数信息。如果了解Mybatis的话,你应该知道,此处的逻辑重点在于构建一个MappedStatment对象,所传递参数也都是构建MappedStatement所必须的。事实上,由于其超长的参数列表,为初学者阅读Mybatis源码造成了一定的困惑。

进一步,在业务开发中,如果一个业务代码的参数列表写成这样,且变量名缺乏命名规范,这就导致分析清楚参数列表的这些参数的含义就需要耗费大量的时间。此外,还会导致:

  1. 测试困难:测试长参数列表的方法变得更加复杂。为了覆盖不同的参数组合,需要编写大量的测试用例,这会增加测试的工作量。

  2. 可扩展性问题:长参数列表使得方法的可扩展性变差。如果需要为方法添加更多的参数,代码复杂性将进一步增加。

  3. 记混参数传递顺序:长参数列表可能导致误解参数的顺序,从而引发错误。如果参数没有明确的名称或注释,很容易混淆它们的传递顺序。

针对长参数列表的问题,笔者有如下几条最佳实践:

  1. 使用对象封装参数:如果方法需要大量相关参数,可以考虑创建一个包含这些参数的对象,然后将对象作为方法的参数传递。这可以提高代码的清晰度,并将相关参数组织在一起。
  2. 使用默认参数值:如果某些参数的值在大多数情况下是相同的,可以为这些参数提供默认值,从而在调用方法时只需提供必要的参数。
  3. 拆分方法:如果方法参数列表过长,可能意味着方法承担了太多的功能。考虑将方法拆分为多个小方法,每个方法只接受一部分参数,以更好地组织代码。
  4. 注释和文档:如果实在迫不得已需要编写“长参数”方法,那么记得提供一份清晰的注释文档,以解释每个参数的作用和使用方式。

忽视逻辑拆分导致长函数

“长函数” 有着与 “长参数” 列表相同“痛点”,其也是我们在编写代码中最易忽视的一个问题。

不知道你在实际工作中遇到最长的函数有多长,反正笔者最近在维护项目时就接触到一个600多行的函数。其实作为开发者,我们实都知道,所谓的“长函数”就是指那些将大量的处理逻辑都塞在一个方法中行为,而这种行为将而导致方法的体量不断扩张。

“长函数” 的成因是多样的,但最本质的原因还是因为我们对这个“长”没有一个准确的定义,有的人可能认为100行代码就已经算长的了,而有的人却不以为然。事实上,只有我们脑海中有了对“长”的认识,我们才能在编写代码时想到对方法逻辑进行拆分处理。

如果你有分析过Nacos源码的话,你会发现Nacos源码中的方法行数一般不会超过一屏,在笔者看来这就是一种对于“长”的很好的定义。

方法中的重复代码

在实际开发中,我们无法避免的总会复制/粘贴某些现有的代码逻辑,或是对原有代码稍作修改,然后写几个简单的测试,如果达到预期则认为问题已经解决。然而,这种方法未来会埋下许多潜在的问题。

如果后期一旦需要更改或是已复制的代码中的任何一部分逻辑,就必须在所有相关的复制粘贴位置进行同样的更改。 而只要忽略了一处修改,就会在某个时刻引发潜在的问题,使情况变得非常尴尬。这就导致当初看似省事,省力的操作如今却变得十分费力,并且还使得代码十分难于维护。

事实上,复制/粘贴操作往往是重复代码产生的温床。杜绝这类问题发生一个最简单明了的建议就是:将所复用代码提取到一个独立的函数中,然后在需要的地方调用该函数。

判断逻辑不断嵌套

在开始分析之前,我们先来看一个判断逻辑不断嵌套的例子:

public double calculateFinalPayment(double orderAmount,
                        boolean isVIPCustomer, 
                        boolean isPromotionApplied) {
    double finalPayment = 0.0;

    if (orderAmount > 100.0) {
        finalPayment = orderAmount;

        if (isVIPCustomer) {
            finalPayment *= 0.9; // 10% 折扣
        }

        if (isPromotionApplied) {
            finalPayment *= 0.95; // 额外 5% 折扣
        }

        if (finalPayment > 150.0) {
            finalPayment -= 20.0; // 减去 20 元
        }
    } else {
        finalPayment = orderAmount;

        if (isVIPCustomer) {
            finalPayment *= 0.95; // 5% 折扣
        }

        if (isPromotionApplied) {
            finalPayment *= 0.98; // 额外 2% 折扣
        }

        if (finalPayment > 50.0) {
            finalPayment -= 10.0; // 减去 10 元
        }
    }

    return finalPayment;
}

上述例子模拟一个订单处理系统,其主要逻辑在于根据订单的金额和优惠条件来计算最终支付金额。首先,检查订单金额是否大于100元,然后根据VIP客户和促销是否应用进行不断嵌套的条件判断。如果订单金额大于100元,会进行一系列的折扣和减免操作,而如果订单金额小于等于100元,会执行不同的折扣和减免操作。

这种判断逻辑不断嵌套的情况,在小规模情况下还能够理解,但随着条件的增加和复杂性的提高,将会导致代码难以维护和测试。

事实上,为了改善这种代码中这种逻辑不断嵌套的情况,可以将条件逻辑提取成更清晰和可维护的方式,或是使用策略模式或将条件判断提取成独立的方法来对代码进行重构处理。

总结

本章我们对方法编写过程中容易忽视的:长参数列表、长函数、重复代码、判断逻辑嵌套等问题进行总结和归纳,意在提醒开发者注意自己代码中的味道。事实上,只有我们潜意识中有了这些概念,我们才能在实际开发中加以注意和修改。

其实代码的重构并不是一日之功,所以不要想着一下就写出复用性极强的代码,日常开发中我们还是应以完成功能需求为主要目标,当确保业务功能实现的基础上,再去追求代码的重构,千万不要本末倒置。