设计模式笔记

目录

设计模式

面向对象编程

设计原则

单一职责原则(Single Resbosibility Principle)

开闭原则(Open Closed Principle)

里式替换原则

接口隔离原则

依赖反转原则

KISS、YAGNI原则

DRY原则

迪米特法则(LOD, Law of Demeter)

规范与重构

单元测试

编程规范

设计模式与范式

单例模式

工厂模式

Dependency Injection框架

构造者模式

建造者模式

原型模式(不常用)

代理模式(Proxy Design Pattern)

桥接模式(Bridge Design Pattern)

装饰器模式

适配者模式(Adapter Design Pattern)

代理、桥接、装饰器、适配器 4 种设计模式的区别

门面模式

组合模式

享元模式(Flyweight Design Pattern)

观察者模式(Observe Design Pattern)/发布订阅者模式(Publish-Subscribe Design Pattern)——EventBus

模板模式(Template Method Design Pattern)

策略模式


设计模式

面向对象编程

  • 以类和对象作为基本单元,封装/抽象/继承/多态为特性
    1. 封装。实现信息隐藏、保护数据功能。暴露有限的访问接口,授权外部仅能通过类提供的方法访问内部数据或信息,需要编程语言提供权限访问控制来支持。
    2. 抽象。只关注功能不关注具体实现的设计思路。提高代码的扩展性和维护性,修改方法具体实现而不用修改定义。
    3. 继承。实现代码复用
    4. 多态。继承+方法重写 | 接口类语法 | duck-typing。利用多态特性,可以把不同的类对象传给相同的方法,执行不同的逻辑。

设计原则

单一职责原则(Single Resbosibility Principle)

  • 如何判断一个类的职责足够专一?
    • 类中的代码行数、函数或属性过多,会影响代码的可读性和可维护性,我们就需要考虑对类进行拆分;
    • 类依赖的其他类过多,或者依赖类的其他类过多,不符合高内聚、低耦合的设计思想,我们就需要考虑对类进行拆分;
    • 私有方法过多,我们就要考虑能否将私有方法独立到新的类中,设置为 public 方法,供更多的类使用,从而提高代码的复用性;
    • 比较难给类起一个合适名字,很难用一个业务名词概括,或者只能用一些笼统的 Manager、Context 之类的词语来命名,这就说明类的职责定义得可能不够清晰;
    • 类中大量的方法都是集中操作类中的某几个属性,比如,在 UserInfo 例子中,如果一半的方法都是在操作 address 信息,那就可以考虑将这几个属性和对应的方法拆分出来。
  • 单一职责原则是为了实现代码的高内聚、低耦合,提高代码的复用性、可读性、可维护性。

开闭原则(Open Closed Principle)

  • 对扩展开放,对修改关闭。原则上都是提高代码的扩展性,增加新的功能,应该是通过在已有的代码基础上扩展代码(新增模块、类、方法、属性等),而非修改已有代码的方式来完成。
  • 方法:最常用来提高代码扩展性的方法有——多态/依赖注入/基于接口而非实现编程

里式替换原则

  • 里式替换原则:子类在设计的时候,要遵守父类的行为约定(协议)。父类定义了函数的行为约定,那子类可以改变函数的内部实现逻辑,但不能改变函数原有的 行为约定。行为约定包括:
    • 函数声明要实现的功能;
    • 对输入、输出、异常的约定;
    • 包括注释中所罗列的任何特殊说明;
  • 里式替换原则与多态的不同:
    • 多态 是面向对象编程的一大特性,也是面向对象编程语言的一种语法;
    • 里式替换 是一种设计原则,用来指导继承关系中子类该如何设计,子类的设计要保证在替换父类的时候,不改变原有程序的逻辑及不破坏原有程序的正确性;

接口隔离原则

