设计模式之迭代器模式

迭代器模式提供一种方法顺序访问一个聚合对象中的各个元素,而又不暴露其内部的表示。

解析这个定义:

  1. 聚合对象”,表示对象中有数组、集合、字典等数据类型属性。
  2. 顺序访问”,表示模式需提供一个遍历的方式。
  3. 不暴露其内部的表示”,表明不想让客户端知道太多内部细节。

综合来看,此模式的设计意图非常明确,将聚合对象的遍历访问职责封装抽离出来。

直接抛出 Iterator 接口和 next()hasNext() 方法有些“上帝视角”了,让我们抛开预设的实现,从零开始推导如何设计这个“抽离聚合对象访问”的模式,一步步探索最终如何自然演化到经典的迭代器结构。

第一步:明确核心问题

假设我们有一个聚合对象(比如一个自定义集合 MyCollection),内部用数组存储数据。现在需要让外部能遍历它的元素,但不允许直接暴露内部数组。如何设计?

初始代码(暴露内部实现,不符合封装原则):

class MyCollection {
    private String[] data = {"A", "B", "C"};

    // 直接返回内部数组(危险!)
    public String[] getData() {
        return data;
    }
}

// 客户端使用时需要知道内部是数组
String[] data = collection.getData();
for (int i = 0; i < data.length; i++) {
    System.out.println(data[i]);
}

问题:客户端依赖具体存储结构(数组),一旦 MyCollection 内部改用链表,所有客户端代码都要修改。

第二步:尝试初步封装遍历

为了隐藏内部结构,我们可以在聚合对象内部实现遍历逻辑:

class MyCollection {
    private String[] data = {"A", "B", "C"};

    public void traverse() {
        for (String item : data) {
            System.out.println(item); // 或通过回调处理元素
        }
    }
}

问题:遍历逻辑硬编码在聚合类中(比如只能用顺序遍历)。无法灵活支持多种遍历方式(如逆序、过滤等)。

第三步:分离遍历职责

既然遍历逻辑可能变化,我们需要将其从聚合类中抽离出来。这时自然会想到——用一个独立对象专门负责遍历创建独立的遍历器

// 聚合类提供创建遍历器的方法
class MyCollection {
    private String[] data = {"A", "B", "C"};

    public ??? createTraverser() {
        return new ???(data); // 返回某种遍历器
    }
}

在返回某种遍历器时,可以看到将data作为参数传入,也就是将这个要遍历的内部数据交给了遍历器,剩下的遍历动作,直接找遍历器就可以了,遍历器要做的就是规范会访问的方式,所谓规范化,就是要统一,各种有遍历诉求的对象及不同的聚合类型,都要有统一的方法,客户端在调用过程中,也会更加清晰。

既然要规范化,自然利用接口定义好方法。

关键问题:遍历器的接口应该是什么?如何保证遍历器能访问聚合对象的内部数据(但又不暴露给客户端)?

第四步:设计遍历器接口

遍历器需要提供一种通用的元素访问方式,但不依赖具体存储结构。可能的操作包括:

  • 判断是否还有元素(避免越界)。
  • 获取当前元素并移动到下一个位置

尝试定义接口,注意接口名称及其具体的方法,根据实际需求去定义即可,后续可能还会有逆序要求,则需要扩展接口。

interface Traverser {
    boolean isDone();  // 是否遍历结束
    String current();  // 获取当前元素
    void next();       // 移动到下一个元素
}

(注:这里故意不用 Iterator 和 hasNext()/next() 命名,避免先入为主。)

第五步:实现具体遍历器

针对数组实现一个具体的遍历器:

class ArrayTraverser implements Traverser {
    private String[] array;
    private int index = 0;

    public ArrayTraverser(String[] array) {
        this.array = array;
    }

    @Override
    public boolean isDone() {
        return index >= array.length;
    }

    @Override
    public String current() {
        return array[index];
    }

    @Override
    public void next() {
        index++;
    }
}

修改聚合类:

class MyCollection {
    private String[] data = {"A", "B", "C"};

    public Traverser createTraverser() {
        return new ArrayTraverser(data);
    }
}

第六步:客户端使用

MyCollection collection = new MyCollection();
Traverser traverser = collection.createTraverser();

while (!traverser.isDone()) {
    System.out.println(traverser.current());
    traverser.next();
}

客户端完全不知道内部是数组。若要改为链表存储,只需修改 ArrayTraverser 为 LinkedListTraverser,客户端代码不变。

第七步:发现模式共性

观察上述设计,可以发现:

  1. Traverser 接口的本质是抽象遍历行为
  2. 不同数据结构的遍历器实现不同,但接口一致。
  3. 聚合对象返回遍历器。

此时,将 Traverser 改名为 IteratorisDone() 改为 hasNext()current() + next() 合并为 next(),就是经典的迭代器模式!

