每天认识一个设计模式-迭代器模式:解耦遍历与聚合的利器

一、前言:遍历的困境与模式价值​

在软件开发的世界里,对集合数据的遍历操作可谓是无处不在。无论是处理用户信息列表、订单数据集合,还是文件系统中的文件目录树,我们都常常需要遍历集合中的每一个元素,以完成诸如数据统计、数据处理、数据展示等各类任务。然而,在实际开发过程中,集合遍历并非总是一帆风顺,常常会遭遇一系列棘手的问题。:

Q1:遍历逻辑耦合​

最常见的问题之一就是遍历逻辑与集合本身的强耦合。以 Java 中的 ArrayList 为例,传统的遍历方式通常是使用 for 循环,通过索引来访问集合中的元素:

List<String> list = new ArrayList<>();
list.add("元素1");
list.add("元素2");
// 传统for循环遍历
for (int i = 0; i < list.size(); i++) {
    String element = list.get(i);
    // 处理元素
}

这种方式虽然简单直接,但存在明显的弊端。一旦集合的内部实现发生变化,比如将 ArrayList 替换为 LinkedList,由于 LinkedList 的随机访问性能较差,基于索引的遍历方式效率会大幅下降,此时就需要在代码中大量修改遍历逻辑,这无疑增加了代码的维护成本和出错风险。

Q2:异构集合处理困难​

当面对异构集合(即包含多种不同类型数据结构的集合)时,问题变得更加复杂。假设我们有一个系统,需要处理不同类型的数据存储,包括关系型数据库中的表格数据(可以抽象为二维数组形式的集合)、内存中的链表结构数据以及树形结构的文件目录数据。

我们定义一个简单的关系型数据库表格数据(用二维数组模拟):

# 模拟关系型数据库表格数据
table_data = [
    [1, "Alice", 25],
    [2, "Bob", 30],
    [3, "Charlie", 35]
]

遍历这种数据结构的代码如下:

# 遍历表格数据
for row in table_data:
    print(row)

 但是如果换成一个简单的链表结构:

# 定义链表节点类
class ListNode:
    def __init__(self, value):
        self.value = value
        self.next = None


# 构建链表 1 -> 2 -> 3
head = ListNode(1)
node2 = ListNode(2)
node3 = ListNode(3)
head.next = node2
node2.next = node3

那我们就又得换一种遍历方式:

# 遍历链表
current = head
while current:
    print(current.value)
    current = current.next

这表明如果没有统一的遍历方式,针对每种数据结构都需要编写不同的遍历代码,这不仅使得代码量剧增,而且使得系统的扩展性和维护性变得极差。比如,当需要新增一种数据结构时,又得重新编写一套遍历逻辑,这显然不符合软件设计中的开闭原则(对扩展开放,对修改关闭)。

这些问题的背后,隐藏着一个核心矛盾,那就是数据存储结构与遍历行为的强耦合。集合的内部实现(如数组、链表、树等不同的数据结构)决定了如何进行遍历,这使得遍历逻辑依赖于集合的具体类型,无法独立变化。当集合类型发生改变时,遍历逻辑也必须随之改变,这就像把两个原本应该独立发展的部分紧紧捆绑在了一起,牵一发而动全身。

而迭代器模式的出现,为解决上述问题提供了有效的方案。它的核心价值体现在两个关键方面:统一访问接口和解耦遍历逻辑。​

统一访问接口意味着无论集合的内部结构是怎样的,都可以通过一个统一的迭代器接口来进行遍历操作。就好比我们有不同品牌和型号的汽车(不同的数据结构集合),但只要它们都配备了统一规格的钥匙孔(迭代器接口),我们就可以使用一把通用的钥匙(迭代器)来启动和驾驶它们。

在 Java 中,各种集合类如 ArrayList、LinkedList、HashSet 等都实现了 Iterator 接口,这使得我们可以使用相同的代码来遍历不同类型的集合:

