前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Java设计模式 笔记

Java设计模式 笔记

作者头像
MashiroT
发布2023-03-20 10:35:29
6930
发布2023-03-20 10:35:29
举报
文章被收录于专栏:MashiroのBlogMashiroのBlog

Java设计模式 笔记

UML

统一建模语言(Unified Modeling Language,UML)是用来设计软件的可视化建模语言。它的特点是简单、统一、图形化、能表达软件设计中的动态与静态信息 UML从目标系统的不同角度出发,定义了用例图、类图、对象图、状态图、活动图、时序图、协作图、构件图、部署图等

类图

类图(Class diagram)是显示了模型的静态结构,特别是模型中存在的类、类的内部构造以及它们与其他类的关系。类图不显示暂时性的信息。类图是面向对象建模的主要组成部分。

类图的作用

  • 在软件工程中,类图是一种静态的结构图,描述了系统的类的集合,类的属性和类之间的关系,简化了人们对系统的理解。
  • 类图是系统分析和设计阶段的重要产物,是系统编码和测试的重要模型。

类图的表示法

类的表示方式

在UML类图中,类使用包含类名、属性(field)和方法(method),且带有分割线的矩形来表示。

类图示例.jpg
类图示例.jpg

属性/方法前的加减号表示可见性

  • +:表示public
  • -:表示private
  • #:表示protected

属性的完整表示:可见性 名称:类型 [ = 初始化值] 方法的完整表示:可见性 名称(参数) [ :返回值类型]

类与类之间的的表示方式
关联关系

关联关系是对象之间的一种引用关系,用于表示一类对象和另一类对象之间的联系,分为一般关联关系、聚合关系和组合关系。

一般关联关系分为单项关联、双向关联、自关联

关联关系有单向关联和双向关联。如果两个对象都知道(即可以调用)对方的公共属性和操作,那么二者就是双向关联。如果只有一个对象知道(即可以调用)另一个对象的公共属性和操作,那么就是单向关联。大多数关联都是单向关联,单向关联关系更容易建立和维护,有助于寻找可重用的类。

在UML图中,双向关联关系用带双箭头的实线或者无箭头的实线双线表示。单向关联用一个带箭头的实线表示,箭头指向被关联的对象,自关联用指向自己的带箭头实线表。 这就是导航性(Navigatity)。

类图-关联关系.jpg
类图-关联关系.jpg

一个对象可以持有其它对象的数组或者集合。在UML中,通过放置多重性(multipicity)表达式在关联线的末端来表示。多重性表达式可以是一个数字、一段范围或者是它们的组合。多重性允许的表达式示例如下:

  • 数字:精确的数量
  • 或者0..:表示0到多个
  • 0..1:表示0或者1个,在Java中经常用一个空引用来实现
  • 1..*:表示1到多个
聚合关系与组合关系

聚合(Aggregation)是关联关系的一种特例,它体现的是整体与部分的拥有关系,即 “has a” 的关系。 聚合对象也是通过成员对象来实现的,其中成员对象是整体对象的一部分,但是成员对象可以脱离整体而独立存在,它们可以具有各自的生命周期,部分可以属于多个整体对象,也可以为多个整体对象共享,所以聚合关系也常称为共享关系。 例如,公司部门与员工的关系,一个员工可以属于多个部门,一个部门撤消了,员工可以转到其它部门。

聚合关系用空心菱形加实线箭头表示,空心菱形在整体一方,箭头指向部分一方。

类图-聚合关系.jpg
类图-聚合关系.jpg

组合(Composition)也是关联关系的一种特例,它同样体现整体与部分间的包含关系,即 “contains a” 的关系。 但此时整体与部分是不可分的,部分也不能给其它整体共享,作为整体的对象负责部分的对象的生命周期。这种关系比聚合更强,也称为强聚合。 如果A组合B,则A需要知道B的生存周期,即可能A负责生成或者释放B,或者A通过某种途径知道B的生成和释放。

例如,人包含头、躯干、四肢,它们的生命周期一致。当人出生时,头、躯干、四肢同时诞生。当人死亡时,作为人体组成部分的头、躯干、四肢同时死亡。

组合关系用实心菱形加实线箭头表示,实心菱形在整体一方,箭头指向部分一方。

类图-组合关系.jpg
类图-组合关系.jpg

在Java代码形式上,聚合和组合关系中的部分对象是整体对象的一个成员变量。但是,在实际应用开发时,两个对象之间的关系到底是聚合还是组合,有时候很难区别。在Java中,仅从类代码本身是区分不了聚合和组合的。如果一定要区分,那么如果在删除整体对象的时候,必须删掉部分对象,那么就是组合关系,否则可能就是聚合关系。从业务角度上来看,如果作为整体的对象必须要部分对象的参与,才能完成自己的职责,那么二者之间就是组合关系,否则就是聚合关系。

依赖关系

依赖(Dependency)关系是一种使用关系,它是对象之间耦合度最弱的一种关联方式,是临时性的关联。如果对象A用到对象B,但是和B的关系不是太明显的时候,就可以把这种关系看作是依赖关系。如果对象A依赖于对象B,则 A “use a” B。

例如驾驶员和汽车的关系,驾驶员使用汽车,二者之间就是依赖关系。

依赖关系用一个带虚线的箭头表示,由使用方指向被使用方,表示使用方对象持有被使用方对象的引用。

类图-依赖关系.jpg
类图-依赖关系.jpg

依赖关系在Java中的具体代码表现形式为 B为A的构造器方法中的局部变量、方法构造器的参数、方法的返回值 ,或者 A调用B的静态方法

泛化关系

泛化关系(Generalization)是指对象与对象之间的继承关系。如果对象A和对象B之间的“is a”关系成立,那么二者之间就存在继承关系,对象B是父对象,对象A是子对象。 例如,一个年薪制员工“is a”员工,很显然年薪制员工Salary对象和员工Employee对象之间存在继承关系,Employee对象是父对象,Salary对象是子对象。

泛化关系用空心三角和实线组成的箭头表示,从子类指向父类。 在Java代码中,对象之间的泛化关系可以直接翻译为关键字 extends

类图-继承关系.jpg
类图-继承关系.jpg
实现关系

实现关系是指接口及其实现类之间的关系。

实现关系用空心三角和虚线组成的箭头来表示,从实现类指向接口。 在Java代码中,实现关系可以直接翻译为关键字 implements

类图-实现关系.jpg
类图-实现关系.jpg

软件设计原则

在软件开发中,为了提高软件系统的可维护性和复用性,增加软件的可拓展性和灵活性,程序员要尽量根据7条原则来开发程序,从而提高软件开发效率、节约软件开发成本和维护成本。

单一职责原则

单一职责原则(Single Responsibility Principle,SRP)是指:所有的对象都应该有单一的职责,它提供的所有的服务也都仅围绕着这个职责。换句话说就是:一个类而言,应该仅有一个引起它变化的原因,永远不要让一个类存在多个改变的理由。

要理解单一职责原则,首先我们要理解什么是类的职责。类的职责是由该类的对象在系统中的角色所决定的。

举例来讲,教学管理系统中,老师就代表着一种角色,这个角色决定老师的职责就是教学。而要完成教学的职责,老师需要讲课、批改作业,而讲课、批改作业的行为就相当于我们在程序中类的方法,类的方法和属性就是为了完成这个职责而设置的。

类的单一职责是说一个类应该只做一件事情。如果类中某个方法或属性与它所要完成的职责无关,或是为了完成另外的职责,那么这样的设计就不符合类的单一职责原则。而这样的设计的缺点是降低了类的内聚性,增强了类的耦合性。由此带来的问题是当我们使用这个类时,会把原本不需要的功能也带到了代码中,从而造成冗余代码或代码的浪费。单一职责原则并不是极端地要求我们只能为对象定义一个职责,而是利用极端的表述方式重点强调:在定义对象职责时,必须考虑职责与对象之间的所属关系。

开闭原则

对拓展开放,对修改关闭。在程序需要进行拓展的时候,不能去修改原有代码,实现一个热插拔的效果。简言之,是为了使程序的拓展性好,易于维护和升级。

要达到这样的效果,我们需要使用接口和抽象类。 因为抽象灵活性好,适用性广,只要抽象的合理,基本可以保持软件架构的稳定。而软件中易变的细节可以从抽象派生的实现类进行拓展,当软件需要发生变化时,只需要根据需求重新派生一个实现类来进行拓展就可以了。

里氏代换原则

里氏替换原则译自Liskov substitution principle。Liskov是一位计算机科学家,也就是Barbara Liskov,麻省理工学院教授,也是美国第一个计算机科学女博士,师从图灵奖得主John McCarthy教授,人工智能概念的提出者。

里氏代换原则是面向对象设计的基本原则之一,一种关于其内容的描述是Robert Martin在《敏捷软件开发:原则、模式与实践》一书中对原论文的解读:子类型(subtype)必须能够替换掉他们的基类型(base type)。这个是更简明的一种表述。 换言之,子类可以拓展父类的功能,但不能改变父类原有的功能,也就是说 把父类替换成它的子类,程序的行为没有变化。

如果通过重写父类的方法来完成新的功能,这样写起来虽然简单,但整个继承体系的可复用性会比较差,特别是运用多态比较频繁时,程序运行出错的概率会非常大。

下面是里氏代换原则的经典例子:正方形不是长方形

里氏代换原则-正方形不是长方形.jpg
里氏代换原则-正方形不是长方形.jpg
代码语言:javascript
复制
/*代码清单1 Rectangle.java
 * 矩形类的实现代码,用于演示LSP
 */
public class Rectangle {
  private double length;
  private double width;
  public double getLength() {
    return length;
  }

  public void setLength(double length) {
    this.length = length;
  }

  public double getWidth() {
    return width;
  }

  public void setWidth(double width) {
    this.width = width;
  }
}
/*代码清单2 Square.java
 * 正方形的实现代码,用于演示LSP
 */
public class Square extends Rectangle {
  public void setWidth(double width) {
    super.setLength(width);
    super.setWidth(width);
  }

  public void setLength(double length) {
    super.setLength(length);
    super.setWidth(length);
  }
}

下面是测试用例

代码语言:javascript
复制
public class TestRect {
  public static void main(String[] args) {
    TestRect tr = new TestRect();
    Rectangle r = new Rectangle();
    tr.g(r);
//	如果替换成下面的代码,则报错
//  Rectangle s = new Square();
//  tr.g(s);
  }
  public void g(Rectangle r) {
    r.setWidth(5);
    r.setLength(4);
    if (r.getWidth()*r.getLength()!=20) {
      throw new RuntimeException();
    }
  }
}

