设计模式:迭代器模式

211 阅读8分钟
文章内容输出来源:拉勾教育大数据高薪训练营

通过我这次在 Java 预科班的“回炉重造”(不得不说这两个月时间的预科内容是真的十足!),勾起了我对设计模式的一些兴趣及思考,紧接着就安排自己记录一下自己曾经模糊的一些概念和知识。

结合课程和一些书籍资料,本文将着重讲述迭代器模式。

迭代器模式是一种行为设计模式,可让我们遍历集合的元素而无需暴露其基础表示(列表,堆栈,树等)。

行为型模式:关注系统中对象之间的相互交互,研究系统在运行时对象之间的相互通信和协作,进一步明确对象的职责。

问题

集合是编程中最常用的数据类型之一。尽管如此,集合只是一组对象的容器而已。

                                                                 各种类型的集合。

大多数的集合是使用简单的列表存储元素,但有些集合还会使用栈、树、图等复杂的数据结构。

无论集合的构成方式如何,它都必须提供某种访问元素的方式,便于其它代码使用其中的元素。集合应提供一种能够遍历元素的方式,且保证它不会周而复始地访问同一元素。

如果集合是基于列表的,这听起来很容易,只需要遍历所有元素。但是,如何遍历复杂数据结构(例如树)中的元素呢?

例如,今天需要使用深度优先算法来遍历树结构,明天可能会需要广度优先算法;下次又会需要其它方式(比如随机存取树中的元素)。

                                                   可通过不同的方式遍历相同的集合。

不断向集合中添加遍历算法会模糊其 “高效存储数据” 的主要职责。 此外, 有些算法可能是根据特定应用订制的, 将其加入泛型集合类中会显得非常奇怪。

另一方面, 使用多种集合的客户端代码可能并不关心存储数据的方式。 不过由于集合提供不同的元素访问方式, 你的代码将不得不与特定集合类进行耦合。

解决方案

迭代器模式的主要思想是将集合的遍历行为抽取为单独的迭代器对象。

                         迭代器可实现多种遍历算法。 多个迭代器对象可同时遍历同一个集合。

除实现自身算法外, 迭代器还封装了遍历操作的所有细节, 例如当前位置和末尾剩余元素的数量。 因此, 多个迭代器可以在相互独立的情况下同时访问集合。

迭代器通常会提供一个获取集合元素的基本方法。 客户端可不断调用该方法直至它不返回任何内容, 这意味着迭代器已经遍历了所有元素。

所有迭代器必须实现相同的接口。 这样一来, 只要有合适的迭代器, 客户端代码就能兼容任何类型的集合或遍历算法。 如果你需要采用特殊方式来遍历集合, 只需创建一个新的迭代器类即可, 无需对集合或客户端进行修改。

现实中的类比

                                                            在罗马漫步的各种方式。

当我们计划在罗马参观几个主要景点,发现可能会迷失方向并且浪费了大量的时间盘旋甚至最后连斗兽场都找不到。

一方面,我们可以用手机购买虚拟指南App并将其用于导航。这种方式即智能又便宜,可以根据所需来使用它。

另一方面,可以话费一定的预算,在当地雇一名向导,他对这个城市了如指掌。该向导会根据我们的喜好定制游览路线,向我们展示每个经典,并讲述着每个故事。唯一的代价就是要花费更多的钱。

所有这些选择(自由漫步、 手机导航或真人向导)都是这个由众多罗马景点组成的集合的迭代器。

结构体

  1. 迭代器 (Iterator) 接口声明了遍历集合所需的操作: 获取下一个元素、 获取当前位置和重新开始迭代等。

  2. 具体迭代器 (Concrete Iterators) 实现遍历集合的一种特定算法。 迭代器对象必须跟踪自身遍历的进度。 这使得多个迭代器可以相互独立地遍历同一集合。

  3. 集合 (Collection) 接口声明一个或多个方法来获取与集合兼容的迭代器。 请注意, 返回方法的类型必须被声明为迭代器接口, 因此具体集合可以返回各种不同种类的迭代器。

  4. 具体集合 (Concrete Collections) 会在客户端请求迭代器时返回一个特定的具体迭代器类实体。 你可能会琢磨, 剩下的集合代码在什么地方呢? 不用担心, 它也会在同一个类中。 只是这些细节对于实际模式来说并不重要, 所以我们将其省略了而已。

  5. 客户端 (Client) 通过集合和迭代器的接口与两者进行交互。 这样一来客户端无需与具体类进行耦合, 允许同一客户端代码使用各种不同的集合和迭代器。

客户端通常不会自行创建迭代器, 而是会从集合中获取。 但在特定情况下, 客户端可以直接创建一个迭代器 (例如当客户端需要自定义特殊迭代器时)。

适用性

🤨 集合背后为复杂的数据结构, 且你希望对客户端隐藏其复杂性时 (出于使用便利性或安全性的考虑), 可以使用迭代器模式。

迭代器封装了与复杂数据结构进行交互的细节, 为客户端提供多个访问集合元素的简单方法。 这种方式不仅对客户端来说非常方便, 而且能避免客户端在直接与集合交互时执行错误或有害的操作, 从而起到保护集合的作用。

🤨 使用该模式可以减少程序中重复的遍历代码。

重要迭代算法的代码往往体积非常庞大。 当这些代码被放置在程序业务逻辑中时, 它会让原始代码的职责模糊不清, 降低其可维护性。 因此, 将遍历代码移到特定的迭代器中可使程序代码更加精炼和简洁。

🤨 如果希望代码能够遍历不同的甚至是无法预知的数据结构, 可以使用迭代器模式。

该模式为集合和迭代器提供了一些公共接口。如果在写代码中使用了这些接口,那么将其它实现了这些接口的集合和迭代器传递给它时,它仍将可以正常运行。

如何实施

  1. 声明迭代器接口。该接口必须提供至少一个方法来获取集合中的下个元素。但为了使用方便,还可以添加一些其他方法,例如获取前一个元素、记录当前位置和判断迭代是否已结束。

  2. 声明集合接口并描述一个获取迭代器的方法。其返回值必须是迭代器接口。如果计划拥有多组不同的迭代器,则可以声明多个类似的方法。

  3. 为希望使用迭代器进行遍历的集合实现具体迭代器类。迭代器对象必须与单个集合实体链接。链接关系通常通过迭代器的构造函数建立。

  4. 在集合类中实现集合接口。其主要思想是针对特定集合为客户端代码提供创建迭代器的快捷方式。集合对象必须将自身传递给迭代器的构造函数来创建两者之间的链接。

  5. 检查客户端代码,使用迭代器替代所有集合遍历代码。每当客户端需要遍历集合元素时都会获取一个新的迭代器。

⚖️ 利 弊

✅ 单一责任原则。可以通过将大量遍历算法提取到单独的类中来清理客户端代码和集合。

开闭原则。你可实现新型的集合和迭代器并将其传递给现有代码,无需修改现有代码。

✅ 可以并行遍历同一集合,因为每个迭代器对象都包含其自身的遍历状态。

✅ 可以暂停遍历并在需要时继续。

❎ 如果程序只与简单的集合进行交互,应用该模式可能会矫枉过正。

❎ 对于某些特殊集合,使用迭代器可能比直接遍历的效率低。