全量查询与分页查询合二为一的思考

统一业务场景的查询,有时候会全量查询和分页查询都需要的情况。一般情况下,会让一个类提供两个方法,一个完成全量查询,一个完成分页查询。比如这样:
1. 全量查询方法: list query(Conditon condition);
2. 分页查询方法:list query(Condition condition, int pageNum, int pageSize);

一个方法实现分页查询和全量查询

上文已经提到只提供全量查询的服务,扩展不出物理分页的查询功能。那么只提供分页查询方法,能够实现出全量查询的功能吗?回答是肯定的。

比如只有list query(Condition condition, int pageNum, int pageSize),怎么能够全量查询呢?有两种方法:
1. 方法一:分页方法只需实现分页逻辑即可:我们可以pageNum传1,pageSizeInteger.MAX_VALUE。这么做对数据和方法是有要求的,所以是有风险的。
- 数据:数据的总条数不能多于Integer.MAX_VALUE
- 方法:不能对pageSize大小有限制
2. 方法二:我们知道正常逻辑下,pageNumpageSize不能小于1,那么可以约定,当传参pageNumpageSize有一个小于1时,就做全量查询,否则就做分页查询。相当于,我们把全量查询方法和分页查询方法合二为一了,一个方法实现两套逻辑,好处是没有风险。(也可以把pageNumpageSize都使用包装类Integer,当有一个为null时做全量查询,道理是一样的)

方法一是有风险的,方法二是在一个方法即实现了全量又实现了分页。以上方法都是对pageNumpageSize传参做要求,当满足条件时,就是全量查询。总之我们能够做到一个方法既能全量,又能分页。下文我们称这类方法为合二为一

合二为一的优势分析

下面分别从方法定义方和方法使用方说说其优势:
1. 定义方:如果采用的是方法一,只需实现分页逻辑即可,因为其把(pageNum=1,pageSize=Integer.MAX_VALUE)的分页查询视作是全量查询
2. 使用方:全量和分页,只需要熟悉一个方法的使用就行了

合二为一的劣势分析

下面分别从方法定义方和方法使用方说说其劣处:
1. 定义方
根据单一职责原则,一个函数只做一件事且做好这件事。这里,其实违背了单一职责原则。
1. 采用方法一:
要求数据的总条数不能多于Integer.MAX_VALUE,不能对pageSize大小有限制。
2. 采用方法二:
一个方法需要针对参数不同实现两套逻辑,混杂在一起容易混乱。
p.s. 当然内部可以使用Extract Method重构手法,将分页和全量抽取不同的私有方法,降低复杂度。然而这已经违背我们初衷了,初衷是想一个方法提供两种服务,但其实您却在内部已经实现了两种查询服务,却不开放。
2. 使用方
全量和分页,虽然只需要熟悉一个方法的使用就行了,但这个方法是复杂的,不易使用的。理由如下
1. 两个方法,就是二选一,全量就用A,分页就用B;
合二为一,通过参数来区分,理论上,int的可能取值有Integer.MAX_VALUE - Integer.MIN_VALUE + 1
pageNumpageSize加起来的取值就有(Integer.MAX_VALUE - Integer.MIN_VALUE + 1) * (Integer.MAX_VALUE - Integer.MIN_VALUE + 1) = 18446744073709551616种可能性,我们必须使用文档约束哪些可能性是用于全量查询的。对于使用方来说,他需要查看文档说明,才知道究竟怎么传参才是全量查询。而且这种约束不是强制性的,好比我现在是要做全量查询的,本该传(-1, -1)的,却不经意写成了(1, 1),然而写错了编译也不报错,使用不到编译器强制保证提前报错的好处 ;运行时也不会出错,因为其能返回一页的数据(用户可能会误以为,这一页的数据就已经是全部的数据了)。(如果是使用包装类Integer,可能性就更多了)
2. 再看一下调用方式
“`java
//第一种方法
query(condition, 1, Integer.MAX_VALUE);
//第二种方法
query(condition, -1, -1);
query(condition, null, null);

    //如果不合二为一,多定义一个全量查询方法
    query(condition);
    //或
    queryAll(condition);
    ```
    只看代码:
    第一种方法还更好一点,但是也需要想一会儿,才领会到该使用方大概是想获取全量数据的吧?然而该方法支持一次获取这么大量吗?总数据量超过了`Integer.MAX_VALUE`呢?
    第二种方式的调用,我们完全看不出使用方是在做一次全量查询,而且这种调用方式好像是在故意使坏,好像是试图诱发程序的bug,因为明明第-1页每页-1条的数据页根本就不存在,`null`就更离谱了。
    我们看`query(conditon)`,它除了`conditon`没有更多的参数,没有分页参数传入,我们就会想这应该是做全量查询的吧?如果方法名明确使用`queryAll`就更加明晰了,我们就有更大的把握,这是在做一次全量查询的。如果方法名是`queryAll`,却做了返回部分数据的事,那方法就名不副实了,这是定义方的错误,要极力避免的。