由此,我们得出结论:父类Rectangle不能被子类Square替换,如果进行了替换就得不到预期结果。因此,Square类和Rectangle类之间的继承关系违反了里氏替换原则,它们之间的继承关系不成立,正方形不是长方形。

“正方形不是长方形”,正方形是长方形也不是长方形,这样结论似乎就是个悖论。 产生这种混乱的原因有两个:

  • 原因一:对类的继承关系的定义没有搞清楚 面向对象的设计关注的是对象的行为,它是使用“行为”来对对象进行分类的,只有行为一致的对象才能抽象出一个类来。我们说类的继承关系就是一种“is a”关系,实际上指的是行为上的“is a”关系,可以把它描述为“表现为,act as”。

正方形在设置长度和宽度这两个行为上,与长方形显然是不同的。所以,如果我们把这种行为加到父类长方形的时候,就导致了正方形无法继承这种行为。 我们“强行”把正方形从长方形继承过来,就造成无法达到预期的结果。

  • 原因二:设计要依赖于用户需求和具体环境 继承关系要求子类要具有基类全部的行为。这里的行为是指落在需求范围内的行为。

这里我们以另一个理解里氏替换原则的经典例子“鸵鸟非鸟”来做示例。生物学中对于鸟类的定义是“恒温动物,卵生,全身披有羽毛,身体呈流线形,有角质的喙,眼在头的两侧。前肢退化成翼,后肢有鳞状外皮,有四趾”。从生物学角度来看,鸵鸟肯定是一种鸟,是一种继承关系。但是根据上一个“正方形非长方形”的例子,鸵鸟和鸟之间的继承关系又可能不成立。那么,鸵鸟和鸟之间到底是不是继承关系如何判断呢?这需要根据用户需求来判断。

A需求期望鸟类提供与飞翔有关的行为,即使鸵鸟跟普通的鸟在外观上就是100%的相像,但在A需求范围内,鸵鸟在飞翔这一点上跟其它普通的鸟是不一致的,它没有这个能力,所以,鸵鸟类无法从鸟类派生,鸵鸟不是鸟。

B需求期望鸟类提供与羽毛有关的行为,那么鸵鸟在这一点上跟其它普通的鸟一致的。虽然它不会飞,但是这一点不在B需求范围内,所以,它具备了鸟类全部的行为特征,鸵鸟类就能够从鸟类派生,鸵鸟就是鸟。

所有子类的行为功能必须和使用者对其父类的期望保持一致,如果子类达不到这一点,那么必然违反里氏替换原则。在实际的开发过程中,不正确地滥用继承关系是非常有害的。伴随着软件开发规模的扩大,参与的开发人员也越来越多,每个人都在使用别人提供的组件,也会为别人提供组件。最终,所有人的开发的组件经过层层包装和不断组合,被集成为一个完整的系统。每个开发人员在使用别人的组件时,只需知道组件的对外裸露的接口,那就是它全部行为的集合,至于内部到底是怎么实现的,无法知道,也无须知道。所以,对于使用者而言,它只能通过接口实现自己的预期,如果组件接口提供的行为与使用者的预期不符,错误便产生了。里氏替换原则就是在设计时避免出现子类与父类不一致的行为。

对于“正方形非长方形”问题,既然二者之间的继承关系违反了里氏替换原则,我们就应该重新设计二者之间的关系。我们可以采用第一种方案,正方形和长方形的共同行为(getLength()、getWidth()方法)抽象并封装转移到一个抽象类或者接口中,比如一个“四方形”接口或者抽象类,然后让正方形和长方形分别实现四方形接口或者继承四方形抽象类,如下图所示。

里氏代换原则-最终类图.jpg
里氏代换原则-最终类图.jpg

一般来说,只要有可能,就不要从具体类继承。下图就给出了一个继承形成的等级结构的典型例子。从图可以看出,所有的继承都是从抽象类开始,而所有的具体类都没有子类。也就是说,在一个由继承关系形成的等级结构中,树叶节点都应当是具体类,树枝节点都应该是抽象类或者接口。

里氏代换原则-继承结构.jpg
里氏代换原则-继承结构.jpg

里氏替换原则实现了开闭原则中的对扩展开放。实现开闭原则的关键步骤是抽象化,父类与子类之间的继承关系就是一种抽象化的体现。 因此,里氏替换原则是实现抽象化的一种规范。违反里氏替换原则意味着违反了开闭原则,反之未必。里氏替换原则是使代码符合开闭原则的一个重要保证。

依赖倒转原则

  • 高层模块不应该依赖低层模块,两者都应该依赖其抽象
  • 抽象不应该依赖细节,细节应该依赖抽象。

依赖倒转原则带来的一个启示是:针对接口编程,而不是针对实现编程。也就是说,当客户要使用一个接口的实现类功能时,应该针对定义这些功能的接口编程,而不是针对该接口的实现类编程。 依赖倒转原则用于指导我们如何正确地消除模块间的依赖关系。

所谓依赖是指如果一个模块A使用另一个模块B,我们称模块A依赖模块B。在应用程序中,有一些低层次的类,这些类实现了一些基本的或初级的操作,我们称之为低层模块;另外,有一些高层次的类,这些类封装了某些复杂的逻辑,这些类我们称之为高层模块。高层次模块要完成自己封装的功能,就必须要使用低层模块,于是高层模块就依赖于低层模块。

那么,如何让低层模块依赖于高层模块呢?我们知道,高层模块肯定要使用低层模块提供的服务,不可能不让二者之间完全不存在依赖关系。

如果高层模块直接调用低层模块提供的服务,那么就是具体耦合关系,这样高层模块依赖于低层模块就不可避免。但是,如果我们使用抽象耦合关系,在高层模块和低层模块之间定义一个抽象接口,高层模块调用抽象接口定义的方法,低层模块实现该接口。这样,就消除了高层模块和低层模块之间的直接依赖关系。现在,高层模块就不依赖于低层模块了,二者都依赖于抽象。同时也实现了“抽象不应该依赖于细节,细节应该依赖于抽象”。

接口隔离原则

客户端不应该被迫依赖于它不使用的方法;一个类对另一个类的依赖应该建立在最小的接口上。 换句话说,就是不能强迫用户去依赖那些他们不使用的接口。

接口隔离原则实际上包含了两层意思:

  • 接口的设计原则:接口的设计应该遵循最小接口原则,不要把用户不使用的方法塞进同一个接口里。如果一个接口的方法没有被使用到,则说明该接口过胖,应该将其分割成几个功能专一的接口,使用多个专门的接口比使用单一的总接口要好。
  • 接口的继承原则:如果一个接口A继承另一个接口B,则接口A相当于继承了接口B的方法,那么继承了接口B后的接口A也应该遵循上述原则:不应该包含用户不使用的方法。反之,则说明接口A被B给污染了,应该重新设计它们的关系。

迪米特法则

迪米特法则(Law of Demeter,简称LOD),又称为“最少知识原则”,它的定义为:一个软件实体应当尽可能少的与其他实体发生相互作用。这样,当一个模块修改时,就会尽量少的影响其他的模块,扩展会相对容易。迪米特法则是对软件实体之间通信的限制,它对软件实体之间通信的宽度和深度做出了要求。

迪米特的其它表述方式为:

  1. 只与你直接的朋友们通信
  2. 不要跟“陌生人”说话
  3. 每一个软件单位对其他的单位都只有最少的知识,而且局限于那些与本单位密切相关的软件单位

那么,如何界定朋友圈和陌生人呢?迪米特法则指出,做为“朋友”的条件为:

  1. 当前对象本身(this)
  2. 被当做当前对象的方法的参数传入进来的对象
  3. 当前对象的方法所创建或者实例化的任何对象
  4. 当前对象的任何组件(被当前对象的实例变量引用的任何对象)

任何一个对象,如果满足上面的条件之一,就是当前对象的“朋友”;否则就是“陌生人”。

迪米特法则指出:就任何对象而言,在该对象的方法内,我们只应该调用属于上述“朋友圈”对象的方法。也就是说:如果两个类不必彼此直接通信,那么这两个类就不应当发生直接的相互作用。如果其中的一个类需要调用另一个类的某一个方法的话,可以通过第三者转发这个调用。不要同陌生人说话,也就是不要调用陌生人的方法。

迪米特法则是一种面向对象系统设计风格的一种法则,尤其适合做大型复杂系统设计指导原则。但是也会造成系统的不同模块之间的通信效率降低,使系统的不同模块之间不容易协调等缺点。同时,因为迪米特法则要求类与类之间尽量不直接通信,如果类之间需要通信就通过第三方转发的方式,这就直接导致了系统中存在大量的中介类,这些类存在的唯一原因是为了传递类与类之间的相互调用关系,这就毫无疑问的增加了系统的复杂度。解决这个问题的方式是:使用依赖倒转原则,这要就可以是调用方和被调用方之间有了一个抽象层,被调用方在遵循抽象层的前提下就可以自由的变化,此时抽象层成了调用方的朋友。

合成复用原则

组合/聚合复用原则(Composite/Aggregation Reuse Principle,CARP)是指要尽量使用组合/聚合而非继承来达到复用目的。另一种解释是在一个新的对象中使用一些已有的对象,使之成为新对象的一部分;新的对象通过向这些对象委托功能达到复用这些对象的目的。

继承复用虽然后简单和易实现的特点,但也存在以下缺点:

  1. 继承复用破坏了类的封装性。因为继承会将父类的实现细节暴露给子类,父类对子类的透明的,所以这种复用又称为“白箱”复用。
  2. 子类与父类的耦合度高。父类的任何改变都会导致子类的实现发生变化,不利于类的拓展与维护。
  3. 限制了复用的灵活性。从父类继承而来的实现是静态的,在编译时已经定义,所以在运行时不可能发生变化。

正是因为继承有上述缺点,所以应首先使用组合/聚合,其次才考虑继承,达到复用的目的。并且在使用继承时,要严格遵循里氏替换原则。有效地使用继承会有助于对问题的理解,降低复杂度,而滥用继承会增加系统构建、维护时的难度及系统的复杂度。

采用组合或聚合复用时,可已经已有对象纳入新对象中,使之成为新对象的一部分,新对象可以调用已有对象的功能,它有以下优点:

  1. 维持了类的封装性。因为成分对象的内部细节是新对象看不见的,所以这种复用又称为“黑箱”复用。
  2. 对象间的耦合度低。可以在类的成员位置声明抽象。
  3. 这种复用所需的依赖较少。
  4. 每一个新的类可以将焦点集中在一个任务上。
  5. 复用的灵活性高。这种复用可以在运行时动态进行,新对象可以动态的引用与成分对象类型相同的对象。

当然,这种复用也有缺点。其中最主要的缺点就是系统中会有较多的对象需要管理。

