设计模式——2_3 迭代器(Iterator)

生活就像一颗巧克力,你永远不知道下一颗是什么味道

——《阿甘正传》

文章目录

  • 定义
  • 图纸
  • 一个例子:假如你的供应商提供了不同类型的返回值
    • 单独的遍历流程
      • 实现
  • 碎碎念
    • 如果读写同时进行会发生啥?
    • 外部迭代和内部迭代
    • 迭代器和其他模式
      • 迭代器和组合
      • 迭代器和状态

定义

提供一种方法顺序访问一个聚合对象中的各个元素,而又不需要暴露该对象的内部表示

迭代器模式,又称为游标模式。所以迭代器和游标本质上是一回事,虽然在各个语言中的具体实现和命名会有差异

比如Java里面的容器类有的那个东西叫迭代器、但是操作数据库时的那个ResultSet我们又将他称之为游标,但本质上他们是一回事

迭代器在Java和.net环境中的应用随处可见,两者的默认容器的实现中都使用的迭代器模式,而且他们的foreach遍历,本质上都是对迭代器遍历写法的简化



图纸

在这里插入图片描述



一个例子:假如你的供应商提供了不同类型的返回值

这个名字好,不去写本日系轻小说都可惜了,恨不得一个标题就把内容全剧透完

虽然这个标题又俗又长,但是这种情况其实非常常见。至少在鄙人目前还不算长的职业生涯里,几乎每次换设备供应商都会有 这种情况的出现。因为这玩意也没个标准,一些老式设备返还的甚至都不是json。所以这个例子算抛砖引玉,但我由衷地希望 各位道友不需要考虑这样的问题

准备好了吗?这次的例子开始了:


假定你的公司升级考勤机,从原来的磁卡考勤机,换成人脸考勤机。但是磁卡考勤机不会直接报废,因为某些特殊位置依然可以使用他。这样一来,你每次读取数据的时候就要同时读取两种考勤机上的数据。

接着,我们根据两种考勤机的API,构造出这样一个结构:

在这里插入图片描述

我们定义了一个 DataReader(数据读取器) 接口用于定义可以从考勤机硬件上获取到的信息(比如考勤机号、ip地址等数据),在里面我们通过 read() 方法读取考勤机上的数据,并把读取到的数据以返回值的形式返回给 client代码

但是在写 DataReader 的具体实现的时候,出现了一个很严重的问题,我们发现A类型的考勤机API和B类型的考勤机API,在读取考勤机上的数据时,提供了完全不同的返回值

为了简化所以使用了 String 类型作为返回值,但是实际情况考勤机返回的一定是各家考勤机自己定义数据格式的原始数据。这时候一般做法是:定义一个包含所需信息的数据bean,并在 read() 方法中把读取到的原始数据转换成你自己定义的bean对象


这就很尴尬了,不同的集合类型,我们没办法用相同的方式去遍历他们,这就意味着在操作A考勤机的返回值时我们要这样写:

for(int i = 0;i<list.size();i++){String element = list.get(i);//对element做的操作
}

而当考勤机是B时,我们要这样写:

for(int i = 0;i<array.lenth;i++){String element = array[i];//对element做的操作
}

现在我们的需求是:无论从哪里读取上来的数据,对element做的操作 这部分是一致的,不一致的是对容器对象的遍历方式

也就是说,我们需要一个方案,可以把对容器对象的遍历过程进行解耦


单独的遍历流程

迭代器模式 这时候是你的最优选,具体的做法是这样的:

在这里插入图片描述

实现

/*** 数据读取器*/
public interface DataReader {/*** 从考勤机上读取记录和各类信息*/void read();/*** 创建对应的迭代器*/Iterator<String> createIterator();
}/*** 迭代器*/
public interface Iterator<E> {/*** 还有下一个吗?** @return true:有*/boolean hasNext();/*** 下一条记录*/E next();
}/*** A类型的数据读取器*/
public class ADataReader implements DataReader {//从考勤机里读取上来的记录private List<String> data;@Overridepublic void read() {System.out.println("A类型考勤机正在从考勤机里读取数据。。。");data = new ArrayList<>();for (int i = 0; i < 5; i++) {data.add(i + "");}System.out.println("A类型考勤机读取完毕");}@Overridepublic Iterator<String> createIterator() {if (data == null) {throw new RuntimeException("还没有执行对考勤机的读取");} else {return new AIterator();}}class AIterator implements Iterator<String> {private int cursor = 0;@Overridepublic boolean hasNext() {return cursor < data.size();}@Overridepublic String next() {return data.get(cursor++);}}
}/*** B类型的数据读取器*/
public class BDataReader implements DataReader {private String[] data;@Overridepublic void read() {System.out.println("B类型考勤机正在从考勤机里读取数据。。。");data = new String[5];for (int i = 0; i < 5; ) {data[i] = (char) (++i + 96) + "";}System.out.println("B类型考勤机读取完毕");}@Overridepublic Iterator<String> createIterator() {if (data == null) {throw new RuntimeException("还没有执行对考勤机的读取");} else {return new BIterator();}}class BIterator implements Iterator<String> {private int cursor = 0;@Overridepublic boolean hasNext() {return cursor < data.length;}@Overridepublic String next() {return data[cursor++];}}
}