Read more

痛风带来的思考

昨晚一罐冰啤酒下去,睡觉时就感觉脚踝隐隐发作,果然早上起床直接下不来地。跟崴脚的感觉十分相似,无法行走,只能坐在一起上滑动,公司上班也去不了了,呆呆得躺在家里,下午疼痛感加剧,整个心思都在左脚的疼痛上,没有其他任何多余的精力去关注其他事情,而此刻的阳台,乃最美人间四月天,春日的微风吹拂着阳台的花儿,温暖的阳光抛洒下来,一切都如此惬意,而我却无心欣赏。 人在健康时,生活中有好多问题,但人在不健康时,生活中只剩一个问题。 我对这句话的理解更深刻了。人是健忘的,在疫情期间、在手术期间,这种感悟其实已经很深刻了,但是病情好转之后,人还是会被日常的琐碎、工作的烦扰搅乱心绪,没有专注的去享受生活本身的美好。 幸福的秘密在于,去享受我们所拥有的,而不是顽固的去追求所没有拥有的。阳光、草木、微风,都是幸福的玩意儿,应尽情的享受。 再等两天,脚完全恢复好了,身体健健康康后,我要以更轻盈的姿态去生活,不纠结他人的看法,不执着别人的认可,关注自己的能力,享受拥有的生后。 还有一个反省,针对咖啡、酒、烟,

By 李浩

设计模式之命令模式

命令模式将“请求”封装成对象,以便使用不同的请求、队列或者日志来参数化其他对象,命令模式也支持可撤销的操作。 来解析这个定义: 1. “将一个请求封装为一个对象”,请求原本是一个方法,现在要封装成一个对象,说明要新增类来完成。 2. “可以用不同的请求对客户进行参数化”,说明是将命令对象作为参数进行传递。 3. “队列”说明需要维护命令多个命令的列表队列。 4. “撤销”说明有命令对象有undo撤销方法。 命令模式在设计模式中,算是一个比较不好理解的模式,很重要的原因是不清楚设计意图,不清楚不用这个模式前有何问题,这个模式带了哪些好处,能解决什么问题。 上一篇状态模式中,看到了状态模式抽离的是状态(属性),向上提成状态对象。有了这个基础,再来理解命令模式就相对简单了。命令模式抽离的是行为(方法),向上提成命令对象。 两者都通过“对象化”来解耦和扩展系统,但解决的问题不同: * 状态模式:处理对象内部状态驱动的行为变化。 * 命令模式:处理行为请求的封装与调度。 💡智能家居遥控器,假设我们有一个智能家居遥控器,可以控制 灯(Light) 和

By 李浩

设计模式之状态模式

状态模式允许对象在内部状态改变时改变它的行为,对象看起来好像修改了它的类。 来解析这个定义: 1. “内部状态”表明对象内部有一个属性来表示状态。 2. “内部状态改变时改变它的行为,对象看起来好像修改了它的类”说明状态改变后对象的行为发生了非常大的变化,不像是同一类的行为。 从目前的分析中似乎无法推导出状态模式的类图结构。 从实际的例子出来,来看看状态模式是如何演进而来。 💡我们有一个文档审批系统,文档有以下状态和转换: 1. 草稿(Draft) → 提交 → 待审批(PendingReview) 2. 待审批 → 批准 → 已发布(Published) 3. 待审批 → 拒绝 → 草稿 4. 已发布 → 撤回 → 草稿 从直觉出发,会使用条件语句实现需求逻辑。 public class Document { private String state = "DRAFT"; // 初始状态为草稿 public void submit(

By 李浩

设计模式之代理模式

代理模式为另一个对象提供一个替身或占位符以控制对这个对象的访问。 解析这个定义: 1. “替身”表明在客户端看来,代理类与被代理类是同一类别,对客户端来说看上去没什么区别,依然能够满足诉求。如此可以看出代理类与被代理类来自同一个超类。 2. “控制对这个对象的访问”,能够控制访问,说明前提是能够访问,才能在访问之前做这个限制,即代理持有对真实对象的引用(或能创建它)。 当然可能会质疑,用继承的方式不是也能完成目标吗,用代理类去继承被代理类,然后重写方法,加入控制逻辑。但这违背了"组合优于继承"原则,代理类与被代理类强耦合。 💡我们要开发一个图片查看器,需求如下: 1. 图片加载开销大(从磁盘或网络加载耗时),希望首次显示时才加载(延迟加载)。 2. 某些图片需要权限校验,只有授权用户才能查看。 3. 客户端代码应统一接口,无需关心是直接加载图片还是通过代理。 // 1. 抽象接口(Subject) interface Image { void display(); } // 2. 真实对象(RealSubject)

By 李浩