要正确的选择组合/聚合和继承,必须透彻的理解里氏代换原则和Coad法则。里氏代换原则前面学习过,Coad法则由Peter Coad提出,总结了一些什么时候使用继承作为复用工具的条件。只有当以下的Coad条件全部被满足时,才应当使用继承关系:

  • 子类是父类的一个特殊种类,而不是父类的一个角色,也就是区分“has-a”和“is-a”。只有“is-a”关系才符合继承关系,“has-a”关系应当用组合/聚合来描述。 永远不会出现需要将子类换成另外一个类的子类的情况。如果不能肯定将来是否会变成另外一个子类的话,就不要使用继承。
  • 子类具有扩展父类的责任,而不是具有置换(重写)或注销掉父类的责任。如果一个子类需要大量的置换掉父类的行为,那么这个类就不应该是这个父类的子类。
  • 只有在分类学角度上有意义时,才可以使用继承。不要从工具类继承。

错误的使用继承而不是组合/聚合的一个常见原因是错误的把“has-a”当成了“is-a”。“is-a”代表一个类是另外一个类的一种;“has-a”代表一个类是另外一个类的一个角色,而不是另外一个类的特殊种类。

我们看一个例子。如果我们把“人”当成一个类,然后把“雇员”、“经理”、“学生”当成是“人”的子类,如下图所示。这种设计的错误在于把“角色”的等级结构和“人”的等级结构混淆了。“经理”、“雇员”、“学生”是一个人的角色,一个人可以同时拥有上述角色。如果按继承来设计,那么如果一个人是雇员的话,就不可能是经理,也不可能是学生,这显然不合理。

合成复用原则-滥用继承.jpg
合成复用原则-滥用继承.jpg

正确的设计是有个抽象类“角色”,“人”可以拥有多个“角色”(聚合),“雇员”、“经理”、“学生”是“角色”的子类,如下图所示

合成复用原则-滥用继承.jpg
合成复用原则-滥用继承.jpg

此外,只有两个类满足里氏替换原则的时候,才可能是“is-a”关系。也就是说,如果两个类是“has-a”关系,但是设计成了继承,那么肯定违反里氏代换原则。

创建型模式

用于描述“怎样创建对象”,主要特点是“将对象的创建与使用分离”。如:单例、原型、工厂方法、抽象工厂、建造者等

单例模式

单例模式是Java中最简单的设计模式之一。这种类型的设计模式属于创建者模式,它提供了一种创建对象的最佳方式。

这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。

单例模式的实现

单例模式分为两种:

  • 饿汉式:类加载就会导致该单例对象被创建
  • 懒汉式:类加载不会导致该单例对象被创建,而是首次使用该对象才会创建
饿汉单例
  1. 方式一 - 静态变量
代码语言:javascript
复制
public class Singleton {
    private static final Singleton instance = new Singleton();
    
    private Singleton() {}
    
    public static Singleton getSingleton() {
        return instance;
    }
}
  1. 方式二 - 静态代码块
代码语言:javascript
复制
public class Singleton {
    private static Singleton instance;
    
    static {
        this.instance = new Singleton();
    }
    
    private Singleton() {}
    
    public static Singleton getSingleton() {
        return instance;
    }
}
  1. 方式三 - 枚举方式 枚举方式是所有单例实现中唯一一种不会被破坏的单例实现模式(反射)。
代码语言:javascript
复制
public enum Singleton {
    INSTANCE;
}
懒汉单例
  1. 方式一 - 线程不安全
代码语言:javascript
复制
public class Singleton {
    private static Singleton instance;
    
    private Singleton() {}
    
    public static Singleton getSingleton() {
        if (instance == null) {
            this.instance = new Singleton();
        }
        return instance;
    }
}
  1. 方式二 - 线程安全
代码语言:javascript
复制
public class Singleton4 {
    private static Singleton instance;
    
    
    private Singleton() {}
    
    public static synchronized Singleton getSingleton() {
        if (instance == null) {
            this.instance = new Singleton();
        }
        return instance;
    }
}
  1. 方式三 - 双重检查锁
代码语言:javascript
复制
public class Singleton {
    private static volatile Singleton instance;
    
    private Singleton() {}
    
    public static Singleton getSingleton() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    this.instance = new Singleton();
                }
            }
        }
        return instance;
    }
}
  1. 方式四 - 静态内部类 由于 JVM 在加载外部类的过程中,是不会加载静态内部类的,只有内部类的方法/属性被调用时才会加载,并初始化其静态属性。静态属性由于被 static 修饰,保证了指令顺序和实例化次数。
代码语言:javascript
复制
public class Singleton {
    private Singleton() {}
    
    private static class SingletonHolder() {
        private static final Singleton INSTANCE = new Singleton();
    }
    
    public static Singleton getSingleton() {
        return SingletonHolder.INSTANCE;
    }
}

静态内部类单例模式是一种优秀的单例模式,是开源项目中比较常用的一种单例模式。在没有加任何锁的情况下,保证了线程安全,并且没有任何性能影响和空间的浪费。

存在的问题
破坏单例模式

使用反射或序列化可以破坏除枚举外的单例模式

测试单例类:

代码语言:javascript
复制
public class Singleton implement Serializable {
    private Singleton() {}
    
    private static class SingletonHolder() {
        private static final Singleton INSTANCE = new Singleton();
    }
    
    public static Singleton getSingleton() {
        return SingletonHolder.INSTANCE;
    }
}
  • 序列化反序列化
代码语言:javascript
复制
public class test {
    public static void main(String...args) {
        serializeObject();
        deserializeObject();
        System.out.println(deserializeObject().equals(deserializeObject()));
//		输出false
    }
    
    public static Singleton deserializeObject() throw Exception {
        var ois = new ObjectInputStream(new FileInputStream("./test.text"));
        Singleton instance = (Singleton) ois.readObject();
        ois.close();
        return instance;
    }
    
    public static void serializeObject() throw Exception {
        Singleton instance = Singleton.getSingleton();
        var oos = new ObjectOutputStream(new FileOutputStream("./test.txt"));
        oos.writeObject(instance);
        oos.close;
    }
}
  • 反射
代码语言:javascript
复制
public class test {
    public static void main(String...args) {
        Class clazz = Singleton.class;
        var constructs = clazz.getDeclaredConstructor();
        constructs.setAccessible(true);
        Singleton s1 = (Singleton) constructs.newInstance();
        System.out.println(s1.equals(Singleton.getSingleton());
//		输出 false
    }
}
解决

反射无解 序列化反序列化解决:

代码语言:javascript
复制
public class Singleton implement Serializable {
    private Singleton() {}
    
    private static class SingletonHolder() {
        private static final Singleton INSTANCE = new Singleton();
    }
    
    public static Singleton getSingleton() {
        return SingletonHolder.INSTANCE;
    }
    
//	增加如下代码
    public Object readResolve() {
        return SingletonHolder.INSTANCE;
    }
}

工厂模式

工厂模式最大的优点是与具体对象解耦

简单工厂模式

简单工厂不是一种设计模式,反而比较像是编程习惯。

结构
  • 抽象产品:定义了产品的规范,描述了产品的主要特性和功能
  • 具体产品:实现或继承抽象产品的子类
  • 具体工厂:提供了创建产品的方法,调用者通过该方法来创建产品
实现
代码语言:javascript
复制
/** 咖啡工厂 */
public class CoffeeFactory {
//	这里是静态工厂
    public static Coffee createCoffee(String type) {
        Coffee coffee;
        if ("american".equals(type)) {
            coffee = new AmericanCoffee();
        } else if ("latte".equals(type)) {
            coffee = new LatteCoffee();
        }
        return coffee;
    }
}
工厂方法模式

定义一个用于创建对象的接口,让子类决定实例化哪个产品类对象。工厂方法使用一个产品类的实例化延迟到其工厂的子类。

结构
  • 抽象工厂:提供了创建产品的接口,调用者通过它访问具体工厂的工厂方法来创建产品
  • 具体工厂:实现抽象工厂中的抽象方法,完成具体产品的创建
  • 抽象产品:定义了产品的规范,描述了产品的主要特性和功能
  • 具体产品:实现了抽象产品角色所定义的接口,由具体工厂来创建,它同具体工厂之间 一一对应
抽象工厂模式

抽象工厂模式是工厂方法模式的升级版本,工厂方法模式只生产一个等级的产品,而抽象工厂模式可生产多个等级的产品

抽象工厂-产品族.png
抽象工厂-产品族.png

纵向是产品族,横向是产品等级

结构
  • 抽象工厂:提供了创建产品的接口,它包含多个创建产品的方法,可以创建多个不同等级的产品
  • 具体工厂:主要是实现抽象方法中的多个抽象方法,完成具体产品的创建
  • 抽象产品:定义了产品的规范,描述了产品的主要特性和功能,抽象工厂有多个抽象产品
  • 具体产品:实现了抽象产品角色所定义的接口,由具体工厂来创建,它同具体工厂之间是多对一的关系
抽象工厂-类图.png
抽象工厂-类图.png
优缺点

优: 当一个产品族中的多个对象被设计成一起工作时,它能保证客户端始终只使用同一个产品族中的对象

劣: 当产品族中需要增加一个新的产品时,所有的工厂类都需要进行修改

使用场景
  • 当需要创建的对象是一系列相互关联或相互依赖的产品族
  • 系统中有多个产品族,但每次只是用其中的某一族产品
  • 系统中提供了产品的类库,且所有产品的接口相同,客户端不依赖产品实例的创建细节和内部结构

Collection接口是抽象工厂类,ArrayList是具体的工厂类;Iterator接口是抽象商品类,ArrayList类中的Iter内部类是具体的商品类。在具体的工厂类中iterator()方法创建具体的商品类对象。

> 补充:

  1. DateFormat类中的getInstance()方法使用的是工厂模式
  2. Calendar类中的getInstance()方法使用的是工厂模式

原型模式

用一个已经创建的实例作为原型,通过复制该原型对象来创建一个和原型对象相同的新对象

结构
  • 抽象原型类:规定了具体原型对象必须实现的 clone() 方法
  • 具体原型类:实现抽象原型类的 clone() 方法,它是可被复制的对象
  • 访问类:使用具体原型类中的 clone() 方法来复制新的对象
实现
原型模式-类图.png
原型模式-类图.png

原型模式的克隆分为浅拷贝和深拷贝

  • 浅拷贝:创建了一个对象,新对象的属性和原来对象完全相同,对于非基本类型属性,仍指向原有属性所指向的地址
  • 深拷贝:创建一个新对象,属性中引用的其他对象也会被克隆,不在指向原有对象地址。

Java的Object类提供的 clone() 方法实现的是浅拷贝。Cloneable 接口是抽象原型类,实现了 Cloneable 接口的是具体原型类

使用场景
  • 对象的创建非常复杂,可以使用原型模式快捷的创建对象
  • 性能和安全要求比较高
深拷贝

通过序列化反序列化

建造者模式

将一个复杂对象的结构与表示分离,使得同样的构造过程可以创建不同的表示