3. 再看以下方法调用,这里我们使用传`null`代表全量
    ```java
    @RestController
    public class Controller {
        @Autowired
        private Service service;

        @GetMapping(path="/getAll")
        public List getAll(@ModelAttribute Condition condition) {
            return service.query(condition, null, null);
        }
    }

    @Service
    public class Service {
        @Autowired
        private Dao dao;

        public List query(Condition condition, Integer pageNum, Integer pageSize) {
            ...
            dao.query(condition, pageNum, pageSize);
            ...
        }
    }

    @Repository
    public class Dao {
        // 合二为一
        public List query(Condition condition, Integer pageNum, Integer pageSize) {
            if (pageNum == null || pageSize == null) {
                //全量
            } else {
                //分页
            }
        }
    }
    ```
    `Service.query()`是合二为一的,`Dao.query()`也是合二为一的,`Service.query()`只是调用`Dao.query()`实现自己的合二为一逻辑。这里只有两层这种调用,但可以想象,如果都采用合二为一,实际开发中,可能会有好多层。
    我们只看中间层,比如在这里,`Service.query()`中对`Dao.query()`的调用,我们就不明确调用的意图是分页还是全量,得向前追查。实参的定义与真正要用参数的最底层`query()`也隔开了距离,为了调试全量查询的bug,我们得往前查找,看究竟是哪层的调用出现了传参错误。
     
    如果两个方法,一个全量,一个分页呢:
    ```java
    @RestController
    public class Controller {
        @Autowired
        private Service service;

        @GetMapping(path="/getAll")
        public List getAll(@ModelAttribute Condition condition) {
            return service.queryAll(condition);
        }
    }

    @Service
    public class Service {
        @Autowired
        private Dao dao;

        //分页
        public List queryPage(Condition condition, Integer pageNum, Integer pageSize) {
            ...
            dao.queryPage(condition, pageNum, pageSize);
            ...
        }

        //全量
        public List queryAll(Condition condition) {
            ...
            dao.queryAll(condition);
            ...
        }
    }

    @Repository
    public class Dao {
        //分页
        public List queryPage(Condition condition, Integer pageNum, Integer pageSize) {
        }

        //全量
        public List queryAll(Condition condition) {
        }
    }
    ```
    再看这里的中间层,是不是就很明晰了呢?

推荐

定义一个方法就是为了使用的,优劣更应该考虑的是使用方的感受。阅读代码的时间也远比写代码的时间更多。个人觉得,定义两个方法(一个全量,一个分页),然后再加上使用语义清晰的方法名,是值得的,整体上优于只用一个方法既能全量又能分页的。

实际上很多jdk源码或开源源码都做了完成类似功能定义多个方法的做法。这里举两个jdk中的例子:

// 虽然只是简单调用另一个wait,但是定义了一个新的wait
public class Object {

    /**
     * 一直等待
     */
    public final void wait() throws InterruptedException {
        wait(0);
    }

    /**
     * 等待tomeout时间,如果超时,不再等待。如果timeout为0,一直等待
     */
    public final native void wait(long timeout) throws InterruptedException;
}

// 虽然只是简单调用另一个parseInt,但是定义了一个新的parseInt
public class Integer {

    /**
     * 转换10进制
     */
    public static int parseInt(String s) throws NumberFormatException {
        return parseInt(s,10);
    }

    /**
     * 指定进制转换
     */
    public static int parseInt(String s, int radix) {
        ...
    }
}

如果jdk不提供不带参wait()方法,我们的所有代码都需要使用wait(0)这样调用。如果确定就是永久等待,调用wait(0)虽然实现了相同的功能,但比wait()却让我们脑子先转一个弯,去想参数的意义。

虽然jdk增加了不带参的wait()parseInt方法,增多了jdk的代码量,但我们使用者使用的时候是减少了我们代码量,因为不用传参了。

当然,带来的好处,更多的比代码量要重要的多。比如parseInt, 虽说jdk既有parseInt(String s)又有parseInt(String s, int radix)。如果您更喜欢更加通用的带参的parseInt(String s, int radix)的方法,即使是10进制格式String转int时,完全可以忽视parseInt(String s),每次这样调用:

Integer.parseInt("17895341", 10)`;
Integer.parseInt("45612", 10)`;

然而,针对大多数情况下String转int时radix都是10的这种事实,jdk还是定义了不带radix参数(默认是10)的int parseInt(String s)方法,把更通用的int parseInt(String s, int radix)反而作为了例外
参数少总是好于参数多,如果不仅多还是同一类型的就更加讨厌。比如query(2, 5, 6, 1, 6)

你可能感兴趣的:(web,java)