设为首页 加入收藏

TOP

改善代码可测性的若干技巧(四)
2018-01-11 06:06:47 】 浏览:589
Tags:改善 代码 若干 技巧
t, String cf, List<String> columns) { return StreamUtil.map( rowKeyList, rowKey -> { String rowKeyNotEmpty = (rowKey == null ? "null" : rowKey); Get get = new Get(Bytes.toBytes(rowKeyNotEmpty)); if (columns != null && !columns.isEmpty()) { for (String col: columns) { get.addColumn(Bytes.toBytes(cf), Bytes.toBytes(col)); } } return get; }); } private List<Result> buildResult(List<String> rowKeyList, Result[] results, boolean allowNull) { List<Result> rsList = new ArrayList<>(); for (int i = 0; i < rowKeyList.size(); i++) { if (!allowNull && isResultEmpty(results[i])) { logger.warn("cant't get record for rowkey:{}", rowKeyList.get(i)); continue; } rsList.add(results[i]); } return rsList; }

重构后的代码中,(tableName, rowKeyList, cfName, columns, allowNull) 这些原子性参数都聚合到参数对象 hbaseFetchParamObject 中,大幅减少了方法参数个数。现在,getRows(hbaseFetchParamObject, getFromHbaseFunc) 这个从Hbase获取数据的核心函数变成无依赖外部的纯函数了,可以更容易滴单测,而原来的方法则变成了一个接口不变的外壳供外部调用。 这说明了, 任何一个依赖外部服务的非纯函数,总可以分为一个不依赖外部服务的具备核心逻辑的纯函数和一个调用外部服务的壳函数。而单测正是针对这个具备核心逻辑的纯函数。

此外,将构建 gets 和 results 的逻辑分离出来,使得 getRows 流程更加清晰。现在 getRows(hbaseFetchParamObject, getFromHbaseFunc) , buildGets, buildResult 都是纯函数,对三者编写单测后,对从Hbase获取数据的基础函数的质量会更加自信了。

只要方法中的调用服务调用不多于2个(不包括调用方法中的服务依赖),都可以采用这种方法来解决单测的问题。

使用函数接口将外部依赖隔离。

代码模式

纵观业务系统里的代码,主要原子代码模式主要有五种:

  • 构建参数
  • 判断条件是否满足
  • 组装数据
  • 调用服务查询数据
  • 调用服务执行操作

前三者是可单测的,后两者是不可测的。而代码常常将前三者和后两者混杂在一起,必须想办法将其分离开。

依赖于外部服务的代码模式主要有如下五种:

  • 构建参数 – 判断条件满足后调用服务查询数据 – 判断逻辑或组装数据;
  • 构建参数 – 判断条件满足后调用服务执行操作 – 判断逻辑或组装数据;
  • 构建参数 – 判断条件满足后调用服务查询数据 – 判断逻辑或组装数据 – 判断条件满足后调用服务执行操作 – 判断逻辑或组装数据;
  • 构建参数 – 判断条件满足后调用服务执行操作 – 判断逻辑或组装数据 – 判断条件满足后调用服务查询数据 – 判断逻辑或组装数据;
  • 以上的任意可能的组合。

一般前四种都可以采用函数接口的方式来解耦外部依赖。

面向接口编程

面向接口编程有两层含义:类级别,面向接口编程; 方法级别,面向函数接口编程。

当要编写单测时,很容易编写接口的mock类或lambda表达式。 比如 A 对象依赖 B 对象里的 M 方法,而 M 方法会从数据库里读取数据。那么 A 就不要直接依赖 B 的实体类,而引用 B 的接口。 当对 A 编写单测时,只要注入 B 的 mock 实现即可。 同理,方法中含有 service 调用时,不要直接依赖 service 调用,而是依赖函数接口,在函数接口中传递 service 调用,如上面的做法。这样,编写单测时,只要传入 lambda 表达式返回mock数据即可。

假设有 m1, m2, m3 方法,m1调用m2, m2调用m3, m1, m2 都是纯函数, m3 会调用外部服务依赖。由于 m3 不纯以及调用关系,导致 m1, m2 也不纯。解耦的方法是面向函数接口编程。 m3 不依赖于外部服务,而是依赖函数接口。在 m3 的参数中提供一个函数接口,m1, m2 传入一个 lambda 表达式。如果 m1, m2 也有很多业务逻辑要测试,那么 m1, m2 也提供相同的函数接口传入服务依赖,直到某一层只是一层“壳函数”。 这样,含有业务逻辑的方法都可以方便地单测,而且更容易理解(函数接口表达了需要什么外部依赖), 而壳函数不需要单测。 当然,这需要对编程方式和习惯的一种改变,而目前大部分编程习惯就是直接在方法里调用service,看上去直观,却会导致方法耦合了外部依赖,难以单测。

小结

良好的编程习惯会带来可测性更佳的代码,对软件的质量和开发效率都有积极影响。代码语义化、分离通用逻辑、将实例状态放在参数中、参数对象、面向接口编程等都是一些小的技巧和做法,结合起来使用就能让代码表达更加容易理解和维护;而函数编程,则可以解耦外部服务依赖,分离出容易测试的具有核心业务逻辑的纯函数。

面向对象/函数式编程是非常强大的混合编程范式。面向对象提供了贴近现实的自然的表达方法,为应用系统提供一个优秀的外部视角; 而函数编程则着重于内部结构优化,可以让内部实现解耦得更加清晰。 两者是相辅相成的,而非对立的。

首页 上一页 1 2 3 4 下一页 尾页 4/4/4
】【打印繁体】【投稿】【收藏】 【推荐】【举报】【评论】 【关闭】 【返回顶部
上一篇Java Proxy 和 CGLIB 动态代理原理 下一篇200 行 Java 代码搞定计算器程序

最新文章

热门文章

Hot 文章

Python

C 语言

C++基础

大数据基础

linux编程基础

C/C++面试题目