使用迭代器模式后,我们把 read 方法的职能进一步细化了,这个方法从此只负责从硬件上读取对应的数据到内存中,并存储在对应的 DataReader 内的 data 属性中

接着我们通过定义 createIterator 方法,对不同 DataReader 中不同 data 类型的遍历过程进行了解耦。这些变化被封装到我们定义的 Iterator 迭代器类树中(为了方便迭代器对底层容器的访问,我们还把两个AB两个迭代器写成了ABDataReader的子类)

至此 client代码 在遍历从考勤机中读取到的数据时不再关心底层的数据到底是以何种结构聚合在一起,而我们的程序将来如果要面对新的容器类型时,也只是新增一个 Iterator 子类的工作量而已


而这正是一个标准的迭代器实现



碎碎念

如果读写同时进行会发生啥?

但上面的实现存在一个问题:如果 client代码 调用迭代器进行迭代到一半,我又调用了一次 read 刷新了被迭代的容器。这时候迭代器的状态就失效了,可能会出现迭代越界,又或者重复迭代/漏迭代的问题

这是很危险的,所以一个健壮的迭代器应该可以保证即便在迭代过程中对容器进行修改也不会出现这么危险的行为


如果你在Java或者C#中对被迭代的容器进行修改,那么当你调用下一个next方法时会得到一个异常。为了实现这样的效果,一种常用的手法就是在迭代器被创建出来的时候向被遍历的容器注册自身,而容器在修改自身的时候可以向迭代器发出通知,就像这样(以A为例):

/*** A类型的数据读取器*/
public class ADataReader implements DataReader {//从考勤机里读取上来的记录private List<String> data; //进行注册的迭代器列表private final Set<AIterator> iteratorSet = new HashSet<>();@Overridepublic void read() {//把所有当前注册的迭代器都置为无效for (AIterator aIterator : iteratorSet) {aIterator.isUsable = false;//无效化}iteratorSet.clear();//清空注册列表,以便资源回收System.out.println("A类型考勤机正在从考勤机里读取数据。。。");data = new ArrayList<>();for (int i = 0; i < 5; i++) {data.add(i + "");}System.out.println("A类型考勤机读取完毕");}@Overridepublic Iterator<String> createIterator() {if (data == null) {throw new RuntimeException("还没有执行对考勤机的读取");} else {AIterator iterator = new AIterator();iteratorSet.add(iterator);//注册迭代器return iterator;}}class AIterator implements Iterator<String> {private boolean isUsable = true;private int cursor = 0;@Overridepublic boolean hasNext() {if (isUsable) {return cursor < data.size();} else {throw new RuntimeException("此迭代器已失效");}}@Overridepublic String next() {if (isUsable) {return data.get(cursor++);} else {throw new RuntimeException("此迭代器已失效");}}}
}

在改良后的代码里,我们为 AIterator 引入了 isUsable(是否可用) 的概念。只有当这个值为true的时候,迭代器里的方法才能正常调用,否则会抛出一个异常

然后我们在 ADataReader 中新增了一个 iteratorSet 用于记录当前生效的这一批迭代器

接着,当我们每次调用 read 试图更新 data 的值的时候都会把 iteratorSet 里注册的所有迭代器的可用性变为false

这就实现了我们对迭代器的保护


外部迭代和内部迭代

在上例中,我们封装了不同类型的容器的遍历过程,以实现 client代码 可以随时随地用自己喜欢的方式遍历 DataReader 中的内容

事实上还有一种解决方案可以在不使用 Iterator 的情况下实现和上例同样的效果,而且可以不让 client代码 随意遍历 DataReader 里的内容,就像这样(以B为例):


/*** 数据操作手柄*/
public interface DataHandler<E> {/*** 具体的对数据的操作方法*/void handle(E e);
}/*** B类型的数据读取器*/
public class BDataReader {public void read(DataHandler<String> dataHandler) {String[] data = new String[5];for (int i = 0; i < data.length; ) {data[i] = (char) (++i + 96) + "";}for (int i = 0; i < data.length; i++) {dataHandler.handle(data[i]);}}
}

这段代码很简单,看出门道了吗?

对,他封装的区域正好跟上例是相反的

在上例中,我们封装了容器遍历的过程;而本例中,我们封装的是对每个具体元素要做的操作

