重温设计模式-迭代器模式

191 阅读5分钟

引子

对于Java开发来说,只要用过集合类的应该都对迭代器不陌生,对于我来说只是偶尔使用一下它所提供的API,直到一次碰到一个导出数据的需求让我非常头疼,大佬跟我提到了迭代器模式。下面就以我碰到的问题为例子聊聊迭代器模式。

迭代器模式原理

在正式聊场景之前我们先来大概了解一下迭代器模式,我们就以Java中的迭代器为例子,下面是我画的一个类图:

截屏2023-04-20 20.05.36.png 为了方便理解我去除了很多无关紧要的API,在Iterator中定义了遍历的方法,Itr是对Iterator的具体实现,ArrayList则实现了Iterable接口,这个接口的主要作用是提供一个迭代器对象。如果你对于这种结构感到很疑惑那就对了,我一开始也挺不能理解,但是有问题我们就解决问题,我们可以通过一些假设来进行推导。

你可以假设自己处在以下场景,你正在面试,面试官让你实现一个数组,里面除了基本的添加删除元素的方法之外,还需要提供用来迭代的方法,你很简单的构思之后不难想出抽出一个接口定义各种方法,然后定义一个类实现这个方法,如此一来我们的类结构如下:

截屏2023-04-20 20.27.50.png

你快速的写完了并且自信的给面试官看,面试官默然的扫了一眼之后平静的问了句:你的代码还有优化的空间吗?,于是你只能重新检视自己代码,首先你想到了单一职责原则:就一个类而言只有一个引起它变化的原因,数组的对元素的操作与迭代是完全不同的行为,可以将其拆分成两个接口,同时你意识到如果有多种数据的实现,其迭代器的具体可能会不同,索引你拆分出了Iterable返回一个迭代器对象,让数组实现提供其迭代器具体实现,这样你就实现了一个迭代器模式。

实践

聊完了迭代器模式,当然要动手实践一下,下面就来一起看看我碰到的问题。

业务场景

数据导出成Excel是一个很常见的功能,有许多非常好的工具包能够帮助我们实现,比如阿里的EasyExcel,hutool的导出工具包等等,但是我碰到的问题是,除了要支持数据的导出之外,还要对Excel的格式进行非常精细的控制,这让我无法使用现成的工具包,这让当时的我很头大,因为不仅仅要导出数据那么简单,还要记录下每一次导出的记录到数据库同时将导出的文件保存下来支持下载,我们先来梳理一下业务上的要求:

  1. 要能够精细化控制Excel格式。
  2. 要记录每次操作和存储文件。
  3. 要能够支持50W以上的数据导出。

不知道大家是否对这些问题有更好的解决方式,当时我听到这个需求时我的第一反应是将这些操作封装成一个公共方法因为导出,保存记录,文件的上传,这些都是固定流程并不会有变化,同时封装成方法还有一个好处就是不需要我去修改各个模块的导出功能(可以偷懒),只需要告诉各个模块的负责人让他们使用起来就可以了,那么这跟迭代器模式又有什么关系呢?下面就来介绍具体的实现。

具体实现

实现前的考虑

在正式开始实现之前,我们需要分析一下我们的设计过程会碰到什么问题,具体考量如下:

  1. 导出属于IO操作,尤其是导出数据量比较大时使用同步阻塞IO会等待较长时间,且很容易造成内存占用过多。我们可以使用异步,在后台进行处理,同时对于POI选择我们应当选择内存占用小的API(这个就不展开说了,感兴趣的可以自行搜索)。
  2. 对于业务方来说,他们所做的就是查询数据并且调用你的导出方法,所以我们需要考虑降低单次查询的数据量,减少数据库的压力。
  3. 至于对并发的应对,当时框架有现成的线程池,但是最好还是限制导出业务所使用的线程数量,防止并发量高时占用过的系统资源导致其它业务无法正常使用。

具体实现

迭代器
public interface ExportIterable<T,E> {

    ExportIterator<T> iterator(E e);

}

public interface ExportIterator<E> {

    Boolean hasNext();

    List<E> next();
}

使用方式

public class AccountExportImpl implements ExportIterator<LeAccountExportExcelDto> {

    @Override
    public Boolean hasNext() {
        //是否进行下一次查询逻辑
    }
    
    @Override
    public List<LeAccountExportExcelDto> next() {
        //获取数据
    }
}
public class AccountServiceImpl extends ServiceImpl<AccountMapper, Account> implements AccountService , ExportIterable<LeAccountExportExcelDto,AccountQueryReq>{
    @Override
    public ExportIterator<LeAccountExportExcelDto> iterator(AccountQueryReq e) {
       return new AccountExportImpl(accountService,e);
    }
}

上面是由业务方提供的具体实现,而我们的导出方法伪代码如下:

    public class ExcelExportUtil{
        
        public static <T>void exportExcel(Class clazz, ExportIterable<T> itr, String title) {
            //生成excel
            //数据库插入记录
            while(itr.hasNext()){
                List<T> list = itr.next();
                //导入文件写入操作
            }
            //文件上传
            //修改记录
        }

大致的逻辑就是如此,应用迭代器模式,我们很好的与业务解耦,不需要关心业务的具体实现,实现了数据导出。

总结与参考

通过这篇文章,我重温了迭代器模式,也对自己之前实现的业务进行了梳理,虽然没有给出具体的代码实现,但是讲述了大致的思路,当然我觉得上面的实现还有能改进的地方,如果读者有建议可以写在评论区中,最后希望能给有需要的小伙伴带来帮助。 下面是参考的一些资料:

  1. 《大话设计模式》
  2. 《Java设计模式(刘伟)》