  • 分离了部件的构造(由Builder来负责)和装配(由Director负责)。从而可以构造出复杂的对象。
  • 由于实现了构建和装配的解耦。不同的构建器,相同的装配,也可以做出不同的对象;相同的构建器,不同的装配顺序也可以做出不同的对象。也就是实现了构建算法、装配算法的解耦。
  • 建造者模式可以将部件和其组装过程分开,一步一步创建一个复杂的对象。用户只需要指定复杂对象的类型就可以得到该对象,而无须知道其内部的具体构造细节。
结构
  • 抽象建造者类(Builder):这个接口规定要实现复杂对象的那些部分的创建,并不涉及具体的对象部件的创建。
  • 具体建造者类(ConcreteBuilder):实现 Builder 接口,完成复杂产品的各个部件的具体创建方法。在构造过程完成后,提供产品的实例。
  • 产品类(Product):要创建的复杂对象。
  • 指挥者类(Director):调用具体创造者来创建复杂对象的各个部分,在指导者中不涉及具体产品的信息,只负责保证各部分完整创建或按某种顺序创建(本例子中没有使用多个建造方式,真实开发中也并不多见)
建造者模式-类图.jpg
建造者模式-类图.jpg
一般实现
  • Computer - 产品类
代码语言:javascript
复制
package com.example.factory.builder.traditional;

import lombok.Data;

/**
 * @author 墨
 */
@Data
public class Computer {
    private String cpu;
    private String motherboard;
    private String monitor;
    private String keyboard;
    private String mouse;
}

Computer即Product,这个电脑产品就是一个我们最终所需要的,它由CPU、主板、显示器、键盘、鼠标组成。

  • ComputerBuilder - 抽象建造者
代码语言:javascript
复制
package com.example.factory.builder.traditional;

/**
 * @author 墨
 */
public interface ComputerBuilder {
    Computer computer = new Computer();
    default Computer getComputer() {
        return computer;
    }

    void buildCpu(String cpu);
    void buildMotherboard(String motherboard);
    void buildMonitor(String monitor);
    void buildKeyboard(String keyboard);
    void buildMouse(String mouse);
}

ComputerBuilder 是一个Builder类,我们在里面定义了建造所需要的方法,包含了构建CPU、主板、显示器、键盘、鼠标的方法。

下面我们来创建一个ConcreteBuilder,SpecificComputerBuilder实现了ComputerBuilder,并且实现了抽象建造的一些细节。

  • SpecificComputerBuilder - 具体建造者
代码语言:javascript
复制
package com.example.factory.builder.traditional;

/**
 * @author 墨
 */
public class SpecificComputerBuilder implements ComputerBuilder {

    @Override
    public void buildCpu(String cpu) {
        computer.setCpu(cpu);
    }

    @Override
    public void buildMotherboard(String motherboard) {
        computer.setMotherboard(motherboard);
    }

    @Override
    public void buildMonitor(String monitor) {
        computer.setMonitor(monitor);
    }

    @Override
    public void buildKeyboard(String keyboard) {
        computer.setKeyboard(keyboard);
    }

    @Override
    public void buildMouse(String mouse) {
        computer.setMouse(mouse);
    }
}
  • 测试类
代码语言:javascript
复制
package com.example.factory.builder.traditional;

/**
 * @author 墨
 */
public class TestBuilder {
    public static void main(String[] args) {
        SpecificComputerBuilder computerBuilder = new SpecificComputerBuilder();
        computerBuilder.buildCpu("i5 cpu");
        computerBuilder.buildMotherboard("蓝天主板");
        computerBuilder.buildMonitor("小米显示器");
        computerBuilder.buildMouse("双飞燕鼠标");
        computerBuilder.buildKeyboard("双飞燕键盘");

        Computer computer = computerBuilder.getComputer();
        System.out.println(computer);
    }
}
升级-链式调用

前文我们谈论到建造时,创建一个具体建造者每次都使用SpecificComputerBuilder 的方法去构建一个产品的属性,这不是很符合我们编码的习惯,也不符合建造者一步步建造的思想,特别是当一个产品的属性多起来之后我们要写很多行这样类似的代码,不优雅,也不美观,而链式建造刚好能解决这样的问题。

修改 接口(抽象建造者)的方法,令其组装过程中返回自身,同时修改具体的建造类。

代码语言:javascript
复制
package com.example.factory.builder.traditional;

/**
 * @author 墨
 */
public interface ComputerBuilder {
    Computer computer = new Computer();
    default Computer setComputer() {
        return computer;
    }

    ComputerBuilder buildCpu(String cpu);
    ComputerBuilder buildMotherboard(String motherboard);
    ComputerBuilder buildMonitor(String monitor);
    ComputerBuilder buildKeyboard(String keyboard);
    ComputerBuilder buildMouse(String mouse);
}
代码语言:javascript
复制
package com.example.factory.builder.traditional;

/**
 * @author 墨
 */
public class SpecificComputerBuilder implements ComputerBuilder {

    @Override
    public ComputerBuilder buildCpu(String cpu) {
        computer.setCpu(cpu);
        return this;
    }

    @Override
    public ComputerBuilder buildMotherboard(String motherboard) {
        computer.setMotherboard(motherboard);
        return this;
    }

    @Override
    public ComputerBuilder buildMonitor(String monitor) {
        computer.setMonitor(monitor);
        return this;
    }

    @Override
    public ComputerBuilder buildKeyboard(String keyboard) {
        computer.setKeyboard(keyboard);
        return this;
    }

    @Override
    public ComputerBuilder buildMouse(String mouse) {
        computer.setMouse(mouse);
        return this;
    }
}

测试类

代码语言:javascript
复制
package com.example.factory.builder.traditional;

/**
 * @author 墨
 */
public class TestBuilder {
    public static void main(String[] args) {
        SpecificComputerBuilder computerBuilder = new SpecificComputerBuilder();
        computerBuilder.buildCpu("i5 cpu");
        computerBuilder.buildMotherboard("蓝天主板");
        computerBuilder.buildMonitor("小米显示器");
        computerBuilder.buildMouse("双飞燕鼠标");
        computerBuilder.buildKeyboard("双飞燕键盘");

        Computer computer = computerBuilder.setComputer();
        System.out.println(computer);
        
        Computer chain = new SpecificComputerBuilder().buildCpu("i3").buildMotherboard("白云主板").buildMonitor("大米显示器")
                .buildMouse("单飞燕鼠标").buildKeyboard("单飞燕键盘").setComputer();
        System.out.println(chain);
    }
}
使用案例

OkHttp3 的 OkHttpClient 创建

代码语言:javascript
复制
public static Result<List<CourseDto>> sendCourseReq(CourseSearchDto courseSearchDto) {
    try {
        OkHttpClient httpClient = new OkHttpClient();
        Request request = new Request.Builder()
            .url(ConfigFile.CONFIG.getUrl() + "/api/sel")
            .addHeader("Content-Type", "application/json")
            .post(RequestBody.create("aaa".getBytes()))
            .build();
    } catch (Exception e) {
        e.printStackTrace();
    }
}
整合
代码语言:javascript
复制
package com.example.factory.builder.chain;

import lombok.ToString;

/**
 * @author 墨
 */
@ToString
public class ComputerBuilder {
    private String cpu;
    private String motherboard;
    private String monitor;
    private String keyboard;
    private String mouse;

    public ComputerBuilder(String motherboard, String monitor, String keyboard, String mouse,String cpu) {
        this.motherboard = motherboard;
        this.monitor = monitor;
        this.keyboard = keyboard;
        this.mouse = mouse;
        this.cpu = cpu;
    }

    public static Builder newBuilder() {
        return new Builder();
    }
    
    public static class Builder {
        private String cpu;
        private String motherboard;
        private String monitor;
        private String keyboard;
        private String mouse;

        public Builder() {
        }

        public Builder setCpu(String cpu) {
            this.cpu = cpu;
            return this;
        }

        public Builder setMotherboard(String motherboard) {
            this.motherboard = motherboard;
            return this;
        }

        public Builder setMonitor(String monitor) {
            this.monitor = monitor;
            return this;
        }

        public Builder setKeyboard(String keyboard) {
            this.keyboard = keyboard;
            return this;
        }

        public Builder setMouse(String mouse) {
            this.mouse = mouse;
            return this;
        }
        
        public ComputerBuilder builder() {
            return new ComputerBuilder(motherboard,monitor,keyboard,mouse,cpu);
        }
    }
}
一些细节
  1. 当建造时如果没有创建所有细节时会有空值 我们可以通过Builder的构造方法来设置默认值
代码语言:javascript
复制
public Builder() {
    cpu = "默认cpu";
    motherboard = "默认主板";
    monitor = "默认显示器";
    keyboard = "默认键盘";
    mouse = "默认内存";
}
  1. 当建造时如果创建的参数不符合要求的一些处理 可以在具体建造时对入参一些限制及过滤,例如:输入i5时我们觉得这个CPU太烂了,直接换个更更更好的
代码语言:javascript
复制
public ComputerBuilder builder() {
    if ("i5".equals(cpu)) {
        cpu = "i3";
    }
    return new ComputerBuilder(motherboard,monitor,keyboard,mouse,cpu);
}
优缺点

优点

  • 建造者模式的封装性很好。使用建造者模式可以有效的封装变化,在使用建造者模式的场景中,一般产品类和建造者类是比较稳定的,因此,将主要的业务逻辑封装在Director类中对于整体而言可以取得比较好的稳定性。
  • 建造者模式中,客户端不必知道产品的内部组成细节,将产品本身与产品的创建过程解耦,使得相同的创建过程可以创建不同的产品对象。
  • 可以更加精细地控制产品的创建过程。将复杂产品的创建步骤分解在不同的方法中,使得创建过程更加清晰,也更方便使用程序来控制创建过程。
  • 建造者模式可以很容易进行拓展。如果有新的需求,通过实现一个新的建造者类就可以完成,基本上不用修改之前已经通过测试的代码,因此也就不会对原有功能引入风险。符合开闭原则。

缺点 建造者模式所创建的产品一般具有较多的共同点,其组成部分相似,如果产品之间的差异性很大,则不适合使用建造者模式,因此其适用范围受到一定的限制。

使用场景

建造者模式创建的是复杂对象,其产品的各个部分经常面临着剧烈的变化,但将它们组合在一起的算法却相对稳定,所以通常在以下场合使用:

