设计模式——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;

    @Override
    public void read() {
        System.out.println("A类型考勤机正在从考勤机里读取数据。。。");
        data = new ArrayList<>();
        for (int i = 0; i < 5; i++) {
            data.add(i + "");
        }
        System.out.println("A类型考勤机读取完毕");
    }

    @Override
    public Iterator<String> createIterator() {
        if (data == null) {
            throw new RuntimeException("还没有执行对考勤机的读取");
        } else {
            return new AIterator();
        }
    }

    class AIterator implements Iterator<String> {
        private int cursor = 0;

        @Override
        public boolean hasNext() {
            return cursor < data.size();
        }

        @Override
        public String next() {
            return data.get(cursor++);
        }
    }
}

/**
 * B类型的数据读取器
 */
public class BDataReader implements DataReader {

    private String[] data;

    @Override
    public 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类型考勤机读取完毕");
    }

    @Override
    public Iterator<String> createIterator() {
        if (data == null) {
            throw new RuntimeException("还没有执行对考勤机的读取");
        } else {
            return new BIterator();
        }
    }

    class BIterator implements Iterator<String> {
        private int cursor = 0;

        @Override
        public boolean hasNext() {
            return cursor < data.length;
        }

        @Override
        public 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<>();

    @Override
    public 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类型考勤机读取完毕");
    }

    @Override
    public 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;

        @Override
        public boolean hasNext() {
            if (isUsable) {
                return cursor < data.size();
            } else {
                throw new RuntimeException("此迭代器已失效");
            }

        }

        @Override
        public 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类型考勤读取器时,我们通过一个变量的不同状态来控制同一个迭代器的相同接口的行为,这本质上就是状态模式的一种应用




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

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

AI从截图直接生成代码、前端程序员的福音

简介 项目可以将任何屏幕截图或设计转换为干净的代码&#xff08;支持大多数框架&#xff09;。来自领先公司的开发人员和设计师使用的排名第一的工具。完全开源&#xff0c;在 GitHub 上拥有超过 35,000 颗星。非常受欢迎。 各位小伙伴们感觉有帮助的&#xff0c;可以收藏一…

【促销定价】背后的算法技术3-数据挖掘分析

【促销定价】背后的算法技术3-数据挖掘分析 01 整体分析1&#xff09;整体概览2&#xff09;类别型特征概览3&#xff09;数值型特征概览 02 聚合分析1&#xff09;天维度2&#xff09;品维度3&#xff09;价格维度4&#xff09;数量维度 03 相关分析1&#xff09;1级品类2&…

指针中的回调函数与qsort的深度理解与模拟

今天给大家在更新一下指针类型的知识&#xff0c;这里讲到了一个库函数sqort&#xff0c;以及回调函数的理解。 望喜欢 目录 回调函数 qsort函数 qsort模拟实现 回调函数 回调函数就是⼀个通过函数指针调用的函数。 如果你把函数的指针&#xff08;地址&#xff09;作为参数…

利用文件实现进程间共享数据

概述 文件可以存储任何非结构化字节序列&#xff0c;这个比较简单&#xff0c;就一个写一个读&#xff1b;学习到此&#xff0c;留个记录&#xff0c;以后可以直接抄代码&#xff0c;哈哈 Demo代码 #include <fstream> #include <iostream> #include <thread&…

CMIP6数据处理方法与典型案例分析

气候变化对农业、生态系统、社会经济以及人类的生存与发展具有深远影响&#xff0c;是当前全球关注的核心议题之一。IPCC&#xff08;Intergovernmental Panel on Climate Change&#xff0c;政府间气候变化专门委员会&#xff09;的第六次评估报告明确&#xff1b;指出&#x…

职场卷王:我用可视化大屏模板做工作汇报,惊艳了同事和领导。

2023结束了&#xff0c;我和我的小伙伴们纷纷开始忙碌的年终总结和汇报。 正忙着汇总Excel数据、写word讲稿、找PPT模板时&#xff0c;我发现隔壁组的老王独自在大会议室偷偷调试起了那台汇报用的电视机。 不会吧不会吧&#xff0c;年终汇报还有一周呢&#xff0c;这家伙PPT都…

Java中文件的相关知识及文件IO操作

在我们日常生活中&#xff0c;会把许多东西都称之为文件。比如&#xff0c;一份纸质报告&#xff0c;或u盘中的一些文档&#xff0c;都会把它们称为文件。那么&#xff0c;这里说的文件是以操作系统的角度出发的。在操作系统中&#xff0c;会把许多硬件设备和软件资源都抽象成“…

Kafka | SpringBoot集成Kafka

SpringBoot集成Kafka 一、前言二、项目1. pom2. application.properties4. 消息生产者-测试5. 消息消费者 三、启动测试四、有总结的不对的地方/或者问题 请指正, 我在努力中 一、前言 该文章中主要对SpringBoot 集成Kafka 主要是 application.properties 与 pom坐标就算集成完…