List<String> list = new ArrayList<>();
Set<String> set = new HashSet<>();
// 使用迭代器遍历List
Iterator<String> listIterator = list.iterator();
while (listIterator.hasNext()) {
    String element = listIterator.next();
    // 处理元素
}
// 使用迭代器遍历Set
Iterator<String> setIterator = set.iterator();
while (setIterator.hasNext()) {
    String element = setIterator.next();
    // 处理元素
}

通过这种方式,我们无需关心集合的具体类型,只需要使用迭代器提供的hasNext()和next()方法,就可以轻松遍历集合中的元素,大大提高了代码的通用性和可维护性。​

遍历逻辑解耦则是将遍历操作从集合对象中分离出来,交由迭代器对象来负责。集合对象只需要关心数据的存储和管理,而迭代器对象专注于遍历逻辑的实现。这就如同图书馆的书架(集合对象)只负责存放书籍,而读者(迭代器对象)可以自由地按照自己的方式(如从左到右、从右到左、按照类别等)来浏览书架上的书籍,两者相互独立,互不干扰。当需要改变遍历方式时,只需要修改迭代器的实现,而无需触动集合对象的代码,从而有效降低了代码的耦合度,提高了系统的扩展性和灵活性。​

迭代器模式通过统一访问接口和解耦遍历逻辑,为集合遍历带来了前所未有的便利和优势,它就像一把万能钥匙,打开了高效、灵活处理集合数据的大门,让我们在软件开发的道路上能够更加从容地应对各种复杂的集合遍历场景。让我们一起来仔细了解一下~

 二、迭代器模式的定义与结构解析

迭代器模式(Iterator Pattern)是 Java 和 .Net 编程环境中非常常用的设计模式。其核心定义为:提供一种方法顺序访问一个聚合对象中的各个元素,而又不需要暴露该对象的内部表示 。它就像是一个智能的导航器,在不窥探集合内部复杂结构的前提下,引导我们逐一访问集合中的每一个元素。​

从 UML 图的视角来看,迭代器模式主要由以下几个关键部分构成:

在上述 UML 图中,我们可以清晰地看到各个角色之间的关系和职责划分:

Aggregate(抽象聚合):定义了创建迭代器对象的接口,通常包括一个工厂方法用于创建迭代器对象。

它如同一个抽象的容器规范制定者,定义了创建迭代器的方法createIterator() 。这个方法就像是一把 “万能钥匙” 的模具,所有具体的聚合类都需要依据这个模具来打造属于自己的 “钥匙”(迭代器),从而为客户端提供统一的访问接口。

在 Java 的集合框架中,Collection接口就充当了抽象聚合的角色,它定义了iterator()方法,为所有实现该接口的具体集合类(如ArrayList、LinkedList等)提供了创建迭代器的统一规范。​

Iterator(抽象迭代器):定义了访问和遍历聚合对象中各个元素的方法,通常包括获取下一个元素、判断是否还有元素、获取当前位置等方法。

抽象迭代器是遍历操作的 “通用契约”,它声明了一系列用于遍历聚合对象的接口方法,其中最核心的是hasNext()和next()方法。hasNext()方法就像一个 “前方探测器”,用于判断聚合对象中是否还有下一个元素可供访问;next()方法则如同一个 “前进按钮”,用于返回当前元素并将迭代器的位置移动到下一个元素。此外,有些抽象迭代器还可能包含remove()等其他方法,用于在遍历过程中对元素进行操作。​

ConcreteAggregate(具体聚合):实现了聚合对象接口,负责创建具体的迭代器对象,并提供需要遍历的数据。

具体聚合是抽象聚合的实际执行者,它实现了抽象聚合中定义的创建迭代器的方法createIterator()。在实现过程中,它会根据自身的数据结构和存储方式,创建出一个与之适配的具体迭代器实例。