  • 创建的对象较复杂,由多个部件构成,各部件面临着复杂的变化,但构件间的建造顺序是稳定的
  • 创建复杂对象的算法独立于该对象的组成部分以及它们的装配方式,即产品的构建过程和最终的表示是独立的

结构型模式

用于描述如何将类或对象按某种布局组成更大的结构。如代理、适配器、桥接、装饰、外观、享元、组合等

代理模式

由于某些原因需要给某对象提供一个代理以控制该对象的访问。这时,访问对象不适合或不能直接引用目标对象,代理对象作为访问对象和目标对象之间的中介。

Java中的代理按照代理类生成时机不同分为静态代理和动态代理。静态代理代理类在编译时就生成,而动态代理代理类是在Java运行时动态生成。动态代理又有JDK代理和CGLib两种。

结构
  • 抽象主题类(Subject):通过接口或抽象类声明真实主题和代理对象的业务方法
  • 真实主题类(Real Subject):实现了抽象主题中的具体业务,是代理对象所代表的真实对象,是最终要引用的对象
  • 代理类(Proxy):提供了与真实主题类相同的接口,其内部含有对真实主题的引用,它可以访问、控制或拓展真实主题的功能
代理模式-类图.jpg
代理模式-类图.jpg
静态代理

以租房为例,我们一般用租房软件、找中介或者找房东。这里的中介就是代理者。

首先定义一个提供了租房方法的接口

代码语言:javascript
复制
public interface IRentHouse {
    void rentHouse();
}

定义租房的实现类

代码语言:javascript
复制
public class RentHouse implements IRentHouse {
    @Override
    public void rentHouse() {
        System.out.println("租了一间房子。。。");
    }
}

我要租房,房源都在中介手中,所以找中介

代码语言:javascript
复制
public class IntermediaryProxy implements IRentHouse {

    private IRentHouse rentHouse;

    public IntermediaryProxy(IRentHouse irentHouse){
        rentHouse = irentHouse;
    }

    @Override
    public void rentHouse() {
        System.out.println("交中介费");
        rentHouse.rentHouse();
        System.out.println("中介负责维修管理");
    }
}

测试类

代码语言:javascript
复制
public class Main {
    public static void main(String[] args){
        //定义租房
        IRentHouse rentHouse = new RentHouse();
        //定义中介
        IRentHouse intermediary = new IntermediaryProxy(rentHouse);
        //找中介租房
        intermediary.rentHouse();
    }
}

这就是静态代理,因为中介这个代理类已经事先写好了,只负责代理租房业务

JDK动态代理

Java中提供了一个动态代理类Proxy,Proxy并不是我们上述所说的代理对象的类,而是提供了一个创建代理方法 (newProxyInstance方法 来获取代理对象

1.提供一个接口和实现类
代码语言:javascript
复制
public interface Skill {
    void run();
    void swim();
}

public class Althletes implements Skill{

    @Override
    public void run() {
        System.out.println("Run fast");
    }

    @Override
    public void swim() {
        System.out.println("Swim fast");
    }
}
2.创建代理工厂 传入目标对象

只是代理工厂,代理类在内存中动态生成

代码语言:javascript
复制
public class AlthleteProxy {
    public Skill AlthleteProxy(Althletes obj) {
        return (Skill) Proxy.newProxyInstance(obj.getClass().getClassLoader(), obj.getClass().getInterfaces()
            , ((proxy, method, args) ->  {
            System.out.println("Start");
            Object result = method.invoke(proxy, args);
            System.out.println("Finish");
            return result;
            };));
    }
}
3.调用代理对象
代码语言:javascript
复制
public class Main { 
    public static void main(String[] args) {
        Skill s = new AlthleteProxy(new Althletes);
        s.run();
        s.swim();
    }
}
CGLib动态代理

CGLib动态代理的实现机制是生成目标类的子类,通过调用父类(目标类)的方法实现,在调用父类方法时再代理中进行增强。

基本类

代码语言:javascript
复制
package top.ytao.demo.proxy;

/**
 * Created by YangTao
 */
public class Cat {
    public void call() {
        System.out.println("喵喵喵 ~");
    }
}
实现 MethodInterceptor 接口

相比于 JDK 动态代理的实现,CGLIB 动态代理不需要实现与目标类一样的接口,而是通过方法拦截的方式实现代理,代码实现如下,首先方法拦截接口 net.sf.cglib.proxy.MethodInterceptor

代码语言:javascript
复制
/**
 * Created by YangTao
 */
public class TargetInterceptor implements MethodInterceptor {

    @Override
    public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
        System.out.println("CGLIB 调用前");
        Object result = proxy.invokeSuper(obj, args);
        System.out.println("CGLIB 调用后");
        return result;
    }
}

通过方法拦截接口调用目标类的方法,然后在该被拦截的方法进行增强处理,实现方法拦截器接口的 intercept 方法里面有四个参数:

  • obj 代理类对象
  • method 当前被代理拦截的方法
  • args 拦截方法的参数
  • proxy 代理类对应目标类的代理方法
创建CGLib动态代理类

创建 CGLIB 动态代理类使用 net.sf.cglib.proxy.Enhancer 类进行创建,它是 CGLIB 动态代理中的核心类,首先创建个简单的代理类:

代码语言:javascript
复制
/**
 * Created by YangTao
 */
public class CglibProxy {
    public static Object getProxy(Class<?> clazz) {
        Enhancer enhancer = new Enhancer();
        // 设置类加载
        enhancer.setClassLoader(clazz.getClassLoader());
        // 设置被代理类
        enhancer.setSuperclass(clazz);
        // 设置方法拦截器
        enhancer.setCallback(new TargetInterceptor());
        // 创建代理类
        return enhancer.create();
    }
}

测试类

代码语言:javascript
复制
public class Main {
    @Test
    public void dynamicProxy() throws Exception {
        Animal cat = (Animal) CglibProxy.getProxy(Cat.class);
        cat.call();
    }
}

Enhancer 在使用过程中,常用且有特色功能还有回调过滤器 CallbackFilter 的使用,它在拦截目标对象的方法时,可以有选择性的执行方法拦截,也就是选择被代理方法的增强处理。使用该功能需要实现 net.sf.cglib.proxy.CallbackFilter 接口。不做展开,需要了解可以看参考12。

优缺点

  • 代理模式在客户端与目标对象之间起到一个中介作用和保护目标对象的作用
  • 代理对象可以拓展目标对象的功能
  • 代理模式能将客户端与目标对象分离,在一定程度上降低了系统的耦合度

  • 增加了系统的复杂度
使用场景
  • 远程代理 本地服务通过网络请求远程服务。为了实现本地到远程的通信,我们需要实现网络通信,处理其中可能的异常。为良好的代码设计和可维护性,我们将网络通信部分隐藏起来,只暴露给本地服务一个接口,通过接口即可访问远程服务提供的功能,而不必过多关心通信部分的细节(RPC)
  • 防火墙代理 防火墙将请求转发给互联网,再将响应转发给你
  • 保护代理
  • 控制对一个对象的访问,如果需要,可以给不同的用户提供不同级别的使用权限。

适配器模式

将一个类的接口转换成客户希望的另一个接口,使得原本由于接口不兼容而不能一起工作的类能一起工作。

适配器模式(Adapter)分为类适配器模式和对象适配器模式,前者类之间的耦合度比较高,且要求程序员了解现有组件库中的相关组件内部接口,所以应用相对较少。

结构
  • 目标(Target)接口:当前系统业务所期待的接口,它可以是抽象类或接口。
  • 适配者(Adaptee)类:它是被访问和适配的现存组件库中的组件接口。
  • 适配器(Adapter)类:它是一个转换器,通过继承或引用适配者的对象,把适配者接口转换成目标接口,让客户按目标接口的格式访问适配者。
类适配器模式

实现方式:定义一个适配器类来实现当前系统的业务接口,同时又继承现有组件库中已存在的组件

适配器模式-类适配器-类图.jpg
适配器模式-类适配器-类图.jpg
  • 适配者类
代码语言:javascript
复制
/**
 * 两孔插座
 */
public class TwoHoleSocket {
    public void twoHole() {
        System.out.println("插入两孔插座");
    }
}
  • 目标接口
代码语言:javascript
复制
/**
 * 三孔插座
 */
public interface ThreeHoleSocket {
    void threeHole();
}
  • 适配器类
代码语言:javascript
复制
/**
 * 适配器:可以同时使用两孔和三孔插座
 */
public class Adapter extends TwoHoleSocket implements ThreeHoleSocket{
    @Override
    public void threeHole() {
        System.out.println("插入三孔插座");
    }
}
  • 测试方法
代码语言:javascript
复制
public static void main(String[] args) {
        Adapter adapter = new Adapter();
        adapter.twoHole();//使用两孔插座
        adapter.threeHole();//使用三孔插座
    }

类适配器模式违背了合成复用原则,仅在目标接口规范的情况下可用

对象适配器模式

通过组合关系来实现适配器功能

代码语言:javascript
复制
/**
 * 适配器
 */
public class Adapter implements ThreeHoleSocket{
    //通过组合持有两孔插座的对象,内部引用两孔插座来适配
    private TwoHoleSocket twoHoleSocket;

    public Adapter(TwoHoleSocket twoHoleSocket) {
        this.twoHoleSocket = twoHoleSocket;
    }

    public void twoHole() {
        twoHoleSocket.twoHole();
    }

    @Override
    public void threeHole() {
        System.out.println("插入三孔插座");
    }
}

测试方法

代码语言:javascript
复制
public static void main(String[] args) {
        Adapter adapter = new Adapter(new TwoHoleSocket());
        adapter.twoHole();//两孔插座
        adapter.threeHole();//三孔插座
    }
接口适配器模式

接口适配器模式也称作缺省适配模式,就是有时候一个接口的方法太多,我只想用其中的一两个,不想为其他方法提供实现,就可以通过一个抽象类为这个接口的所有方法,提供空实现,如果想用哪个方法,再提供一个子类继承这个抽象类,覆盖父类某个方法即可。

应用场景
  • 以前开发的系统存在满足新系统功能需求的类,但其接口定义和新系统不一致
  • 使用第三方组件库,但组件接口定义和自己要求的接口定义不同
JDK使用

Reader(字符流)、InputStream(字节流)的适配使用的就是 InputStreamReader 和 OutputStreamWriter

InputStreamReader 和 OutputStreamWriter 分别继承自 java.io包下的 Reader 和 Writer,对它们中的抽象的未实现的方法给出实现。

适配器模式-JDK使用-类图.png
适配器模式-JDK使用-类图.png

InputStreamReader 做了 InputStream字节流 到 Reader字符流 之间的转换。 从类图来看,StreamDecoder 的设计实现采用了(对象)适配器模式。

装饰者模式

在不改变现有对象结构的情况下,动态地给该对象增加一些职责(额外功能)

结构
  • 抽象构件角色(Component):定义一个抽象接口以规范准备接收附加职责的对象
  • 具体构件角色(ConcreteComponent):实现抽象构件,并通过装饰角色为其添加职责
  • 抽象装饰角色(Decorator):继承或实现抽象构件,并包含具体构件的实例,可以通过其子类拓展具体构件的功能
  • 具体装饰角色(ConcreateDecorator):实现抽象装饰的相关方法,并给具体构件对象添加附加职责
装饰者模式-类图.jpg
装饰者模式-类图.jpg
优缺点

