Java使用设计模式对分页功能进行封装实战

版权声明:转载需注明出处!! https://blog.csdn.net/u012101920/article/details/82825279

0.分页情景介绍

相信有过开发经验的java程序员对“分页”这个词一定不陌生。我们在编写DAO层代码的时候,对需要分页查询的字段通常会加上offsetlimit这两个参数,这样便能从数据库中取出某一段数据。
本文向您阐述如何利用设计模式中的各种基本设计原理,去将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.面向接口编程&封装相同的部分。

我们观察到,所有的分页操作其实都是一个含有offsetlimit参数的方法调用,并且通常会返回一个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的具体实现封装了具体的分页操作算法。只要这个算法是分页操作,可以传入offsetlimit参数,并返回一个List<E>数据类型,那么就可以去实现这个接口。其他的小的编程习惯,例如抛出异常、对属性的封装等,不是这篇文章的重点,不在这里赘述。(不过还是提一嘴,这里用了一个内部类去封装外部类的属性,属于一个没什么用的操作,是我自己觉得很多属性方法比较乱,所以放到了内部类中。)
你看!我们实现了Iterator接口,并扩展了下一页、首页、尾页、以及直接跳转到第n页的所有操作(这里没有实现前一页操作,感兴趣的可以自己实现~)。
这个类真正干活的是public List<T> page(long pageNumber)方法,其他所有的操作都调用了这个方法,我们很容易的通过分页的固定操作,算出limitoffset的值,然后,真正的分页迭代操作委托给了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封装的思想和设计模式的原则才是这篇文章最想表达的。

猜你喜欢

转载自blog.csdn.net/u012101920/article/details/82825279