ArrayList类作为Collection接口的具体实现类,它实现了iterator()方法,返回一个ArrayList特有的迭代器对象,该迭代器能够根据ArrayList内部数组的结构特点,准确地遍历其中的每一个元素。​

ConcreteIterator(具体迭代器):实现了迭代器接口,负责对聚合对象进行遍历和访问,同时记录遍历的当前位置。

具体迭代器是遍历算法的真正实现者,它实现了抽象迭代器中定义的所有方法,完成对聚合对象的具体遍历操作。在遍历过程中,它会维护一个指向当前遍历位置的索引或指针,通过hasNext()和next()方法的协同工作,实现对聚合对象中元素的顺序访问。

ArrayList的迭代器实现类会使用一个整型变量来记录当前遍历的位置,在next()方法中,会先返回当前位置的元素,然后将位置索引加 1,以便下次访问下一个元素;而hasNext()方法则通过比较当前位置索引与集合大小,来判断是否还有下一个元素可供访问。

通过这四个核心角色的紧密协作,迭代器模式成功地将集合对象的遍历行为从集合自身中分离出来,实现了数据存储结构与遍历行为的解耦,为我们在软件开发中处理各种集合遍历场景提供了一种高效、灵活且优雅的解决方案。

三、迭代器模式在开源框架中的实践

3.1Java Collection 框架的 Iterator 接口体系​

Java Collection 框架是 Java 编程中广泛使用的集合框架,它提供了丰富的集合类,如ArrayList、LinkedList、HashSet、HashMap等。在这个框架中,迭代器模式得到了充分的应用,Iterator接口是迭代器模式的核心体现。​

Iterator接口定义了三个主要方法:hasNext()用于判断集合中是否还有下一个元素;next()用于返回集合中的下一个元素,并将迭代器的位置移动到下一个元素;remove()用于从集合中移除当前迭代器指向的元素(可选操作)。所有实现了Collection接口的集合类都必须实现iterator()方法,该方法返回一个实现了Iterator接口的迭代器对象,从而使得客户端可以通过统一的迭代器接口来遍历不同类型的集合。​

当我们使用ArrayList时,可以通过以下方式获取迭代器并进行遍历:

List<String> list = new ArrayList<>();
list.add("元素1");
list.add("元素2");
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
    String element = iterator.next();
    System.out.println(element);
}

通过这种方式,Java Collection 框架实现了数据存储结构与遍历行为的解耦,使得我们可以更加方便地操作集合数据,提高了代码的通用性和可维护性。

3.2Spring Data 的 Cursor 实现分页遍历

Spring Data 是 Spring 家族中用于简化数据库访问的框架,它提供了统一的编程模型来访问不同类型的数据库,如关系型数据库、NoSQL 数据库等。在 Spring Data 中,迭代器模式被应用于实现分页遍历功能,其中Cursor接口就是迭代器模式的一个具体应用。​

Cursor接口继承自Iterator接口,它提供了一种流式的、可分页的方式来遍历数据库查询结果。与传统的Iterator不同,Cursor在遍历过程中不会一次性将所有结果加载到内存中,而是按需从数据库中获取数据,这对于处理大量数据非常有效,可以大大减少内存的占用。

在使用 Spring Data MongoDB 进行分页查询时,可以通过以下方式使用Cursor:

import org.springframework.data.domain.Slice;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.data.mongodb.core.query.Sort;
import org.springframework.data.mongodb.core.query.Update;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
public class UserService {

    private final MongoTemplate mongoTemplate;

    public UserService(MongoTemplate mongoTemplate) {
        this.mongoTemplate = mongoTemplate;
    }

    public List<User> findUsersByPage(int page, int size) {
        Query query = new Query();
        query.with(Sort.by(Sort.Direction.ASC, "id"));
        query.skip((page - 1) * size);
        query.limit(size);

        // 使用Cursor进行分页遍历
        return mongoTemplate.find(query, User.class);
    }
}