理解 接口

  • 一组API接口集合

    举个例子。微服务用户系统提供了一组API,包括注册、登陆、获取用户信息等。现在后台管理系统需要用户系统需要删除用户的接口。但是这个接口只希望后台管理系统来调用,因此,可以将对应的api放在另一个单独的interface。

  • 单个API接口或函数

    单个函数的功能不要太复杂。比如

     

    public class Statistics { private Long max; private Long min; private Long average; private Long sum; private Long percentile99; private Long percentile999; //...省略constructor/getter/setter等方法... } public Statistics count(Collection dataSet) { Statistics statistics = new Statistics(); //...省略计算逻辑... return statistics; }

    现在的接口的count函数功能性太复杂,包含了很多不同的统计功能,按照接口隔离原则,应该把count()函数拆成几个更小粒度的函数。满足接口隔离原则之后,可以改进为:

     

    public Long max(Collection dataSet) { //... } public Long min(Collection dataSet) { //... } public Long average(Colletion dataSet) { //... } // ...省略其他统计函数...

    不过,这也需要结合具体场景。比如,在业务场景中,每个统计需求Statistics定义的那几个统计信息都有涉及,那原来的count()函数设计就是合理的;如果每个统计需求只涉及到其中一部分,而count()函数都需要把所有信息都算一遍,这就会影响代码性能。

  • OOP中的接口概念

    可以理解为面向对象编程中的接口语法,那接口的设计要尽量单一,不要让接口的实现类或者调用者,依赖不需要的接口。

依赖反转原则

控制反转(IOC)

控制反转是一个比较笼统的设计思想,并不是一种具体的实现方法,一般用来指导框架层面的设计。我们按照框架设计的接口或者定义的规则编码,框架会自动调用我们的代码。比如Spring的初始化,我们实现一些Component/Configuration,框架会自动帮我们注册。

依赖注入(DI)

这是一种具体的编程技巧。简单来说就是,不通过new的方式在类的内部创建依赖类的对象,而是在外部创建好,然后通过构造函数/函数参数等方式传递(注入)给类来使用。

依赖注入框架(DI Framework)

我们通过依赖注入框架提供的扩展点,简单配置一下所需要的类及其类与类之间依赖关系,就可以实现由框架来自动创建对象、管理对象的生命周期、依赖注入等原本需要程序员来做的事情。

依赖反转原则(DIP)

高层模块不依赖底层模块,他们共同依赖同一个抽象。抽象不要依赖具体的实现细节,具体实现细节依赖抽象。

KISS、YAGNI原则

KISS(Keep It Simple and Stupid)

  • 不要使用同事可能不懂的技术来实现代码;
  • 不要重复造轮子,要善于使用已经有的工具类库
  • 不要过度优化

YAGNI(You Ain't Gonna Need It)

不要做过度设计。不要去设计当前用不到的功能,不要编写当前用不到的代码,可以留好扩展点。

DRY原则

DRY(Don't Repeat Yourself)

代码重复的三种情况:实现逻辑重复、功能语义重复、代码执行重复。

迪米特法则(LOD, Law of Demeter)

高内聚,松耦合

  • 何为“高内聚”。相近的功能应该放到同一个类中(往往会被同时修改),不相近的功能不要放到同一个类中。单一职责原则是实现高内聚的设计原则。
  • 何为“松耦合”。在代码中,类与类之间的依赖关系简单清晰。一个类的代码改动不会活很少导致依赖类的代码改动。依赖注入、接口隔离、基于接口而非实现编程,以及迪米特法则,都是为了实现代码的松耦合
  1. 单一职责原则
  • 适用对象:模块,类,接口
  • 侧重点:高内聚,低耦合
  • 思考角度:自身
  1. 接口隔离原则
  • 适用对象:接口,函数
  • 侧重点:低耦合
  • 思考角度:调用者
  1. 基于接口而非实现编程
  • 适用对象:接口,抽象类
  • 侧重点:低耦合
  • 思考角度:调用者
  1. 迪米特法则
  • 适用对象:模块,类
  • 侧重点:低耦合
  • 思考角度:类关系

规范与重构

单元测试

单元测试 VS 集成测试

集成测试的测试对象是整个系统或者某个功能模块,比如测试用户注册、登录功能是否正常,是一种端到端(end to end)的测试。

单元测试的测试对象是类或者函数,用来测试一个类和函数是否都按照预期的逻辑执行。这是代码层级的测试

编程规范

命名

  1. 对于大家熟悉的词,推荐用缩写。比如sec->second, str->string...
  2. 利用上下文简化命名。如User类里面的字段,可以直接叫name,而不是userName;函数参数也可以借助函数这个上下文来简化命名,比如:
 
  

public void uploadUserAvatarImageToAliyun(String userAvatarImageUri); //利用上下文简化为: public void uploadUserAvatarImageToAliyun(String imageUri);

  1. 命名要可读可搜索。可读指的是,不要用生僻、难发音的英文单词来命名;可搜索指的是,在命名的时候,最好能符合整个项目的命名习惯,大家都用“selectXXX”表示查询,你就不要用“queryXXX”;大家都用“insertXXX”表示插入一条数据,你就要不用“addXXX”

注释

注释的内容主要包含这样三个方面:做什么、为什么、怎么做。对于一些复杂的类和接口,我们可能还需要写明“如何用”。

https://static001.geekbang.org/resource/image/f3/d3/f3262ef8152517d3b11bfc3f2d2b12d3.png?wh=5013*3903

设计模式与范式

设计模式要干的事情就是解耦,创建型模式是将创建和使用代码解耦,结构型模式是将不同功能代码解耦,行为型模式是将不同的行为代码解耦 。

单例模式

单例设计模式。一个类只允许创建一个对象(或者实例),这个类就是一个单例类,这种设计模式就叫单例设计模式,简称单例模式。

工厂模式

简单工厂模式

工厂模式

当创建逻辑比较复杂,是一个“大工程”的时候,我们就考虑使用工厂模式,封装对象的创建过程,将对象的创建和使用相分离。

何为创建逻辑比较复杂?

  • 第一种:类似规则配置解析的例子,代码中存在if-else分支判断,动态地根据不同的类型创建不同的对象。可以将这一大坨if-else创建对象的代码抽离出来,放到工厂类中。
  • 第二种:尽管我们不需要根据不同的类型创建不同的对象,但是,单个对象本身的创建过程比较复杂,比如前面提到的要组合其他类对象,做各种初始化操作。在这种情况下我们也可以考虑使用工厂模式,将对象的创建过程封装到工厂类中。

对于第一种情况,如果创建对象过程不复杂,用简单工厂模式,如果创建对象过程比较复杂,为了避免设计一个过于庞大的简单工厂类,推荐使用工厂模式,将创建逻辑拆分得更细,每个对象的创建逻辑独立到各自的工厂类中。对于第二种,工厂模式。

工厂模式

  • 封装变化:创建逻辑有可能变化,封装成工厂类之后,创建逻辑的变更对调用者透明
  • 代码复用:创建逻辑的代码 抽离到独立的工厂类之后可以复用
  • 隔离复杂性:封装复杂的创建逻辑,调用者无需了解如何创建对象
  • 控制复杂度:将创建代码抽离出来,让本来的函数或职责更单一,代码更简洁

Dependency Injection框架

DI容器底层最基本的设计思路就是基于工厂模式。DI容器相当于一个大的工厂类

构造者模式

什么情况下使用建造者模式?

  • 我们规定在创建对象的时候就强制需要设置某些值时。如果参数过多,放在构造函数里面太长了;如果用set方法去设置,那一些参数的校验就无处安放了。这种情况下就考虑使用建造者模式。
  • 如果类属性之间有一定的依赖关系或者约束条件,我们如果继续使用构造函数配合set()方法的思路,那这些依赖关系和约束条件的校验逻辑就无处安放了。
  • 如果我们希望创建不可变对象,也就是说,对象在创建好之后,就不能再修改内部的属性值,要实现这个功能,就不能在类中暴露set()方法。

工厂模式 VS 建造者模式 工厂模式是用来创建不同但是相关类型的对象(继承同一父类或者接口的一组子类),根据给定参数来决定创建哪种类型的对象。 建造者模式是用来创建一种类型的复杂对象,可以通过设置不同的可选参数,“定制化”地创建不同的对象。

建造者模式

什么情况下使用建造者模式?

  • 我们规定在创建对象的时候就强制需要设置某些值时。如果参数过多,放在构造函数里面太长了;如果用set方法去设置,那一些参数的校验就无处安放了。这种情况下就考虑使用建造者模式。
  • 如果类属性之间有一定的依赖关系或者约束条件,我们如果继续使用构造函数配合set()方法的思路,那这些依赖关系和约束条件的校验逻辑就无处安放了。
  • 如果我们希望创建不可变对象,也就是说,对象在创建好之后,就不能再修改内部的属性值,要实现这个功能,就不能在类中暴露set()方法。

工厂模式 VS 建造者模式
工厂模式是用来创建不同但是相关类型的对象(继承同一父类或者接口的一组子类),根据给定参数来决定创建哪种类型的对象。
建造者模式是用来创建一种类型的复杂对象,可以通过设置不同的可选参数,“定制化”地创建不同的对象。

原型模式(不常用)

原型模式:如果对象的创建成本比较大,而同一个类的不同对象之间差别不大(大部分字段都相同),在这种情况下,我们可以利用对已有对象(原型)进行复制(拷贝)的方式来创建新对象,以达到节省创建时间的目的。

原型模式的两种实现方法:

  1. 浅拷贝。只会复制对象中基本数据类型数据和引用对象的内存地址,不会递归地复制引用对象,以及引用对象的引用对象···
  2. 深拷贝。得到的是一个完全独立的对象。

虽然深拷贝比浅拷贝更加耗时,耗内存空间,但是对于可变对象来说,浅拷贝得到的对象和原始对象会共享部分数据,有可能出现数据被修改的风险,所以除非深拷贝非常耗时,不推荐浅拷贝。

代理模式(Proxy Design Pattern)

代理模式在不改变原石类(或叫被代理类)代码的情况下,通过引入代理类来给原始类附加功能。

  1. 静态代理:让代理类和原始类实现同样的接口,如果原始类没有定义接口并且原始类代码并不是我们开发维护的,可以通过让代理类继承原始类的方法来实现代理模式。
  2. 动态代理:静态代理需要针对每个类都创建一个代理类,并且每个代理类中的代码都有点像模版式的“重复”代码,增加了维护成本和开发成本。动态代理就可以,不用事先为每个原始类编写代理类,而是在运行的时候动态地创建原始类对应的代理类,然后在系统中用代理类替换掉原始类。

应用场景:常用在业务系统中开发一些非功能性需求:监控、统计、鉴权、限流、事务、幂等、日志。除此之外,代理模式还可以用在RPC、缓存等应用场景。

桥接模式(Bridge Design Pattern)

将抽象和实现解耦,让它们可以独立变化。比较难理解 可以结合JDBC驱动理解桥接模式。

装饰器模式

装饰器模式和静态代理模式很像。主要作用是给原始类增强功能,装饰器类需要跟原始类继承相同的抽象类或者接口。

适配者模式(Adapter Design Pattern)

适配者模式的原理和与实现

这个模式就是用来做接口适配的,他将不兼容的接口转换为可兼容的接口,类比USB转换器。它有两种实现方式:类适配器 & 对象适配器

类适配器:使用继承关系来实现
对象适配器:使用组合方式来实现

怎么选择实现方式?

  • 如果Adaptee接口不多,两种方式都可以
  • 如果Adaptee接口很多,而且Adaptee和ITarget接口大部分定义都相同,推荐使用类适配器。因为Adapter复用父类Adaptee的接口,比起对象适配器的方式,Adapter的代码要少一些。
  • 如果Adaptee接口很多,而且Adaptee和ITarget接口大部分不相同,推荐使用对象适配器。因为组合结构相对于继承更加灵活。

// 类适配器: 基于继承
public interface ITarget {
  void f1();
  void f2();
  void fc();
}

public class Adaptee {
  public void fa() { //... }
  public void fb() { //... }
  public void fc() { //... }
}

public class Adaptor extends Adaptee implements ITarget {
  public void f1() {
    super.fa();
  }
  
  public void f2() {
    //...重新实现f2()...
  }
  
  // 这里fc()不需要实现,直接继承自Adaptee,这是跟对象适配器最大的不同点
}

// 对象适配器:基于组合
public interface ITarget {
  void f1();
  void f2();
  void fc();
}

public class Adaptee {
  public void fa() { //... }
  public void fb() { //... }
  public void fc() { //... }
}

public class Adaptor implements ITarget {
  private Adaptee adaptee;
  
  public Adaptor(Adaptee adaptee) {
    this.adaptee = adaptee;
  }
  
  public void f1() {
    adaptee.fa(); //委托给Adaptee
  }
  
  public void f2() {
    //...重新实现f2()...
  }
  
  public void fc() {
    adaptee.fc();
  }
}

适配器的使用场景

  • 封装有缺陷的接口设计——二次封装
  • 统一多个类的接口设计—— 某个功能的实现依赖多个外部系统(或者说类)。通过适配器模式,将它们的接口适配为统一的接口定义,然后我们就可以使用多态的特性来复用代码逻辑。
  • 替换依赖的外部系统
  • 兼容老版本接口——老版本接口里实际上调用的是新方法
  • 适配不同格式的数据

代理、桥接、装饰器、适配器 4 种设计模式的区别

尽管四种模式的代码结构相似,但是他们要解决的问题、应用场景完全不同。

代理模式

代理模式在不改变原始类接口的条件下,为原始类定义一个代理类,主要目的是控制访问,而非加强功能,这是它跟装饰器模式最大的不同。

桥接模式

桥接模式的目的是将接口部分和实现部分分离,从而让它们可以较为容易、也相对独立地加以改变。

装饰器模式

装饰者模式在不改变原始类接口的情况下,对原始类功能进行增强,并且支持多个装饰器的嵌套使用。

适配器模式

适配器模式是一种事后的补救策略。适配器提供跟原始类不同的接口,而代理模式、装饰器模式提供的都是跟原始类相同的接口。

门面模式

门面模式为子系统提供一组统一的接口,定义一组高层接口让子系统更易用。

门面模式 VS 适配器模式

适配器模式:接口转换,解决的是原接口和目标接口不匹配的问题。
门面模式:接口整合,解决的是多接口调用带来的问题。

组合模式

组合模式的设计思路,与其说是一种设计模式,倒不如说是对业务场景的一种数据结构和算法的抽象。其中,数据可以表示成树这种数据结构,业务需求可以通过在树上的递归遍历算法来实现。

享元模式(Flyweight Design Pattern)

享元,顾名思义就是被共享的单元。享元模式的意图就是复用对象,节省内存,前提是享元对象是不可变对象。

例子

棋牌游戏(象棋),在游戏大厅里,有成千上万个房间,对应成千上万个棋局,每个棋局需要保存几十个棋子的信息,包括棋子的类型(象、车、士等),棋子的颜色,棋子的位置,如果不用享元模式,那保存这么多棋局对象就会消耗大量的内存。在上述的实现方式中,在内存中会有大量的相似对象,这些相似对象的id/text/color都是一样的,只是positionX/positionY不一样,那我们可以把前面的属性抽出来,设计成独立的类,并且作为享元供多个棋局复用。


// 享元类
public class ChessPieceUnit {
  private int id;
  private String text;
  private Color color;

  public ChessPieceUnit(int id, String text, Color color) {
    this.id = id;
    this.text = text;
    this.color = color;
  }

  public static enum Color {
    RED, BLACK
  }

  // ...省略其他属性和getter方法...
}

public class ChessPieceUnitFactory {
  private static final Map pieces = new HashMap<>();

  static {
    pieces.put(1, new ChessPieceUnit(1, "車", ChessPieceUnit.Color.BLACK));
    pieces.put(2, new ChessPieceUnit(2,"馬", ChessPieceUnit.Color.BLACK));
    //...省略摆放其他棋子的代码...
  }

  public static ChessPieceUnit getChessPiece(int chessPieceId) {
    return pieces.get(chessPieceId);
  }
}

public class ChessPiece {
  private ChessPieceUnit chessPieceUnit;
  private int positionX;
  private int positionY;

  public ChessPiece(ChessPieceUnit unit, int positionX, int positionY) {
    this.chessPieceUnit = unit;
    this.positionX = positionX;
    this.positionY = positionY;
  }
  // 省略getter、setter方法
}

public class ChessBoard {
  private Map chessPieces = new HashMap<>();

  public ChessBoard() {
    init();
  }

  private void init() {
    chessPieces.put(1, new ChessPiece(
            ChessPieceUnitFactory.getChessPiece(1), 0,0));
    chessPieces.put(1, new ChessPiece(
            ChessPieceUnitFactory.getChessPiece(2), 1,0));
    //...省略摆放其他棋子的代码...
  }

  public void move(int chessPieceId, int toPositionX, int toPositionY) {
    //...省略...
  }
}

享元 vs 单例、缓存、对象池

  • 享元 vs 单例

单例模式下,一个类只能创建一个对象;在享元模式下,一个类可以创建多个对象,每个对象被多处代码引用共享,有点类似于单例的变体:多例。从设计意图上看,应用享元模式是为了对象复用,节省内存,而应用多例模式是为了限制对象的个数。

  • 享元 vs 缓存

缓存主要是为了提高访问效率,而非复用。

  • 享元 vs 对象池

池化技术中的“复用”理解为重复使用,主要是为了节省时间。

观察者模式(Observe Design Pattern)/发布订阅者模式(Publish-Subscribe Design Pattern)——EventBus

观察者模式是为了接耦观察者和被观察者代码。比如用户注册的例子,当一个新的用户注册后,我们需要给用户下发消费券,同时还要发送一封欢迎信,那我们可以定义一个接口,接口包含一个f()方法,对于新用户注册,每当我们需要新增一个事件,可以看成需要新增一个观察者,比如赠送积分,那可以新增一个该接口的实现,在用户注册(调用方法register())时,这些观察者都做出对应的操作,即f()。

实现方式

这个模式在不同场景下可以有不用的实现方式,可以是同步阻塞,观察者和被观察者代码在同一个线程内执行,被观察者一直阻塞,直到所有的观察者代码都执行完成之后,才执行后续的代码。也可以是异步非阻塞,可以在执行f()时,都创建一个新的线程去执行,或者更优雅的方法,比如EventBus。

模板模式(Template Method Design Pattern)

模板模式的作用:复用扩展。模板方法模式在一个方法中定义一个算法骨架,并将某些步骤推迟到子类中实现。模板方法模式可以让子类在不改变算法整体结构的情况下,重新定义算法中的某些步骤。
其中,复用指的是,所有的子类可以复用父类中提供的模板方法的代码。扩展指的是,框架通过模板模式提供功能扩展点,让框架用户可以在不修改框架源码的情况下,基于扩展点定制化框架的功能。

策略模式

策略模式定义一族算法类,将每个算法分别封装起来,让他们可以互相替换。策略模式可以使算法的变化独立于使用他们的客户端(使用算法的代码)。
策略模式用来解耦策略的定义、创建、使用。实际上,一个完整的策略模式就会是由这三个部分组成的。

  • 策略类的定义,包含一个策略接口和一组实现这个接口的策略类;
  • 策略的创建由工厂类来完成,封装策略创建的细节;
  • 策略模式包含一组策略可选,客户端代码如何选择使用哪个策略,有两种确定方法:编译时静态确定和运行时动态确定。其中,“运行时动态确定”才是策略模式最典型的应用场景。

(mark:公司代码里,handler继承或者扩展一个接口/类,同时实现/重写ableHandle()就是使用了策略模式)

职责链模式(Chain of Responsibility Design Pattern)

职责链模式将请求和接受解耦,让多个接受对象都有机会处理这个请求。将这些接受对象串成一条链,并沿着这条链传递这个请求,直到链上的某个接受对象能够处理它为止。

有两种实现方式,例子:Handler是所有处理器类的抽象父类,handle()是抽象方法。每个具体的处理器类(HandlerA\HandlerB)的handle()函数的代码结构类似,如果它能处理该请求,就不继续往下传递,如果不能,则交给后面的处理器来处理。

第一种是HandlerChain是处理器链,记录了head和tail,记录tail是为了方便添加处理器。
第二种是HandlerChain类用数组而非链表来保存所有的处理器,在HandlerChain的handle()函数中,一次调用每个处理器的handle()函数。

还有一种变体是,请求会被所有的处理器都处理一遍,不存在中途终止的情况。

应用场景

  • 敏感信息过滤
  • 过滤器
  • 拦截器

状态模式

是状态机的一种实现方式。
状态机又叫有限状态机,它有3个部分组成:状态、事件、动作。

  • 分支逻辑法。利用if-else或者switch-case分之逻辑,参照状态转移图,将每一个状态转移原模原样地直译成代码。
  • 查表法。对于状态很多、状态转移比较复杂的状态机来说,查表法比较合适。通过二维数组来表示状态转移图,能极大地提高代码的可读性和可维护性。
  • 状态模式。对于状态不多、状态转移也比较简单,但事件触发执行的动作包含的业务逻辑可能比较复杂的状态机来说,首选这种方式。

迭代器模式(Iterator Design Pattern)/游标模式(Cursor Design Pattern)

一个完整的迭代器模式,一般会涉及容器和容器迭代器两部分内容。为了达到基于接口而非实现编程的目的,容器又包含容器接口、容器实现类,迭代器又包含迭代器接口、迭代器实现类。容器中需要定义 iterator() 方法,用来创建迭代器。迭代器接口中需要定义 hasNext()、currentItem()、next() 三个最基本的方法。容器对象通过依赖注入传递到迭代器类中。

遍历集合一般有三种方式:for 循环、foreach 循环、迭代器遍历。后两种本质上属于一种,都可以看作迭代器遍历。相对于 for 循环遍历,利用迭代器来遍历有下面三个优势:

  • 迭代器模式封装集合内部的复杂数据结构,开发者不需要了解如何遍历,直接使用容器提供的迭代器即可;
  • 迭代器模式将集合对象的遍历操作从集合类中拆分出来,放到迭代器类中,让两者的职责更加单一;
  • 迭代器模式让添加新的遍历算法更加容易,更符合开闭原则。除此之外,因为迭代器都实现自相同的接口,在开发中,基于接口而非实现编程,替换迭代器也变得更加容易。

使用Iterator遍历集合的同时,如果增删集合元素会发生什么?
在java的Iterator实现里面,当调用next()方法时会去判断modCount和expectedModCount是否相等,如果不相等,会抛出ConcurrentModificationException异常。

 

你可能感兴趣的:(设计模式,java,开发语言)