  • 装饰者模式可以带来比继承更加灵活的扩展功能,可以通过组合不同的装饰着对象来获取具有不同行为状态的多样化的结果。
  • 遵循开闭原则
  • 继承是静态的附加责任,装饰者模式是动态的附加责任

  • 多层装饰比较复杂,提高了系统复杂度
使用场景
  • 当不能采用继承的方式对系统进行扩充或采用集成不利于系统扩展和维护时 不能采用继承的情况: -- 系统中存在大量独立的扩展,为支持每一种组合将产生大量的子类,使得子类数目呈爆炸型增长 -- 类定义为不能继承
  • 不影响其他对象的情况下,以动态、透明的方式给单个对象添加职责
  • 当对象的功能需要可以动态地添加,也可以再动态地撤销时
JDK使用

I/O流中的包装类使用了装饰者模式。BufferedInputStream, BufferedOutputStream, BufferedReader, BufferedWriter

装饰者模式-JDK使用-类图.png
装饰者模式-JDK使用-类图.png

BufferedInputStream 使用装饰者模式对 InputStream 的子实现类 FilterInputStream 进行增强,添加了缓冲区,提高了写效率

根据上图可以看出:

  • 抽象构件(Component)角色:由InputStream扮演。这是一个抽象类,为各种子类型提供统一的接口。
  • 具体构件(ConcreteComponent)角色:由ByteArrayInputStream、FileInputStream、PipedInputStream、StringBufferInputStream等类扮演。它们实现了抽象构件角色所规定的接口。
  • 抽象装饰(Decorator)角色:由FilterInputStream扮演。它实现了InputStream所规定的接口。
  • 具体装饰(ConcreteDecorator)角色:由几个类扮演,分别是BufferedInputStream、DataInputStream以及两个不常用到的类LineNumberInputStream、PushbackInputStream。

桥接模式

将抽象和实现分离,是它们可以独立变化。它是用组合关系代替继承关系来实现,从而降低了抽象和实现这两个可变维度的耦合度。

结构
  • 抽象化角色(Abstraction):定义抽象类,并包含一个对实现化对象的引用。
  • 拓展抽象化角色(RefinedAbstraction):是抽象化角色的子类,实现父类中的业务方法,并通过组合关系调用实现化角色中的业务方法。
  • 实现化角色(Implementor):这个角色给出实现化角色的接口,但不给出具体的实现。必须指出的是,这个接口不一定和抽象化角色的接口定义相同,实际上,这两个接口可以非常不一样。实现化角色应当只给出底层操作,而抽象化角色应当只给出基于底层操作的更高一层的操作。
  • 具体实现化角色(ConcreteImplementor):给出实现化角色接口的具体实现。
桥接模式-类图.jpg
桥接模式-类图.jpg
使用场景
  • 当一个类存在两个(以上)独立变化的维度,且这两个维度都需要进行扩展
  • 当一个系统不希望使用继承或因为多层继承导致系统类的个数急剧增加
  • 当一个系统需要在构建的抽象化角色和具体化角色之间增加更多的灵活性。避免在两个层次之间建立静态的继承联系,通过桥接模式可以在它们的抽象层建立一个关联关系

外观模式

外观模式,又称门面模式,是一种通过为多个复杂的子系统提供一个一致的接口,而使这些子系统更加容易被访问的模式。该模式对外有一个统一接口,外部应用程序不用关心系统内部的具体实现,这样会大大降低程序的复杂度,提高程序的可维护性。

外观模式是 “迪米特法则” 的典型应用

结构
  • 外观角色(Facade):为多个子系统对外提供一个共同的接口
  • 子系统角色(Sub System):实现系统的部分功能,客户可以通过外观角色访问它
外观模式-类图.jpg
外观模式-类图.jpg
优缺点

  • 降低了子系统与客户端之间的耦合度,使得子系统的变化不会影响调用它的客户端
  • 对客户屏蔽了子系统组件,减少了客户处理的对象数目,并使得子系统使用起来更加容易

  • 不符合开闭原则
使用场景
  • 对分层结构系统构建时,使用外观模式定义子系统中每层的入口点可以简化子系统之间的依赖关系
  • 当一个复杂系统的子系统很多时,外观模式可以为系统设计一个简单的接口供外界访问
  • 当客户端与多个子系统之间存在很大的联系时,引入外观模式可将他们分离,从而提高子系统的独立性和可移植性
源码使用

使用 Tomcat 作为Web容器时,接收浏览器发送过来的请求,Tomcat 会将请求信息封装成 ServletRequest 对象。 但 ServletRequest 是一个接口,它还有一个子接口 HttpServletRequest,而我们知道该 reqest 对象肯定是 HttpServletRequest 对象的子实现类对象,即 RequestFacade 对象

外观模式-源码-类图.png
外观模式-源码-类图.png

组合模式

组合模式,又称部分整体模式,是用于把一组相似的对象当做一个单一的对象。组合模式依据树形结构来组合对象,用来表示部分以及整体层次。

结构
  • 抽象根节点(Component):定义系统各层次对象的共有方法和属性,可以预先定义一些默认行为和属性。
  • 树枝节点(Composite):定义树枝节点的行为,存储子节点,组合树枝节点和叶子结点形成一个树结构。
  • 叶子结点(Leaf):叶子结点对象
分类
  • 透明组合模式

透明组合模式中,抽象根节点角色中声明了所有用于管理成员对象的方法,这样做的好处是确保所有的构建类都有相同的接口。透明组合模式也是组合模式的标准形式。

  • 安全组合模式

在安全组合模式中,抽象构建角色中没有声明任何用于管理成员对象的方法,而是在树枝节点中声明并实现这些方法。安全组合模式的缺点是不够透明,因为叶子构建和容器构建具有不同的方法,且容器构建中那些用于管理成员对象的方法没有在抽象构建类中定义,因此客户端不能完全针对抽象编程,必须有区别的对待叶子构件和容器构件。

优缺点

  • 组合模式可以清楚地定义分层次的复杂对象,表示对象的全部或部分层次,它让客户端忽略了层次的差异,方便对整个层次结构进行控制
  • 客户端可以一致的使用一个组合结构或其中单个对象,不比关心处理的是单个对象还是整个组合结构,优化了客户端代码
  • 在组合模式中添加新的树枝节点和叶子结点都很方便,无需对现有类库进行任何修改,符合开闭原则
  • 组合模式为树形结构的面型对象实现提供了一种灵活的解决方案,通过叶子结点和树枝节点的递归组合,可以形成复杂的树形结构,但对树形结构的控制缺非常简单
使用场景

组合模式正是对应树形结构而生,所以组合模式的使用场景就是出现树形结构的地方。如:文件目录显示,多级目录呈现等。

享元模式

运用共享技术来有效地支持大量细粒度对象的复用。它通过共享已经存在的对象来大幅度减少需要创建的对象数量,避免大量相似对象的开销,从而提高系统资源的利用率。

结构

享元模式(Fluweight)存在以下两种状态:

  1. 内部状态 - 即不会随着环境的改变而改变的可共享部分
  2. 外部状态 - 指随着环境改变而改变的不可共享的部分。享元模式的实现要领就是区分应用中的这两种状态,并将外部状态外部化。

主要角色:

  • 抽象享元角色(Flyweight):通常是一个接口或抽象类,在抽象享元类中声明了具体享元类公共方法,这些方法可以向外部提供享元对象的内部数据(内部状态),同时也可以通过这些方法设置外部数据(外部状态)。
  • 具体享元角色(ConcreteFlyweight):它实现了抽象享元类,称为享元对象。在具体享元类中为内部状态提供了存储空间。通常结合单例模式设计具体享元类,为每个具体享元类提供唯一的享元对象。
  • 非享元角色(UnsharableFlyweight):并不是所有的抽象享元类的子类都需要被共享,不能被共享的子类可设计为非共享具体享元类,当你需要一个非共享具体享元类的对象时可以直接通过实例化创建。
  • 享元工厂角色(FlyweightFactory):负责创建和管理享元角色。当客户对象请求一个享元对象时,享元工厂检查系统中会否存在符合要求的享元对象,如果存在则提供给客户;如果不存在,则创建一个新的享元对象。
优缺点

  • 极大减少了内存中相似或相同对象数量,节约系统资源, 提高系统性能
  • 享元模式中的外部状态相对独立,且不影响内部状态

  • 为了使对象可以共享,需要将享元对象的部分状态外部化,分离内部状态和外部状态,使程序逻辑复杂
使用场景
  • 一个系统有大量相同或相似的对象,造成内存的大量耗费
  • 对象的大部分状态都可以外部化,可以将这些外部状态传入对象中
  • 在使用享元模式时需要维护一个存储享元对象的享元池,这需要耗费一定的系统资源。因此应当在需要多次重复使用享元对象时才值得使用享元模式
JDK使用

int 的包装类 Integer类 使用了享元模式

代码语言:javascript
复制
public static void main(String...args) {
    Integer i1 = 127;
    Integer i2 = 127;
    Integer i3 = 128;
    Integer i4 = 128;
    
//	true
    System.out.println(i1 == i2);
//	false
    System.out.println(i3 == i4);
}

直接给 Integer 类型的变量赋值基本数据类型的操作,底层调用了 Integer.valueOf() 方法

代码语言:javascript
复制
@IntrinsicCandidate
    public static Integer valueOf(int i) {
        if (i >= IntegerCache.low && i <= IntegerCache.high)
            return IntegerCache.cache[i + (-IntegerCache.low)];
        return new Integer(i);
    }

可以看到 Integer.valueOf() 先进行了一个判断,判断入参 i 是否在 IntegerCachelowhigh 之间

代码语言:javascript
复制
private static class IntegerCache {
        static final int low = -128;
        static final int high;
        static final Integer[] cache;
        static Integer[] archivedCache;