这里mongoTemplate.find(query, User.class)方法返回一个List<User>,实际上它内部使用了Cursor来实现分页遍历。通过这种方式,Spring Data 实现了高效的分页查询功能,同时也体现了迭代器模式在数据库访问中的应用优势。

3.3MyBatis 的 RowBounds 分页处理

MyBatis 是一个优秀的持久层框架,它提供了灵活的 SQL 映射和数据访问功能。在 MyBatis 中,迭代器模式被应用于实现分页功能,RowBounds类就是迭代器模式的一个具体体现。​

RowBounds类用于在查询时指定分页的偏移量和每页的记录数,它可以在不修改 SQL 语句的情况下实现分页功能。通过RowBounds,MyBatis 可以将查询结果按照指定的分页参数进行分割,从而实现分页遍历。​

当我们在使用 MyBatis 进行分页查询时,可以通过以下方式使用RowBounds:

import org.apache.ibatis.session.RowBounds;
import org.mybatis.spring.SqlSessionTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
public class UserService {

    @Autowired
    private SqlSessionTemplate sqlSessionTemplate;

    public List<User> findUsersByPage(int page, int size) {
        int offset = (page - 1) * size;
        RowBounds rowBounds = new RowBounds(offset, size);
        return sqlSessionTemplate.selectList("UserMapper.findUsers", null, rowBounds);
    }
}

在上述代码中,sqlSessionTemplate.selectList("UserMapper.findUsers", null, rowBounds)方法通过RowBounds指定了分页参数,从而实现了分页查询。这种方式使得分页功能的实现更加灵活和方便,同时也体现了迭代器模式在 MyBatis 中的应用价值。

 3.4迭代器模式的使用建议

当涉及到处理多数据源聚合和复杂数据结构遍历时,迭代器模式是一个非常推荐的选择。​

在处理多数据源聚合时,不同的数据源可能具有不同的数据结构和访问方式,比如一个系统中可能同时包含关系型数据库、NoSQL 数据库和文件系统等数据源。通过使用迭代器模式,我们可以为每个数据源封装一个迭代器,然后将这些迭代器组合起来,实现对多个数据源数据的统一遍历。这样可以有效地解耦数据源的访问逻辑和业务逻辑,提高系统的可维护性和扩展性。​

对于复杂数据结构的遍历,如树形结构、图结构等,传统的遍历方式可能会非常复杂且难以维护。而迭代器模式可以将复杂的数据结构的遍历逻辑封装在迭代器中,为客户端提供一个简单统一的遍历接口。客户端只需要使用迭代器的hasNext()和next()方法,就可以轻松地遍历复杂数据结构中的元素,而无需关心数据结构的内部细节。

虽然迭代器模式在很多场景下都非常有用,但在简单集合遍历的情况下,直接使用语言内置迭代器即可,无需过度设计使用迭代器模式。​

在 Java 中,对于简单的集合遍历,如ArrayList、LinkedList等集合的遍历,使用语言内置的for - each循环或者Iterator接口已经非常方便和高效。例如:

List<String> list = new ArrayList<>();
list.add("元素1");
list.add("元素2");
// 使用for - each循环遍历
for (String element : list) {
    System.out.println(element);
}
// 使用Iterator接口遍历
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
    String element = iterator.next();
    System.out.println(element);
}

在这种情况下,如果再引入迭代器模式,可能会增加代码的复杂度和维护成本,因为需要定义额外的迭代器接口和实现类,反而得不偿失。

当然了为了进一步提升迭代器模式的功能和灵活性,大家可以结合访问者模式去实现复杂遍历逻辑。访问者模式主要用于在不改变对象结构的前提下,定义新的操作。当与迭代器模式结合时,可以在遍历集合元素的过程中,对每个元素执行不同的操作。这个我们后续再聊~

四、迭代器模式的基础应用:多数据源查询聚合

