GetData(JSONObject jsonObject) {
if (i == 0) { return new ArrayList<>(); }
i--;
return jsonObject.getJSONArray("list").toJavaList(Integer.class);
}
外部依赖引入源
综上例子,一个方法的外部依赖引入源主要有:
(1) 方法所在类的实例变量,在方法里引用就如同引用了可能被随时修改的全局变量,是非常破坏方法的纯粹性的;
(2) 方法所在类注入的Service, 在方法里使用就成了方法的外部依赖,往往要写Mock外部依赖的结果数据才能进行单测;
(3) 方法调用了依赖外部服务的下层方法,导致方法有间接依赖。
对于(1),含有业务逻辑的方法应当将实例变量作为函数参数; 对于 (2) 和 (3), 使用函数接口和lambda表达式隔离和模拟依赖服务。
不过这里有两个问题:
(1) 如果一个方法依赖了多个 service 或 多个方法,怎么办? 那就要传入多个 Function 参数了。 另一种办法是,遵循单一职责原则,尽量编写短小的只含有至多一个Service或方法依赖的方法。每个方法只做明确的一件事。 很多调用多个Service 或多个方法的方法,就是做了太多事情了,每件事都不彻底,导致每次扩展都要在一个方法里增加很多条件分支。
(2) 大量的函数接口和lambda表达式可能像回调一样,容易将人绕晕。因此,一个函数最多两个函数接口为宜。 而函数接口和lambda表达式的使用,需要整体策略来控制,保持工程的可理解性和可维护性。 毕竟,可测性只是工程质量的一个属性,不能过于追求一个属性而破坏其他属性。
工程的“版图”
一个工程里应当被划分为“两半版图”:版图A是依赖于各种外部服务的调用,版图B是不依赖于任何外部服务的独立业务方法和工具类。版图B中的独立业务方法充满着各种业务逻辑和判断,是容易编写单测的,而版图A是没有必要写单测的,因为里面没有逻辑。这样,我们将工程中的外部依赖“驱逐到”版图A,类似于第九区里的“外星人管理区”。
理想情况下,版图B应该是占90%的领土,版图A应该占10%的领土。不过,实际工程中正好相反,版图A占了90%的领土,版图B却被驱逐到util包下,只占10%,单测还往往被忽视。 怎么改造呢? 实际上也很简单: 一旦从A的业务方法 FA 中发现外部依赖,就抽离出一个独立方法 FB 来隔离外部依赖,放到版图B里,然后对 FB 进行仔细单测,而 FA 只作为一个壳或外观模式,通过联调来确保正确。
对外部依赖的隔离,使得更容易编写单测,更容易获得更高的单测覆盖率和单测质量。
此外,导致单测编写困难的另一个“罪魁祸首”,就是不好的编程习惯,将大量多个逻辑放在同一个方法里。这样,为了测试一个东西,要构造大量的对象;同时,对其中的子部分则不容易测试彻底,导致隐藏的BUG。
对于增强代码可测性的唯一建议就是: 拆解、隔离。
单测策略
并不是所有代码都需要写单测的。也不是所有代码用单测更有效率。 在我看来,如果是纯顺序的逻辑,可以通过接口测试来保证,尤其是对于那些依赖外部服务的单行调用,既无法写单测也不必要写单测。而对于具有条件分支、循环分支等的逻辑,则要尽可能隔离成独立方法或函数,从而更容易滴更有效率地单测。
单测并不需要100%的覆盖率,也不应当花费过度的成本去追求高的覆盖率。 100%的覆盖率也不代表质量杠杠滴。 在单测覆盖率和软件开发成本中,必须有一个平衡。更好的软件质量,应当是较高的单测覆盖率与适当的接口用例覆盖的双重护航而保障,而不是把注都押在单测上。
疑虑
当然,使用任何一种新方式,总会有疑虑的。
高阶函数不易掌握
使用函数接口,或者说高阶函数的写法,对于很多童鞋可能还很不适应。 不过,这种写法以后很可能会成为主流。 因为它便捷、安全,而且很容易产生通用化的方法。通过高阶框架函数以及许多自定义业务函数的反复组合,构建起整个软件。
事实上,高阶函数并不陌生。在 C 语言时代,就已经通过函数指针支持传入函数参数了。 因此,高阶函数,只是将函数指针“对象化”了,并不是新鲜玩意。
多出的方法
从上面的例子可以看到,每一个被改造的方法,最终会得到两个方法: 一个隔离了外部依赖的独立函数,一个依赖外部服务的单行调用。独立函数便于测试,而单行调用通常通过联调来保证OK。这对软件测试是个福音,不过对于程序员来说,会不会是额外的负担呢?可能取决于各自的选择吧。至少在我看来,多一个方法,却能够更方便地测试,甩掉繁重的mock单测框架,是非常值得的。此外,通常还能从中挖掘出更通用的方法,消除重复的业务代码,也是另一个好消息。
工程隐患
在生产环境的工程中大量使用函数接口和lambda表达式,是否有隐患呢?目前还没有确切证据。如果有了,可以不断积累经验,但不应当因噎废食。一种新技术、新方式,总要踩上若干坑,才能成为成熟的技术,将软件开发推向一个新的里程碑。
在我所负责的订单导出工程里,已经大量使用了函数接口和lambda表达式。如果运行不稳定,那么也可以得到第一手的资料。且让我们拭目以待。
自动生成单测
一旦我们尽可能将依赖外部服务的函数转化为“非依赖于外部服务的独立函数+外部服务的单行调用”,编写单测的工作就变成了对独立函数的单测。而独立函数的单测是可以自动生成的。后续会专门有一篇文章来谈到Java单测类模板的自动生成。目前仅仅谈及思路。
单测的编写模板无非是:解析方法签名; 创建对象; 设置对象值; 设置外部服务返回数据; 检测返回结果。 解析方法签名通过可以使用正则表达式;创建对象和设置对象属性,可使用java反射机制; 设置外部服务返回数据, 可创建简单的 lambda 表达式来模拟。