我们定义了一个 DataHandler(数据手柄) 用于盛放我们要对每个具体元素要做的操作,然后把这个操作作为参数传递给 read 方法,我们把遍历的过程依然保留在 read 方法中,抽离出对每个元素具体的操作,这样也能实现我们的需求

这种写法在四人组的设计模式中把他称之为内部迭代器,因为迭代的过程依然被保留在容器的内部。但是事实上这种写法远不只是在迭代器中被使用,我们封装一段操作,然后把这段操作作为参数传给某个方法;然后当方法执行到符合某个条件时调用这段操作。

当你需要在很多操作的执行前和执行后执行一些固定的操作,而这些操作又不值当你用装饰者模式时,这种写法就是你的最优解。而且在JavaScript中,他还有个专有名词——回调函数



迭代器和其他模式

迭代器和组合

组合模式本身就是多个对象的聚合,所以应用组合模式的对象大多数都会使用迭代器模式


迭代器和状态

在我们改写A类型考勤读取器时,我们通过一个变量的不同状态来控制同一个迭代器的相同接口的行为,这本质上就是状态模式的一种应用




万分感谢您看完这篇文章,如果您喜欢这篇文章,欢迎点赞、收藏。还可以通过专栏,查看更多与【设计模式】有关的内容

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/722580.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

vue 之 Quill编辑器封装

安装 npm install vue-quill-editor --save <template><div><el-upload:action"uploadUrl":before-upload"handleBeforeUpload":on-success"handleUploadSuccess":on-error"handleUploadError"name"file":sh…

字节同事问我:我的Postman为什么连不了数据库?

postman本身没有数据库连接功能&#xff0c;所以用到了node.js中的xmysql实现Rest API的生成&#xff0c;利用postman进行请求&#xff0c;获取需要的数据&#xff0c;来做数据准备或断言。 1 安装 安装node.js&#xff1a;要求版本大于等于7.6 首先保证你的环境上有node.js…

JavaScript观察者模式:实现对象间的事件通信!

&#x1f90d; 前端开发工程师、技术日更博主、已过CET6 &#x1f368; 阿珊和她的猫_CSDN博客专家、23年度博客之星前端领域TOP1 &#x1f560; 牛客高级专题作者、打造专栏《前端面试必备》 、《2024面试高频手撕题》 &#x1f35a; 蓝桥云课签约作者、上架课程《Vue.js 和 E…

Minio容器化部署并整合SpringBoot

1、启动minio容器 docker run -p 9000:9000 -p 9090:9090 --name minio -d --restartalways -e MINIO_ACCESS_KEYminio -e MINIO_SECRET_KEYminio -v /usr/local/minio/data:/data -v /usr/local/minio/config:/root/.minio minio/minio server /data --console-addr…

循环队列:一道使数据结构萌新知道什么是“愁滋味“的题目

这破题目肝了我一天半才搞明白,也正是因为这道题目,我才豁然明白了李煜所说的"剪不断,理还乱...别是一般滋味在心头"到底是什么"滋味".在完全搞明白之前,真的是放有放不下,理也理不清... 但是理解之后你会发现,嘛い---,也就那么个回事嘛O(∩_∩)O 目录 1…

三国野史秘闻翻译视频剪辑 条条爆品 一条视频增粉1w (附888G素材内容)

我将为大家分享一个全新的主题——三国野史秘闻。这个主题本身就充满了趣味性&#xff0c;再加上我们独特的解读&#xff0c;由于粉丝们对此类内容非常热衷&#xff0c;因此很容易在评论区引发热烈讨论&#xff0c;这使得我们的短视频有很大的机会在抖音上走红。 项目 地 址 &…

详解C#之WinForm版利用RichTextBox 制作文本编辑器【附源码】

在Windows应用程序开发中&#xff0c;刚刚介绍了WPF版的利用RichTextBox实现文本编辑器&#xff0c;今天继续推出WinForm版的利用RichTextBox实现文本编辑器。本文利用一个简单的小例子&#xff0c;简述如何在WinForm开发中&#xff0c;利用RichTextBox开发文本编辑器&#xff…

数电票又增新票种 百望云率先推出机动车数电票解决方案

经过两年多的试点推广&#xff0c;数电票工程已经覆盖全国大部分省区市&#xff08;含计划单列市&#xff09;&#xff0c;随着系统建设的深入&#xff0c;不仅是应用功能逐渐完善&#xff0c;对票种的支持也走向全面。 2023年11月天津市正式开启机动车数电票的开具功能&#x…

SpringBoot自定义注解+反射实现 excel 导入的数据组装及字段校验

在前段时间的开发工作中&#xff0c;接手了一个很简单&#xff0c;很普通的开发任务。 要求实现一个单表的基础数据的批量导入功能。 评估下来&#xff0c;用户每次批量导入的数据量也就几千条&#xff0c;也不大。 是不是很简单&#xff0c;没有骗你们吧。但是呢&#xff0…