        static {
            // high value may be configured by property
            int h = 127;
            String integerCacheHighPropValue =
                VM.getSavedProperty("java.lang.Integer.IntegerCache.high");
            if (integerCacheHighPropValue != null) {
                try {
                    h = Math.max(parseInt(integerCacheHighPropValue), 127);
                    // Maximum array size is Integer.MAX_VALUE
                    h = Math.min(h, Integer.MAX_VALUE - (-low) -1);
                } catch( NumberFormatException nfe) {
                    // If the property cannot be parsed into an int, ignore it.
                }
            }
            high = h;

            // Load IntegerCache.archivedCache from archive, if possible
            CDS.initializeFromArchive(IntegerCache.class);
            int size = (high - low) + 1;

            // Use the archived cache if it exists and is large enough
            if (archivedCache == null || size > archivedCache.length) {
                Integer[] c = new Integer[size];
                int j = low;
                for(int i = 0; i < c.length; i++) {
                    c[i] = new Integer(j++);
                }
                archivedCache = c;
            }
            cache = archivedCache;
            // range [-128, 127] must be interned (JLS7 5.1.7)
            assert IntegerCache.high >= 127;
        }

        private IntegerCache() {}
    }

可以看到,如果 Integer.valueOf() 的入参在 -128 ~ 127 之间,会直接返回 IntegerCache.archivedCache 这个数组中已经存在的对象,而不会新建一个对象。

Java 虚拟机会在启动时先初始化一些系统类,例如 java.lang.Integer 类,而 IntegerCache.archivedCache 数组就是在这个时候被初始化的。

行为型模式

用于描述类或对象之间怎样相互协作共同完成单个对象无法完成的任务,以及怎样分配职责。如:模板方法、策略、命令、职责链、状态、观察者、中介者、迭代器、访问者、备忘录、解释器等

行为型模式分为 类行为型模式 和 对象行为型模式,前者采用继承机制在类间分派行为,后者采用组合或聚合在对象间分配行为。由于组合关系或聚合关系比继承关系耦合度低,满足 合成复用原则,所以对象行为模式比 类行为型模式 具有更大的灵活性。

模板方法模式

定义一个操作中的算法骨架,而将算法的一些步骤延迟到子类中,使子类可以在不改变该算法结构的情况下重定义改算法的某些步骤。

结构
  • 抽象类:负责给出一个算法的轮廓
    • 模板方法:定义了算法的骨架,安某种顺序调用其包含的基本方法
    • 基本方法:是实现算法各个步骤的方法,是模板方法的组成部分。可分为三种:
      • 抽象方法(AbstractMethod):一个抽象方法由抽象类声明、由其具体子类实现
      • 具体方法(ConcreteMethod):一个具体方法由一个具体抽象类或具体类声明并实现,其子类可以进行覆盖, 也可以直接继承
      • 钩子方法(HookMethod):在抽象类中已经实现,包括用于判断的逻辑方法和需要重写的子类的空方法两种 一般钩子方法用于逻辑判断的方法,方法名一般为 isXxx,返回值为 boolean 类型
  • 具体子类:实现抽象类中所定义的抽象方法和钩子方法,它们是一个顶层逻辑的组成步骤
优缺点

  • 提高代码复用性
  • 符合开闭原则

  • 对每个不同实现都需要一个子类,导致类的个数增加
  • 父类中的抽象方法由子类实现,子类执行的结果会影响父类的结果,这导致一种反向的控制结构,它提高了代码阅读的难度
JDK使用

InputStream类 使用了模板方法模式,在 InputStream类 定义了多个 read() 方法,具体实现由子类实现。

策略模式

将每个算法封装起来,使它们可以互相替换,且算法的变化不会影响使用算法的客户。 策略模式属于对象行为模式,它通过对算法的封装,把使用算法的责任和算法的实现分割开来,并委派给不同的对象对这些算法进行管理。

结构
  • 抽象策略类(Strategy):是一个抽象角色,通常由一个接口或抽象类实现。此角色给出所有的具体策略类所需的接口
  • 具体策略类(ConcreteStrategy):实现了抽象策略定义的接口,提供具体的算法实现或行为
  • 环境类(Context):持有一个策略类的引用,最终给客户端调用
策略模式-类图.png
策略模式-类图.png
优劣

  • 策略类之间可以自由切换
  • 易于扩展
  • 避免使用较多 (if-else),充分体现面向对象设计思想

  • 客户端必须知道所有的策略类,并自行决定使用哪一个策略类
  • 策略模式将产生很多策略类,可以通过使用享元模式在一定程度上减少对象的数量
使用场景
  • 一个系统需要动态地在几种算法中选择一种时,可将每个算法封装到策略类中
  • 一个类定义了多种行为,并且这些行为在这个类的操作中以多个条件语句形式出现,可将每个分支移入它们各自的策略类中以代替这些条件语句。
  • 系统中各算法彼此完全独立,且要求对客户隐藏具体算法的实现细节时
  • 系统要求使用算法的客户不应该知道其操作的数据时,可使用策略模式来隐藏与算法相关的数据结构
  • 多个类只区别在行为不同,可以使用策略模式,在运行时动态选择要执行的行为
JDK使用

Comparator 类中的策略模式。 在 Arrays 类中的 sort() 方法

代码语言:javascript
复制
public static <T> void sort(T[] a, Comparator<? super T> c) {
...
}

参数列表里可以接一个 Comparator 对象,即新的策略

命令模式

将一个请求封装为一个对象,使发出请求的责任和执行请求的责任分割开。这样两者之间通过命令对象进行沟通,方便将命令对象进行存储、传递、调用与管理

结构
  • 抽象命令类(Command):定义命令的接口,声明执行的方法
  • 具体命令类(ConcreteCommand):具体的命令,实现命令接口;通常会持有接收者,并调用接收者的功能来完成命令要执行的操作
  • 实现者/接收者(Receiver):接收者,真正的命令执行对象。任何类都可能成为一个接收者,只要它能够实现命令要求实现的相应功能
  • 调用者/请求者(Invoker):要求命令对象执行请求,通常会持有命令对象,可以持有很多命令对象。这个是客户端真正触发的命令并要求命令执行相应操作的地方,相当于使用命令对象的入口
优劣

  • 降低系统耦合度。命令模式能将调用操作的对象与实现该操作的对象解耦
  • 增加或删除命令非常方便。采用命令模式增加与删除命令不会影响其他类,满足开闭原则,易于扩展
  • 可以实现宏命令。命令模式可以与组合模式结合,将多个命令装配成一个组合命令,即宏命令
  • 方便实现 Undo 与 Redo 操作。命令模式可以与备忘录模式结合,实现命令的撤销与恢复

  • 使用命令模式可能会导致某些系统有过多的具体命令类
  • 系统结构更加复杂
使用场景
  • 系统需要将请求调用者和请求接收者解耦,使得调用者和接收者不直接交互时
  • 系统需要在不同的时间指定请求,将请求排队和执行请求时
  • 系统需要支持命令的撤销操作和恢复操作时
JDK使用

Runnable 是典型的命令模式,Runnable是命令对象,Thread充当调用者, start方法就是其执行方法

责任链模式

责任链模式,又称职责链模式,为了避免请求发送者与多个请求处理者耦合在一起,将所有请求的处理者通过前一对象记住其下一个对象的引用而连成的一条链;当有请求发生时,可将请求沿着这条链传递,直到有对象处理它为止

结构
  • 抽象处理者(Handler):定义一个处理请求的接口或抽象类,包含抽象处理方法和有一个后继连接
  • 具体处理者(ConcreteHandler):实现抽象处理者的处理方法,判断能否处理本次请求,如果可以处理请求则处理,否则将该请求传给它的后继者
  • 客户类:创建处理链,并向head的具体处理者对象提交请求,它不关心处理细节和请求的传递过程
优劣

  • 降低了对象之间的耦合度
  • 增强了系统的扩展性
  • 增强了给对象指派职责的灵活性
  • 责任链简化了对象之间的连接
  • 责任分担

  • 不能保证每个请求一定被处理。由于一个请求没有明确的接受者,所以不能保证它一定会被处理,该请求可能一直传到链的 tail 都得不到处理
  • 对比较长的责任链,请求的处理可能涉及多个处理对象,系统性能将收到一定影响
  • 责任链建立的合理性要靠客户端来保证,增加了客户端的复杂性,可能会有雨责任链的错误设置而导致系统出错,例如循环调用
源码使用

Servlet 的 FilterChain过滤器 是责任链模式的典型应用

状态模式

对有状态的对象,把复杂的逻辑判断提取到不同的状态对象中,允许状态对象在其内部状态发生改变时改变其行为

结构
  • 环境角色:定义了客户端需要的接口,维护一个当前状态,并将与状态相关的操作委托给当前状态对象来处理
  • 抽象状态角色:定义一个接口,用以封装环境对象中的特定状态所对应的行为
  • 具体状态角色:实现抽象状态所对应的行为
优劣

  • 将所有与某个状态有关的行为放到一个类中,并且可以方便地增加新的状态,只需要改变对象状态即可改变对象的行为
  • 允许状态转换逻辑与状态对象合成一体,而不是某个巨大的条件语句块

  • 状态模式的使用必然会增加系统类的对象和个数
  • 状态模式的结构与实现都较为复杂,如使用不当将导致程序结构和代码混乱
  • 不符合开闭原则
使用场景
  • 当一个对象的行为取决于它的状态,并且它必须在运行时根据状态改变它的行为时,就可以考虑使用状态模式
  • 一个操作中含有庞大的分支结构,并且这些分支决定于对象的状态时

观察者模式

观察者模式,又称为 发布-订阅模式(Publish/Subscribe),它定义了一种一对多的依赖关系,让多个观察者对象同时监听某个主题对象。这个主题对象在状态变化时,会通知所有的观察者对象,使它们能够自动更新自己。

结构
  • 抽象主题(Subject):抽象主题角色把所有观察者对象保存在一个集合里,每个主题都可以有任意数量的观察者,抽象主题提供一个接口,可以增加和删除观察者对象
  • 具体主题(ConcreteSubject):将有关状态存入具体观察者对象,在具体主题的内部发生改变时,给所有注册过的观察者发送通知
  • 抽象观察者(Observer):观察者的抽象类,它定义了一个更新接口,使得在主题更改通知时更新自己
  • 具体观察者(ConcreteObserver):实现抽象观察者的更新接口
优劣

  • 降低了目标与观察者之间的耦合关系,两者之间是抽象耦合关系
  • 被观察者发送通知,所有注册的观察者都会收到信息【广播】

  • 如果观察者非常多的话,那么所有的观察者收到被观察者发送的通知会耗时
  • 如果被观察者有循环依赖的话,那么被观察者发送通知会使观察者循环调用,导致系统崩溃
使用场景
  • 对象间存在一对多关系, 一个对象的状态发生改变会影响其他对象时
  • 当一个抽象模型有两个方面,其中一个方面依赖于另一方面时
JDK提供的实现

通过 java.util.Observable类 和 java.util.Observer接口 定义了观察者模式,只要实现它们的子类就可以编写观察者模式实例

Observable类

Observable类 是抽象目标类(被观察者),它有一个 Vector 集合成员变量,用于保存所有要通知的观察者对象

