0.分页情景介绍
相信有过开发经验的java程序员对“分页”这个词一定不陌生。我们在编写DAO层代码的时候,对需要分页查询的字段通常会加上offset
和limit
这两个参数,这样便能从数据库中取出某一段数据。
本文向您阐述如何利用设计模式中的各种基本设计原理,去将java中的分页操作封装成一个统一的类或者说一组接口。
以MyBatis为例,如果我们有一个user表,页面上每次需要展示n个用户我们会这么写:
/**
* @param type 用户类型,admin、normal,等等。
*/
@Select({"select", select_field, "from", table_name, "where type = #{type} limit = #{limit} offset = #{offset}"})
List<User> selectUserByPage(int limit, int offset, int type);
在Service等代码上层就可以直接调用selectUserByPage
方法取得某一部分user数据。
好了,基础的部分到这里就介绍完毕了。
有没有更加优雅的方法,对分页这项操作进行封装呢?直接调用方法并传值这点并没有什么不对,但是进行过分页操作的同学都知道,分页还需要一些额外的步骤,比如:一共有多少数据?一共能分多少页?当前是多少页?当前的页面含有哪些数据?等等,如果每次分页都需要这么操作一次的话,那就进行了太多次重复的编码。
1.封装相同的部分。
我们看看哪些东西是相同的:
- 数据总数
- 总页数
- 每页含有多少个数据
- 当前页数
- 当前数据
现在就已经确定了需要封装的数据项。由于不同分页含有的数据类型是不同的,所以这里很自然的想到,之后我们肯定需要泛型。
2.面向接口编程&封装相同的部分。
我们观察到,所有的分页操作其实都是一个含有offset
和limit
参数的方法调用,并且通常会返回一个List
。我们可以简单将一个分页方法抽象为:
List<T> query(long limit, long offset);
这个方法就是所有分页操作的相同部分。不管是什么样的分页,都可以用这个方法来进行描述。至于分页中的其他参数,(例如在上面的MyBatis的例子中,我们分页选择User的时候传入了type参数。)我们不用管他们,因为他们跟“分页”这项操作没有任何关系,我们只关心分页操作。
其次,我们推荐面向接口编程而不是面向过程和实现编程。
自然的,我们将这个方法封装到一个接口里面:
public interface Method <T> {
List<T> query(long limit, long offset);
}
3.对分页操作的抽象
上面我们已经对分页操作的数据、分页的调用方法进行了抽象,最后我们需要对我们的分页操作进行抽象。
仔细观察我们的分页操作:最基本的分页操作就是,先到第一页,再一页一页的往后翻…等等,这不就是迭代器么吗?!但是常见的翻页操作还有:我们的分页操作一般是先取得第一个页面,然后再有上一页、尾页、以及直接跳转到第几页(想想在一般的网站上看到的分页是否是这些操作)等操作,这些Iterator
接口没有提供的方法,我们自己扩展就好了,没有太大的关系。不如我们就基于Iterator
接口来试试看吧!
4.想好了封装策略,接下来编码!
我给这个封装类起名为Pager
:
public class Pager<T> implements Iterator<List<T>> {
/**
* 每页多少个数据
*/
private long itemsPerPage;
/**
* 页数,由 itemsPerPage 和 totalItems 算出。
*/
private long totalPages;
/**
* 数据总量。 在构造时传入,一般由DAO中 "select count(*)" 的方法调用获得。
*/
private long totalItems;
/**
* 目前的页面下标 - 1,用于迭代器迭代的游标,用于内部计算。
*/
private long cursor;
/**
* 目前的页面下标。
*/
private long currentPage;
/**
* 封装的翻页操作实体的 offset 参数。
*/
private long offset;
/**
* 封装的翻页操作实体的 limit 参数,绝大多数情况下等于 itemsPerPage。
*/
private long limit;
/**
* 当前页面的数据
*/
private List<T> currentItems;
/**
* 翻页操作实体的封装接口。
*/
private Method<T> query;
public Pager(long itemsPerPage, long totalItems, Method<T> query) {
validateItemsPerPage(itemsPerPage);
validateTotalItems(totalItems);
this.itemsPerPage = itemsPerPage;
this.totalItems = totalItems;
this.query = query;
totalPages = (totalItems + itemsPerPage - 1) / itemsPerPage;
offset = 0;
limit = itemsPerPage;
currentPage = 1;
cursor = 0;
currentItems = query.query(limit, offset);
}
public List<T> getCurrentItems() {
return currentItems;
}
@Override
public boolean hasNext() {
return cursor != totalPages;
}
@Override
public List<T> next() {
return page(++cursor);
}
@Override
public void remove() {
throw new UnsupportedOperationException("remove");
}
public List<T> first() {
cursor = 0;
return next();
}
public List<T> last() {
cursor = totalPages - 1;
return next();
}
public List<T> page(long pageNumber) {
if (pageNumber > totalPages) {
throw new NoSuchElementException("页面越界");
} else if (pageNumber < 1) {
throw new NoSuchElementException("pageNumber不能小于1");
}
if (currentPage == pageNumber) {
return (currentItems);
}
if (currentPage == 0 && pageNumber == 1) {
return (currentItems);
}
setOffset(pageNumber);
currentItems = query.query(limit, offset);
currentPage = pageNumber;
return (currentItems);
}
private void setOffset(long pageNumber) {
offset = (pageNumber - 1) * itemsPerPage;
}
private void validateTotalItems(long totalItems) {
if (totalItems < 0) {
throw new IllegalArgumentException("totalItems不能小于零");
}
}
private void validateItemsPerPage(long itemsPerPage) {
if (itemsPerPage < 0) {
throw new IllegalArgumentException("itemsPerPage不能小于零");
}
if (itemsPerPage == 0) {
throw new IllegalArgumentException("itemsPerPage不能等于零");
}
}
/* --- properties --- */
/**
* Properties 对外部类属性的封装。
*/
public class Properties {
public long getItemsPerPage() {
return itemsPerPage;
}
public long getTotalPages() {
return totalPages;
}
public long getTotalItems() {
return totalItems;
}
public long getCurrentPage() {
return currentPage;
}
public long getOffset() {
return offset;
}
public long getLimit() {
return limit;
}
public List<T> getCurrentItems() {
return currentItems;
}
public Method<T> getQuery() {
return query;
}
}
}
这段代码比较简单,唯一需要注意的地方就是,我们加入了一个
private Method<T> query;
成员变量。熟悉设计模式的朋友们,这里应该使用的是策略模式,不同的Method的具体实现封装了具体的分页操作算法。只要这个算法是分页操作,可以传入offset
和limit
参数,并返回一个List<E>
数据类型,那么就可以去实现这个接口。其他的小的编程习惯,例如抛出异常、对属性的封装等,不是这篇文章的重点,不在这里赘述。(不过还是提一嘴,这里用了一个内部类去封装外部类的属性,属于一个没什么用的操作,是我自己觉得很多属性方法比较乱,所以放到了内部类中。)
你看!我们实现了Iterator
接口,并扩展了下一页、首页、尾页、以及直接跳转到第n页的所有操作(这里没有实现前一页操作,感兴趣的可以自己实现~)。
这个类真正干活的是public List<T> page(long pageNumber)
方法,其他所有的操作都调用了这个方法,我们很容易的通过分页的固定操作,算出limit
和offset
的值,然后,真正的分页迭代操作委托给了query.query(limit, offset);
方法。
5.怎么用?
好了,骚操作来了,怎么用呢?我在这里写了两个测试类:
public class TestService {
private static List<Integer> a = new ArrayList<>(128);
static {
for (int i = 0; i < 100; i++) {
a.add(i);
}
}
//常规的分页操作
public List<Integer> get(int limit, int offset) {
return a.subList(offset, limit + offset);
}
public int count(){
return a.size();
}
//封装之后的分页操作
public Pager<Integer> getByPager(int itemsPerPage) {
return new Pager<>(itemsPerPage, count(), (limit, offset) -> get((int) limit, (int) offset));
}
}
下面这一句可能有点难度:
return new Pager<>(itemsPerPage, count(), (limit, offset) -> get((int) limit, (int) offset));
这里面使用了java8的语法,实际上就是实现了一个匿名内部类,然后调用了外部类的方法,需要一定的java基础才能看懂。
这个类用来模拟Spring框架中的Service层。最后以文章开始的MyBatis例子看一个实际操作:
//UserService.java
@Service
public class UserService {
@Autowired
private UserDAO userDAO;
public List<User> getUserByPage(int limit, int offset, int type) {
return userDAO.selectUserByPage(limit, offset, type);
}
public int getUserCount() {
return userDAO.getUserCount();
}
public Pager<User> getUserPager(int itemsPerPage, int type) {
return new Pager<>(itemsPerPage, getUserCount(), (limit, offset) -> getUserByPage((int) limit, (int) offset, type));
}
}
关于Pager的具体使用没有列出例子,不过相信仔细看了上诉源码的同学不可能不知道。关于Pager类的具体的实用性,仁者见仁智者见智,java封装的思想和设计模式的原则才是这篇文章最想表达的。