在当今复杂多变的企业级应用开发中,数据来源的多样性已成为常态。以一个电商业务系统为例,订单数据可能出于性能、架构设计等多方面的考虑,被分散存储在不同的数据源中。其中,近期高频访问的订单数据被缓存到 Redis 中,以减少数据库的压力,提高响应速度;历史订单数据则存储在 MySQL 数据库中,便于长期保存和复杂查询;而一些用于数据分析的订单数据,为了满足快速检索和分析的需求,被存储在 ElasticSearch 中。​

现在,业务上提出了一个需求:需要聚合查询这三个数据源中的订单数据,以便进行统一的处理和展示,比如生成全量订单报表、进行订单数据的综合分析等。此时我们就可以通过迭代器模式来构建业务,首先,我们定义一个Order类来表示订单数据,它包含了订单的各种属性,如订单编号、客户信息、订单金额、下单时间等。

public class Order {
    private Long orderId;
    private String customerName;
    private BigDecimal amount;
    private LocalDateTime orderTime;
    // 省略getter和setter方法
}

接下来,是核心迭代器的实现。MultiSourceOrderIterator类实现了Iterator<Order>接口,它负责统一遍历多个数据源中的订单数据。

import java.util.Iterator;
import java.util.List;

public class MultiSourceOrderIterator implements Iterator<Order> {
    private final List<DataSourceIterator> iterators;
    private int currentIndex = 0;

    public MultiSourceOrderIterator(List<DataSourceIterator> iterators) {
        this.iterators = iterators;
    }

    @Override
    public boolean hasNext() {
        while (currentIndex < iterators.size()) {
            if (iterators.get(currentIndex).hasNext()) {
                return true;
            }
            currentIndex++;
        }
        return false;
    }

    @Override
    public Order next() {
        while (currentIndex < iterators.size()) {
            if (iterators.get(currentIndex).hasNext()) {
                return iterators.get(currentIndex).next();
            }
            currentIndex++;
        }
        throw new NoSuchElementException();
    }
}

在上述代码中,MultiSourceOrderIterator持有一个DataSourceIterator列表,DataSourceIterator是一个自定义的接口,每个具体的数据源迭代器(如RedisOrderIterator、MySQLOrderIterator、ElasticSearchOrderIterator)都实现了这个接口。hasNext方法会依次检查每个数据源迭代器是否还有下一个元素,如果有则返回true;next方法会从当前数据源迭代器中获取下一个元素,如果当前数据源迭代器已遍历完,则切换到下一个数据源迭代器继续获取。​

然后,我们在 Spring Service 中使用这个迭代器来查询订单数据。

import org.springframework.stereotype.Service;

import java.util.Arrays;
import java.util.Iterator;

@Service
public class OrderQueryService {
    public Iterator<Order> queryOrders(SearchCondition condition) {
        List<DataSourceIterator> sources = Arrays.asList(
            new RedisOrderIterator(condition),
            new MySQLOrderIterator(condition),
            new ElasticSearchOrderIterator(condition)
        );
        return new MultiSourceOrderIterator(sources);
    }
}

这样我们在OrderQueryService中,queryOrders方法接收一个SearchCondition对象,用于封装查询条件,如订单时间范围、客户 ID 等。

方法内部创建了三个具体数据源的迭代器,并将它们传递给MultiSourceOrderIterator进行统一管理,最终返回一个可以遍历多数据源订单数据的迭代器。

通过迭代器封装不同数据源的遍历差异是整个实现的关键。每个数据源都有其独特的数据结构和查询方式,如 Redis 是基于键值对的缓存,MySQL 是关系型数据库,ElasticSearch 是搜索引擎,但通过实现统一的DataSourceIterator接口,将这些差异隐藏在迭代器内部,对外提供统一的遍历接口。​