  • void addObserver(Observer o) 用于将新的观察者对象添加到集合中
  • void notifyObservers(Object arg) 调用集合中的所有观察者对象的 update 方法,通知它们刷新数据
  • void setChange() 用来设置一个 boolean类型 的内部标志,注明目标对象发生了变化。当它为 true 时,notifyObservers() 才会通知观察者
Observer接口

Observer接口 是抽象观察者,它检视目标对象的变化,当目标对象发生变化时,观察者得到通知,并调用 update 方法,进行相应的工作

中介者模式

又名 调停模式,顶定义一个中介角色来封装一系列对象的交互,使原有对象之间的耦合松散,且可以独立改变它们之间的交互。

结构

抽象中介者(Mediator)角色:它是中介者的接口,提供了同事对象注册与转发同事对象信息的抽象方法。 具体中介者(ConcreteMediator)角色:实现中介者接口,定义一个 List 来管理同事对象,协调各个同事角色之间的交互关系,因此它依赖于同事角色。 抽象同事类(Colleague)角色:定义同事类的接口,保存中介者对象,提供同事对象交互的抽象方法,实现所有相互影响的同事类的公共功能。 具体同事类(Concrete Colleague)角色:是抽象同事类的实现者,当需要与其他同事对象交互时,由中介者对象负责后续的交互。

中介者模式-类图.jpg
中介者模式-类图.jpg
实现

如 Mirai QQ 的 Message 设计

优缺点

  • 松散耦合

中介者模式通过把多个同事对象之间的交互封装到中介者对象内,使得同事对象之间的耦合度降低,基本可以做到依赖互补。这样一来,同事对象就可以独立的变化和复用。

  • 集中控制交互

多个同事对象的交互,被封装在中介者对象内集中管理,使得这些交互行为变化时,只要修改中介者对象即可。

  • 同事类太多时,中介者的职责将很大,它会变得复杂而庞大,以至于系统难以维护
使用场景
  • 系统中对象间存在复杂的引用关系,系统结构混乱且难以理解
  • 当想创建一个运行于多个类之间的对象,又不想生成新的子类时

迭代器模式

提供一个对象来顺序访问聚合对象中的一系列数据,而不暴露聚合对象的内部表示

结构
  • 抽象聚合角色:定义存储、添加、删除聚合元素以及创建迭代器对象的接口
  • 具体聚合角色:实现抽象聚合角色,返回一个具体迭代器实例
  • 抽象迭代器角色:定义访问和遍历聚合元素的接口,通常包含 hasNext(), next() 等方法
  • 具体迭代器角色:实现抽象迭代器接口,完成聚合对象的遍历,记录遍历的位置
迭代器模式-类图.jpg
迭代器模式-类图.jpg
优缺点

  • 支持以不同的方式遍历一个聚合对象,在同一个聚合对象上可以定义多种遍历方式。在迭代器模式中只需要一个不同的迭代器来替换原有的迭代器即可改变遍历算法,我们可以自己定义迭代器的子类以支持新的遍历方式
  • 迭代器简化了聚合类。由于引入了迭代器,在原有的聚合对象中不需要再自行提供数据遍历等方法
  • 在迭代器模式中,由于引入了抽象层,增加新的聚合类和迭代器类都很方便,满足开闭原则
使用场景
  • 需要为聚合对象提供多种遍历方式时
  • 需要为遍历不同的聚合结构提供一个统一的接口时
  • 当访问一个聚合对象的内容而无需暴露其内部细节时

访问者模式

封装一些作用于某种数据结构中的各元素的操作,它可以在不改变这个数据结构的前提下定义作用于这些元素的新操作

结构
  • 抽象访问者(Visitor):定义了与元素(Element)对应的方法,一般是有几个元素就相应的有几个方法。
  • 具体访问者(ConcreteVisitor):Visitor的实现类
  • 抽象元素(Element):是一个接口,代表在ObjectStructure里面的元素。里面定义了一个accept(Visitor)的方法,通过此方法元素可以将自己交给Visitor访问。
  • 具体元素(ConcreteElement):Element 的实现类
  • 对象结构(ObjectStructure):包含各种元素,而且要求元素稳定且可以迭代访问这些元素。
访问者模式-类图.png
访问者模式-类图.png
优缺点

  • 扩展性好,在不修改对象结构中的元素的情况下,为对象结构中的元素添加新功能
  • 复用性好,通过访问者来定义整个对象结构通用的功能,从而提复用性
  • 分离无关行为,把相关的行为封装在一起,构成一个访问者,符合单一职责原则

  • 访问者模式依赖了具体类,而没有依赖抽象类,违反了依赖倒转原则
  • 太复杂,特别是伪动态双分派,不仔细理解很难想清楚
使用场景
  • 对象结构相对稳定,但其算法经常变化的程序
  • 对象结构中的对象需要提供多种不同且不相关的操作,而且要避免让这些操作的变化影响对象的结构
双分派(dispatch)

访问者模式存在一个叫"伪动态双分派”的技术,访问者模式之所以是最复杂的设计模式与其有很大的关系。

什么叫分派?根据对象的类型而对方法进行的选择,就是分派(Dispatch)。

  • 发生在编译时的分派叫静态分派,例如重载(overload)
  • 发生在运行时的分派叫动态分派,例如重写(overwrite)

单分派与多分派

  • 单分派 依据单个宗量进行方法的选择就叫单分派,Java 动态分派只根据方法的接收者一个宗量进行分配,所以其是单分派
  • 多分派 依据多个宗量进行方法的选择就叫多分派,Java 静态分派要根据方法的接收者与参数这两个宗量进行分配,所以其是多分派

先看在 BigHuYouCompany 类里的分派代码:slave.accept(visitor); 中 accept方法的分派是由 slave的运行时类型 决定的。若slave是Programer就执行Programer的accept方法。若slave是Tester那么就执行Tester的accept方法。(具体可见参考16)

简单来说就是根据入参对象的实际类型执行不同的逻辑

备忘录模式

又名 快照模式,在不破坏封装性的前提下,捕获一个对象的内部状态,并在对象之外保存这个状态,以便以后当需要时能够将该对象恢复到保存的状态

结构
  • 发起人角色(Originator):记录当前时刻的内部状态提供创建备忘录和恢复备忘录的功能,实现其他业务功能,它可以访问备忘录里所有的信息
  • 备忘录角色(Memento):负责存储发起人的内部状态,在需要的时候提供这些内部状态给发起人
  • 管理者角色(Caretaker):对备忘录进行管理,提供保存与获取备忘录的功能,但不能对备忘录的内容进行访问和修改

备忘录有两个等效接口:

  • 窄接口:管理者对象(和其他发起人对象)看到的是备忘录的窄接口,这个窄接口只允许它把备忘录传递给其他的对象
  • 宽接口:发起人对象可以看到一个宽接口,允许它读取所有数据,以便根据这些数据恢复这个发起人对象的内部状态
优劣

  • 提供了一种可以恢复状态的机制
  • 实现了内部状态的封装,除了创建它的发起人之外,其他对象都不能访问这些状态信息
  • 简化了 发起人 类。发起人不需要管理和保存其内部状态的各个部分,所有状态信息都保存在备忘录中,并由管理者进行管理,这符合单一职责原则

  • 空间复杂度高
使用场景
  • 需要保存与恢复数据的场景
  • 需要提供一个可回滚操作的场景,如 Word、PS、IDE等的 Ctrl + Z 快捷键的实线,以及数据库的事务

解释器模式

给定一个语言,定义它的语法表示,并定义一个解释器,这个解释器使用语法解释语言中的句子

如 逆波兰表达式 转换成 普通表达式,定义一个使用栈的算法

语法规则
代码语言:javascript
复制
expression ::= value | plus | minus
plus ::= expression '+' expression
minus ::= expression '-' expression
value ::= integer

> 注: 这里的 ::= 表示 定义为| 表示 或

上面的规则描述为: 表达是可以是一个值,也可以是 plus 或 minus 运算,而 plus 和 minus 又是由表达式结合运算符构成,值的类型为 Integer

抽象语法树

在计算机学科中,抽象语法树(AbstractSyntaxTree,AST),或简称语法树(SyntaxTree),是源代码语法的一种抽象表示。 它以树状的形式表现编程语言的语法结构,树上的每个节点都表示源代码的一种结构。

结构
  • 抽象表达式:定义一个解释器具有哪些操作,通常的实现类有 终结符解释器 和 非终结符解释器
  • 终结符表达式: 终结符一般是文法中的运算单元,如简单公式R=R1+R2,其中的R1和R2就是终-结符,对应R1和R2的解释器就是终结符表达式
  • 非终结符表达式:文法中的每条规则,比如说上述的 + 和 = 就是非终结表达式,可以预测到这种模式是可以递归调用的
  • 上下文环境:一般是填充占位符,给对象赋值的过程
解释器模式-类图.png
解释器模式-类图.png
优缺点

  • 易于实现语法,一条语法用一个解释器对象解释执行
  • 易于扩展新语法,只需创建对应解释器,抽象语法树时使用即可

  • 可使用场景少,复用性不高,除了发明新的编程语言或对某些新硬件进行解释外,很少用,特定数据结构,扩展性也低
  • 维护成本高,每种规则至少要定义一个解释类,语法规则越多,类越难管理和维护
  • 执行效率低,递归调用方法,解释句子语法复杂时,会执行大量循环语句
使用场景
  • 语言语法较为简单,且对执行效率要求不高时,如正则判断IP是否合法
  • 问题重复出现,且可用简单语法来进行表达时,如if-else统一解释为条件语句
  • 当一个语言需要解释执行时,如XML中 <> 括号标识不同的结点含义
本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2023 年 03 月,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • Java设计模式 笔记
    • UML
      • 类图
      • 类图的作用
      • 类图的表示法
    • 软件设计原则
      • 单一职责原则
      • 开闭原则
      • 里氏代换原则
      • 依赖倒转原则
      • 接口隔离原则
      • 迪米特法则
      • 合成复用原则
    • 创建型模式
      • 单例模式
      • 工厂模式
      • 原型模式
      • 建造者模式
    • 结构型模式
      • 代理模式
      • 适配器模式
      • 装饰者模式
      • 桥接模式
      • 外观模式
      • 组合模式
      • 享元模式
    • 行为型模式
      • 模板方法模式
      • 策略模式
      • 命令模式
      • 责任链模式
      • 状态模式
      • 观察者模式
      • 中介者模式
      • 迭代器模式
      • 访问者模式
      • 备忘录模式
      • 解释器模式
相关产品与服务
容器服务
腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档