TB-03F-Kit 基础AT指令的使用

文章目录 前言一、使用前准备1. 确保TB-03F-Kit已经连接到电脑&#xff0c;并且已经烧录好固件。2. 准备号调试助手&#xff0c;并且电脑上已经安装好了串口驱动3. 打开串口调试助手并复位模组 二、指令介绍三、基础指令1. 准备测试2. AT 测试指令3. ATHELP 查看指令4. ATRST 模…

阻塞队列介绍

阻塞队列 kafka是目前来说性能最好的消息队列服务器&#xff0c;能处理TB级别的数据 作用:点赞、评论时&#xff0c;服务器会自动给某个用户发送通知 kafka是个框架&#xff0c;如果不用框架还要解决类似问题&#xff0c;就要用到阻塞队列 BlockingQueue 阻塞队列就是一个…

AI加速引擎PAI-TorchAcc:整体介绍与性能概述

作者&#xff1a;沈雯婷、黄奕桐、艾宝乐、王昂、李永 1、简介 PAI-TorchAcc(Torch Accelerator)是阿里云人工智能平台开发的Pytorch上的大模型训练加速框架。 PAI-TorchAcc提供了一套基于Pytorch的简洁、易用的接口&#xff0c;无需进行模型转换就可以无缝地接入HuggingFac…

Vue+OpenLayers7入门到实战目录

前言 本篇作为《VueOpenLayers7入门到实战》所有文章的二合一汇总目录&#xff0c;方便查找。 本专栏源码是由OpenLayers7.x版本结合Vue框架编写。 本专栏从Vue搭建脚手架到如何引入OpenLayers7依赖的每一步详细新手教程&#xff0c;再到通过各种入门案例和综合性的实战案例&a…

基于springboot+vue的体育馆管理系统

博主主页&#xff1a;猫头鹰源码 博主简介&#xff1a;Java领域优质创作者、CSDN博客专家、阿里云专家博主、公司架构师、全网粉丝5万、专注Java技术领域和毕业设计项目实战&#xff0c;欢迎高校老师\讲师\同行交流合作 ​主要内容&#xff1a;毕业设计(Javaweb项目|小程序|Pyt…

已经连接过github远程库,如何再次推送及删除远程库的内容

基于上次将文件推送到已经建好的github远程库上&#xff0c;此篇文章主要介绍如何再次推送文件去直接已经连接过的远程库&#xff0c;以此如何删除远程库中不想要的文件。 一、推送文件到远程库 1.将所需推送的文件拉入本地库所建的文件夹下&#xff1a;{ex&#xff1a;JVM相…

java面试(消息队列MQ)

MQ有什么用&#xff1f;有哪些场景&#xff1f; MQ&#xff08;MessageQueue&#xff09;消息队列。队列&#xff08;FIFO&#xff09;先进先出的数据结构&#xff0c;消息由生产者发送到MQ&#xff0c;后由消费者对消息进行处理。QQ&#xff0c;微信就是MQ场景。 MQ作用&…

搜索准确性提升 20%,Jina Reranker 成为 RAG 优化的新标杆!

在整合大型语言模型&#xff08;LLM&#xff09;到业务流程时&#xff0c;企业经常会遇到一些头疼的问题&#xff0c;比如怎样保持数据时效性、避免幻觉现象&#xff0c;以及如何保护数据安全等等。为了解决这些问题&#xff0c;检索增强生成&#xff08;RAG&#xff09;技术应…

盘点国内大厂的10个AI创作工具,看看你都用过哪些?

国内大厂的 AI 创作工具&#xff0c;目前已经非常多了&#xff0c;而且有很多都是大家耳熟能详的。 下面整理了一些&#xff0c;包含 AI 绘画、AI 视频、AI 智能体、AI 大模型等多个方向的国内大厂 AI 创作工具。 发现有几款 AI 工具&#xff0c;还真的非常好用。看看这些 AI…

深入浅出Redis(三):Redis数据的存储、删除以及淘汰

引言 Redis是一款基于键值对的数据结构存储系统&#xff0c;它的特点是基于内存操作、单线程处理命令、IO多路复用模型处理网络请求、键值对存储与简单丰富的数据结构等等 本篇文章不像以往文章围绕Redis某个特点来讲解&#xff0c;而是作为过渡介绍&#xff0c;来说一说Redi…

基于springboot的大型商场应急预案管理系统论文

大型商场应急预案管理系统 摘要 随着信息技术在管理上越来越深入而广泛的应用&#xff0c;管理信息系统的实施在技术上已逐步成熟。本文介绍了大型商场应急预案管理系统的开发全过程。通过分析大型商场应急预案管理系统管理的不足&#xff0c;创建了一个计算机管理大型商场应急…