win11系统中nginx简单的代理配置

一.背景 为了公司安排的师带徒任务。 操作系统版本&#xff1a;win11家庭版 nginx版本&#xff1a;1.24.0 二.配置代理 之前文章已经说明了nginx简单的安装&#xff0c;要看阅读这个文章哈。web服务器nginx下载及在win11的安装-CSDN博客 1.配置需求识别 前端服务nginx(80…

【面试题】webpack的五大核心、构建流程、性能优化

【面试题】webpack的五大核心、webpack的构建流程、webpack的性能优化 webpack是什么?webpack的五大核心webpack的构建流程webpack性能优化 webpack是什么? js静态模块打包工具。 功能 将多个文件打包成更小的文件&#xff0c;(压缩)翻译 babal-loader es6进行降级兼容。 …

低代码:数智化助力新农业发展

随着科技的飞速发展和数字化转型的深入推进&#xff0c;低代码开发平台正逐渐成为软件开发的热门话题。尤其在农业领域&#xff0c;低代码技术为传统农业注入了新的活力&#xff0c;助力新农业实现高效、智能的发展。 低代码开发平台的概念与特点 随着科技的飞速发展&#xff0…

猫咪冻干的价格差别为什么那么大?价格实惠的主食冻干分享

随着养猫科学知识的普及&#xff0c;越来越多的铲屎官选择更符合猫咪饮食天性的主食冻干喂养。尽管有些铲屎官因价格犹豫&#xff0c;但像我这样的资深铲屎官深知其益处。尽管其价格稍高于烘焙粮和膨化粮&#xff0c;但主食冻干为猫咪健康带来的实际好处是无法估量的。 对于像我…

代码学习记录11

随想录日记part11 t i m e &#xff1a; time&#xff1a; time&#xff1a; 2024.03.04 主要内容&#xff1a;今天的主要内容是深入了解栈和队列中比较难的题录类型&#xff1a;滑动窗口最大值与前 K K K 个高频元素&#xff0c;最后对于这三天学习的队列和栈的知识进行总结。…

结构体详解

结构体 什么是结构体 结构体是一种用户自定义的数据类型&#xff0c;可以组合多个相关值成为一个单一类型。它是由一批数据组合而成的结构型数据&#xff0c;结构体可以包含多个不同类型的字段&#xff0c;如基本数据类型、其他结构体、枚举类型等。在Rust中&#xff0c;结构…

Ubantu 18.04 配置固定IP

1.首先在终端里输入命令,将你的网关和ip&#xff0c;记下来 ifconfig 2. 执行命令&#xff1a; sudo gedit /etc/network/interfaces 3.在弹出来的框里输入 auto后面的就是网关&#xff0c;address是你虚拟机的ip&#xff0c;gateway是你的网关ip&#xff0c;netmask是你的子…

Python从0到100(二):Python语言介绍及第一个Pyhon程序

前言&#xff1a; 零基础学Python&#xff1a;Python从0到100最新最全教程。 想做这件事情很久了&#xff0c;这次我更新了自己所写过的所有博客&#xff0c;汇集成了Python从0到100&#xff0c;共一百节课&#xff0c;帮助大家一个月时间里从零基础到学习Python基础语法、Pyth…

如何通过抖捧轻松开启AI常态化自动直播间

在如今的互联网时代&#xff0c;短视频和直播已成为大多数企业与实体商家必备的经营技能&#xff0c;不只是全国头部的品牌&#xff0c;他们纷纷加码直播&#xff0c;更有一些已经开启了直播矩阵的体系&#xff0c;包括中小型的商家&#xff0c;他们也在考虑一件事情&#xff0…

前端接收流,并下载到本地

碰到一个大坑&#xff0c;附件文件存在华为云上&#xff0c;查询列表里记录的附件给了一个https开头的url&#xff0c;要求点击附件图标&#xff0c;下载附件到本地&#xff0c; 思路1.直接<a hrefurl downloadfileName >下载</a> 实际效果&#xff1a;跨域下载不…

Java批量修改文件目录名称(树行结构、批量重命名)

Java批量修改文件目录名称(树行结构、批量重命名) 1.读取某个路径的文件目录结构 2.递归批量修改目录文件前缀进行递增 3.结果截图 4.代码 package com.zfi.server.device;import java.io.File; import java.util.Arrays; import java.util.Comparator;public class FileTest…

【ArcPy】游标访问几何数据

访问质心坐标相关数据 结果展示 代码 import arcpy shppath r"C:\Users\admin\Desktop\excelfile\a2.shp" with arcpy.da.SearchCursor(shppath, ["SHAPE","SHAPEXY","SHAPETRUECENTROID","SHAPEX","SHAPEY",&q…