客户端统一使用hasNext()和next()进行遍历,无需关心数据来自哪个具体的数据源以及数据源的内部实现细节。这使得代码具有更好的可维护性和扩展性,当需要更换数据源或者修改数据源的查询逻辑时,只需要在对应的迭代器实现类中进行修改,而不会影响到其他部分的代码。​

支持动态增加新的数据源类型也是该实现的一大优势。随着业务的发展,可能会有新的数据源加入,比如未来可能会将订单数据存储在 MongoDB 中。在这种情况下,我们只需要实现一个新的MongoDBOrderIterator类,实现DataSourceIterator接口,然后将其添加到OrderQueryService的数据源迭代器列表中,就可以轻松实现对新数据源的支持,而不需要对整体的遍历逻辑进行大规模的修改。

 五、总结

迭代器模式在软件开发领域有着不可忽视的核心价值,这些价值贯穿于整个软件生命周期,对代码的质量、可维护性和扩展性产生着深远的影响。​

解耦价值是迭代器模式的一大亮点。它巧妙地将集合对象与遍历行为分离开来,使得集合对象专注于数据的存储和管理,而迭代器则专职负责遍历逻辑的实现。

就像在一个大型图书馆中,书架负责存放书籍(集合对象的职责),而读者拿着借阅卡按照自己的方式在书架间穿梭寻找书籍(迭代器的遍历行为),书架和读者之间相互独立,互不干扰。当书架的摆放方式(集合对象的内部结构)发生改变时,读者(客户端代码)无需关心,依然可以通过借阅卡(迭代器)按照原来的方式借阅书籍(进行遍历操作),这极大地降低了代码的耦合度,提高了系统的灵活性和可维护性。​

扩展价值来看,迭代器模式为系统的扩展提供了广阔的空间。它支持透明地增加新的聚合类型,当业务发展需要引入新的数据结构或集合类型时,只需要实现相应的具体聚合类和具体迭代器类,就可以无缝地将新的聚合类型融入到现有的系统中,而不会对其他已有的代码产生影响。

例如,在一个电商系统中,最初订单数据存储在关系型数据库中,使用一种迭代器进行遍历。随着业务的拓展,引入了分布式缓存来存储高频访问的订单数据,此时只需要为分布式缓存订单数据实现新的迭代器,就可以轻松地将其纳入到订单数据的统一遍历体系中,无需对原有的订单遍历逻辑进行大规模修改。​

迭代器模式还具有重要的规范价值。它为集合访问提供了统一的接口,无论集合的内部实现是数组、链表还是其他复杂的数据结构,客户端都可以通过统一的迭代器接口(如hasNext()和next()方法)来进行遍历操作。

当然,迭代器模式并非完美无缺,它也存在一些局限性,在实际应用中需要我们谨慎权衡:

迭代器模式在一定程度上会增加系统的复杂度。对于简单的集合遍历场景,使用迭代器模式可能会显得过于繁琐。因为它需要定义额外的迭代器接口和实现类,引入更多的类和对象,这对于原本简单直接的遍历操作来说,反而增加了代码的复杂性和维护成本。比如,当我们只是需要遍历一个简单的数组,使用传统的for循环可能更加简洁高效,而引入迭代器模式则会使代码结构变得复杂,增加了不必要的学习和理解成本。​

遍历过程中的状态维护也是迭代器模式需要面对的一个问题。迭代器在遍历过程中需要维护当前的遍历位置等状态信息,这就要求在多线程环境下,需要额外的机制来保证状态的一致性和线程安全。如果处理不当,可能会出现数据不一致、并发修改异常等问题。

例如,当一个线程正在使用迭代器遍历集合时,另一个线程对集合进行了修改,就可能导致迭代器的状态出现混乱,从而引发程序错误。

迭代器模式为我们在集合遍历方面提供了强大的工具和思路,但设计模式的世界丰富多彩,还有众多其他优秀的设计模式等待我们去探索和学习。在后续的文章中,我们将继续深入剖析各种设计模式~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

深情不及里子

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值