Design patterns

1.1设计模式产生的背景

“设计模式”最初并不是出现在软件设计中,而是被用于建筑领域的设计中。

1977年美国著名建筑大师、加利福尼亚大学伯克利分校环境结构中心主任克里斯托夫·亚历山大(Christopher Alexander)在他的著作《建筑模式语言:城镇、建筑、构造》中描述了一些常见的建筑设计问题,并提出了 253 种关于对城镇、邻里、住宅、花园和房间等进行设计的基本模式。

1990年软件工程界开始研讨设计模式的话题,后来召开了多次关于设计模式的研讨会。直到1995 年,艾瑞克·伽马(ErichGamma)、理査德·海尔姆(Richard Helm)、拉尔夫·约翰森(Ralph Johnson)、约翰·威利斯迪斯(John Vlissides)等 4 位作者合作出版了《设计模式:可复用面向对象软件的基础》一书,在此书中收录了 23 个设计模式,这是设计模式领域里程碑的事件,导致了软件设计模式的突破。这 4 位作者在软件开发领域里也以他们的“四人组”(Gang of Four,GoF)著称。

1.2软件设计模式的概念

软件设计模式(Software Design Pattern),又称设计模式,是针对软件开发中经常遇到的一些设计问题,根据基本的设计原则,总结出来的一套通用的解决方案或者设计思路。

1.3 学习设计模式的必要性

设计模式的本质是面向对象设计原则的实际运用,是对类的封装性、继承性和多态性以及类的关联关系和组合关系的充分理解。

正确使用设计模式具有以下优点。

  • 可以提高程序员的思维能力、编程能力和设计能力。
  • 使程序设计更加标准化、代码编制更加工程化,使软件开发效率大大提高,从而缩短软件的开发周期。
  • 使设计的代码可重用性高、可读性强、可靠性高、灵活性好、可维护性强。

1.4 设计模式分类

  • 创建型模式

    用于描述“怎样创建对象”,它的主要特点是“将对象的创建与使用分离”。

  • 结构型模式

    用于描述如何将类或对象按某种布局组成更大的结构

  • 行为型模式

    用于描述类或对象之间怎样相互协作共同完成单个对象无法单独完成的任务,以及怎样分配职责。

序号 模式 包括
1 创建型模式 工厂模式(Factory Pattern)抽象工厂模式(Abstract Factory Pattern)单例模式(Singleton Pattern)建造者模式(Builder Pattern)原型模式(Prototype Pattern)
2 结构型模式 适配器模式(Adapter Pattern)桥接模式(Bridge Pattern)过滤器模式(Filter、Criteria Pattern)组合模式(Composite Pattern)装饰器模式(Decorator Pattern)外观模式(Facade Pattern)享元模式(Flyweight Pattern)代理模式(Proxy Pattern)
3 行为型模式 责任链模式(Chain of Responsibility Pattern)命令模式(Command Pattern)解释器模式(Interpreter Pattern)迭代器模式(Iterator Pattern)中介者模式(Mediator Pattern)备忘录模式(Memento Pattern)观察者模式(Observer Pattern)状态模式(State Pattern)空对象模式(Null Object Pattern)策略模式(Strategy Pattern)模板模式(Template Pattern)访问者模式(Visitor Pattern)

2.UML类图

统一建模语言(Unified Modeling Language,UML)是用来设计软件的可视化建模语言。它的特点是简单、统一、图形化、能表达软件设计中的动态与静态信息。

UML 从目标系统的不同角度出发,定义了用例图、类图、对象图、状态图、活动图、时序图、协作图、构件图、部署图等 9 种图。

2.1 类图概述

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

2.2 类图的作用

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

2.3类图表示法

2.3.1 类的表示方式

在UML类图中,类使用包含类名、属性(field) 和方法(method) 且带有分割线的矩形来表示,比如下图表示一个Employee类,它包含name,age和address这3个属性,以及work()方法。

属性/方法名称前加的加号和减号表示了这个属性/方法的可见性,UML类图中表示可见性的符号有三种:

  • +:表示public

  • -:表示private

  • #:表示protected

属性的完整表示方式是: 可见性 名称 :类型 [ = 缺省值]

方法的完整表示方式是: 可见性 名称(参数列表) [ : 返回类型]

注意:

1. 中括号中的内容表示是可选的
1. 也有将类型放在变量名前面,返回值类型放在方法名前面

上图Demo类定义了三个方法:

  • method()方法:修饰符为public,没有参数,没有返回值。
  • method1()方法:修饰符为private,没有参数,返回值类型为String。
  • method2()方法:修饰符为protected,接收两个参数,第一个参数类型为int,第二个参数类型为String,返回值类型是int。

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

2.3.2.1 关联关系

关联关系是对象之间的一种引用关系(在一个类中声明了另一个类的变量),用于表示一类对象与另一类对象之间的联系。

关联又可以分为单向关联,双向关联,自关联

1.单向关联

在UML类图中单向关联用一个带箭头的实线表示。上图表示每个顾客都有一个地址,这通过让Customer类持有一个类型为Address的成员变量类实现。

2.双向关联

从上图中我们很容易看出,所谓的双向关联就是双方各自持有对方类型的成员变量。

在UML类图中,双向关联用一个不带箭头的直线表示。上图中在Customer类中维护一个List<Product>,表示一个顾客可以购买多个商品;在Product类中维护一个Customer类型的成员变量表示这个产品被哪个顾客所购买。

3,自关联

自关联在UML类图中用一个带有箭头且指向自身的线表示。上图的意思就是Node类包含类型为Node的成员变量,也就是“自己包含自己”。

2.3.2.2 聚合关系

聚合关系是关联关系的一种,是强关联关系,是整体和部分之间的关系

聚合关系也是通过成员对象来实现的,其中成员对象是整体对象的一部分,但是成员对象可以脱离整体对象而独立存在。例如,学校与老师的关系,学校包含老师,但如果学校停办了,老师依然存在。

在 UML 类图中,聚合关系可以用带空心菱形的实线来表示,菱形指向整体。下图所示是大学和教师的关系图:

2.3.2.3 组合关系

组合聚合关系是关联关系的一种,但它是一种更强烈的聚合关系,表示类之间的整体与部分的关系。

在组合关系中,整体对象可以控制部分对象的生命周期,一旦整体对象不存在,部分对象也将不存在,部分对象不能脱离整体对象而存在。例如,头和嘴的关系,没有了头,嘴也就不存在了。

在 UML 类图中,组合关系用带实心菱形的实线来表示,菱形指向整体。下图所示是头和嘴的关系图:

2.3.2.4 依赖关系

依赖关系是一种使用关系,它是对象之间耦合度最弱的一种关联方式,是临时性的关联。在代码中,某个类的方法通过局部变量、方法的参数或者对静态方法的调用来访问另一个类(被依赖类)中的某些方法来完成一些职责。

在 UML 类图中,依赖关系使用带箭头的虚线来表示,箭头从使用类指向被依赖的类。下图所示是司机和汽车的关系图,司机驾驶汽车:

2.3.2.5 继承关系

继承关系是对象之间耦合度最大的一种关系,表示一般与特殊的关系,是父类与子类之间的关系,是一种继承关系。

在 UML 类图中,泛化关系用带空心三角箭头的实线来表示,箭头从子类指向父类。在代码实现时,使用面向对象的继承机制来实现泛化关系。例如,Student 类和 Teacher 类都是 Person 类的子类,其类图如下图所示:

2.3.2.6 实现关系

实现关系是接口与实现类之间的关系。在这种关系中,类实现了接口,类中的操作实现了接口中所声明的所有的抽象操作。

在 UML 类图中,实现关系使用带空心三角箭头的虚线来表示,箭头从实现类指向接口。例如,汽车和船实现了交通工具,其类图如图 9 所示。

3.软件设计原则

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

3.1 开闭原则(Open Close Principle)

开闭原则的核心思想是:软件实体(模块、类、方法等)应当对扩展开放,对修改关闭。也就是说,在不修改现有代码的前提下,应当能够通过扩展来增加新的功能。

遵循开闭原则有助于提高软件系统的可维护性和可复用性。它要求我们在设计软件时,应当尽量将变化的部分和不变的部分分离,把变化的部分封装起来,以便在需要增加新功能时,只需扩展变化的部分,而不必修改不变的部分。

实现开闭原则的方式是通过使用抽象类、接口、模板方法模式、策略模式等技术。例如,在需求变更时,我们可以通过新增实现接口的具体类或者新增实现某个抽象类的具体子类等方式来实现扩展,而不必修改原有的代码。

举个例子,假设我们要开发一个绘图程序,支持绘制多种形状(如圆形、矩形、三角形等)。按照开闭原则,我们应当定义一个抽象的Shape类,表示形状,然后为每种具体的形状定义一个子类(如CircleRectangle等),并在子类中实现绘制方法。这样,在需要增加新的形状时,只需添加一个新的子类,而不必修改现有的代码。

未命名文件 (18)

  1. Shape抽象类
public abstract class Shape {
    /**
     * 绘制形状方法
     */
    public abstract void draw();
}
  1. Circle继承Shape抽象类
public class Circle extends Shape {
    @Override
    public void draw() {
        System.out.println("绘制圆形");
    }
}
  1. Rectang继承Shape抽象类
public class Rectangle extends Shape{
    @Override
    public void draw() {
        System.out.println("绘制矩形");
    }
}
  1. 测试类
public class OpenCloseApplication {
    public static void main(String[] args) {
        Shape shape = new Rectangle();
        shape.draw();
        shape = new Circle();
        shape.draw();
    }
}

image-20230712110803003

3.2 里氏代换原则(Liskov Substitution Principle)

里氏替换原则的核心思想是:在使用基类对象的地方都可以使用其子类对象,而不会产生任何错误或异常。也就是说,子类对象应当能够完全替代父类对象,而不影响程序的正确性和预期行为。

遵循里氏替换原则有助于确保继承关系的正确性,提高代码的可复用性和可维护性。它要求子类在继承父类时,不仅要保留父类的方法和属性,还要遵守父类定义的功能,确保子类能够完全替代父类。

举个例子,假设有一个父类Rectangle,它有两个属性widthheight,分别表示矩形的宽和高。现在有一个子类Square继承自Rectangle,表示正方形。由于正方形的宽和高相等,所以我们可能会在Square类中重写setWidth()setHeight()方法,使它们同时修改widthheight属性。但这样做会违反里氏替换原则,因为使用Square对象替换Rectangle对象时,程序的行为将发生改变。

未命名文件 (16)

  1. Rectangle类
public class Square extends Rectangle {
    @Override
    public void setLength(double length) {
        super.setLength(length);
        super.setWidth(length);
    }

    @Override
    public void setWidth(double width) {
        super.setWidth(width);
        super.setLength(width);
    }

    @Override
    public void getArea(){
        System.out.println("正方形的面积为:" + this.getLength() * this.getWidth());
    }
}
  1. Square类,重写set方法,那么就违反了里氏替换原则
public class Square extends Rectangle {
    @Override
    public void setLength(double length) {
        super.setLength(length);
        super.setWidth(length);
    }

    @Override
    public void setWidth(double width) {
        super.setWidth(width);
        super.setLength(width);
    }

    @Override
    public void getArea(){
        System.out.println("正方形的面积为:" + this.getLength() * this.getWidth());
    }
}
  1. 测试类
public class LiskovsunstitutionApplication {
    public static void main(String[] args) {
        Rectangle rectangle = new Rectangle();
        rectangle.setLength(20);
        rectangle.setWidth(10);
        rectangle.getArea();
        Square square = new Square();
        square.setLength(20);
        square.getArea();
    }
}

image-20230712112706140

3.3依赖倒转原则(Dependence Inversion Principle)

依赖倒转原则的核心思想是:是高层模块不应该依赖底层模块,它们都应该依赖于抽象。而抽象不应该依赖于具体实现,具体实现应该依赖于抽象。依赖倒转原则降低模块之间的耦合性。

这个原则的实现可以通过以下几个步骤:

  1. 抽象出接口或者抽象类。高层模块依赖的是这个接口或者抽象类,而不是底层模块的具体实现。
  2. 底层模块实现接口或者继承抽象类。具体实现依赖于抽象,而不是高层模块。
  3. 高层模块通过依赖注入的方式获取底层模块的具体实现。一般通过构造函数、属性、方法参数等方式将具体实现传递给高层模块即可。

举个例子,假设有一个学生正在学习「语文」和「数学」的课程。如果我们直接在学生类中定义学习「语文」和「数学」的方法,那么当学生需要学习新的课程时,我们就需要在学生类中添加新的方法,并且在调用时也需要修改代码。这样会导致代码不稳定,难以维护。

为了解决这个问题,我们可以定义一个课程接口(Course),并让不同的课程实现这个接口。然后在学生类中定义一个学习方法,接受一个课程接口作为参数。这样当学生需要学习新的课程时,我们只需要创建一个新的课程类实现课程接口,并在调用时传入这个新的课程类即可。这样就避免了对学生类和调用代码的修改。

未命名文件 (17)

  1. Course接口
public interface Course {

    void study();
}
  1. Student类
public class Student {
    
    public void study(Course course){
        course.study();
    }
}
  1. Chinese类
public class Chinese implements Course{
    @Override
    public void study() {
        System.out.println("学习语文");
    }
}
  1. Math类
public class Math implements Course{
    @Override
    public void study() {
        System.out.println("学习数学");
    }
}
  1. 测试类
public class DependenceApplication {
    public static void main(String[] args) {
        Student student = new Student();
        student.study(new Chinese());
        student.study(new Math());
    }
}

image-20230712114519717

3.4 接口隔离原则(Interface Segregation Principle)

接口隔离原则的核心思想是:类不应该被迫依赖它不需要的接口。它要求程序设计应该建立单一接口,而不要建立臃肿的大接口。这样可以降低类之间的耦合度,使类具有高内聚性和低耦合性,提高软件的可维护性和可扩展性。

举个例子,假设我们正在开发一个动物园程序,动物园里有鸟类和鱼类。如果我们定义一个庞大的动物接口,其中包含了飞行和游泳的方法,那么当我们需要实现鸟类时,它仍然需要实现游泳方法。这样会导致代码不必要的复杂度和冗余。

为了解决这个问题,我们可以将动物接口拆分成鸟类接口和鱼类接口。这样当我们需要实现鸟类时,它只需要实现鸟类接口即可。这样就避免了实现不需要的方法,从而减少了类之间的依赖关系。

未命名文件 (19)

  1. Bird接口
public interface Bird {

        /**
        * 飞行
        */
        void fly();

        /**
        * 吃
        */
        void eat();
}
  1. Fish接口
public interface Fish {

        /**
        * 游泳
        */
        void swim();

        /**
        * 吃
        */
        void eat();
}
  1. Cuckoo类实现Bird接口
public class Cuckoo implements Bird {

    @Override
    public void fly() {
        System.out.println("布谷鸟飞行");
    }

    @Override
    public void eat() {
        System.out.println("布谷鸟吃虫子");
    }
}
  1. Shark类实现Fish接口
public class Shark implements Fish {

    @Override
    public void swim() {
        System.out.println("鲨鱼游泳");
    }

    @Override
    public void eat() {
        System.out.println("鲨鱼吃肉");
    }
}
  1. 测试
public class InterfaceSegregationApplication {
    public static void main(String[] args) {
        Bird bird = new Cuckoo();
        bird.eat();
        bird.fly();
        Fish fish = new Shark();
        fish.eat();
        fish.swim();
    }
}

image-20230712120630808

3.5迪米特法则(Law of Demeter,LoD)

**迪米特法则(Law of Demeter,LoD)又叫作最少知识原则(Least Knowledge Principle,LKP)**,它指出一个对象应当对其他对象有尽可能少的了解。它要求一个模块应当尽量减少与其他模块的交互,如果两个模块之间不必彼此有直接的联系,那么这两个模块就不应当发生直接的相互作用。如果其中一个模块需要调用另一个模块的某一个方法,则可以通过第三者转发这个调用。

举个例子,假设我们正在开发一个娱乐圈管理系统,其中包括明星、经纪人、粉丝和媒体公司四个类。如果我们在明星类中直接访问粉丝和媒体公司的细节,那么当粉丝和媒体公司的实现发生变化时,我们就需要修改明星类的代码。这样会导致代码不稳定,难以维护。

为了解决这个问题,我们可以定义一个经纪人类作为中介者,负责管理粉丝和媒体公司。然后在明星类中只与经纪人类进行交互,而不是直接访问粉丝和媒体公司的细节。这样当粉丝和媒体公司的实现发生变化时,我们只需要修改经纪人类的代码即可,而不需要修改明星类的代码。

未命名文件 (20)

  1. Star类
public class Star {
    private String name;

    public Star(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }
}
  1. Fans类
public class Fans {
    private String name;

    public Fans(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }
}
  1. Company类
public class Company {
    private String name;

    public Company(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }
}
  1. Broker类
public class Broker {
    private Star star;
    private Fans fans;
    private Company company;

    public void setStar(Star star) {
        this.star = star;
    }

    public void setFans(Fans fans) {
        this.fans = fans;
    }

    public void setCompany(Company company) {
        this.company = company;
    }

    public void meeting() {
        System.out.println(fans.getName() + "与明星" + star.getName() + "见面了。");
    }

    public void business() {
        System.out.println(company.getName() + "与明星" + star.getName() + "洽淡业务。");
    }
}
  1. 测试类
public class LodApplication {
    public static void main(String[] args) {
        Broker broker = new Broker();
        broker.setStar(new Star("马修·范德普尔"));
        broker.setFans(new Fans("Xu huaiang"));
        broker.setCompany(new Company("欧贝青公司"));
        broker.meeting();
        broker.business();
    }
}

image-20230712121832200

3.6 合成复用原则(Composite Reuse Principle)

合成复用原则(Composite Reuse Principle,CRP)又叫组合/聚合复用原则(Composition/Aggregate Reuse Principle,CARP)它要求在软件复用时,要尽量先使用组合或者聚合等关联关系来实现,其次才考虑使用继承关系来实现。如果要使用继承关系,则必须严格遵循里氏替换原则

通常类的复用分为继承复用和合成复用两种。

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

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

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

  1. 它维持了类的封装性。因为成分对象的内部细节是新对象看不见的,所以这种复用又称为“黑箱”复用。
  2. 对象间的耦合度低。可以在类的成员位置声明抽象。
  3. 复用的灵活性高。这种复用可以在运行时动态进行,新对象可以动态地引用与成分对象类型相同的对象。

假设我们要设计一个汽车类,它有一个发动机成员变量。我们可以使用继承来实现,让汽车类继承发动机类。但这样会导致汽车类和发动机类之间的耦合度很高,如果发动机类发生变化,汽车类也需要相应地修改。

另一种方法是使用聚合关系,让汽车类包含一个发动机对象作为成员变量。这样,汽车类和发动机类之间的耦合度就降低了,发动机类的变化不会直接影响到汽车类。

未命名文件 (21)

  1. Car类
public class Car {
    private String name;

    private Engine engine;

    public Car(String name, Engine engine) {
        this.name = name;
        this.engine = engine;
    }

    public void getCar(){
        System.out.println("车名:"+name+" 引擎:"+engine.getName());
    }
}
  1. Engine类
public class Engine {
    private String name;

    public Engine(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }
}
  1. 测试类
public class CompositeReuseApplication {
    public static void main(String[] args) {
        Engine engine = new Engine("2.0升LSY直列四缸涡轮");
        Car car = new Car("凯迪拉克CT5", engine);
        car.getCar();
        System.out.println("更换引擎");
        engine = new Engine("6.2升V8机械增压引擎");
        car = new Car("凯迪拉克CT5", engine);
        car.getCar();
    }
}

image-20230712124212430

3.7软件设计原则之间的关系

6大软件设计原则包括:开闭原则、里氏替换原则、依赖倒置原则、接口隔离原则、迪米特法则和合成复用原则。这些原则之间存在着密切的联系。

首先,开闭原则是总纲,它告诉我们要对扩展开放,对修改关闭。其他五个原则都是开闭原则的具体实现规范。

其次,里氏替换原则告诉我们不要破坏继承体系,它与依赖倒置原则相辅相成。依赖倒置原则告诉我们要面向接口编程,而不是面向实现编程。

接口隔离原则告诉我们在设计接口的时候要精简单一,它与合成复用原则相辅相成。合成复用原则告诉我们要优先使用组合或者聚合关系复用,少用继承关系复用。

迪米特法则告诉我们要降低耦合度,它与其他五个原则都有关系。它要求我们尽量减少模块之间的交互,使模块之间的依赖关系简单清晰。

4.创建者模式

创建型模式的主要关注点是“怎样创建对象?”,它的主要特点是“将对象的创建与使用分离”。

这样可以降低系统的耦合度,使用者不需要关注对象的创建细节。

创建型模式分为:

  • 单例模式
  • 工厂方法模式
  • 抽象工厂模式
  • 原型模式
  • 建造者模式

4.1 单例设计模式

4.1.1单例模式概述

单例模式(Singleton Pattern)是 Java 中最简单的设计模式之一。旨在确保一个类只有一个实例,并提供一个全局访问点以访问该实例。

4.1.1 单例模式的结构

单例模式的主要有以下角色:

  • 单例类。只能创建一个实例的类
  • 访问类。使用单例类

4.1.2 单例模式的实现

单例设计模式分类两种:

饿汉式类加载就会导致该单实例对象被创建

懒汉式类加载不会导致该单实例对象被创建,而是首次使用该对象时才会创建

4.1.2.1饿汉式方式一(静态变量)

说明:该方式在成员位置声明Singleton类型的静态变量,并创建Singleton类的对象instance。instance对象是随着类的加载而创建的。如果该对象足够大的话,而一直没有使用就会造成内存的浪费。

/**
 * 饿汉式(静态变量)
 *
 * @author Xu huaiang
 * @date 2023/07/12
 */
public class StaticFieldSingleton {
    //    1. 私有化构造器
    private StaticFieldSingleton() {
    }

    //    2.在本类中创建一个本类对象
    private static StaticFieldSingleton instance = new StaticFieldSingleton();

    //    3.提供一个公共的访问方式,让外界获取该对象。
    //    该方法必须是静态的,因为外界没有办法创建对象,只能通过类名调用
    public static StaticFieldSingleton getInstance() {
        return instance;
    }
}

测试:

public class SingletonApplication {
    public static void main(String[] args) {
        StaticFieldSingleton fieldInstance1 = StaticFieldSingleton.getInstance();
        StaticFieldSingleton fieldInstance2 = StaticFieldSingleton.getInstance();
        System.out.println("fieldInstance1 == fieldInstance2 ? " + (fieldInstance1 == fieldInstance2));
    }
}

image-20230712150731361

4.1.2.2饿汉式方式二(静态代码块)

说明:该方式在成员位置声明Singleton类型的静态变量,而对象的创建是在静态代码块中,也是对着类的加载而创建。

public class StaticAreaSingleton {
    //    1. 私有化构造器
    private StaticAreaSingleton() {
    }

    //    2.声明StaticAreaSingleton类型的变量
    private static StaticAreaSingleton instance; //null

    //    3.在静态代码块中,创建单例对象
    static {
        instance = new StaticAreaSingleton();
    }

    //    4.提供一个公共的访问方式,让外界获取该对象。
    public static StaticAreaSingleton getInstance() {
        return instance;
    }

}

测试:

public class SingletonApplication {
    public static void main(String[] args) {
        StaticAreaSingleton areaInstance1 = StaticAreaSingleton.getInstance();
        StaticAreaSingleton areaInstance2 = StaticAreaSingleton.getInstance();
        System.out.println("areaInstance1 == areaInstance2 ? " + (areaInstance1 == areaInstance2));
    }
}

image-20230712152537539

4.1.2.3饿汉式方式三(枚举)

枚举类实现单例模式是目前最为推荐的单例实现方式之一。枚举类型是线程安全的,并且只会被装载一次。设计者可以充分利用枚举的这个特性来实现单例模式,从而避免了其他单例实现方式可能存在的线程安全问题和不稳定性。

使用枚举类实现单例模式非常简单,只需声明一个枚举变量,然后将其定义为唯一的实例。由于枚举类型是 final 的,因此该实例不能被改变,而且枚举类型的构造方法只会被调用一次,因此在整个应用程序的生命周期中,该实例都会存在且唯一。

另外,使用枚举类实现单例模式是不会被破坏的。即使在面对序列化、反射等特殊情况时,枚举类型的唯一实例也不会被破坏。

更进一步的,枚举类型还可以实现很多其他的设计模式,比如状态模式、策略模式等。

/**
 * enum单例
 *
 * @author Xu huaiang
 * @date 2023/07/12
 */
public enum EnumSingleton {
    INSTANCE;
}

测试:

public class SingletonApplication {
    public static void main(String[] args) {
        System.out.println("-----------------------------");
        EnumSingleton enumSingleton1 = EnumSingleton.INSTANCE;
        EnumSingleton enumSingleton2 = EnumSingleton.INSTANCE;
        System.out.println("enumSingleton1 == enumSingleton2 ? " + (enumSingleton1 == enumSingleton2));
    }
}

image-20230712161101152

4.1.2.3懒汉式方式一(线程不安全)

说明:该方式在成员位置声明Singleton类型的静态变量,并没有进行对象的赋值操作,那么什么时候赋值的呢?当调用getInstance()方法获取Singleton类的对象的时候才创建Singleton类的对象,这样就实现了懒加载的效果。但是,如果是多线程环境,会出现线程安全问题。

public class ThreadUnSafeSingleton {
//    1.私有化构造器
    private ThreadUnSafeSingleton(){
    }

//    2.创建该类的对象实例
     private static ThreadUnSafeSingleton instance;

//    3.提供一个公共的访问方式,让外界获取该对象。
    public static ThreadUnSafeSingleton getInstance(){
        if (instance == null){
            instance = new ThreadUnSafeSingleton();
        }
        return instance;
    }
}

测试:

public class SingletonApplication {
    public static void main(String[] args) {
        ThreadUnSafeSingleton threadUnsafeInstance1 = ThreadUnSafeSingleton.getInstance();
        ThreadUnSafeSingleton threadUnsafeInstance2 = ThreadUnSafeSingleton.getInstance();
        System.out.println("threadUnsafeInstance1 == threadUnsafeInstance2 ? " + (threadUnsafeInstance1 == threadUnsafeInstance2));
    }
}

image-20230712152438859

4.1.2.4懒汉式方式二(线程安全)

说明:该方式也实现了懒加载效果,同时又解决了线程安全问题。但是在getInstance()方法上添加了synchronized关键字,导致该方法的效率特别低。从代码我们可以看出,其实就是在初始化instance的时候才会出现线程安全问题,一旦初始化完成就不存在了。

/**
 * 懒汉式
 *  线程安全
 */
public class Singleton {
    //私有构造方法
    private Singleton() {}

    //在成员位置创建该类的对象
    private static Singleton instance;

    //对外提供静态方法获取该对象
    public static synchronized Singleton getInstance() {

        if(instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

测试:

public class SingletonApplication {
    public static void main(String[] args) {
        ThreadSafeSingleton threadSafeInstance1 = ThreadSafeSingleton.getInstance();
        ThreadSafeSingleton threadSafeInstance2 = ThreadSafeSingleton.getInstance();
        System.out.println("threadSafeInstance1 == threadSafeInstance2 ? " + (threadSafeInstance1 == threadSafeInstance2));
    }
}

image-20230712152732890

4.1.2.5懒汉式方式三(双重检查锁)

再来讨论一下懒汉模式中加锁的问题,对于 getInstance() 方法来说,绝大部分的操作都是读操作,读操作是线程安全的,所以我们没必让每个线程必须持有锁才能调用该方法,我们需要调整加锁的时机。第一次判断,如果instance对象不为空则直接返回,如果为空,则进行同步代码块创建instance对象。由此也产生了一种新的实现模式:双重检查锁模式

public class DoubleLockSingleton {
    //    1.创建私有构造方法
    private DoubleLockSingleton() {
    }

    //    2.创建私有变量
    private static DoubleLockSingleton instance;

    //    3.提供一个公共的访问方式,使外界对象能够获取到当前实例
    public static DoubleLockSingleton getInstance() {
//        4.第一次判断,如果instance为空,进入同步代码块。
//          不为空,直接返回instance
        if (instance == null) {
            synchronized (DoubleLockSingleton.class) {
                if (instance == null) {
                    instance = new DoubleLockSingleton();
                }
            }
        }
        return instance;
    }
}

测试:

public class SingletonApplication {
    public static void main(String[] args) {
        DoubleLockSingleton doubleLockInstance1 = DoubleLockSingleton.getInstance();
        DoubleLockSingleton doubleLockInstance2 = DoubleLockSingleton.getInstance();
        System.out.println("doubleLockInstance1 == doubleLockInstance2 ? " + (doubleLockInstance1 == doubleLockInstance2));

    }
}

image-20230712153906887

双重检查锁模式是一种非常好的单例实现模式,解决了单例、性能、线程安全问题,上面的双重检测锁模式看上去完美无缺,其实是存在问题,在多线程的情况下,可能会出现空指针问题,出现问题的原因是JVM在实例化对象的时候会进行优化和指令重排序操作。

要解决双重检查锁模式带来空指针异常的问题,只需要使用 volatile 关键字, volatile 关键字可以保证可见性和有序性。

public class DoubleLockSingleton {
    //    1.创建私有构造方法
    private DoubleLockSingleton() {
    }

    //    2.创建私有变量
    private static volatile DoubleLockSingleton instance;

    //    3.提供一个公共的访问方式,使外界对象能够获取到当前实例
    public static DoubleLockSingleton getInstance() {
//        4.第一次判断,如果instance为空,进入同步代码块。
//          不为空,直接返回instance
        if (instance == null) {
            synchronized (DoubleLockSingleton.class) {
                if (instance == null) {
                    instance = new DoubleLockSingleton();
                }
            }
        }
        return instance;
    }
}

小结:

添加 volatile 关键字之后的双重检查锁模式是一种比较好的单例实现模式,能够保证在多线程的情况下线程安全也不会有性能问题。

4.1.2.6懒汉式方式四(静态内部类方式)

静态内部类单例模式中实例由内部类创建,由于 JVM 在加载外部类的过程中, 是不会加载静态内部类的,只有内部类的属性/方法被调用时才会被加载,并初始化其静态属性。静态属性由于被 static 修饰,保证只被实例化一次,并且严格保证实例化顺序。

public class StaticInnerSingleton {
//    1.私有化构造器
    private StaticInnerSingleton(){
    }
//    2.创建内部类
    private static class InnerClass{
        private static final StaticInnerSingleton INSTANCE =
                new StaticInnerSingleton();
    }
//    3.提供一个公共的访问方式,让外界获取该对象。
    public static StaticInnerSingleton getInstance(){
        return InnerClass.INSTANCE;
    }
}

说明:第一次加载StaticInnerSingleton类时不会去初始化INSTANCE,只有第一次调用getInstance,JVM虚拟机加载InnerClass并初始化INSTANCE,这样不仅能确保线程安全,也能保证 Singleton 类的唯一性。

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

4.1.3 存在的问题

4.1.3.1序列化反序列化破坏单例模式

当我们将单例对象序列化到文件中,然后从文件中反序列化出来时,实际上是创建了一个新的单例对象。这是因为所创建的对象不是通过 getInstance() 方法获取的,而是通过反序列化过程创建的。

下面采用静态变量的方式实现单例模式:

  1. StaticFieldSingleton类
/**
 * 静态字段单例
 *
 * @author Xu huaiang
 * @date 2023/07/12
 */
public class StaticFieldSingleton implements Serializable {
    //    1.创建私有化构造器
    private StaticFieldSingleton() {
    }

    //    2.创建私有化静态变量
    private static StaticFieldSingleton instance = new StaticFieldSingleton();

    //    3.提供公共的访问方式
    public static StaticFieldSingleton getInstance() {
        return instance;
    }
}
  1. SerializeQuestion类
/**
 * 序列化问题
 *
 * @author Xu huaiang
 * @date 2023/07/12
 */
public class SerializeQuestion {
    public static void main(String[] args) {
        try {
            writeObjectToFile();
            readObjectFromFile();
            readObjectFromFile();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    public static void writeObjectToFile() throws Exception {
//        1.获取单例对象
        StaticFieldSingleton instance = StaticFieldSingleton.getInstance();
//        2.创建对象输出流,将对象写入文件
        ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("D:\\instance.txt"));
//        3.将对象写入文件
        objectOutputStream.writeObject(instance);
//        4.关闭流
        objectOutputStream.close();
    }

    public static void readObjectFromFile() throws Exception {
//        1.创建对象输入流,读取文件中的对象
        ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream("D:\\instance.txt"));
//        2.读取对象
        StaticFieldSingleton instance = (StaticFieldSingleton) objectInputStream.readObject();
        System.out.println(instance);
//        3.关闭流
        objectInputStream.close();
    }
}

查看测试结果:

image-20230712171713447

上面代码运行结果是两个不同的对象,表明序列化和反序列化已经破坏了单例设计模式。

4.1.3.2反射破坏单例模式

通过反射,可以访问单例类的私有构造方法,从而创建多个单例对象。

/**
 * 反射问题
 *
 * @author Xu huaiang
 * @date 2023/07/12
 */
public class ReflectQuestion {
    public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
//        1.获取到该类的字节码文件对象
        Class clazz = StaticFieldSingleton.class;
//        2.获取私有化构造器
        Constructor constructor = clazz.getDeclaredConstructor();
//        3.取消访问检查,暴力反射,破坏单例,创建多个对象
        constructor.setAccessible(true);
//        4.通过构造器创建对象
        StaticFieldSingleton instance1 = (StaticFieldSingleton) constructor.newInstance();
        StaticFieldSingleton instance2 = (StaticFieldSingleton) constructor.newInstance();
        System.out.println(instance1);
        System.out.println(instance2);
    }
}

image-20230712172544356

上面代码运行结果是两个不同的对象,表明序列化和反序列化已经破坏了单例设计模式。

4.1.3.3 序列化和反序列化的问题解决

当我们将单例对象序列化到文件中,然后从文件中反序列化出来时,实际上是创建了一个新的单例对象。这是因为所创建的对象不是通过 getInstance() 方法获取的,而是通过反序列化过程创建的。

为了解决这个问题,我们需要在单例类中添加一个 readResolve() 方法,该方法在反序列化时返回单例实例。这样,即使通过反序列化创建了一个新的对象,它也会被丢弃,而不是覆盖原有的单例对象。

反序列化机制会检测该对象是否有 readResolve() 方法。如果有,则会在反序列化过程中调用该方法,并将其返回值作为反序列化后的对象。

查看源码:

image-20230712174640874

StaticFieldSingleton添加readResolve()

public class StaticFieldSingleton implements Serializable {
    //    1.创建私有化构造器
    private StaticFieldSingleton() {
    }

    //    2.创建私有化静态变量
    private static StaticFieldSingleton instance = new StaticFieldSingleton();

    //    3.提供公共的访问方式
    public static StaticFieldSingleton getInstance() {
        return instance;
    }

    //    4.解决序列化破坏单例问题,添加该方法,
    //    返回单例对象,该方法会在序列化时被调用,并将该对象返回
    public Object readResolve() {
        return instance;
    }
}

测试类

public class SerializeQuestion {
    public static void main(String[] args) {
        try {
//            writeObjectToFile();
            readObjectFromFile();
            readObjectFromFile();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    public static void writeObjectToFile() throws Exception {
//        1.获取单例对象
        StaticFieldSingleton instance = StaticFieldSingleton.getInstance();
//        2.创建对象输出流,将对象写入文件
        ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("D:\\instance.txt"));
//        3.将对象写入文件
        objectOutputStream.writeObject(instance);
//        4.关闭流
        objectOutputStream.close();
    }

    public static void readObjectFromFile() throws Exception {
//        1.创建对象输入流,读取文件中的对象
        ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream("D:\\instance.txt"));
//        2.读取对象
        StaticFieldSingleton instance = (StaticFieldSingleton) objectInputStream.readObject();
        System.out.println(instance);
//        3.关闭流
        objectInputStream.close();
    }
}

从结果可以看出是同一个实例对象

image-20230712173741382

4.1.3.4反射问题解决

为了解决反射导致单例模式被破坏,可以在单例类的构造方法中添加一个检查,如果已经创建了一个实例,则抛出异常或者返回该实例。

  1. StaticFieldSingleton中的私有构造器中添加同步代码块,并且添加判断,如果该类已经被实例化了,则抛出异常,防止反射破坏单例。
public class StaticFieldSingleton implements Serializable {
    //    1.创建私有化构造器
    private StaticFieldSingleton() {
        synchronized (StaticFieldSingleton.class) {
//          1.1在构造器中添加判断,如果该类已经被实例化了,则抛出异常,防止反射破坏单例
            if (instance != null) {
                throw new RuntimeException("该类已经被实例化了,不能再次被实例化");
            }
        }
    }

    //    2.创建私有化静态变量
    private static StaticFieldSingleton instance = new StaticFieldSingleton();

    //    3.提供公共的访问方式
    public static StaticFieldSingleton getInstance() {
        return instance;
    }
}
  1. 测试类
public class ReflectQuestion {
    public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
//        1.获取到该类的字节码文件对象
        Class clazz = StaticFieldSingleton.class;
//        2.获取私有化构造器
        Constructor constructor = clazz.getDeclaredConstructor();
//        3.取消访问检查,暴力反射,破坏单例,创建多个对象
        constructor.setAccessible(true);
//        4.通过构造器创建对象
        StaticFieldSingleton instance1 = (StaticFieldSingleton) constructor.newInstance();
        StaticFieldSingleton instance2 = (StaticFieldSingleton) constructor.newInstance();
        System.out.println(instance1);
        System.out.println(instance2);
    }
}

image-20230712175923607

4.1.4 JDK源码解析-Runtime类

Runtime类就是使用的单例设计模式。

  1. 通过源代码查看使用的是哪儿种单例模式

    public class Runtime {
        private static Runtime currentRuntime = new Runtime();
    
        /**
         * Returns the runtime object associated with the current Java application.
         * Most of the methods of class <code>Runtime</code> are instance
         * methods and must be invoked with respect to the current runtime object.
         *
         * @return  the <code>Runtime</code> object associated with the current
         *          Java application.
         */
        public static Runtime getRuntime() {
            return currentRuntime;
        }
    
        /** Don't let anyone else instantiate this class */
        private Runtime() {}
        ...
    }

    从上面源代码中可以看出Runtime类使用的是饿汉式(静态属性)方式来实现单例模式的。

4.2 工厂模式

4.2.1 问题引入

需求:设计一个咖啡店点餐系统。

设计一个咖啡类(Coffee),并定义其两个子类(美式咖啡AmericanCoffee和拿铁咖啡LatteCoffee);再设计一个咖啡店类(CoffeeStore),咖啡店具有点咖啡的功能。

具体类的设计如下:

未命名文件 (22)

  1. 抽象类Coffee
public abstract class Coffee {
    public abstract String getName();

    public void addSuger() {
        System.out.println("加糖");
    }

    public void addMilk() {
        System.out.println("加奶");
    }
}
  1. AmericanCoffee类
public class AmericanCoffee extends Coffee {
    @Override
    public String getName() {
        return "美式咖啡";
    }
}
  1. LatteCoffee类
public class LatteCoffee extends Coffee{
    @Override
    public String getName() {
        return "拿铁咖啡";
    }
}
  1. CoffeeStore类
public class CoffeeStore {
     public Coffee orderCoffee(String type) {
        Coffee coffee = null;
        if ("american".equals(type)) {
            coffee = new AmericanCoffee();
        } else if ("latte".equals(type)) {
            coffee = new LatteCoffee();
        } else {
            throw new RuntimeException("不支持的咖啡类型");
        }
        coffee.addSuger();
        coffee.addMilk();
        return coffee;
    }
}
  1. TestDemo类
public static void main(String[] args) {
    CoffeeStore coffeeStore = new CoffeeStore();
    Coffee coffee = coffeeStore.orderCoffee("american");
    System.out.println(coffee.getName());
}

image-20230712220013417

存在的问题:在java中,万物皆对象,这些对象都需要创建,如果创建的时候直接new该对象,就会对该对象耦合严重,假如我们要更换对象,所有new对象的地方都需要修改一遍,这显然违背了软件设计的开闭原则。如果我们使用工厂来生产对象,我们就只和工厂打交道就可以了,彻底和对象解耦,如果要更换对象,直接在工厂里更换该对象即可,达到了与对象解耦的目的;所以说,工厂模式最大的优点就是:解耦

4.2.2工厂模式概述

工厂模式是一种创建型设计模式,它允许将对象的创建过程封装在一个单独的类中,从而避免了客户端直接与对象的创建过程交互。工厂模式包含如下几种不同的类型:

  1. 简单工厂模式(Simple Factory Pattern)简单工厂模式实质上不是一种设计模式,而是一种编程习惯性的写法。它定义一个工厂类来创建对象,由该工厂类根据客户端传入的参数返回不同的具体产品类对象。简单工厂模式违反了“开闭原则”,因为每次添加或删除产品都需要修改工厂类中的创建方法。因此,它的使用范围有限,并且只适用于创建数量较少的对象。
  2. 工厂方法模式(Factory Method Pattern)工厂方法模式定义一个抽象工厂接口,由具体的工厂类来实现该接口,从而创建不同的具体产品对象。客户端可以通过该抽象工厂接口来调用工厂方法,从而获得具体产品的实例。工厂方法模式具有良好的扩展性,可以通过添加新的工厂类和产品类来灵活地扩展系统的功能。
  3. 抽象工厂模式(Abstract Factory Pattern)抽象工厂模式提供一个抽象工厂接口,由具体的工厂类来实现该接口,从而创建不同的一组相关或相互依赖的产品对象。抽象工厂模式通常与工厂方法模式一起使用,以提供更高层次的抽象。

4.2.2 简单工厂模式

4.2.2.1概念

简单工厂模式实质上不是一种设计模式,而是一种编程习惯性的写法。它定义一个工厂类来创建对象,由该工厂类根据客户端传入的参数返回不同的具体产品类对象。简单工厂模式违反了“开闭原则”,因为每次添加或删除产品都需要修改工厂类中的创建方法。因此,它的使用范围有限,并且只适用于创建数量较少的对象。

4.2.2.2 结构

简单工厂包含如下角色:

  • 抽象产品 :定义了产品的规范,描述了产品的主要特性和功能。
  • 具体产品 :实现或者继承抽象产品的子类
  • 具体工厂 :提供了创建产品的方法,调用者通过该方法来获取产品。

4.2.2.3 实现

现在使用简单工厂对上面案例进行改进,类图如下:

未命名文件 (23)

工厂类代码如下:

  1. 抽象类Coffee(抽象产品
public abstract class Coffee {
    public abstract String getName();

    public void addSuger() {
        System.out.println("加糖");
    }

    public void addMilk() {
        System.out.println("加奶");
    }
}
  1. AmericanCoffee类(具体产品
public class AmericanCoffee extends Coffee {
    @Override
    public String getName() {
        return "美式咖啡";
    }
}
  1. LatteCoffee类(具体产品
public class LatteCoffee extends Coffee{
    @Override
    public String getName() {
        return "拿铁咖啡";
    }
}
  1. SimpleFactory类(具体工厂
public class SimpleCoffeeFactory {
    public Coffee createCoffee(String type) {
        Coffee coffee = null;
        if ("american".equals(type)) {
            coffee = new AmericanCoffee();
        } else if ("lattee".equals(type)) {
            coffee = new LateeCoffee();
        } else {
            throw new RuntimeException("没有你要的咖啡");
        }
        return coffee;
    }
}
  1. CoffeeStore类
public class CoffeeStore {
    public Coffee orderCoffee(String type) {
        Coffee coffee = SimpleCoffeeFactory.createCoffee(type);
        coffee.addMilk();
        coffee.addSuger();
        return coffee;
    }
}
  1. TestDemo类
public class TestDemo {
    public static void main(String[] args) {
        CoffeeStore coffeeStore = new CoffeeStore();
        Coffee coffee = coffeeStore.orderCoffee("american");
        System.out.println(coffee.getName());
    }
}

image-20230712231921961

工厂(factory)处理创建对象的细节,一旦有了SimpleCoffeeFactoryCoffeeStore类中的orderCoffee()就变成此对象的客户,后期如果需要Coffee对象直接从工厂中获取即可。这样也就解除了和Coffee实现类的耦合,同时又产生了新的耦合,CoffeeStore对象和SimpleCoffeeFactory工厂对象的耦合,SimpleCoffeeFactory工厂对象和商品对象的耦合。

后期如果再加新品种的咖啡,我们势必要需求修改SimpleCoffeeFactory的代码,违反了开闭原则。工厂类的客户端可能有很多,比如创建美团外卖等,这样只需要修改工厂类的代码,省去其他的修改操作。

4.2.2.4 优缺点

优点:

封装了创建对象的过程,可以通过参数直接获取对象。把对象的创建和业务逻辑层分开,这样以后就避免了修改客户代码,如果要实现新产品直接修改工厂类,而不需要在原代码中修改,这样就降低了客户代码修改的可能性,更加容易扩展。

缺点:

增加新产品时还是需要修改工厂类的代码,违背了“开闭原则”。

4.2.2.3 扩展—静态工厂

在开发中也有一部分人将工厂类中的创建对象的功能定义为静态的,这个就是静态工厂模式,它也不是23种设计模式中的。代码如下:

public class SimpleCoffeeFactory {
    public static Coffee createCoffee(String type) {
        Coffee coffee = null;
        if ("american".equals(type)) {
            coffee = new AmericanCoffee();
        } else if ("lattee".equals(type)) {
            coffee = new LateeCoffee();
        } else {
            throw new RuntimeException("没有你要的咖啡");
        }
        return coffee;
    }
}

4.2.3 工厂方法模式

4.2.3.1 概念

工厂方法模式定义一个抽象工厂接口,由具体的工厂类来实现该接口,从而创建不同的具体产品对象。客户端可以通过该抽象工厂接口来调用工厂方法,从而获得具体产品的实例。工厂方法模式具有良好的扩展性,可以通过添加新的工厂类和产品类来灵活地扩展系统的功能。

4.2.3.2 结构

工厂方法模式的主要角色:

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

4.2.3.3 实现

使用工厂方法模式对上例进行改进,类图如下:

未命名文件 (27)

  1. Coffee抽象类(抽象产品
public abstract class Coffee {
    public abstract String getName();

    public void addMilk() {
        System.out.println("加奶");
    }

    public void addSuger() {
        System.out.println("加糖");
    }
}
  1. LatteeCoffee类(具体产品
public class LatteeCoffee extends Coffee {
    @Override
    public String getName() {
        return "拿铁咖啡";
    }
}
  1. AmericanCoffee类(具体产品
public class AmericanCoffee extends Coffee {
    @Override
    public String getName() {
        return "美式咖啡";
    }
}
  1. CoffeeFactory接口(抽象工厂
public interface CoffeeFactory {
    /**
     * 生产咖啡
     *
     * @return {@link Coffee}
     */
    Coffee createCoffee();
}
  1. AmericanCoffeeFactory类(具体工厂
public class AmericanCoffeeFactory implements CoffeeFactory{
    @Override
    public Coffee createCoffee() {
        return new AmericanCoffee();
    }
}
  1. LatteeCoffeeFactory类(具体工厂
public class LatteeCoffeeFactory implements CoffeeFactory{
    @Override
    public Coffee createCoffee() {
        return new LatteeCoffee();
    }
}
  1. CoffeeStore类
public class CoffeeStore {
    private CoffeeFactory coffeeFactory;

    public void setCoffeeFactory(CoffeeFactory coffeeFactory) {
        this.coffeeFactory = coffeeFactory;
    }

    public Coffee orderCoffee(){
        Coffee coffee = coffeeFactory.createCoffee();
        coffee.addMilk();
        coffee.addSuger();
        return coffee;
    }
}
  1. TestDemo测试类
public class TestDemo {
    public static void main(String[] args) {
//        创建咖啡店对象
        CoffeeStore coffeeStore = new CoffeeStore();
//        设置咖啡工厂对象
        coffeeStore.setCoffeeFactory(new AmericanCoffeeFactory());
//        下单
        Coffee coffee = coffeeStore.orderCoffee();
        System.out.println(coffee.getName());
    }
}

image-20230713091936242

从以上的编写的代码可以看到,要增加产品类时也要相应地增加工厂类,不需要修改工厂类的代码了,这样就解决了简单工厂模式的缺点。

工厂方法模式是简单工厂模式的进一步抽象。由于使用了多态性,工厂方法模式保持了简单工厂模式的优点,而且克服了它的缺点。

4.2.3.4 优缺点

优点:

  • 用户只需要知道具体工厂的名称就可得到所要的产品,无须知道产品的具体创建过程;
  • 在系统增加新的产品时只需要添加具体产品类和对应的具体工厂类,无须对原工厂进行任何修改,满足开闭原则;

缺点:

  • 每增加一个产品就要增加一个具体产品类和一个对应的具体工厂类,这增加了系统的复杂度。

4.2.4 抽象工厂模式

前面介绍的工厂方法模式中考虑的是一类产品的生产,如畜牧场只养动物、电视机厂只生产电视机等。

这些工厂只生产同种类产品,同种类产品称为同等级产品,也就是说:工厂方法模式只考虑生产同等级的产品,但是在现实生活中许多工厂是综合型的工厂,能生产多等级(种类) 的产品,如电器厂既生产电视机又生产洗衣机或空调,大学既有软件专业又有生物专业等。

本节要介绍的抽象工厂模式将考虑多等级产品的生产,将同一个具体工厂所生产的位于不同等级的一组产品称为一个产品族,下图所示横轴是产品等级,也就是同一类产品;纵轴是产品族,也就是同一品牌的产品,同一品牌的产品产自同一个工厂。

4.2.4.1 概念

抽象工厂模式提供一个抽象工厂接口,由具体的工厂类来实现该接口,从而创建不同的一组相关或相互依赖的产品对象。抽象工厂模式通常与工厂方法模式一起使用,以提供更高层次的抽象。

对于工厂方法模式和抽象工厂模式来说,工厂方法模式适合创建只包含单一种类对象的系统,而抽象工厂模式适合创建一组相互依赖的对象。

4.2.4.2 结构

抽象工厂模式的主要角色如下:

  • 抽象工厂(Abstract Factory):提供了创建产品的接口,它包含多个创建产品的方法,可以创建多个不同等级的产品。
  • 具体工厂(Concrete Factory):主要是实现抽象工厂中的多个抽象方法,完成具体产品的创建。
  • 抽象产品(Product):定义了产品的规范,描述了产品的主要特性和功能,抽象工厂模式有多个抽象产品。
  • 具体产品(ConcreteProduct):实现了抽象产品角色所定义的接口,由具体工厂来创建,它 同具体工厂之间是多对一的关系。

4.2.4.2 实现

现咖啡店业务发生改变,不仅要生产咖啡还要生产甜点,如提拉米苏、抹茶慕斯等,要是按照工厂方法模式,需要定义提拉米苏类、抹茶慕斯类、提拉米苏工厂、抹茶慕斯工厂、甜点工厂类,很容易发生类爆炸情况。其中拿铁咖啡、美式咖啡是一个产品等级,都是咖啡;提拉米苏、抹茶慕斯也是一个产品等级;拿铁咖啡和提拉米苏是同一产品族(也就是都属于意大利风味),美式咖啡和抹茶慕斯是同一产品族(也就是都属于美式风味)。所以这个案例可以使用抽象工厂模式实现。类图如下:

未命名文件 (31)

  1. Coffee抽象类(抽象产品
public abstract class Coffee {
    public abstract String getName();

    public void addMilk() {
        System.out.println("加奶");
    }

    public void addSuger() {
        System.out.println("加糖");
    }
}
  1. LatteeCoffee类(具体产品
public class LatteeCoffee extends Coffee {
    @Override
    public String getName() {
        return "拿铁咖啡";
    }
}
  1. AmericanCoffee类(具体产品
public class AmericanCoffee extends Coffee {
    @Override
    public String getName() {
        return "美式咖啡";
    }
}
  1. Dessert抽象类(抽象产品
public abstract class Dessert {
    public abstract void show();
}
  1. Tiramisu类(具体产品
public class Trimisu extends Dessert {
    @Override
    public void show() {
        System.out.println("提拉米苏");
    }
}
  1. MatchaMouse类(具体产品
public class MatchaMouse extends Dessert {
    @Override
    public void show() {
        System.out.println("抹茶慕斯");
    }
}
  1. DessertFactory接口(抽象工厂
public interface DessertFactory {
    /**
     * 生产咖啡
     *
     * @return 咖啡
     */
    Coffee createCoffee();

    /**
     * 生产甜点
     *
     * @return 甜点
     */
    Dessert createDessert();
}
  1. AmericanDessertFactory(具体工厂
public class AmericanDessertFactory implements DessertFactory{
    @Override
    public Coffee createCoffee() {
        return new AmericanCoffee();
    }

    @Override
    public Dessert createDessert() {
        return new MatchaMouse();
    }
}
  1. ItalyDessertFactory(具体工厂
public class ItalyDessertFactory implements DessertFactory{
    @Override
    public Coffee createCoffee() {
        return new LatteeCoffee();
    }

    @Override
    public Dessert createDessert() {
        return new Trimisu();
    }
}
  1. 测试类
public class TestDemo {
    public static void main(String[] args) {
        ItalyDessertFactory italyDessertFactory = new ItalyDessertFactory();
        Coffee coffee = italyDessertFactory.createCoffee();
        Dessert dessert = italyDessertFactory.createDessert();
        System.out.println(coffee.getName());
        dessert.show();
    }
}

image-20230713100221792

如果要加同一个产品族的话,只需要再加一个对应的工厂类即可,不需要修改其他的类。

4.2.4.3 优缺点

优点:

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

缺点:

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

4.2.4.4 使用场景

  • 当需要创建的对象是一系列相互关联或相互依赖的产品族时,如电器工厂中的电视机、洗衣机、空调等。

  • 系统中有多个产品族,但每次只使用其中的某一族产品。如有人只喜欢穿某一个品牌的衣服和鞋。

  • 系统中提供了产品的类库,且所有产品的接口相同,客户端不依赖产品实例的创建细节和内部结构。

如:输入法换皮肤,一整套一起换。生成不同操作系统的程序。

4.2.5 工厂模式+配置文件

可以通过简单工厂模式+配置文件的方式解除工厂对象和产品对象的耦合。在工厂类中加载配置文件中的全类名,并创建对象进行存储,客户端如果需要对象,直接进行获取即可。如果新增了具体产品,那也不需要新添加具体工厂,或者是修改具体工厂,而是在配置文件中添加新增的具体产品的全类名即可。

  1. 定义配置文件

为了演示方便,我们使用properties文件作为配置文件,名称为application.properties

american=com.xha.model.factory.simple_factory_config.AmericanCoffee
lattee=com.xha.model.factory.simple_factory_config.LatteeCoffee
  1. 改进工厂类
import java.io.InputStream;
import java.util.HashMap;
import java.util.Properties;
import java.util.Set;

/**
 * 咖啡工厂
 *
 * @author Xu huaiang
 * @date 2023/07/13
 */
public class CoffeeFactory {
    //    1.加载配置文件,获取配置文件中配置的全类名并创建对象,存储在map中
    private static HashMap<String, Coffee> map = new HashMap<>();

    //    2.加载配置文件,只需要加载一次
    static {
//        2.1创建Properties对象
        Properties properties = new Properties();
//        2.2获取输入流,使用类加载器,获取字节输入流,加载配置文件,获取文件路径
        InputStream resourceAsStream = CoffeeFactory.class.getClassLoader().getResourceAsStream("application.properties");
//        2.2加载配置文件
        try {
            properties.load(resourceAsStream);
//            2.3获取配置文件中配置的全类名并创建对象,存储在map中
            Set<Object> keys = properties.keySet();
            for (Object key : keys) {
                String className = properties.getProperty(key.toString());
//                2.4反射创建对象
                Class<?> clazz = Class.forName(className);
                Coffee coffee = (Coffee) clazz.newInstance();
                map.put(key.toString(), coffee);
            }
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * 根据咖啡名称获取咖啡对象
     *
     * @return {@link Coffee}
     */
    public static Coffee createCoffee(String name) {
        return map.get(name);
    }
}
  1. 测试类
public class TestDemo {
    public static void main(String[] args) {
        Coffee coffee = CoffeeFactory.createCoffee("american");
        System.out.println(coffee.getName());
        System.out.println("==============");
        coffee = CoffeeFactory.createCoffee("lattee");
        System.out.println(coffee.getName());
    }
}

image-20230713110207447

上面代码的执行总结:

  1. 创建了一个HashMap<String, Coffee>类型的静态变量map,用于存储咖啡对象;
  2. 创建一个Properties对象properties,用于读取配置文件;
  3. 获取并加载配置文件application.properties;
  4. 通过反射获取全类名并创建对应的咖啡对象,然后将其存储在静态变量map中。

这样做的好处是可以将咖啡对象的创建过程封装起来,当需要获取某一种咖啡对象时,只需要通过key调用map中的对象,而不需要再去创建对象,从而提高了代码的复用性和扩展性。同时,对于创建的咖啡对象,也可以通过修改配置文件application.properties来动态的进行修改或添加。

4.2.6 JDK源码解析-Collection.iterator方法

public class Demo {
    public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        list.add("令狐冲");
        list.add("风清扬");
        list.add("任我行");

        //获取迭代器对象
        Iterator<String> it = list.iterator();
        //使用迭代器遍历
        while(it.hasNext()) {
            String ele = it.next();
            System.out.println(ele);
        }
    }
}

对上面的代码大家应该很熟,使用迭代器遍历集合,获取集合中的元素。而单列集合获取迭代器的方法就使用到了工厂方法模式。我们看通过类图看看结构:

image-20230713111044246

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

另:

​ 1. DateForamt类中的getInstance()方法使用的是工厂模式;

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

4.3 原型模式

4.3.1 概述

原型模式是一种创建型设计模式,它允许通过复制现有对象来创建新对象,而无需通过明确的构造函数进行实例化。它的核心思想是通过克隆已有的对象来创建新的对象,从而避免了重复创建对象的过程。

4.3.2 结构

原型模式包含如下角色:

  • 抽象原型(Prototype):声明一个克隆自身的接口,一般是一个抽象类或者接口。在Java中,通常使用Cloneable接口来标识抽象原型。
  • 具体原型(Concrete Prototype):实现抽象原型接口,提供克隆自身的具体实现。实现类需要实现Cloneable接口,重写clone()方法来进行对象的克隆。
  • 客户端(Client):使用具体原型类中的 clone() 方法来复制新的对象。

接口类图如下:

4.3.3 实现

原型模式的克隆分为浅克隆深克隆

浅克隆:创建一个新对象,新对象的属性和原来对象完全相同,对于非基本类型属性,仍指向原有属性所指向的对象的内存地址。

浅克隆会创建一个新的克隆对象,和原来的原型对象的内存地址并不相同。但是对于对象的属性,如果是引用类型,克隆对象和原型对象公用一个内存地址的成员属性。如果在克隆对象中修改引用类型的属性,那么原型对象中对应的引用类型属性的值也会更改。

深克隆:创建一个新对象,属性中引用的其他对象也会被克隆,不再指向原有对象地址。

实现 Cloneable 接口并重写 clone() 方法。Java 提供了 Cloneable 接口和 clone() 方法,用于支持对象克隆。代码如下:

Realizetype(具体原型类):

Realizetype实现Cloneable接口,重写clone()方法。

public class Realizetype implements Cloneable {

    public Realizetype() {
        System.out.println("具体原型创建成功");
    }

    @Override
    protected Realizetype clone() throws CloneNotSupportedException {
        System.out.println("具体原型复制成功");
        return (Realizetype) super.clone();
    }

}

PrototypeTest(客户端):

public class TestDemo {

        public static void main(String[] args) throws CloneNotSupportedException {
//            1.创建原型对象
            Realizetype realizetype = new Realizetype();
//            2.克隆原型对象
            Realizetype clone = realizetype.clone();
//            3.比较原型对象和克隆对象
            System.out.println(realizetype == clone);
        }
}

从解释结果可以看出,clone底层并不是通过构造器的方式来创建对象的。clone的对象并不和原型对象的内存地址相同。

image-20230713120321330

4.3.4 案例

用原型模式生成“三好学生”奖状

同一学校的“三好学生”奖状除了获奖人姓名不同,其他都相同,可以使用原型模式复制多个“三好学生”奖状出来,然后在修改奖状上的名字即可。

类图如下:

代码如下:

  1. Citation类(原型模型
public class Citation implements Cloneable{

    private String name;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public void show() {
        System.out.println(name + "同学:在2023学年第一学期中表现优秀,被评为三好学生。特发此状!");
    }

    @Override
    protected Citation clone() throws CloneNotSupportedException {
        return (Citation) super.clone();
    }
}
  1. 测试类
public class TestDemo {
    public static void main(String[] args) throws CloneNotSupportedException {
//        1.创建原型对象
        Citation citation = new Citation();
        citation.setName("张三");
//        2.克隆原型对象
        Citation clone = citation.clone();
        clone.setName("李四");

        citation.show();
        clone.show();
    }
}

image-20230713122204403

clone的对象并不和原型对象的内存地址相同。

4.3.5 使用场景

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

4.3.6 扩展(深克隆)

4.3.6.1浅克隆

将上面的“三好学生”奖状的案例中Citation类的name属性(String引用类型)修改为Student类型的属性。代码如下:

  1. Student类
public class Student {
    private String name;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    @Override
    public String toString() {
        return "Student{" +
                "name='" + name + '\'' +
                '}';
    }
}
  1. Citation类(原型模型
public class Citation implements Cloneable{
    private Student stu;

    public void setStu(Student stu) {
        this.stu = stu;
    }

    public Student getStu() {
        return stu;
    }

    public void show() {
        System.out.println(stu.getName() + "同学:在2023学年第一学期中表现优秀,被评为三好学生。特发此状!");
    }

    @Override
    protected Citation clone() throws CloneNotSupportedException {
        return (Citation) super.clone();
    }
}
  1. 测试类
public class TestDemo {
    public static void main(String[] args) throws CloneNotSupportedException {
//        1.创建原型对象
        Citation citation = new Citation();
        Student student1 = new Student();
        student1.setName("张三");
        citation.setStu(student1);

//        2.克隆原型对象
        Citation clone = citation.clone();
        clone.getStu().setName("李四");

        citation.show();
        clone.show();
    }
}

测试结果:

image-20230713131533340

说明:

引用类型student1student2是同一个对象,由于两个对象指向同一内存地址,就会产生将student1对象中name属性值改为“李四”,两个Citation对象中显示的都是李四。这就是浅克隆的效果,对具体原型类(Citation)中的引用类型的属性进行引用的复制。

4.3.6.2深克隆

要将浅拷贝转换为深拷贝,你可以使用以下几种方法:

  1. 使用序列化和反序列化:这是一种常用的方法,可以通过将对象序列化为字节流,然后再反序列化回一个新的对象来实现深拷贝。Java中的Serializable接口和ObjectInputStream/ObjectOutputStream类可以帮助你实现这个过程。
  2. 递归复制:对于类中包含其他引用类型成员变量的情况,你可以使用递归的方式来复制这些成员变量。递归地将每个成员变量复制到新创建的对象中,以实现深拷贝。
  3. 使用克隆方法:在要进行深拷贝的类中,实现Cloneable接口并重写clone()方法。在clone()方法中,通过创建一个新的对象,并将原始对象的属性值逐个复制到新对象中,实现深拷贝。

深克隆需要使用对象流。代码如下:

public class TestDemo {
    public static void main(String[] args) throws Exception {
//        1.创建原型对象
        Citation citation = new Citation();
        Student student1 = new Student();
        student1.setName("张三");
        citation.setStu(student1);

//        1.创建对象输出流
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("D:\\deep.txt"));
//        2.将对象写入文件
        oos.writeObject(citation);
//        3.关闭流
        oos.close();

//        4.创建对象输入流
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("D:\\deep.txt"));
//        5.从文件中读取对象
        Citation clone = (Citation) ois.readObject();
//        6.关闭流
        ois.close();

        clone.getStu().setName("李四");
        citation.show();
        clone.show();
    }
}

运行结果为:

image-20230713133152670

注意:Citation类和Student类必须实现Serializable接口,否则会抛NotSerializableException异常。

4.5 建造者模式

4.4.1 概述

建造者模式(Builder Pattern)是一种创建型设计模式,用于创建复杂对象。该模式将对象的构建过程与其表示分离,使得相同的构建过程可以创建不同的表示。

在某些情况下,对象的构建过程可能非常复杂,需要多个步骤,并且构建的对象可能有不同的配置选项。使用建造者模式,可以将这些步骤和选项封装到一个独立的建造者类中,从而简化客户端代码并提高可读性。

4.4.2 结构

建造者(Builder)模式包含如下角色:

  1. 产品(Product):要创建的复杂对象。产品类通常具有多个属性,并且可能包含其他对象。
  2. 抽象建造者(Abstract Builder):定义了构建产品的抽象接口,包括各个步骤的方法。
  3. 具体建造者(Concrete Builder):实现抽象建造者接口,具体实现构建产品的各个步骤,并返回最终的产品。
  4. 指挥者(Director):负责使用建造者来构建产品。可以根据需要定义多个不同的指导者。

类图如下:

4.4.3 实例

创建共享单车

生产自行车是一个复杂的过程,它包含了车架,车座等组件的生产。而车架又有碳纤维,铝合金等材质的,车座有橡胶,真皮等材质。对于自行车的生产就可以使用建造者模式。

这里Bike是产品,包含车架,车座等组件;Builder是抽象建造者,MobikeBuilderOfoBuilder是具体的建造者;Director是指挥者。类图如下:

具体的代码如下:

  1. Bike(产品类
public class Bike {
    private String frame;
    private String seat;

    public String getFrame() {
        return frame;
    }

    public Bike setFrame(String frame) {
        this.frame = frame;
        return this;
    }

    public String getSeat() {
        return seat;
    }

    public Bike setSeat(String seat) {
        this.seat = seat;
        return this;
    }
    
    @Override
    public String toString() {
        return "Bike{" +
                "frame='" + frame + '\'' +
                ", seat='" + seat + '\'' +
                '}';
    }
}
  1. Builder(抽象建造者
public abstract class Builder {

    protected Bike bike = new Bike();

    public abstract void buildFrame();

    public abstract void buildSeat();

    public abstract Bike createBike();
}
  1. MobileBuilder(具体建造者
public class MobileBuilder extends Builder{
    @Override
    public void buildFrame() {
        bike.setFrame("碳纤维车架");
    }

    @Override
    public void buildSeat() {
        bike.setSeat("真皮车座");
    }

    @Override
    public Bike createBike() {
        return bike;
    }
}
  1. OfoBuilder(具体建造者
public class OfoBuilder extends Builder {
    @Override
    public void buildFrame() {
        bike.setFrame("铝合金车架");
    }

    @Override
    public void buildSeat() {
        bike.setSeat("橡胶车座");
    }

    @Override
    public Bike createBike() {
        return bike;
    }
}
  1. Director类(指挥者
public class Director {
//    1.创建指挥者对象
    private Builder builder;

//    2.构造器传入具体建造者
    public Director(Builder builder) {
        this.builder = builder;
    }

//    3.指挥者指挥建造者建造对象
    public Bike construct(){
        builder.buildFrame();
        builder.buildSeat();
        return builder.createBike();
    }
}
  1. 测试类
public class TestDemo {
    public static void main(String[] args) {
//        1.创建指挥者对象
        Director director = new Director(new MobileBuilder());
//        2.指挥者指挥建造者建造对象
        Bike bike = director.construct();
        System.out.println(bike.getFrame());
        System.out.println(bike.getSeat());
    }
}

image-20230713155505706

注意:

上面示例是 Builder模式的常规用法,指挥者类Director在建造者模式中具有很重要的作用,它用于指导具体构建者如何构建产品,控制调用先后次序,并向调用者返回完整的产品类,但是有些情况下需要简化系统结构,可以把指挥者类和抽象建造者进行结合

// 抽象 builder 类
public abstract class Builder {

    protected Bike mBike = new Bike();

    public abstract void buildFrame();
    public abstract void buildSeat();
    public abstract Bike createBike();
    
    public Bike construct() {
        this.buildFrame();
        this.BuildSeat();
        return this.createBike();
    }
}

说明:

这样做确实简化了系统结构,但同时也加重了抽象建造者类的职责,也不是太符合单一职责原则,如果construct() 过于复杂,建议还是封装到Director 中。

4.4.4 优缺点

优点:

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

缺点:

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

4.4.5 使用场景

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

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

4.4.6 模式扩展

建造者模式除了上面的用途外,在开发中还有一个常用的使用方式,就是当一个类构造器需要传入很多参数时,如果创建这个类的实例,代码可读性会非常差,而且很容易引入错误。此时就可以利用建造者模式进行重构。

重构前代码如下:

public class Phone {
    private String cpu;
    private String screen;
    private String memory;
    private String mainboard;

    public Phone(String cpu, String screen, String memory, String mainboard) {
        this.cpu = cpu;
        this.screen = screen;
        this.memory = memory;
        this.mainboard = mainboard;
    }

    public String getCpu() {
        return cpu;
    }

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

    public String getScreen() {
        return screen;
    }

    public void setScreen(String screen) {
        this.screen = screen;
    }

    public String getMemory() {
        return memory;
    }

    public void setMemory(String memory) {
        this.memory = memory;
    }

    public String getMainboard() {
        return mainboard;
    }

    public void setMainboard(String mainboard) {
        this.mainboard = mainboard;
    }

    @Override
    public String toString() {
        return "Phone{" +
                "cpu='" + cpu + '\'' +
                ", screen='" + screen + '\'' +
                ", memory='" + memory + '\'' +
                ", mainboard='" + mainboard + '\'' +
                '}';
    }
}

测试类:

public class Client {
    public static void main(String[] args) {
        //构建Phone对象
        Phone phone = new Phone("intel","三星屏幕","金士顿","华硕");
        System.out.println(phone);
    }
}

上面在客户端代码中构建Phone对象,传递了四个参数,如果参数更多呢?代码的可读性及使用的成本就是比较高。

使用建造者模式进行重构:

public class Phone {
    private String cpu;
    private String mainboard;
    private String memory;
    private String screen;

    @Override
    public String toString() {
        return "Phone{" +
                "cpu='" + cpu + '\'' +
                ", mainboard='" + mainboard + '\'' +
                ", memory='" + memory + '\'' +
                ", screen='" + screen + '\'' +
                '}';
    }

//    私有化构造器,参数为Builder
    private Phone(Builder builder) {
//        将Builder中的属性赋值给Phone
        this.cpu = builder.cpu;
        this.mainboard = builder.mainboard;
        this.memory = builder.memory;
        this.screen = builder.screen;
    }
//    静态内部类,用于构建Phone对象,并且可以设置属性,最后调用build方法返回Phone对象
    public static final class Builder {
        private String cpu;
        private String mainboard;
        private String memory;
        private String screen;

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

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

        public Builder screen(String screen) {
            this.screen = screen;
            return this;
        }
//        调用私有化构造器,返回Phone对象
        public Phone build() {
            return new Phone(this);
        }
    }
}

测试类:

public class TestDemo {
    public static void main(String[] args) {
        Phone phone = new Phone.Builder()
                .cpu("骁龙888")
                .mainboard("高通")
                .memory("16G")
                .screen("三星")
                .build();
        System.out.println(phone);
    }
}

image-20230713161842947

重构后的代码在使用起来更方便,某种程度上也可以提高开发效率。从软件设计上,对程序员的要求比较高。

4.6 创建者模式对比

4.6.1 工厂方法模式VS建造者模式

工厂方法模式注重的是整体对象的创建方式;而建造者模式注重的是部件构建的过程,意在通过一步一步地精确构造创建出一个复杂的对象。

我们举个简单例子来说明两者的差异,如要制造一个超人,如果使用工厂方法模式,直接产生出来的就是一个力大无穷、能够飞翔、内裤外穿的超人;而如果使用建造者模式,则需要组装手、头、脚、躯干等部分,然后再把内裤外穿,于是一个超人就诞生了。

4.6.2 抽象工厂模式VS建造者模式

抽象工厂模式实现对产品家族的创建,一个产品家族是这样的一系列产品,具有不同分类维度的产品组合,采用抽象工厂模式则是不需要关心构建过程,只关心什么产品由什么工厂生产即可。

建造者模式则是要求按照指定的蓝图建造产品,它的主要目的是通过组装零配件而产生一个新产品。

如果将抽象工厂模式看成汽车配件生产工厂,生产一个产品族的产品,那么建造者模式就是一个汽车组装工厂,通过对部件的组装可以返回一辆完整的汽车。

5.结构型模式

结构型模式描述如何将类或对象按某种布局组成更大的结构。它分为类结构型模式对象结构型模式,前者采用继承机制来组织接口和类,后者釆用组合或聚合来组合对象。

由于组合关系或聚合关系比继承关系耦合度低,满足“合成复用原则”,所以对象结构型模式比类结构型模式具有更大的灵活性。

结构型模式分为以下 7 种:

  • 代理模式
  • 适配器模式
  • 装饰者模式
  • 桥接模式
  • 外观模式
  • 组合模式
  • 享元模式

5.1 代理模式

5.1.1 概述

代理模式通过创建一个代理对象来控制对实际对象的访问。

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

5.1.2 结构

代理(Proxy)模式分为三种角色:

  • 抽象主题(Subject)定义了真实主题和代理主题的共同接口,这样代理和真实主题可以互换使用。
  • 真实主题(Real Subject):定义代理对象所代表的真实对象。
  • 代理(Proxy)持有一个真实主题对象的引用,并实现了与真实主题相同的接口,可以在接口方法的调用前后添加额外的处理逻辑。

5.1.3 静态代理

静态代理是一种在编译时就已经确定代理关系的代理模式。在静态代理中,代理类和真实类是在编译期间就已经确定的,代理类在编写代码时就已经存在。

在静态代理中,代理类通过实现与真实类相同的接口或继承与真实类相同的父类,来实现对真实类方法的代理。当客户端调用代理对象的方法时,代理对象会执行额外的处理,例如调用真实对象的方法之前或之后添加日志记录、权限验证等功能。

静态代理的实现步骤如下:

  1. 创建接口:定义被代理类和代理类共同实现的接口,即被代理接口
  2. 创建真实类:实现被代理接口的类,即真实对象。
  3. 创建代理类:实现被代理接口,并在代理类中维护一个指向真实对象的引用。在代理类的方法中添加额外的处理逻辑,并在适当的时机调用真实对象的相应方法。
  4. 客户端调用:通过代理类来调用方法,代理类会将请求转发给真实对象进行处理。

静态代理的优点是简单易用,对于简单的代理需求可以较快实现。但是静态代理的缺点是扩展性较差,每当有新的代理需求时,需要重新编写新的代理类。当代理类较多时,会产生大量的重复代码,导致代码冗余。

【例】火车站卖票

如果要买火车票的话,需要去火车站买票,坐车到火车站,排队等一系列的操作,显然比较麻烦。而火车站在多个地方都有代售点,我们去代售点买票就方便很多了。这个例子其实就是典型的代理模式,火车站是真实主题,代售点是代理类。类图如下:

  1. SellTickets接口(抽象主题
public interface SellTickets {
    void sell();
}
  1. TrainStation类(真实主题
public class TrainStation implements SellTickets{
    @Override
    public void sell() {
        System.out.println("火车站卖票");
    }
}
  1. ProxyPoint类(代理
public class ProxyPoint implements SellTickets{

//    声明火车站对象
    private TrainStation trainStation = new TrainStation();

    @Override
    public void sell() {
        System.out.println("代售点收取一些服务费用");
//        调用火车站卖票方法
        trainStation.sell();
    }
}
  1. 测试类
public class TestDemo {
    public static void main(String[] args) {
//        1.创建代售点对象
        ProxyPoint proxyPoint = new ProxyPoint();
//        2.调用代售点的卖票方法
        proxyPoint.sell();
    }
}

从上面代码中可以看出测试类直接访问的是ProxyPoint类对象,也就是说ProxyPoint作为访问对象和目标对象的中介。同时也对sell方法进行了增强(代理点收取一些服务费用)。

5.1.4 动态代理

5.1.4.1动态代理概述

动态代理允许在运行时创建和使用代理对象,而无需在编译时显式地指定代理类。代理类会在运行时根据需求动态生成,通过代理类可以间接地访问实际对象,并在访问前后执行一些额外的操作。

动态代理可以分为两种主要类型:基于接口的动态代理和基于类的动态代理。

  1. 基于接口的动态代理:这种代理方式是在运行时,动态生成一个实现指定接口的代理类,代理类通过实现接口的方法来执行真正的业务逻辑。Java中常用的基于接口的动态代理是使用Java的反射机制实现的,例如 java.lang.reflect.Proxy 类和 InvocationHandler 接口。
  2. 基于类的动态代理:这种代理方式是在运行时,动态生成一个继承自指定类的代理类,代理类通过继承和重写父类的方法来执行真正的业务逻辑。基于类的动态代理通常使用字节码操作库来生成代理类,例如Java字节码操作库CGLIB、ASM等。

5.1.4.2JDK动态代理

动态代理是一种基于反射机制的代理模式,它可以在运行时动态地创建代理类和代理实例,而无需在编译时就确定代理类的关系。

在动态代理中,代理类是根据被代理的接口信息动态生成的。当客户端调用代理对象的方法时,实际上是通过代理对象转发给了真实对象进行处理。代理对象可以在真实对象的方法调用前后添加额外的逻辑,例如日志记录、性能监控等。

动态代理主要涉及到以下几个组成部分:

  1. 接口:定义了真实对象和代理对象共同实现的方法。
  2. InvocationHandler(调用处理器)接口:它是实现动态代理的关键部分,代理对象的所有方法调用都会经过该调用处理器的invoke()方法进行处理。
  3. Proxy类负责动态生成代理类和代理实例的类。

具体实现动态代理的步骤如下:

  1. 定义接口:定义需要被代理的接口,该接口包含一组要被代理的方法。
  2. 通过Proxy类的静态方法newProxyInstance()动态生成代理类和代理实例:该方法需要三个参数,第一个参数是类加载器,第二个参数是实现了接口的数组,第三个参数是InvocationHandler的实例。
  3. 实现InvocationHandler接口:创建一个调用处理器的实现类,实现invoke()方法,在invoke()方法中可以添加额外的逻辑。
  4. 使用代理对象:通过代理对象调用方法,实际上会通过InvocationHandlerinvoke()方法转发给真实对象进行处理。

动态代理的优点是灵活性和可扩展性强,可以在运行时动态地添加和修改代理类的行为,而无需改动被代理的真实对象。它提供了一种统一的方式来处理各种不同的代理需求,例如日志记录、事务管理、权限控制等。

代码如下:

  1. ProxyFactory(**获取代理对象类**)
public class ProxyFactory {
//    声明火车站对象
    private TrainStation trainStation = new TrainStation();

//    获取代理对象
    public SellTickets getProxyObject() {
        //在程序运行期间,动态的创建TrainStation接口的代理对象
        /*
            newProxyInstance()方法参数说明:
                ClassLoader loader : 类加载器,用于加载代理类,可以通过被代理对象获取类加载器
                Class<?>[] interfaces : 被代理类实现的接口字节码对象数组
                InvocationHandler h : 代理对象的调用处理程序
         */
        SellTickets proxyObject = (SellTickets) Proxy.newProxyInstance(
                trainStation.getClass().getClassLoader(), //类加载器
                trainStation.getClass().getInterfaces(), //真实对象所实现的接口的字节码对象数组
                new InvocationHandler() { //调用处理程序
                    /*
                        InvocationHandler中invoke方法参数说明:
                            proxy : 代理对象,和proxyObject对象是同一个对象
                            method : 代理对象调用的方法.对接口中的方法进行封装成method对象
                            args : 代理对象调用接口方法时传递的实际参数
                    */
                    @Override
                    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//                        在invoke()方法中可以添加额外的逻辑
                        System.out.println("代售点收取一些服务费用");
//                        通过反射调用真实对象的方法
                        Object obj = method.invoke(trainStation, args);
                        return obj;
                    }
                }
        );
        return proxyObject;
    }
}

测试类:

public class TestDemo {
    public static void main(String[] args) {
//        1.创建火车站对象
        ProxyFactory proxyFactory = new ProxyFactory();
//        2.调用代理工厂的方法获取代理对象
        SellTickets proxyObject = proxyFactory.getProxyObject();
//        3.调用代理对象的卖票方法
        proxyObject.sell();
    }
}

image-20230713191834680

总结:动态代理的核心是使用了 Java 的Proxy类和 InvocationHandler 接口。Proxy 类提供了一个静态方法 newProxyInstance,通过调用该方法并传入被代理对象的类加载器、实现的接口和 InvocationHandler 对象,可以创建代理对象。

InvocationHandler 接口是一个函数式接口,其中定义了一个 invoke 方法,该方法在代理对象上调用被代理的方法时被触发。invoke方法中,我们可以编写自定义的逻辑来处理被代理方法的调用,例如在方法调用前后进行额外的操作。

总而言之,动态代理利用反射机制在运行时动态地创建代理对象,并通过InvocationHandler来处理代理对象的方法调用。这样可以实现对被代理对象的透明代理,而无需手动编写每个方法的代理代码。

5.1.4.3JDK实现动态代理分析

使用了动态代理,我们思考下面问题:

  1. 定义的ProxyFactory类是代理类吗?

刚刚定义的ProxyFactory不是代理模式中所说的代理类,而代理类是程序在运行过程中动态的在内存中生成的类。通过阿里巴巴开源的 Java 诊断工具(Arthas【阿尔萨斯】)查看代理类的结构:

package com.sun.proxy;

import com.itheima.proxy.dynamic.jdk.SellTickets;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.lang.reflect.UndeclaredThrowableException;

public final class $Proxy0 extends Proxy implements SellTickets {
    private static Method m1;
    private static Method m2;
    private static Method m3;
    private static Method m0;

    public $Proxy0(InvocationHandler invocationHandler) {
        super(invocationHandler);
    }

    static {
        try {
            m1 = Class.forName("java.lang.Object").getMethod("equals", Class.forName("java.lang.Object"));
            m2 = Class.forName("java.lang.Object").getMethod("toString", new Class[0]);
            m3 = Class.forName("com.itheima.proxy.dynamic.jdk.SellTickets").getMethod("sell", new Class[0]);
            m0 = Class.forName("java.lang.Object").getMethod("hashCode", new Class[0]);
            return;
        }
        catch (NoSuchMethodException noSuchMethodException) {
            throw new NoSuchMethodError(noSuchMethodException.getMessage());
        }
        catch (ClassNotFoundException classNotFoundException) {
            throw new NoClassDefFoundError(classNotFoundException.getMessage());
        }
    }

    public final boolean equals(Object object) {
        try {
            return (Boolean)this.h.invoke(this, m1, new Object[]{object});
        }
        catch (Error | RuntimeException throwable) {
            throw throwable;
        }
        catch (Throwable throwable) {
            throw new UndeclaredThrowableException(throwable);
        }
    }

    public final String toString() {
        try {
            return (String)this.h.invoke(this, m2, null);
        }
        catch (Error | RuntimeException throwable) {
            throw throwable;
        }
        catch (Throwable throwable) {
            throw new UndeclaredThrowableException(throwable);
        }
    }

    public final int hashCode() {
        try {
            return (Integer)this.h.invoke(this, m0, null);
        }
        catch (Error | RuntimeException throwable) {
            throw throwable;
        }
        catch (Throwable throwable) {
            throw new UndeclaredThrowableException(throwable);
        }
    }

    public final void sell() {
        try {
            this.h.invoke(this, m3, null);
            return;
        }
        catch (Error | RuntimeException throwable) {
            throw throwable;
        }
        catch (Throwable throwable) {
            throw new UndeclaredThrowableException(throwable);
        }
    }
}

从上面的类中,我们可以看到以下几个信息:

  • 代理类($Proxy0)实现了SellTickets。这也就印证了我们之前说的真实类和代理类实现同样的接口。
  • 代理类($Proxy0)将我们提供了的匿名内部类对象传递给了父类。
  1. 动态代理的执行流程是什么样?

下面是摘取的重点代码:

//程序运行过程中动态生成的代理类
public final class $Proxy0 extends Proxy implements SellTickets {
    private static Method m3;

    public $Proxy0(InvocationHandler invocationHandler) {
        super(invocationHandler);
    }

    static {
        m3 = Class.forName("com.xha.model.construct.proxy.jdk_proxy.SellTickets").getMethod("sell", new Class[0]);
    }

    public final void sell() {
        this.h.invoke(this, m3, null);
    }
}

//Java提供的动态代理相关类
public class Proxy implements java.io.Serializable {
	protected InvocationHandler h;
	 
	protected Proxy(InvocationHandler h) {
        this.h = h;
    }
}

//代理工厂类
public class ProxyFactory {

    private TrainStation station = new TrainStation();

    public SellTickets getProxyObject() {
        SellTickets sellTickets = (SellTickets) Proxy.newProxyInstance(station.getClass().getClassLoader(),
                station.getClass().getInterfaces(),
                new InvocationHandler() {
                    
                    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

                        System.out.println("代理点收取一些服务费用(JDK动态代理方式)");
                        Object result = method.invoke(station, args);
                        return result;
                    }
                });
        return sellTickets;
    }
}


//测试访问类
public class Client {
    public static void main(String[] args) {
        //获取代理对象
        ProxyFactory factory = new ProxyFactory();
        SellTickets proxyObject = factory.getProxyObject();
        proxyObject.sell();
    }
}

执行流程如下:

1. 在测试类中通过代理对象调用`sell()`方法
2. 根据多态的特性,执行的是代理类(`$Proxy0`)中的sell()方法
3. 代理类($Proxy0)中的sell()方法中又调用了`InvocationHandler`接口的子实现类对象的`invoke`方法
4. `invoke`方法通过反射执行了真实对象所属类`(TrainStation)中的sell()`方法

5.1.4.3 CGLIB动态代理

同样是上面的案例,我们再次使用CGLIB代理实现。

如果没有定义SellTickets接口,只定义了TrainStation(火车站类)。很显然JDK代理是无法使用了,因为JDK动态代理要求必须定义接口,对接口进行代理。

CGLIB(Code Generation Library)是一个功能强大的第三方库,用于在Java中创建动态代理。与标准的JDK动态代理(基于接口)不同,CGLIB动态代理可以基于类来创建代理对象。它通过生成目标类的子类,并重写其中的方法来实现代理功能。下面是CGLIB动态代理的执行流程:

  1. 创建目标类:首先,我们需要创建一个目标类(即要被代理的类),该类通常是一个普通的Java类,它包含我们希望代理的方法。
  2. 创建代理工厂:我们需要创建一个代理工厂(ProxyFactory)对象,用于生成代理类和代理对象。
  3. 设置代理目标:在代理工厂中,我们需要设置目标类(Target Class)和方法拦截器(MethodInterceptor)。目标类是我们希望代理的类,而方法拦截器则是在代理对象上调用方法时被调用的拦截器。
  4. 生成代理类:通过代理工厂的create方法,CGLIB根据目标类和方法拦截器生成一个代理类。这个代理类会继承目标类并重写目标类中的方法。
  5. 创建代理对象:在生成代理类后,我们可以使用其构造方法或静态工厂方法来创建代理对象。代理对象是代理类的实例,具有与目标类相同的方法和属性。
  6. 方法拦截:当我们在代理对象上调用方法时,代理对象会将方法调用转发给方法拦截器方法拦截器可以在方法调用前后执行自定义的逻辑,例如修改方法参数、记录日志、执行额外的操作等。

CGLIB是第三方提供的包,所以需要引入jar包的坐标:

<dependency>
    <groupId>cglib</groupId>
    <artifactId>cglib</artifactId>
    <version>2.2.2</version>
</dependency>

代码如下:

  1. TrainStation类(目标类
public class TrainStation {
    public void sell(){
        System.out.println("火车站卖票");
    }
}
  1. ProxyFactory类(代理工厂
/**
 * 代理工厂
 *
 * @author Xu huaiang
 * @date 2023/07/13
 */
public class ProxyFactory implements MethodInterceptor {
//    声明火车站对象
    private TrainStation trainStation = new TrainStation();

    public TrainStation getProxyObject(){
//        1.创建Enhancer对象,类似于JDK代理中的Proxy类,下一步就是设置几个参数
        Enhancer enhancer = new Enhancer();
//        2.由于是继承代理,所以需要设置父类,父类就是目标类
        enhancer.setSuperclass(TrainStation.class);
//        3.设置回调函数,MethodInterceptor接口等效于JDK代理中的InvocationHandler接口
        enhancer.setCallback(this);
//        4.创建代理对象
        return (TrainStation) enhancer.create();
    }

    @Override
    public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
        System.out.println("代售点收取一些服务费用");
        Object obj = method.invoke(trainStation, objects);
        return obj;
    }
}
  1. 测试类
public class TestDemo {
    public static void main(String[] args) {
//        1.创建代理工厂对象
        ProxyFactory proxyFactory = new ProxyFactory();
//        2.调用代理工厂的方法获取代理对象,代理对象就是TrainStation的子类对象
        TrainStation proxyObject = proxyFactory.getProxyObject();
//        3.调用代理对象的卖票方法
        proxyObject.sell();
    }
}

image-20230713203955131

5.1.6 三种代理的对比

  • jdk代理和CGLIB代理

    JDK动态代理:JDK动态代理要求目标对象必须实现至少一个接口。代理对象是目标对象的子类,它们共享相同的接口。JDK代理是基于Java的反射机制实现的。
    CGLIB动态代理:CGLIB动态代理不要求目标对象实现接口。它通过生成目标对象的子类,并覆盖其中的方法来创建代理对象。这种方式允许代理非接口类型的类。

    在JDK1.6、JDK1.7、JDK1.8逐步对JDK动态代理优化之后,在调用次数较少的情况下,JDK代理效率高于CGLib代理效率,只有当进行大量调用的时候,JDK1.6和JDK1.7比CGLib代理效率低一点,但是到JDK1.8的时候,JDK代理效率高于CGLib代理。所以如果有接口使用JDK动态代理,如果没有接口使用CGLIB代理。

  • 动态代理和静态代理

    动态代理与静态代理相比较,最大的好处是接口中声明的所有方法都被转移到调用处理器一个集中的方法(InvocationHandler.invoke)中处理。这样,在接口方法数量比较多的时候,我们可以进行灵活处理,而不需要像静态代理那样每一个方法进行中转。

    如果接口增加一个方法,静态代理模式除了所有实现类需要实现这个方法外,所有代理类也需要实现此方法。增加了代码维护的复杂度。而动态代理不会出现该问题

5.1.7 优缺点

优点:

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

缺点:

  • 增加了系统的复杂度;

5.1.8 使用场景

  • 远程(Remote)代理

    本地服务通过网络请求远程服务。为了实现本地到远程的通信,我们需要实现网络通信,处理其中可能的异常。为良好的代码设计和可维护性,我们将网络通信部分隐藏起来,只暴露给本地服务一个接口,通过该接口即可访问远程服务提供的功能,而不必过多关心通信部分的细节。

  • 防火墙(Firewall)代理

    当你将浏览器配置成使用代理功能时,防火墙就将你的浏览器的请求转给互联网;当互联网返回响应时,代理服务器再把它转给你的浏览器。

  • 保护(Protect or Access)代理

    控制对一个对象的访问,如果需要,可以给不同的用户提供不同级别的使用权限。

5.2 适配器模式

5.2.1 概述

如果去欧洲国家去旅游的话,他们的插座如下图最左边,是欧洲标准。而我们使用的插头如下图最右边的。因此我们的笔记本电脑,手机在当地不能直接充电。所以就需要一个插座转换器,转换器第1面插入当地的插座,第2面供我们充电,这样使得我们的插头在当地能使用。生活中这样的例子很多,手机充电器(将220v转换为5v的电压),读卡器等,其实就是使用到了适配器模式。

适配器模式允许将一个类的接口转换为客户端所期望的另一个接口。适配器模式使得原本由于接口不兼容而无法一起工作的类能够协同工作。

适配器模式可以根据实现方式的不同分为以下两种分类:

  1. 类适配器模式(Class Adapter Pattern): 在类适配器模式中,适配器类通过实现目标接口和继承适配者类来实现目标接口。适配器类具有适配者类的功能,并将客户端的请求转发给适配者对象。由于适配器类继承了适配者类,它可以重新定义适配者类的方法或添加新的方法来满足客户端的需求。类适配器模式使用单继承,因此类适配器模式只能适配一个适配者类。
  2. 对象适配器模式(Object Adapter Pattern): 在对象适配器模式中,适配器类通过持有一个适配者对象的引用来实现目标接口。适配器类实现了目标接口,并将客户端的请求转发给适配者对象。由于适配器类持有适配者对象的引用,它可以调用适配者对象的方法来实现目标接口。对象适配器模式使用对象组合,因此适配器类可以适配多个适配者对象。

5.2.2 结构

适配器模式(Adapter)包含以下主要角色:

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

5.2.3 类适配器模式

实现方式:定义一个适配器类来实现当前系统的业务接口(目标接口),同时又继承适配者类。

【例】读卡器

现有一台电脑只能读取SD卡,而要读取TF卡中的内容的话就需要使用到适配器模式。创建一个读卡器,将TF卡中的内容读取出来。

类图如下:

对于以上类图,目标接口是SDCard,适配者类就是TFCardImpl类,适配器类是SDAdapterTF类。

  1. SDCard类(目标接口
public interface SDCard {
    String readSD();

    void writeSD(String msg);
}
  1. SDCardImpl类
public class SDCardImpl implements SDCard{
    @Override
    public String readSD() {
        String msg = "SDCard read msg : hello word SDCard";
        return msg;
    }

    @Override
    public void writeSD(String msg) {
        System.out.println("SDCard write msg : " + msg);
    }
}
  1. TFCard(适配者接口
public interface TFCard {
    String readTF();

    void writeTF(String msg);
}
  1. TFCardImpl(适配者类
public class TFCardImpl implements TFCard{
    @Override
    public String readTF() {
        String msg = "TFCard read msg : hello word TFCard";
        return msg;
    }

    @Override
    public void writeTF(String msg) {
        System.out.println("TFCard write msg : " + msg);
    }
}
  1. SDAdapterTF(适配器类
public class SDAdapterTF extends TFCardImpl implements SDCard{
    @Override
    public String readSD() {
        System.out.println("adapter read tf card ");
        return readTF();
    }

    @Override
    public void writeSD(String msg) {
        System.out.println("adapter write tf card ");
        writeTF(msg);
    }
}
  1. Computer类
public class Computer {
    public String readSD(SDCard sdCard) {
        if (sdCard == null) {
            throw new NullPointerException("sd card is null");
        }
        return sdCard.readSD();
    }
}
  1. Client类
public class Client {
    public static void main(String[] args) {
//        1.创建计算机对象
        Computer computer = new Computer();
//        2.读取SD卡中的数据
        String msg = computer.readSD(new SDCardImpl());
        System.out.println(msg);
        System.out.println("============");
//        3.读取TF卡中的数据
        String msg1 = computer.readSD(new SDAdapterTF());
        System.out.println(msg1);
    }
}

测试结果:

image-20230713232739865

类适配器模式违背了合成复用原则。类适配器是客户类有一个接口规范的情况下可用,反之不可用。

5.2.4 对象适配器模式

实现方式:对象适配器模式可釆用将现有组件库中已经实现的适配者类引入适配器类中,同时适配器类实现当前系统的目标接口

【例】读卡器

我们使用对象适配器模式将读卡器的案例进行改写。类图如下:

与类适配器的区别就是将原来的适配器类SDAdapterTF继承于适配者类TFCardImpl类更改为在适配器类SDAdapterTF中聚合适配者类TFCardImpl类的接口TFCard

代码如下:

类适配器模式的代码,我们只需要修改适配器类(SDAdapterTF)和测试类。

  1. TFCard接口(适配者接口
public interface TFCard {
    String readTF();

    void writeTF(String msg);
}
  1. TFCardImpl(适配者类
public class TFCardImpl implements TFCard {
    @Override
    public String readTF() {
        String msg = "TFCard read msg : hello word TFCard";
        return msg;
    }

    @Override
    public void writeTF(String msg) {
        System.out.println("TFCard write msg : " + msg);
    }
}
  1. SDCard(目标接口
public interface SDCard {
    String readSD();

    void writeSD(String msg);
}
  1. SDCardImpl类
public class SDCardImpl implements SDCard{
    @Override
    public String readSD() {
        String msg = "SDCard read msg : hello word SDCard";
        return msg;
    }

    @Override
    public void writeSD(String msg) {
        System.out.println("SDCard write msg : " + msg);
    }
}
  1. SDAdapterTF(适配者类
public class SDAdapterTF implements SDCard {

//    声明适配者类
    private TFCard tfCard;

//    通过构造方法传入适配者类
    public SDAdapterTF(TFCard tfCard) {
        this.tfCard = tfCard;
    }

    @Override
    public String readSD() {
        System.out.println("adapter read tf card ");
        return tfCard.readTF();
    }

    @Override
    public void writeSD(String msg) {
        System.out.println("adapter write tf card ");
        tfCard.writeTF(msg);
    }
}
  1. Computer类
public class Computer {
    public String readSD(SDCard sdCard) {
        if (sdCard == null) {
            throw new NullPointerException("sd card is null");
        }
        return sdCard.readSD();
    }
}
  1. Client类
public class Client {
    public static void main(String[] args) {
//        1.创建计算机对象
        Computer computer = new Computer();
//        2.读取SD卡中的数据
        String msg = computer.readSD(new SDCardImpl());
        System.out.println(msg);
        System.out.println("============");
//        3.读取TF卡中的数据
        SDAdapterTF sdAdapterTF = new SDAdapterTF(new TFCardImpl());
        String TFmsg = computer.readSD(sdAdapterTF);
        System.out.println(TFmsg);
    }
}

测试结果:

image-20230713232739865

注意:还有一个适配器模式是接口适配器模式。当不希望实现一个接口中所有的方法时,可以创建一个抽象类Adapter ,实现所有方法。而此时我们只需要继承该抽象类即可。

5.2.5 应用场景

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

5.2.6 JDK源码解析

Reader(字符流)InputStream(字节流)的适配使用的是InputStreamReader

InputStreamReader继承自java.io包中的Reader,对他中的抽象的未实现的方法给出实现。如:

public int read() throws IOException {
    return sd.read();
}

public int read(char cbuf[], int offset, int length) throws IOException {
    return sd.read(cbuf, offset, length);
}

如上代码中的sd(StreamDecoder(流解码器)类对象),在Sun的JDK实现中,实际的方法实现是对sun.nio.cs.StreamDecoder类的同名方法的调用封装。类结构图如下:

Reader抽象类用于处理字符流,InputStream类用于处理字节流。现在就是将字节流转换为字符流。适配器类StreamDecoder继承了抽象类Reader(实现了目标接口),同时存在适配者类InputStream的引用。

InputStreamReader只是对适配器StreamDecoder的封装。

结论:

​ 从表层来看,InputStreamReader做了InputStream字节流类到Reader字符流之间的转换。而从如上Sun JDK中的实现类关系结构中可以看出,是StreamDecoder的设计实现在实际上采用了适配器模式。

5.3 装饰者模式

5.3.1 概述

我们先来看一个快餐店的例子。

快餐店有炒面、炒饭这些快餐,可以额外附加鸡蛋、火腿、培根这些配菜,当然加配菜需要额外加钱,每个配菜的价钱通常不太一样,那么计算总价就会显得比较麻烦。

装饰者模式-使用前

使用继承的方式存在的问题:

  • 扩展性不好

    如果要再加一种配料(火腿肠),我们就会发现需要给FriedRice和FriedNoodles分别定义一个子类。如果要新增一个快餐品类(炒河粉)的话,就需要定义更多的子类。

  • 产生过多的子类

定义:

装饰者模式(Decorator Pattern)允许动态地将行为添加到对象中。它是通过创建一个装饰者来将对象的行为进行扩展。

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

  • 装饰类和被装饰类可以独立发展,不会相互耦合,装饰模式是继承的一个替代模式,装饰模式可以动态扩展一个实现类的功能。

5.3.2 结构

装饰(Decorator)模式中的角色:

  • 抽象组件(Component)定义了具体组件和装饰者的共同接口,可以是抽象类或接口。
  • 具体组件(ConcreteComponent):实现了抽象组件接口,是被装饰的原始对象。
  • 抽象装饰者(Decorator):实现了抽象组件接口,并持有一个指向抽象组件的引用,可以是抽象类或接口。
  • 具体装饰者(ConcreteDecorator):扩展了抽象装饰者,具体实现了装饰者所定义的功能。

5.3.3 案例

我们使用装饰者模式对快餐店案例进行改进,体会装饰者模式的精髓。

类图如下:

一个抽象组件 FastFood,它定义了快餐的基本功能和属性。然后我们有两个具体组件 FriedRiceFriedNoodle,它们是 FastFood 的子类,表示具体的食品。

接下来是抽象装饰者 Garnish,它也是 FastFood 的子类,并且有一个内部成员变量引用 FastFoodGarnish 的构造函数接受一个 FastFood 对象,并在其中保存引用。

最后,我们有两个具体装饰者 EggBacon,它们也是 FastFood 的子类,同时也是 Garnish 的子类。它们分别通过在基本食物上添加鸡蛋和培根来扩展食物的功能。

使用装饰者模式,可以在运行时动态地添加装饰者对象到基本组件上,以添加额外的功能或行为,而无需影响到原始对象的结构或修改其代码。

代码如下:

  1. FastFood(抽象组件
public abstract class FastFood {
    private float price;
    private String desc;

    public float getPrice() {
        return price;
    }

    public void setPrice(float price) {
        this.price = price;
    }

    public String getDesc() {
        return desc;
    }

    public void setDesc(String desc) {
        this.desc = desc;
    }

    public FastFood() {
    }

    public FastFood(float price, String desc) {
        this.price = price;
        this.desc = desc;
    }

    public abstract float cost();
}
  1. FriedRice(具体组件
public class FriedRice extends FastFood{
    public FriedRice() {
        super(10,"炒饭");
    }

    @Override
    public float cost() {
        return getPrice();
    }
}
  1. FriedNoodle(具体组件
public class FriedNoodle extends FastFood{
    public FriedNoodle() {
        super(12,"炒面");
    }

    @Override
    public float cost() {
        return getPrice();
    }
}
  1. Garnish(抽象装饰者
public abstract class Garnish extends FastFood{
//    声明快餐类的引用
    private FastFood fastFood;

    public FastFood getFastFood() {
        return fastFood;
    }

    public void setFastFood(FastFood fastFood) {
        this.fastFood = fastFood;
    }

    public Garnish(float price, String desc, FastFood fastFood) {
        super(price, desc);
        this.fastFood = fastFood;
    }
}
  1. Egg(具体装饰者
public class Egg extends Garnish {

    public Egg(FastFood fastFood) {
        super(1, "鸡蛋", fastFood);
    }

    @Override
    public float cost() {
        return getPrice() + getFastFood().cost();
    }

    @Override
    public String getDesc() {
        return super.getDesc() + getFastFood().getDesc();
    }
}
  1. Bacon(具体装饰者
public class Bacon extends Garnish {

    public Bacon(FastFood fastFood) {
        super(1, "培根", fastFood);
    }

    @Override
    public float cost() {
        return getPrice() + getFastFood().cost();
    }

    @Override
    public String getDesc() {
        return super.getDesc() + getFastFood().getDesc();
    }
}
  1. 客户端
public class Client {
    public static void main(String[] args) {
        FastFood friedRice = new FriedRice();
        System.out.println(friedRice.getDesc() + "  " + friedRice.cost());
        System.out.println("***********************");
        friedRice = new Egg(friedRice);
        System.out.println(friedRice.getDesc() + "  " + friedRice.cost());
        System.out.println("***********************");
        friedRice = new Bacon(friedRice);
        System.out.println(friedRice.getDesc() + "  " + friedRice.cost());
    }
}

测试结果:

image-20230714102825849

5.3.4 使用场景

  • 当不能采用继承的方式对系统进行扩充或者采用继承不利于系统扩展和维护时。

    不能采用继承的情况主要有两类:

    • 第一类是系统中存在大量独立的扩展,为支持每一种组合将产生大量的子类,使得子类数目呈爆炸性增长;
    • 第二类是因为类定义不能继承(如final类)
  • 在不影响其他对象的情况下,以动态、透明的方式给单个对象添加职责。

  • 当对象的功能要求可以动态地添加,也可以再动态地撤销时。

5.3.5 JDK源码解析

IO流中的包装类使用到了装饰者模式。BufferedInputStream,BufferedOutputStream,BufferedReader,BufferedWriter。

我们以BufferedWriter举例来说明,先看看如何使用BufferedWriter

public class Demo {
    public static void main(String[] args) throws Exception{
        //创建BufferedWriter对象
        //创建FileWriter对象
        FileWriter fw = new FileWriter("C:\\Users\\Think\\Desktop\\a.txt");
        BufferedWriter bw = new BufferedWriter(fw);

        //写数据
        bw.write("hello Buffered");

        bw.close();
    }
}

使用起来感觉确实像是装饰者模式,接下来看它们的结构:

小结:BufferedWriter使用装饰者模式对Writer子实现类进行了增强,添加了缓冲区,提高了写数据的效率。

5.3.6 装饰者模式和代理模式的区别

  • 装饰器模式强调的是增强自身,是以对客户端透明的方式来扩展对象的功能,是继承方案的一个替代方案。

  • 代理模式强调使用代理对象来控制对真实对象的访问。

装饰模式是为装饰的对象增强功能;而代理模式对代理的对象访问控制,但不对对象本身的功能进行增强。

5.4 桥接模式

5.4.1 概述

现在有一个需求,需要创建不同的图形,并且每个图形都有可能会有不同的颜色。我们可以利用继承的方式来设计类的关系:

我们可以发现有很多的类,假如我们再增加一个形状或再增加一种颜色,就需要创建更多的类。

试想,在一个有多种可能会变化的维度的系统中,用继承方式会造成类爆炸,扩展起来不灵活。每次在一个维度上新增一个具体实现都要增加多个子类。为了更加灵活的设计系统,我们此时可以考虑使用桥接模式。

桥接模式用于将抽象部分与其实现部分分离,使它们可以独立地变化。

抽象部分提供了高层的接口,定义了抽象的行为和属性,它包含对实现部分的引用。实现部分则提供了具体的实现,它包含了实际的行为和属性。

桥接模式的核心思想是通过将一个系统的抽象部分与其实现部分分离开来,让它们可以独立地变化。这样,抽象部分和实现部分可以独立地扩展和修改,而彼此之间的耦合度较低。

5.4.2 结构

桥接(Bridge)模式包含以下主要角色:

  • 抽象化(Abstraction)定义抽象类,并包含一个对实现化对象的引用(组合)。
  • 扩展抽象化(Refined Abstraction)是抽象化角色的子类,实现父类中的业务方法,并通过组合关系调用实现化角色中的业务方法。
  • 实现化(Implementor)定义实现化角色的接口,供扩展抽象化角色调用。
  • 具体实现化(Concrete Implementor)给出实现化角色接口的具体实现。

5.4.3 案例

【例】视频播放器

需要开发一个跨平台视频播放器,可以在不同操作系统平台(如Windows、Mac、Linux等)上播放多种格式的视频文件,常见的视频格式包括RMVB、AVI、WMV等。该播放器包含了两个维度,适合使用桥接模式。

类图如下:

代码如下:

  1. VideoFile接口(实现化
public interface VideoFile {
        void decode(String fileName);
}
  1. AVIFile类(具体实现化
public class AVIFile implements VideoFile{
    @Override
    public void decode(String fileName) {
        System.out.println("avi视频文件:" + fileName);
    }
}
  1. RMVBFile类(具体实现化
public class RMVBFile implements VideoFile{
    @Override
    public void decode(String fileName) {
        System.out.println("RMVB视频文件:" + fileName);
    }
}
  1. OperatingSystem类(抽象化
public abstract class OperatingSystem {
//    声明实现化角色
    protected VideoFile videoFile;

    public OperatingSystem(VideoFile videoFile) {
        this.videoFile = videoFile;
    }

    public abstract void play(String fileName);
}
  1. Windows类(扩展抽象化
public class Windows extends OperatingSystem{
    public Windows(VideoFile videoFile) {
        super(videoFile);
    }

    @Override
    public void play(String fileName) {
        videoFile.decode(fileName);
    }
}
  1. Mac类(扩展抽象化
public class Mac extends OperatingSystem{
    public Mac(VideoFile videoFile) {
        super(videoFile);
    }

    @Override
    public void play(String fileName) {
        videoFile.decode(fileName);
    }
}
  1. 测试类
public class Client {
    public static void main(String[] args) {
//        1.创建一个具体的实现化角色
        OperatingSystem windows = new Windows(new AVIFile());
        windows.play("银河护卫队3");
    }
}

image-20230714112214830

5.4.4 使用场景

  • 当一个类存在两个独立变化的维度,且这两个维度都需要进行扩展时。
  • 当一个系统不希望使用继承或因为多层次继承导致系统类的个数急剧增加时(类爆炸
  • 当一个系统需要在构件的抽象化角色和具体化角色之间增加更多的灵活性时。避免在两个层次之间建立静态的继承联系,通过桥接模式可以使它们在抽象层建立一个关联关系。

5.5 外观模式

5.5.1 概述

有些人可能炒过股票,但其实大部分人都不太懂,这种没有足够了解证券知识的情况下做股票是很容易亏钱的,刚开始炒股肯定都会想,如果有个懂行的帮帮手就好,其实基金就是个好帮手,支付宝里就有许多的基金,它将投资者分散的资金集中起来,交由专业的经理人进行管理,投资于股票、债券、外汇等领域,而基金投资的收益归持有者所有,管理机构收取一定比例的托管管理费用。

定义:

外观模式提供了一个统一的接口,用于访问子系统中的一组接口。外观模式可以隐藏系统的复杂性,为客户提供一个简单而统一的接口,使得客户端与子系统之间的耦合度降低。

外观模式的主要目的是简化客户端的使用,隐藏系统的复杂性,并提供一个简单的接口来与系统交互。它可以提供系统的高层接口,封装底层的具体实现细节,使得系统更加容易理解和使用。

​ 外观(Facade)模式是“迪米特法则”的典型应用。

好处:

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

缺点:

  • 不符合开闭原则,修改很麻烦

5.5.2 结构

外观(Facade)模式包含以下主要角色:

  • 外观(Facade)角色:为多个子系统对外提供一个共同的接口。
  • 子系统(Sub System)角色:实现系统的部分功能,客户可以通过外观角色访问它。

5.5.3 案例

【例】智能家电控制

小明的爷爷已经60岁了,一个人在家生活:每次都需要打开灯、打开电视、打开空调;睡觉时关闭灯、关闭电视、关闭空调;操作起来都比较麻烦。所以小明给爷爷买了智能音箱,可以通过语音直接控制这些智能家电的开启和关闭。类图如下:

外观类SmartApplicationFacade扮演着外观的角色。它封装了与子系统的交互细节。

代码如下:

  1. Light类
public class Light {
    public void on(){
        System.out.println("打开了灯");
    }
    public void off(){
        System.out.println("关闭了灯");
    }
}
  1. TV类
public class TV {
    public void on(){
        System.out.println("打开了电视");
    }
    public void off(){
        System.out.println("关闭了电视");
    }
}
  1. AirCondition类
public class AirCondition {
    public void on(){
        System.out.println("打开了空调");
    }
    public void off(){
        System.out.println("关闭了空调");
    }
}
  1. SmartApplicationFacade类
public class SmartApplicationFacade {

    private Light light;

    private TV tv;

    private AirCondition airCondition;

    public SmartApplicationFacade() {
        this.light = new Light();
        this.tv = new TV();
        this.airCondition = new AirCondition();
    }

    public void say(String message) {
        if (message.equals("打开")) {
            open();
        } else if (message.equals("关闭")) {
            close();
        } else {
            System.out.println("我听不懂你在说什么");
        }
    }

    private void open() {
        light.on();
        tv.on();
        airCondition.on();
    }

    private void close() {
        light.off();
        tv.off();
        airCondition.off();
    }
}
  1. 测试类
public class Client {
    public static void main(String[] args) {
        SmartApplicationFacade smart = new SmartApplicationFacade();
        smart.say("打开");
        System.out.println("--------------");
        smart.say("关闭");
    }
}

image-20230714130155708

5.5.4 使用场景

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

5.5.5 源码解析

使用tomcat作为web容器时,接收浏览器发送过来的请求,tomcat会将请求信息封装成ServletRequest对象,如下图①处对象。但是大家想想ServletRequest是一个接口,它还有一个子接口HttpServletRequest,而我们知道该request对象肯定是一个HttpServletRequest对象的子实现类对象,到底是哪个类的对象呢?可以通过输出request对象,我们就会发现是一个名为RequestFacade的类的对象。

RequestFacade类就使用了外观模式。先看结构图:

为什么在此处使用外观模式呢?

​ 定义 RequestFacade 类,分别实现 ServletRequest ,HttpServletRequest ,同时定义私有成员变量 Request ,并且方法的实现调用 Request 的实现。然后,将 RequestFacade上转为 ServletRequest 传给 servlet 的 service 方法,这样即使在 servlet 中被下转为 RequestFacade ,也不能访问私有成员变量对象中的方法。既用了 Request ,又能防止其中方法被不合理的访问。

5.6 组合模式

5.6.1 概述

​ 对于这个图片肯定会非常熟悉,上图我们可以看做是一个文件系统,对于这样的结构我们称之为树形结构。在树形结构中可以通过调用某个方法来遍历整个树,当我们找到某个叶子节点后,就可以对叶子节点进行相关的操作。可以将这颗树理解成一个大的容器,容器里面包含很多的成员对象,这些成员对象即可是容器对象也可以是叶子对象。但是由于容器对象和叶子对象在功能上面的区别,使得我们在使用的过程中必须要区分容器对象和叶子对象,但是这样就会给客户带来不必要的麻烦,作为客户而已,它始终希望能够一致的对待容器对象和叶子对象。

组合模式允许将对象组合成树形结构来表示整体-部分的层次结构。通过使用组合模式,客户端可以将单个对象组合对象一视同仁,无需区分它们的差异,从而简化了客户端与对象之间的交互。

5.6.2 结构

组合模式主要包含三种角色:

  • 抽象根节点(Component)定义系统各层次对象的共有方法和属性,可以预先定义一些默认行为和属性(公共抽象类)。
  • 树枝节点(Composite):定义树枝节点的行为,存储子节点,组合树枝节点和叶子节点形成一个树形结构。
  • 叶子节点(Leaf):叶子节点对象,其下再无分支,是系统层次遍历的最小单位。

5.6.3 案例

【例】软件菜单

如下图,我们在访问别的一些管理系 统时,经常可以看到类似的菜单。一个菜单可以包含菜单项(菜单项是指不再包含其他内容的菜单条目),也可以包含带有其他菜单项的菜单,因此使用组合模式描述菜单就很恰当,我们的需求是针对一个菜单,打印出其包含的所有菜单以及菜单项的名称。

要实现该案例,我们先画出类图:

代码实现:

不管是菜单还是菜单项,都应该继承自统一的接口。

  1. MenuComponent(抽象根节点
public abstract class MenuComponent {
    protected String name;
    protected int level;

    /**
     * 添加菜单组件,可以是菜单项,也可以是菜单
     *
     * @param menuComponent 菜单组件
     */
    public void add(MenuComponent menuComponent) {
//        抛出异常,子类实现,这里不实现,因为菜单项没有添加菜单的功能
        throw new UnsupportedOperationException();
    }

    /**
     * 移除菜单组件,可以是菜单项,也可以是菜单
     *
     * @param menuComponent 菜单组件
     */
    public void remove(MenuComponent menuComponent) {
//        抛出异常,子类实现,这里不实现,因为菜单项没有移除菜单的功能
        throw new UnsupportedOperationException();
    }

    /**
     * 获取到指定位置的菜单组件,可以是菜单项,也可以是菜单
     *
     * @param i 我
     * @return {@link MenuComponent}
     */
    public MenuComponent getChild(int i) {
//        抛出异常,子类实现,这里不实现,因为菜单项没有获取菜单的功能
        throw new UnsupportedOperationException();
    }

    public String getName() {
        return name;
    }

    public abstract void print();
}

这里的MenuComponent定义为抽象类,因为有一些共有的属性和行为要在该类中实现,Menu和MenuItem类就可以只覆盖自己感兴趣的方法,而不用搭理不需要或者不感兴趣的方法,举例来说,Menu类可以包含子菜单,因此需要覆盖add()、remove()、getChild()方法,但是MenuItem就不应该有这些方法。这里给出的默认实现是抛出异常,你也可以根据自己的需要改写默认实现。

  1. Menu(树枝节点
public class Menu extends MenuComponent{
//    菜单可以有子菜单或者子菜单项,所以菜单中要有一个菜单项的集合
    private List<MenuComponent> menuComponentList = new ArrayList<>();

    public Menu(String name,int level) {
        this.name = name;
        this.level = level;
    }

    @Override
    public void add(MenuComponent menuComponent) {
        menuComponentList.add(menuComponent);
    }

    @Override
    public void remove(MenuComponent menuComponent) {
        menuComponentList.remove(menuComponent);
    }

    @Override
    public MenuComponent getChild(int i) {
        return menuComponentList.get(i);
    }

    @Override
    public void print() {
        for (int i = 0; i < level; i++) {
            System.out.print("--");
        }
        System.out.println("菜单名字:"+name+",菜单级别:"+level);
        for (MenuComponent menuComponent : menuComponentList) {
            menuComponent.print();
        }
    }
}

Menu类已经实现了除了getName方法的其他所有方法,因为Menu类具有添加菜单,移除菜单和获取子菜单的功能。

  1. MenuItem(叶子节点
public class MenuItem extends MenuComponent{

    public MenuItem(String name, int level) {
        this.name = name;
        this.level = level;
    }

    @Override
    public void print() {
        for (int i = 0; i < level; i++) {
            System.out.print("--");
        }
        System.out.println(name);
    }
}

MenuItem是菜单项,不能再有子菜单,所以添加菜单,移除菜单和获取子菜单的功能并不能实现。

  1. 测试类,组成树形结构
public class Client {
    public static void main(String[] args) {
//        1.创建菜单树
        MenuComponent menu1 = new Menu("菜单管理", 2);
        menu1.add(new MenuItem("页面访问", 3));
        menu1.add(new MenuItem("展开菜单", 3));
        menu1.add(new MenuItem("编辑菜单", 3));
        menu1.add(new MenuItem("删除菜单", 3));
        menu1.add(new MenuItem("新增菜单", 3));
        MenuComponent menu2 = new Menu("权限管理", 2);
        menu2.add(new MenuItem("页面访问", 3));
        menu2.add(new MenuItem("提交保存", 3));
        MenuComponent menu3 = new Menu("用户管理", 2);
        menu3.add(new MenuItem("页面访问", 3));
        menu3.add(new MenuItem("新增用户", 3));
        menu3.add(new MenuItem("修改用户", 3));
//        2.创建一级菜单
        MenuComponent component = new Menu("系统管理", 1);
//        3.将二级菜单添加到一级菜单中
        component.add(menu1);
        component.add(menu2);
        component.add(menu3);
//        4.打印菜单树
        component.print();
    }
}

image-20230714160439904

5.6.4 组合模式的分类

在使用组合模式时,根据抽象构件类的定义形式,我们可将组合模式分为透明组合模式和安全组合模式两种形式。

  • 透明组合模式

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

    透明组合模式的缺点是不够安全,因为叶子对象和容器对象在本质上是有区别的,叶子对象不可能有下一个层次的对象,即不可能包含成员对象,因此为其提供 add()、remove() 等方法是没有意义的,这在编译阶段不会出错,但在运行阶段如果调用这些方法可能会出错(如果没有提供相应的错误处理代码)

  • 安全组合模式

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

5.6.5 优点

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

5.6.6 使用场景

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

5.7 享元模式

5.7.1 概述

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

5.7.2 结构

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

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

享元模式的主要有以下角色:

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

5.7.3 案例实现

【例】俄罗斯方块

下面的图片是众所周知的俄罗斯方块中的一个个方块,如果在俄罗斯方块这个游戏中,每个不同的方块都是一个实例对象,这些对象就要占用很多的内存空间,下面利用享元模式进行实现。

先来看类图:

代码如下:

俄罗斯方块有不同的形状,我们可以对这些形状向上抽取出AbstractBox,用来定义共性的属性和行为。

  1. AbstractBox(抽象享元角色
public abstract class AbstractBox {
    public abstract String getShape();

    //color是外部状态
    public void display(String color){
        System.out.println("形状:"+this.getShape()+",颜色:"+color);
    }
}
  1. IBox(具体享元角色
public class IBox extends AbstractBox{
    @Override
    public String getShape() {
        return "I形状的方块";
    }
}
  1. LBox(具体享元角色
public class LBox extends AbstractBox{
    @Override
    public String getShape() {
        return "L形状的方块";
    }
}
  1. OBox(具体享元角色
public class OBox extends AbstractBox{
    @Override
    public String getShape() {
        return "O形状的方块";
    }
}
  1. BoxFactory(享元工厂角色
public class BoxFactory {
//    1.单例模式
    private static BoxFactory boxFactory;

//    2.创建一个map集合用于存储享元对象
    private static HashMap<String,AbstractBox> map;

//    3.私有化构造方法,在构造方法中初始化map集合
    private BoxFactory(){
        map = new HashMap<>();
        map.put("L",new LBox());
        map.put("I",new IBox());
        map.put("O",new OBox());
    }

//    4.提供一个获取享元对象的方法,根据key获取
    public AbstractBox getShape(String key){
        return map.get(key);
    }

//    5.提供一个获取单例对象的方法,如果对象为空则创建一个新的对象,否则返回已有的对象
    public static BoxFactory getInstance(){
        if (boxFactory == null){
            boxFactory = new BoxFactory();
        }
        return boxFactory;
    }
}
  1. 测试类
public class Client {
    public static void main(String[] args) {
        BoxFactory boxFactory = BoxFactory.getInstance();
        AbstractBox LBox = boxFactory.getShape("L");
        LBox.display("红色");
        AbstractBox OBox = boxFactory.getShape("O");
        OBox.display("蓝色");
        AbstractBox IBox1 = boxFactory.getShape("I");
        IBox1.display("绿色");
        AbstractBox IBox2 = boxFactory.getShape("I");
        IBox2.display("黄色");
        System.out.println("两次获取的I形状方块是否是同一个对象:"+(IBox1==IBox2));
    }
}

image-20230714171547905

5.7.5 优缺点

1.优点

  • 极大减少内存中相似或相同对象数量,节约系统资源,提供系统性能

  • 享元模式中的外部状态相对独立,且不影响内部状态

    如上述例子中color就是外部状态,shape就是内部状态。

2.缺点

为了使对象可以共享,需要将享元对象的部分状态外部化,分离内部状态和外部状态,使程序逻辑复杂

5.7.6使用场景

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

5.7.7 JDK源码解析

Integer类在Java中使用了享元模式。

在Java中,对于整数值范围在-128至127之间的Integer对象,会被缓存在一个内部的整数缓存池中,以减少对象创建的开销。这意味着当我们使用这个范围内的整数时实际上是在使用缓存池中的对象,而不是每次都创建新的对象。

这种共享的实现方式符合享元模式的思想,其中整数值被视为对象的内部状态,而其它的属性(例如对象的引)则是外部状态。通过共享内部状态,减少内存消耗和提高性能。需要注意的是,对于出范围的整数值,仍然创建新的Integer对象,而不会从缓存池中获取。超出范围的整数值不适用于缓存池。

我们先看下面的例子:

public class Demo {
    public static void main(String[] args) {
        Integer i1 = 127;
        Integer i2 = 127;

        System.out.println("i1和i2对象是否是同一个对象?" + (i1 == i2));

        Integer i3 = 128;
        Integer i4 = 128;

        System.out.println("i3和i4对象是否是同一个对象?" + (i3 == i4));
    }
}

运行上面代码,结果如下:

image-20230714172240408

为什么第一个输出语句输出的是true,第二个输出语句输出的是false?通过反编译软件进行反编译,代码如下:

public class Demo {
    public static void main(String[] args) {
        Integer i1 = Integer.valueOf((int)127);
        Integer i2 Integer.valueOf((int)127);
        System.out.println((String)new StringBuilder().append((String)"i1\u548ci2\u5bf9\u8c61\u662f\u5426\u662f\u540c\u4e00\u4e2a\u5bf9\u8c61\uff1f").append((boolean)(i1 == i2)).toString());
        Integer i3 = Integer.valueOf((int)128);
        Integer i4 = Integer.valueOf((int)128);
        System.out.println((String)new StringBuilder().append((String)"i3\u548ci4\u5bf9\u8c61\u662f\u5426\u662f\u540c\u4e00\u4e2a\u5bf9\u8c61\uff1f").append((boolean)(i3 == i4)).toString());
    }
}

上面代码可以看到,直接给Integer类型的变量赋值基本数据类型数据的操作底层使用的是 valueOf() ,所以只需要看该方法即可

public final class Integer extends Number implements Comparable<Integer> {
    
	public static Integer valueOf(int i) {
        if (i >= IntegerCache.low && i <= IntegerCache.high)
            return IntegerCache.cache[i + (-IntegerCache.low)];
        return new Integer(i);
    }
    
    private static class IntegerCache {
        static final int low = -128;
        static final int high;
        static final Integer cache[];

        static {
            int h = 127;
            String integerCacheHighPropValue =
                sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high");
            if (integerCacheHighPropValue != null) {
                try {
                    int i = parseInt(integerCacheHighPropValue);
                    i = Math.max(i, 127);
                    // Maximum array size is Integer.MAX_VALUE
                    h = Math.min(i, Integer.MAX_VALUE - (-low) -1);
                } catch( NumberFormatException nfe) {
                }
            }
            high = h;
            cache = new Integer[(high - low) + 1];
            int j = low;
            for(int k = 0; k < cache.length; k++)
                cache[k] = new Integer(j++);
            // range [-128, 127] must be interned (JLS7 5.1.7)
            assert IntegerCache.high >= 127;
        }

        private IntegerCache() {}
    }
}

可以看到 Integer 默认先创建并缓存 -128 ~ 127 之间数的 Integer 对象,当调用 valueOf 时如果参数在 -128 ~ 127 之间则计算下标并从缓存中返回,否则创建一个新的 Integer 对象。

6.行为型模式

行为型模式用于描述程序在运行时复杂的流程控制,即描述多个类或对象之间怎样相互协作共同完成单个对象都无法单独完成的任务,它涉及算法与对象间职责的分配。

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

行为型模式分为:

  • 模板方法模式
  • 策略模式
  • 命令模式
  • 职责链模式
  • 状态模式
  • 观察者模式
  • 中介者模式
  • 迭代器模式
  • 访问者模式
  • 备忘录模式
  • 解释器模式

以上 11 种行为型模式,除了模板方法模式和解释器模式是类行为型模式,其他的全部属于对象行为型模式。

6.1 模板方法模式

6.1.1 概述

在面向对象程序设计过程中,程序员常常会遇到这种情况:设计一个系统时知道了算法所需的关键步骤,而且确定了这些步骤的执行顺序,但某些步骤的具体实现还未知,或者说某些步骤的实现与具体的环境相关。

例如,去银行办理业务一般要经过以下4个流程:取号、排队、办理具体业务、对银行工作人员进行评分等,其中取号、排队和对银行工作人员进行评分的业务对每个客户是一样的,可以在父类中实现,但是办理具体业务却因人而异,它可能是存款、取款或者转账等,可以延迟到子类中实现。

模板方法模式定义一个操作骨架的算法,并将细节留给子类来实现。算法的整体结构和顺序由父类保留。

6.1.2 结构

模板方法(Template Method)模式包含以下主要角色:

  • 抽象类(Abstract Class):负责给出一个算法的轮廓和骨架。它由一个模板方法和若干个基本方法构成。

    • 模板方法:定义了算法的骨架,按某种顺序调用其包含的基本方法。

    • 基本方法:是实现算法各个步骤的方法,是模板方法的组成部分。基本方法又可以分为三种:

      • 抽象方法(Abstract Method) :一个抽象方法由抽象类声明、由其具体子类实现。

      • 具体方法(Concrete Method) :一个具体方法由一个抽象类或具体类声明并实现,其子类可以进行覆盖也可以直接继承。

      • 钩子方法(Hook Method) :在抽象类中已经实现,包括用于判断的逻辑方法和需要子类重写的空方法两种。

        一般钩子方法是用于判断的逻辑方法,这类方法名一般为isXxx,返回值类型为boolean类型。

  • 具体子类(Concrete Class)实现抽象类中所定义的抽象方法和钩子方法,它们是一个顶级逻辑的组成步骤。

6.1.3 案例实现

【例】炒菜

炒菜的步骤是固定的,分为倒油、热油、倒蔬菜、倒调料品、翻炒等步骤。现通过模板方法模式来用代码模拟。类图如下:

代码如下:

  1. AbstractClass(抽象类

负责给出一个算法的轮廓和骨架。它由一个模板方法和若干个基本方法构成。

模板方法:定义了算法的骨架,按某种顺序调用,这里是cookProcess()

基本方法:是实现算法各个步骤的方法,是模板方法的组成部分

  • 抽象方法:pourVegetable()pourSauce()
  • 具体方法:pourOil()heatOil()fry()
public abstract class AbstractClass {

    /**
     * 模板方法
     */
    public final void cookProcess() {
        this.pourOil();
        this.heatOil();
        this.pourVegetable();
        this.pourSauce();
        this.fry();
    }

    /**
     * 倒油
     */
    public void pourOil() {
        System.out.println("倒油");
    }

    /**
     * 热油
     */
    public void heatOil() {
        System.out.println("热油");
    }

    /**
     * 倒蔬菜
     */
    public abstract void pourVegetable();

    /**
     * 倒调料
     */
    public abstract void pourSauce();

    /**
     * 翻炒
     */
    public void fry() {
        System.out.println("炒啊炒啊炒到熟啊");
    }
}
  1. ConcreteClass_Baochi(具体子类
public class ConcreteClass_Baochi extends AbstractClass {
    @Override
    public void pourVegetable() {
        System.out.println("倒入包菜");
    }

    @Override
    public void pourSauce() {
        System.out.println("倒入辣椒");
    }
}
  1. ConcreteClass_Baochi(具体子类
public class ConcreteClass_Baochi extends AbstractClass {
    @Override
    public void pourVegetable() {
        System.out.println("倒入包菜");
    }

    @Override
    public void pourSauce() {
        System.out.println("倒入辣椒");
    }
}
  1. ConcreteClass_Doufu(具体子类
public class ConcreteClass_Doufu extends AbstractClass {
    @Override
    public void pourVegetable() {
        System.out.println("倒豆腐");
    }

    @Override
    public void pourSauce() {
        System.out.println("倒酱油");
    }
}
  1. 测试类
public class Client {
    public static void main(String[] args) {
        ConcreteClass_Baochi baochi = new ConcreteClass_Baochi();
        baochi.cookProcess();
        System.out.println("====================================");
        ConcreteClass_Doufu doufu = new ConcreteClass_Doufu();
        doufu.cookProcess();
    }
}

image-20230714185743337

注意:为防止恶意操作,一般模板方法都加上 final 关键词。

6.1.3 优缺点

优点:

  • 提高代码复用性

    将相同部分的代码放在抽象的父类中,而将不同的代码放入不同的子类中。

  • 实现了反向控制

    通过一个父类调用其子类的操作,通过对子类的具体实现扩展不同的行为,实现了反向控制 ,并符合“开闭原则”。

缺点:

  • 对每个不同的实现都需要定义一个子类,这会导致类的个数增加,系统更加庞大,设计也更加抽象。
  • 父类中的抽象方法由子类实现,子类执行的结果会影响父类的结果,这导致一种反向的控制结构,它提高了代码阅读的难度。

6.1.4模板方法模式和建造者模式的区别

  1. 建造者模式适用于构建过程复杂,且最终产品的的各个部分需要领过组合的场景。

    而模板方法模式适用于流程固定,但是其中的一些步骤需要根据具体情况进行替换或者扩展的场景。

  2. 建造者模式的目的是将一个复杂对象的构建过程与其表示分离。

    而模板方法的目的是定义一个操作中的算法框架,而将一些步骤延迟到子类当中。

6.1.5适用场景

  • 算法的整体步骤很固定,但其中个别部分易变时,这时候可以使用模板方法模式,将容易变的部分抽象出来,供子类实现。
  • 需要通过子类来决定父类算法中某个步骤是否执行,实现子类对父类的反向控制。

6.1.6 JDK源码解析

InputStream类就使用了模板方法模式。在InputStream类中定义了多个 read() 方法,如下:

public abstract class InputStream implements Closeable {
    //抽象方法,要求子类必须重写
    public abstract int read() throws IOException;

    public int read(byte b[]) throws IOException {
        return read(b, 0, b.length);
    }

    public int read(byte b[], int off, int len) throws IOException {
        if (b == null) {
            throw new NullPointerException();
        } else if (off < 0 || len < 0 || len > b.length - off) {
            throw new IndexOutOfBoundsException();
        } else if (len == 0) {
            return 0;
        }

        int c = read(); //调用了无参的read方法,该方法是每次读取一个字节数据
        if (c == -1) {
            return -1;
        }
        b[off] = (byte)c;

        int i = 1;
        try {
            for (; i < len ; i++) {
                c = read();
                if (c == -1) {
                    break;
                }
                b[off + i] = (byte)c;
            }
        } catch (IOException ee) {
        }
        return i;
    }
}

从上面代码可以看到,无参的 read() 方法是抽象方法,要求子类必须实现。而 read(byte b[]) 方法调用了 read(byte b[], int off, int len) 方法,所以在此处重点看的方法是带三个参数的方法。

在该方法中第18行、27行,可以看到调用了无参的抽象的 read() 方法。

总结如下: 在InputStream父类中已经定义好了读取一个字节数组数据的方法是每次读取一个字节,并将其存储到数组的第一个索引位置,读取len个字节数据。具体如何读取一个字节数据呢?由子类实现。

6.2 策略模式

6.2.1 概述

先看下面的图片,我们去旅游选择出行模式有很多种,可以骑自行车、可以坐汽车、可以坐火车、可以坐飞机。

作为一个程序猿,开发需要选择一款开发工具,当然可以进行代码开发的工具有很多,可以选择Idea进行开发,也可以使用eclipse进行开发,也可以使用其他的一些开发工具。

策略模式允许在运行时动态地选择算法的不实现方式,而不需要更改客户端的代码结构。

6.2.2 结构

策略模式的主要角色如下:

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

6.2.3 案例实现

【例】促销活动

一家百货公司在定年度的促销活动。针对不同的节日(春节、中秋节、圣诞节)推出不同的促销活动,由促销员将促销活动展示给客户。类图如下:

代码如下:

  1. Strategy(抽象策略类
public interface Strategy {
    void show();
}
  1. StrategyA(具体策略类
public class StrategyA implements Strategy{
    @Override
    public void show() {
        System.out.println("买一送一");
    }
}
  1. StrategyB(具体策略类
public class StrategyB implements Strategy{
    @Override
    public void show() {
        System.out.println("满100减20");
    }
}
  1. StrategyC(具体策略类
public class StrategyC implements Strategy{
    @Override
    public void show() {
        System.out.println("满200减50");
    }
}
  1. SalesMan(环境类
public class SalesMan {
    private Strategy strategy;
    
    public SalesMan(Strategy strategy) {
        this.strategy = strategy;
    }
    
    public void salesManShow() {
        strategy.show();
    }
}
  1. 测试类
public class Client {
    public static void main(String[] args) {
        SalesMan salesMan = new SalesMan(new StrategyA());
        salesMan.salesManShow();
        System.out.println("====================================");
        salesMan = new SalesMan(new StrategyB());
        salesMan.salesManShow();
        System.out.println("====================================");
        salesMan = new SalesMan(new StrategyC());
        salesMan.salesManShow();
    }
}

image-20230714193629069

6.2.4 优缺点

优点:

  • 策略类之间可以自由切换

    由于策略类都实现同一个接口,所以使它们之间可以自由切换。

  • 易于扩展

    增加一个新的策略只需要添加一个具体的策略类即可,基本不需要改变原有的代码,符合“开闭原则“

  • 避免使用多重条件选择语句(if else),充分体现面向对象设计思想。

2,缺点:

  • 客户端必须知道所有的策略类,并自行决定使用哪一个策略类。
  • 策略模式将造成产生很多策略类,可以通过使用享元模式在一定程度上减少对象的数量。

6.2.5 使用场景

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

6.2.6 JDK源码解析

Comparator 中的策略模式。在Arrays类中有一个 sort() 方法,如下:

public class Arrays{
    public static <T> void sort(T[] a, Comparator<? super T> c) {
        if (c == null) {
            sort(a);
        } else {
            if (LegacyMergeSort.userRequested)
                legacyMergeSort(a, c);
            else
                TimSort.sort(a, 0, a.length, c, null, 0, 0);
        }
    }
}

Arrays就是一个环境角色类,这个sort方法可以传一个新策略让Arrays根据这个策略来进行排序。就比如下面的测试类。

public class demo {
    public static void main(String[] args) {

        Integer[] data = {12, 2, 3, 2, 4, 5, 1};
        // 实现降序排序
        Arrays.sort(data, new Comparator<Integer>() {
            public int compare(Integer o1, Integer o2) {
                return o2 - o1;
            }
        });
        System.out.println(Arrays.toString(data)); //[12, 5, 4, 3, 2, 2, 1]
    }
}

这里我们在调用Arrays的sort方法时,第二个参数传递的是Comparator接口的子实现类对象。所以Comparator充当的是抽象策略角色,而具体的子实现类充当的是具体策略角色。环境角色类(Arrays)应该持有抽象策略的引用来调用。那么,Arrays类的sort方法到底有没有使用Comparator子实现类中的 compare() 方法吗?让我们继续查看TimSort类的 sort() 方法,代码如下:

class TimSort<T> {
    static <T> void sort(T[] a, int lo, int hi, Comparator<? super T> c,
                         T[] work, int workBase, int workLen) {
        assert c != null && a != null && lo >= 0 && lo <= hi && hi <= a.length;

        int nRemaining  = hi - lo;
        if (nRemaining < 2)
            return;  // Arrays of size 0 and 1 are always sorted

        // If array is small, do a "mini-TimSort" with no merges
        if (nRemaining < MIN_MERGE) {
            int initRunLen = countRunAndMakeAscending(a, lo, hi, c);
            binarySort(a, lo, hi, lo + initRunLen, c);
            return;
        }
        ...
    }   
        
    private static <T> int countRunAndMakeAscending(T[] a, int lo, int hi,Comparator<? super T> c) {
        assert lo < hi;
        int runHi = lo + 1;
        if (runHi == hi)
            return 1;

        // Find end of run, and reverse range if descending
        if (c.compare(a[runHi++], a[lo]) < 0) { // Descending
            while (runHi < hi && c.compare(a[runHi], a[runHi - 1]) < 0)
                runHi++;
            reverseRange(a, lo, runHi);
        } else {                              // Ascending
            while (runHi < hi && c.compare(a[runHi], a[runHi - 1]) >= 0)
                runHi++;
        }

        return runHi - lo;
    }
}

上面的代码中最终会跑到 countRunAndMakeAscending() 这个方法中。我们可以看见,只用了compare方法,所以在调用Arrays.sort方法只传具体compare重写方法的类对象就行,这也是Comparator接口中必须要子类实现的一个方法。

6.3 命令模式

6.3.1 概述

日常生活中,我们出去吃饭都会遇到下面的场景。

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

6.3.2 结构

命令模式包含以下主要角色:

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

6.3.3 案例实现

将上面的案例用代码实现,那我们就需要分析命令模式的角色在该案例中由谁来充当。

服务员: 就是调用者角色,由她来发起命令。

资深大厨: 就是接收者角色,真正命令执行的对象。

订单: 命令中包含订单。

类图如下:

代码如下:

  1. Order类
public class Order {
    // 桌号
    private int diningTable;
    // 餐品及份数
    private Map<String, Integer> foodDic = new HashMap<>();

    public int getDiningTable() {
        return diningTable;
    }

    public void setDiningTable(int diningTable) {
        this.diningTable = diningTable;
    }

    public Map<String, Integer> getFoodDic() {
        return foodDic;
    }

    public void setFood(String name, int num) {
        foodDic.put(name, num);
    }
}
  1. Command(抽象命令类
public interface Command {
    void execute();
}
  1. OrderCommand(具体命令类
public class OrderCommand implements Command{
//    持有接收者对象
    private SeniorChef receiver;
//    持有订单对象
    private Order order;

    public OrderCommand(SeniorChef receiver, Order order) {
        this.receiver = receiver;
        this.order = order;
    }

    @Override
    public void execute() {
        System.out.println(order.getDiningTable() + "号桌的订单");
        Map<String, Integer> foodDic = order.getFoodDic();
        Set<String> keys = foodDic.keySet();
        for (String key : keys) {
            receiver.makeFood(key, foodDic.get(key));
        }
        System.out.println(order.getDiningTable() + "号桌的餐做好了");
    }
}
  1. Waiter(请求者类
public class Waiter {
//    只有多个命令对象
    private List<Command> commands = new ArrayList<>();

    public void setCommand(Command command) {
        commands.add(command);
    }

//    发起请求
    public void orderUp() {
        System.out.println("服务员通知厨师做菜");
        for (Command command : commands) {
            command.execute();
        }
    }
}
  1. SeniorChef(接收者类
public class SeniorChef {
    public void makeFood(String name, int num) {
        System.out.println("厨师正在做:" + name + ",数量:" + num);
    }
}
  1. 测试类
public class Client {
    public static void main(String[] args) {
//        创建订单对象
        Order order1 = new Order();
        order1.setDiningTable(1);
        order1.setFood("鱼香肉丝",1);
        order1.setFood("大米",2);
        Order order2 = new Order();
        order2.setDiningTable(2);
        order2.setFood("麻婆豆腐",1);
        order2.setFood("大米",1);
//        创建厨师对象
        SeniorChef seniorChef = new SeniorChef();
//        创建命令对象
        Command command1 = new OrderCommand(seniorChef,order1);
        Command command2 = new OrderCommand(seniorChef,order2);
//        创建调用者对象
        Waiter waiter = new Waiter();
//        设置命令
        waiter.setCommand(command1);
        waiter.setCommand(command2);
//        发起请求
        waiter.orderUp();
    }
}

image-20230714211017863

6.3.4 优缺点

优点:

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

缺点:

  • 使用命令模式可能会导致某些系统有过多的具体命令类。
  • 系统结构更加复杂。

6.3.5 使用场景

  • 系统需要将请求调用者和请求接收者解耦,使得调用者和接收者不直接交互。
  • 系统需要在不同的时间指定请求、将请求排队和执行请求。
  • 系统需要支持命令的撤销(Undo)操作和恢复(Redo)操作。

6.3.6 JDK源码解析

Runable是一个典型命令模式,Runnable担当命令的角色,Thread充当的是调用者,start方法就是其执行方法

//命令接口(抽象命令角色)
public interface Runnable {
	public abstract void run();
}

//调用者
public class Thread implements Runnable {
    private Runnable target;
    
    public synchronized void start() {
        if (threadStatus != 0)
            throw new IllegalThreadStateException();

        group.add(this);

        boolean started = false;
        try {
            start0();
            started = true;
        } finally {
            try {
                if (!started) {
                    group.threadStartFailed(this);
                }
            } catch (Throwable ignore) {
            }
        }
    }
    
    private native void start0();
}

会调用一个native方法start0(),调用系统方法,开启一个线程。而接收者是对程序员开放的,可以自己定义接收者。

/**
 * jdk Runnable 命令模式
 *		TurnOffThread : 属于具体
 */
public class TurnOffThread implements Runnable{
     private Receiver receiver;
    
     public TurnOffThread(Receiver receiver) {
     	this.receiver = receiver;
     }
     public void run() {
     	receiver.turnOFF();
     }
}
/**
 * 测试类
 */
public class Demo {
     public static void main(String[] args) {
         Receiver receiver = new Receiver();
         TurnOffThread turnOffThread = new TurnOffThread(receiver);
         Thread thread = new Thread(turnOffThread);
         thread.start();
     }
}

6.4 责任链模式

6.4.1 概述

在现实生活中,常常会出现这样的事例:一个请求有多个对象可以处理,但每个对象的处理条件或权限不同。例如,公司员工请假,可批假的领导有部门负责人、副总经理、总经理等,但每个领导能批准的天数不同,员工必须根据自己要请假的天数去找不同的领导签名,也就是说员工必须记住每个领导的姓名、电话和地址等信息,这增加了难度。这样的例子还有很多,如找领导出差报销、生活中的“击鼓传花”游戏等。

责任链模式目的是将一个请求沿着一个处理链依次传递,直到有一个处理者能够处理该请求为止。这种模式将请求的发送者和接收者解耦,并允许多个处理者都有机会处理请求。

6.4.2 结构

职责链模式主要包含以下角色:

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

6.4.3 案例实现

现需要开发一个请假流程控制系统。请假一天以下的假只需要小组长同意即可;请假1天到3天的假还需要部门经理同意;请求3天到7天还需要总经理同意才行。

类图如下:

代码如下:

  1. Handler(抽象处理者
public abstract class Handler {
    protected static final int NUM_ONE = 1;
    protected static final int NUM_THREE = 3;
    protected static final int NUM_SEVEN = 7;

    //    1.设置处理者的处理范围
    private int numStart;
    private int numEnd;
    //    2.设置下一个处理者
    private Handler nextHandler;

    public Handler(int numStart) {
        this.numStart = numStart;
    }

    public Handler(int numStart, int numEnd) {
        this.numStart = numStart;
        this.numEnd = numEnd;
    }

    //    设置上级处理者
    public void setNextHandler(Handler nextHandler) {
        this.nextHandler = nextHandler;
    }

    //    处理请假请求
    protected abstract void handleLeave(LeaveRequest leaveRequest);

//    提交请假请求
    public void submit(LeaveRequest leave) {
        this.handleLeave(leave);
        if (this.nextHandler != null && leave.getNum() > this.numEnd) {
            this.nextHandler.submit(leave);
        } else {
            System.out.println("请假请求已经处理完毕");
        }
    }

}
  1. GroupLeader(具体处理者
public class GroupLeader extends Handler{

    public GroupLeader() {
        super(0,Handler.NUM_ONE);
    }

    @Override
    protected void handleLeave(LeaveRequest leave) {
        System.out.println(leave.getName()+"请假"+leave.getNum()+"天,原因:"+leave.getReason());
        System.out.println("小组长审批...");
    }
}
  1. Manager(具体处理者
public class Manager extends Handler{
    public Manager() {
        super(Handler.NUM_ONE,Handler.NUM_THREE);
    }

    @Override
    protected void handleLeave(LeaveRequest leave) {
        System.out.println(leave.getName()+"请假"+leave.getNum()+"天,原因:"+leave.getReason());
        System.out.println("部门经理审批...");
    }
}
  1. GeneralManager(具体处理者
public class GeneralManager extends Handler {
    public GeneralManager() {
        super(Handler.NUM_THREE, Handler.NUM_SEVEN);
    }

    @Override
    protected void handleLeave(LeaveRequest leave) {
        System.out.println(leave.getName() + "请假" + leave.getNum() + "天,原因:" + leave.getReason());
        System.out.println("总经理审批...");
    }
}
  1. 测试类
public class Client {
    public static void main(String[] args) {
        LeaveRequest leaveRequest = new LeaveRequest("小明", 2, "生病");
        GroupLeader groupLeader = new GroupLeader();
        Manager manager = new Manager();
        GeneralManager generalManager = new GeneralManager();

//        设置处理链
        groupLeader.setNextHandler(manager);
        manager.setNextHandler(generalManager);
//        发起请求
        groupLeader.submit(leaveRequest);
    }
}

6.4.4 优缺点

优点:

  • 降低了对象之间的耦合度

    该模式降低了请求发送者和接收者的耦合度。

  • 增强了系统的可扩展性

    可以根据需要增加新的请求处理类,满足开闭原则。

  • 增强了给对象指派职责的灵活性

    当工作流程发生变化,可以动态地改变链内的成员或者修改它们的次序,也可动态地新增或者删除责任。

  • 责任链简化了对象之间的连接

    一个对象只需保持一个指向其后继者的引用,不需保持其他所有处理者的引用,这避免了使用众多的 if 或者 if···else 语句。

  • 责任分担

    每个类只需要处理自己该处理的工作,不能处理的传递给下一个对象完成,明确各类的责任范围,符合类的单一职责原则。

缺点:

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

6.4.5 源码解析

在javaWeb应用开发中,FilterChain是责任链(过滤器)模式的典型应用,以下是Filter的模拟实现分析:

  • 模拟web请求Request以及web响应Response

    public interface Request{
     
    }
    
    public interface Response{
     
    }
  • 模拟web过滤器Filter

    public interface Filter {
    	public void doFilter(Request req,Response res,FilterChain c);
    }
  • 模拟实现具体过滤器

    public class FirstFilter implements Filter {
        @Override
        public void doFilter(Request request, Response response, FilterChain chain) {
    
            System.out.println("过滤器1 前置处理");
    
            // 先执行所有request再倒序执行所有response
            chain.doFilter(request, response);
    
            System.out.println("过滤器1 后置处理");
        }
    }
    
    public class SecondFilter  implements Filter {
        @Override
        public void doFilter(Request request, Response response, FilterChain chain) {
    
            System.out.println("过滤器2 前置处理");
    
            // 先执行所有request再倒序执行所有response
            chain.doFilter(request, response);
    
            System.out.println("过滤器2 后置处理");
        }
    }
  • 模拟实现过滤器链FilterChain

    public class FilterChain {
    
        private List<Filter> filters = new ArrayList<Filter>();
    
        private int index = 0;
    
        // 链式调用
        public FilterChain addFilter(Filter filter) {
            this.filters.add(filter);
            return this;
        }
    
        public void doFilter(Request request, Response response) {
            if (index == filters.size()) {
                return;
            }
            Filter filter = filters.get(index);
            index++;
            filter.doFilter(request, response, this);
        }
    }
  • 测试类

    public class Client {
        public static void main(String[] args) {
            Request  req = null;
            Response res = null ;
    
            FilterChain filterChain = new FilterChain();
            filterChain.addFilter(new FirstFilter()).addFilter(new SecondFilter());
            filterChain.doFilter(req,res);
        }
    }

    输出结果:

    过滤器1 前置处理
    过滤器2 前置处理
    过滤器2 后置处理
    过滤器1 后置处理

    ​ 通过调用FilterChaindoFilter方法触发过滤器链的执行。首先执行FirstFilterdoFilter方法,在该方法中输出了”过滤器1 前置处理”,然后调用了FilterChaindoFilter方法,继续执行下一个过滤器。接着执行了SecondFilterdoFilter方法,在该方法中输出了”过滤器2 前置处理”,然后同样调用了FilterChaindoFilter方法。

    ​ 由于过滤器链中没有更多的过滤器,因此返回到上一层的doFilter方法,执行了”过滤器2 后置处理”,然后继续返回到更上一层的doFilter方法,执行了”过滤器1 后置处理”。

6.5 状态模式

6.5.1 概述

通过按钮来控制一个电梯的状态,一个电梯有开门状态,关门状态,停止状态,运行状态。每一种状态改变,都有可能要根据其他状态来更新处理。例如,如果电梯门现在处于运行时状态,就不能进行开门操作,而如果电梯门是停止状态,就可以执行开门操作。

类图如下:

代码如下:

public interface ILift {
    //电梯的4个状态
    //开门状态
    public final static int OPENING_STATE = 1;
    //关门状态
    public final static int CLOSING_STATE = 2;
    //运行状态
    public final static int RUNNING_STATE = 3;
    //停止状态
    public final static int STOPPING_STATE = 4;

    //设置电梯的状态
    public void setState(int state);

    //电梯的动作
    public void open();
    public void close();
    public void run();
    public void stop();
}

public class Lift implements ILift {
    private int state;

    @Override
    public void setState(int state) {
        this.state = state;
    }

    //执行关门动作
    @Override
    public void close() {
        switch (this.state) {
            case OPENING_STATE:
                System.out.println("电梯关门了。。。");//只有开门状态可以关闭电梯门,可以对应电梯状态表来看
                this.setState(CLOSING_STATE);//关门之后电梯就是关闭状态了
                break;
            case CLOSING_STATE:
                //do nothing //已经是关门状态,不能关门
                break;
            case RUNNING_STATE:
                //do nothing //运行时电梯门是关着的,不能关门
                break;
            case STOPPING_STATE:
                //do nothing //停止时电梯也是关着的,不能关门
                break;
        }
    }

    //执行开门动作
    @Override
    public void open() {
        switch (this.state) {
            case OPENING_STATE://门已经开了,不能再开门了
                //do nothing
                break;
            case CLOSING_STATE://关门状态,门打开:
                System.out.println("电梯门打开了。。。");
                this.setState(OPENING_STATE);
                break;
            case RUNNING_STATE:
                //do nothing 运行时电梯不能开门
                break;
            case STOPPING_STATE:
                System.out.println("电梯门开了。。。");//电梯停了,可以开门了
                this.setState(OPENING_STATE);
                break;
        }
    }

    //执行运行动作
    @Override
    public void run() {
        switch (this.state) {
            case OPENING_STATE://电梯不能开着门就走
                //do nothing
                break;
            case CLOSING_STATE://门关了,可以运行了
                System.out.println("电梯开始运行了。。。");
                this.setState(RUNNING_STATE);//现在是运行状态
                break;
            case RUNNING_STATE:
                //do nothing 已经是运行状态了
                break;
            case STOPPING_STATE:
                System.out.println("电梯开始运行了。。。");
                this.setState(RUNNING_STATE);
                break;
        }
    }

    //执行停止动作
    @Override
    public void stop() {
        switch (this.state) {
            case OPENING_STATE: //开门的电梯已经是是停止的了(正常情况下)
                //do nothing
                break;
            case CLOSING_STATE://关门时才可以停止
                System.out.println("电梯停止了。。。");
                this.setState(STOPPING_STATE);
                break;
            case RUNNING_STATE://运行时当然可以停止了
                System.out.println("电梯停止了。。。");
                this.setState(STOPPING_STATE);
                break;
            case STOPPING_STATE:
                //do nothing
                break;
        }
    }
}

public class Client {
    public static void main(String[] args) {
        Lift lift = new Lift();
        lift.setState(ILift.STOPPING_STATE);//电梯是停止的
        lift.open();//开门
        lift.close();//关门
        lift.run();//运行
        lift.stop();//停止
    }
}

问题分析:

  • 使用了大量的switch…case这样的判断(if…else也是一样),使程序的可阅读性变差。
  • 扩展性很差。如果新加了断电的状态,我们需要修改上面判断逻辑

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

6.5.2 结构

状态模式包含以下主要角色。

  • 环境(Context)角色:也称为上下文,它定义了客户程序需要的接口,维护一个当前状态,并将与状态相关的操作委托给当前状态对象来处理。
  • 抽象状态(State)角色:定义一个接口,用以封装环境对象中的特定状态所对应的行为。
  • 具体状态(Concrete State)角色:实现抽象状态所对应的行为。

6.5.3 案例实现

对上述电梯的案例使用状态模式进行改进。类图如下:

代码如下:

  1. LiftState(抽象状态
public abstract class LiftState {

    protected Context context;

    public void setContext(Context context) {
        this.context = context;
    }

    public LiftState(Context context) {
        this.context = context;
    }

    public abstract void open();

    public abstract void close();

    public abstract void run();

    public abstract void stop();
}
  1. OpeningState(具体状态
public class OpeningState extends LiftState{

    public OpeningState(Context context) {
        super(context);
    }

    @Override
    public void open() {
        System.out.println("电梯开启");
    }

    @Override
    public void close() {
        context.setLiftState(Context.CLOSING_STATE);
        context.getLiftState().close();
    }

    @Override
    public void run() {
    }

    @Override
    public void stop() {

    }
}
  1. ClosingState(具体状态
public class ClosingState extends LiftState {

    public ClosingState(Context context) {
        super(context);
    }

    @Override
    public void open() {
        context.setLiftState(Context.OPENING_STATE);
        context.getLiftState().open();
    }

    @Override
    public void close() {
        System.out.println("电梯关闭");
    }

    @Override
    public void run() {
        context.setLiftState(Context.RUNNING_STATE);
        context.getLiftState().run();
    }

    @Override
    public void stop() {
        context.setLiftState(Context.STOPPING_STATE);
        context.getLiftState().stop();
    }
}
  1. RunningState(具体状态
public class RunningState extends LiftState {

    public RunningState(Context context) {
        super(context);
    }

    @Override
    public void open() {

    }

    @Override
    public void close() {

    }

    @Override
    public void run() {
        System.out.println("电梯正在运行");
    }

    @Override
    public void stop() {
        context.setLiftState(Context.STOPPING_STATE);
        context.getLiftState().stop();
    }
}
  1. ClosingState(具体状态
public class ClosingState extends LiftState {

    public ClosingState(Context context) {
        super(context);
    }

    @Override
    public void open() {
        context.setLiftState(Context.OPENING_STATE);
        context.getLiftState().open();
    }

    @Override
    public void close() {
        System.out.println("电梯关闭");
    }

    @Override
    public void run() {
        context.setLiftState(Context.RUNNING_STATE);
        context.getLiftState().run();
    }

    @Override
    public void stop() {
        context.setLiftState(Context.STOPPING_STATE);
        context.getLiftState().stop();
    }
}
  1. 测试类
public class Client {
    public static void main(String[] args) {
        Context context = new Context();
        context.setLiftState(new ClosingState(context));
        context.open();
        context.run();
        context.close();
        context.stop();
    }
}

image-20230715103603472

6.5.4 优缺点

优点:

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

缺点:

  • 状态模式的使用必然会增加系统类和对象的个数。
  • 状态模式的结构与实现都较为复杂,如果使用不当将导致程序结构和代码的混乱。
  • 状态模式对”开闭原则”的支持并不太好。

6.5.5 使用场景

  • 当一个对象的行为取决于它的状态,并且它必须在运行时根据状态改变它的行为时,就可以考虑使用状态模式。
  • 一个操作中含有庞大的分支结构,并且这些分支决定于对象的状态时。

6.6 观察者模式

6.6.1 概述

观察者模式表示的是一种对象与对象之间具有依赖关系,当一个对象发生改变的时候,依赖这个对象的所有对象也会做出反应。

6.6.2 结构

在观察者模式中有如下角色:

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

6.6.3 案例实现

【例】微信公众号

在使用微信公众号时,大家都会有这样的体验,当你关注的公众号中有新内容更新的话,它就会推送给关注公众号的微信用户端。我们使用观察者模式来模拟这样的场景,微信用户就是观察者,微信公众号是被观察者,有多个的微信用户关注了程序猿这个公众号。

类图如下:

代码如下:

  1. Subject(抽象主题
public interface Subject {
//    添加观察者
    void addAttach(Observer observer);

//    删除观察者
    void detach(Observer observer);

//    通知观察者
    void notify(String message);
}
  1. SubscriptionSubject(具体主题
public class SubscriptionSubject implements Subject{
    private List<Observer> weixinUserList = new ArrayList<>();

    @Override
    public void addAttach(Observer observer) {
        weixinUserList.add(observer);
    }

    @Override
    public void detach(Observer observer) {
        weixinUserList.remove(observer);
    }

    @Override
    public void notify(String message) {
        for (Observer observer : weixinUserList) {
            observer.update(message);
        }
    }
}
  1. Observer(抽象观察者
public interface Observer {
    void update(String message);
}
  1. WinxinUser(具体观察者
public class WinxinUser implements Observer{
    private String name;

    public WinxinUser(String name) {
        this.name = name;
    }

    @Override
    public void update(String message) {
        System.out.println(name + "-" + message);
    }
}
  1. 测试类
public class Client {
    public static void main(String[] args) {
//        创建主题
        SubscriptionSubject subject = new SubscriptionSubject();

        subject.addAttach(new WinxinUser("张三"));
        subject.addAttach(new WinxinUser("李四"));
        subject.addAttach(new WinxinUser("王五"));

//        主题更新,通知观察者
        subject.notify("内容更新");
    }
}

image-20230715105956682

6.6.4 优缺点

优点:

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

缺点:

  • 如果观察者非常多的话,那么所有的观察者收到被观察者发送的通知会耗时
  • 如果被观察者有循环依赖的话,那么被观察者发送通知会使观察者循环调用,会导致系统崩溃

6.6.5 使用场景

  • 对象间存在一对多关系,一个对象的状态发生改变会影响其他对象。
  • 当一个抽象模型有两个方面,其中一个方面依赖于另一方面时。

6.6.6 JDK中提供的实现

在 Java 中,通过java.util.Observable类(抽象主题类)和java.util.Observer接口(抽象观察者)定义了观察者模式,只要实现它们的子类就可以编写观察者模式实例。

Observable类

Observable 类是抽象目标类(被观察者),它有一个Vector集合成员变量,用于保存所有要通知的观察者对象,下面来介绍它最重要的 3 个方法。

  • void addObserver(Observer o) 方法:用于将新的观察者对象添加到集合中。

  • void notifyObservers(Object arg) 方法:调用集合中的所有观察者对象的 update方法,通知它们数据发生改变。通常越晚加入集合的观察者越先得到通知。

  • void setChange() 方法:用来设置一个 boolean 类型的内部标志,注明目标对象发生了变化。当它为true时,notifyObservers() 才会通知观察者。

Observer 接口

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

【例】警察抓小偷

警察抓小偷也可以使用观察者模式来实现,警察是观察者,小偷是被观察者。代码如下:

小偷是一个被观察者,所以需要继承Observable类

public class Thief extends Observable {

    private String name;

    public Thief(String name) {
        this.name = name;
    }
    
    public void setName(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }

    public void steal() {
        System.out.println("小偷:我偷东西了,有没有人来抓我!!!");
        super.setChanged(); //changed  = true
        super.notifyObservers();
    }
}

警察是一个观察者,所以需要让其实现Observer接口

public class Policemen implements Observer {

    private String name;

    public Policemen(String name) {
        this.name = name;
    }
    public void setName(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }

    @Override
    public void update(Observable o, Object arg) {
        System.out.println("警察:" + ((Thief) o).getName() + ",我已经盯你很久了,你可以保持沉默,但你所说的将成为呈堂证供!!!");
    }
}

客户端代码

public class Client {
    public static void main(String[] args) {
        //创建小偷对象
        Thief t = new Thief("隔壁老王");
        //创建警察对象
        Policemen p = new Policemen("小李");
        //让警察盯着小偷
        t.addObserver(p);
        //小偷偷东西
        t.steal();
    }
}

6.7 中介者模式

6.7.1 概述

一般来说,同事类之间的关系是比较复杂的,多个同事类之间互相关联时,他们之间的关系会呈现为复杂的网状结构,这是一种过度耦合的架构,即不利于类的复用,也不稳定。例如在下左图中,有六个同事类对象,假如对象1发生变化,那么将会有4个对象受到影响。如果对象2发生变化,那么将会有5个对象受到影响。也就是说,同事类之间直接关联的设计是不好的。

如果引入中介者模式,那么同事类之间的关系将变为星型结构,从下右图中可以看到,任何一个类的变动,只会影响的类本身,以及中介者,这样就减小了系统的耦合。一个好的设计,必定不会把所有的对象关系处理逻辑封装在本类中,而是使用一个专门的类来管理那些不属于自己的行为。

中介模式用于解耦具有复杂相互关系的对象之间的通信。在该模式中,一个中介者(Mediator)对象封装了一组对象之间的交互逻辑,使得些对象之间不需要直接相互通信,而是通过中介者进行通信。

6.7.2 结构

中介者模式包含以下主要角色:

  • 抽象中介者(Mediator)角色:它是中介者的接口,提供了同事对象注册与转发同事对象信息的抽象方法。

  • 具体中介者(ConcreteMediator)角色:实现中介者接口,定义一个 List 来管理同事对象,协调各个同事角色之间的交互关系,因此它依赖于同事角色。

  • 抽象同事类(Colleague)角色:定义同事类的接口,保存中介者对象,提供同事对象交互的抽象方法,实现所有相互影响的同事类的公共功能。

  • 具体同事类(Concrete Colleague)角色:是抽象同事类的实现者,当需要与其他同事对象交互时,由中介者对象负责后续的交互。

6.7.3 案例实现

【例】租房

现在租房基本都是通过房屋中介,房主将房屋托管给房屋中介,而租房者从房屋中介获取房屋信息。房屋中介充当租房者与房屋所有者之间的中介者。

类图如下:

代码如下:

  1. Mediator(抽象中介者
public abstract class Mediator {
    public abstract void constact(String message, Person person);
}
  1. Person(抽象同事类
public abstract class Person {
    protected String name;
    protected Mediator mediator;

    public Person(String name, Mediator mediator) {
        this.name = name;
        this.mediator = mediator;
    }
}
  1. Tenant(具体同事类
public class Tenant extends Person{
    public Tenant(String name, Mediator mediator) {
        super(name, mediator);
    }

    public void constact(String message){
        mediator.constact(message, this);
    }

    public void getMessage(String message){
        System.out.println("租房者:" + name + ",获得信息:" + message);
    }
}
  1. HouseOwner(具体同事类
public class HouseOwner extends Person {
    public HouseOwner(String name, Mediator mediator) {
        super(name, mediator);
    }

    public void constact(String message){
        mediator.constact(message, this);
    }

    public void getMessage(String message){
        System.out.println("房主:" + name + ",获得信息:" + message);
    }
}
  1. MediatorStructure(具体中介者
public class MediatorStructure extends Mediator {
    private HouseOwner houseOwner;

    private Tenant tenant;

    public HouseOwner getHouseOwner() {
        return houseOwner;
    }

    public void setHouseOwner(HouseOwner houseOwner) {
        this.houseOwner = houseOwner;
    }

    public Tenant getTenant() {
        return tenant;
    }

    public void setTenant(Tenant tenant) {
        this.tenant = tenant;
    }

    @Override
    public void constact(String message, Person person) {
        if (person == houseOwner) {
            tenant.getMessage(message);
        } else {
            houseOwner.getMessage(message);
        }
    }
}
  1. 测试类
public class Client {
    public static void main(String[] args) {
//        1.创建中介者
        MediatorStructure mediator = new MediatorStructure();
//        2.创建租房者
        Tenant tenant = new Tenant("张三", mediator);
//        3.创建房东
        HouseOwner houseOwner = new HouseOwner("李四", mediator);

//        4.中介者介绍租房者和房东
        mediator.setTenant(tenant);
        mediator.setHouseOwner(houseOwner);

//        5.租房者发送信息给房东
        tenant.constact("我想租一套三室的房子");
//        6.房东发送信息给租房者
        houseOwner.constact("我这里有一套三室的房子,你看看");
    }
}

image-20230715115834094

6.7.4 优缺点

优点:

  • 松散耦合

    中介者模式通过把多个同事对象之间的交互封装到中介者对象里面,从而使得同事对象之间松散耦合,基本上可以做到互补依赖。这样一来,同事对象就可以独立地变化和复用,而不再像以前那样“牵一处而动全身”了。

  • 集中控制交互

    多个同事对象的交互,被封装在中介者对象里面集中管理,使得这些交互行为发生变化的时候,只需要修改中介者对象就可以了,当然如果是已经做好的系统,那么就扩展中介者对象,而各个同事类不需要做修改。

  • 一对多关联转变为一对一的关联

    没有使用中介者模式的时候,同事对象之间的关系通常是一对多的,引入中介者对象以后,中介者对象和同事对象的关系通常变成双向的一对一,这会让对象的关系更容易理解和实现。

缺点:

当同事类太多时,中介者的职责将很大,它会变得复杂而庞大,以至于系统难以维护。

6.7.5 使用场景

  • 系统中对象之间存在复杂的引用关系,系统结构混乱且难以理解。
  • 当想创建一个运行于多个类之间的对象,又不想生成新的子类时。

6.8 迭代器模式

6.8.1 概述

迭代器模式提供一种方法来顺序地访问一个容器(如列表、数组、集合等)中的元素,而又无需暴露该容器的内部表示。

6.8.2 结构

迭代器模式主要包含以下角色:

  • 抽象容器(Aggregate)角色:定义存储、添加、删除聚合元素以及创建迭代器对象的接口。

  • 具体容器(ConcreteAggregate)角色:实现抽象聚合类,返回一个具体迭代器的实例。

  • 抽象迭代器(Iterator)角色:定义访问和遍历聚合元素的接口,通常包含 hasNext()、next() 等方法。

  • 具体迭代器(Concretelterator)角色:实现抽象迭代器接口中所定义的方法,完成对聚合对象的遍历,记录遍历的当前位置。

6.8.3 案例实现

【例】定义一个可以存储学生对象的容器对象,将遍历该容器的功能交由迭代器实现,涉及到的类如下:

代码如下:

  1. Student类
public class Student {
    private String name;

    private int number;

    public Student(String name, int number) {
        this.name = name;
        this.number = number;
    }
    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getNumber() {
        return number;
    }

    public void setNumber(int number) {
        this.number = number;
    }

    @Override
    public String toString() {
        return "Student{" +
                "name='" + name + '\'' +
                ", number=" + number +
                '}';
    }
}
  1. StudentIterator(抽象迭代器

定义迭代器接口,声明hasNext、next方法

public interface StudentIterator {
    boolean hasNext();

    Student next();
}
  1. StudentIteratorImpl(具体迭代器

定义具体的迭代器类,重写所有的抽象方法

public class StudentIteratorImpl implements StudentIterator {
    private List<Student> list;
    private int position = 0;

    public StudentIteratorImpl(List<Student> list) {
        this.list = list;
    }

    @Override
    public boolean hasNext() {
        return position < list.size();
    }

    @Override
    public Student next() {
        Student currentStudent = list.get(position);
        position ++;
        return currentStudent;
    }
}
  1. StudentAggregate(抽象容器类

定义抽象容器类,包含添加元素,删除元素,获取迭代器对象的方法

public interface StudentAggregate {
    void addStudent(Student student);

    void removeStudent(Student student);

    StudentIterator getStudentIterator();
}
  1. StudentAggregateImpl(具体容器类

定义具体的容器类,重写所有的方法

public class StudentAggregateImpl implements StudentAggregate {

    private List<Student> list = new ArrayList<Student>();  // 学生列表

    @Override
    public void addStudent(Student student) {
        this.list.add(student);
    }

    @Override
    public void removeStudent(Student student) {
        this.list.remove(student);
    }

    @Override
    public StudentIterator getStudentIterator() {
        return new StudentIteratorImpl(list);
    }
}
  1. 测试类
public class Client {
    public static void main(String[] args) {
//        1.创建聚合类
        StudentAggregate studentAggregate = new StudentAggregateImpl();
//        2.添加学生
        studentAggregate.addStudent(new Student("张三", 18));
        studentAggregate.addStudent(new Student("李四", 19));
        studentAggregate.addStudent(new Student("王五", 20));

//        3.获取迭代器
        StudentIterator studentIterator = studentAggregate.getStudentIterator();
//        4.遍历
        while (studentIterator.hasNext()){
            System.out.println(studentIterator.next());
        }
    }
}

image-20230715123503766

6.8.4 优缺点

优点:

  • 它支持以不同的方式遍历一个聚合对象,在同一个聚合对象上可以定义多种遍历方式。在迭代器模式中只需要用一个不同的迭代器来替换原有迭代器即可改变遍历算法,我们也可以自己定义迭代器的子类以支持新的遍历方式。
  • 迭代器简化了聚合类。由于引入了迭代器,在原有的聚合对象中不需要再自行提供数据遍历等方法,这样可以简化聚合类的设计。
  • 在迭代器模式中,由于引入了抽象层,增加新的聚合类和迭代器类都很方便,无须修改原有代码,满足 “开闭原则” 的要求。

缺点:

增加了类的个数,这在一定程度上增加了系统的复杂性。

6.8.5 使用场景

  • 当需要为聚合对象提供多种遍历方式时。
  • 当需要为遍历不同的聚合结构提供一个统一的接口时。
  • 当访问一个聚合对象的内容而无须暴露其内部细节的表示时。

6.8.6 JDK源码解析

迭代器模式在JAVA的很多集合类中被广泛应用,接下来看看JAVA源码中是如何使用迭代器模式的。

List<String> list = new ArrayList<>();
Iterator<String> iterator = list.iterator(); //list.iterator()方法返回的肯定是Iterator接口的子实现类对象
while (iterator.hasNext()) {
    System.out.println(iterator.next());
}

看完这段代码是不是很熟悉,与我们上面代码基本类似。单列集合都使用到了迭代器,我们以ArrayList举例来说明

  • List:抽象容器类
  • ArrayList:具体的容器类
  • Iterator:抽象迭代器
  • list.iterator():返回的是实现了 Iterator 接口的具体迭代器对象,即Iterator接口的实例化对象Itr。而Itr是ArrayList中定义的实现Iterator接口的私有成员内部类

具体的来看看 ArrayList的代码实现

public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
    
    public Iterator<E> iterator() {
        return new Itr();
    }
    
    private class Itr implements Iterator<E> {
        int cursor;       // 下一个要返回元素的索引
        int lastRet = -1; // 上一个返回元素的索引
        int expectedModCount = modCount;

        Itr() {}
		
        //判断是否还有元素
        public boolean hasNext() {
            return cursor != size;
        }

        //获取下一个元素
        public E next() {
            checkForComodification();
            int i = cursor;
            if (i >= size)
                throw new NoSuchElementException();
            Object[] elementData = ArrayList.this.elementData;
            if (i >= elementData.length)
                throw new ConcurrentModificationException();
            cursor = i + 1;
            return (E) elementData[lastRet = i];
        }
        ...
}

这部分代码还是比较简单,大致就是在 iterator 方法中返回了一个实例化的 Iterator 对象。Itr是一个内部类,它实现了 Iterator 接口并重写了其中的抽象方法。

注意:

​ 当我们在使用JAVA开发的时候,想使用迭代器模式的话,只要让我们自己定义的容器类实现java.util.Iterable并实现其中的iterator()方法使其返回一个 java.util.Iterator 的实现类就可以了。

6.9 访问者模式

6.9.1 概述

访问者模式允许将数据结构数据操作分离,从而实现对数据结构中元素的不同操作。访问者模式可以在不修改数据结构的情况下,通过在访问者对象中定义新的操作,来实现对数据结构的扩展。

6.9.2 结构

访问者模式包含以下主要角色

  • 抽象访问者(Visitor)角色:定义了对每一个元素(Element)访问的行为,它的参数就是可以访问的元素,它的方法个数理论上来讲与元素类个数(Element的实现类个数)是一样的,从这点不难看出,访问者模式要求元素类的个数不能改变。
  • 具体访问者(ConcreteVisitor)角色:给出对每一个元素类访问时所产生的具体行为。
  • 抽象元素(Element)角色:定义了一个接受访问者的方法(accept),其意义是指,每一个元素都要可以被访问者访问。
  • 具体元素(ConcreteElement)角色: 提供接受访问方法的具体实现,而这个具体的实现,通常情况下是使用访问者提供的访问该元素类的方法。
  • 对象结构(Object Structure)角色:定义当中所提到的对象结构,对象结构是一个抽象表述,具体点可以理解为一个具有容器性质或者复合对象特性的类,它会含有一组元素(Element),并且可以迭代这些元素,供访问者访问。

6.9.3 案例实现

【例】给宠物喂食

现在养宠物的人特别多,我们就以这个为例,当然宠物还分为狗,猫等,要给宠物喂食的话,主人可以喂,其他人也可以喂食。

  • 访问者角色:给宠物喂食的人
  • 具体访问者角色:主人、其他人
  • 抽象元素角色:动物抽象类
  • 具体元素角色:宠物狗、宠物猫
  • 结构对象角色:主人家

类图如下:

代码如下:

  1. Person(抽象访问者

    创建抽象访问者接口

public interface Person {
    void feed(Cat cat);

    void feed(Dog dog);
}
  1. Owner(具体访问者

创建不同的具体访问者角色(主人和其他人),都需要实现 Person接口

public class Owner implements Person {

    @Override
    public void feed(Cat cat) {
        System.out.println("主人喂食猫");
    }

    @Override
    public void feed(Dog dog) {
        System.out.println("主人喂食狗");
    }
}
  1. Someone(具体访问者
public class Someone implements Person {
    @Override
    public void feed(Cat cat) {
        System.out.println("其他人喂食猫");
    }

    @Override
    public void feed(Dog dog) {
        System.out.println("其他人喂食狗");
    }
}
  1. Animal(抽象元素类
public interface Animal {
    void accept(Person person);
}
  1. Dog(具体元素类
public class Dog implements Animal{
    @Override
    public void accept(Person person) {
        person.feed(this);
        System.out.println("好好吃,汪汪汪");
    }
}
  1. Cat(具体元素类
public class Cat implements Animal{
    @Override
    public void accept(Person person) {
        person.feed(this);
        System.out.println("好好吃,喵喵喵");
    }
}
  1. Home(对象结构

定义对象结构,此案例中就是主人的家

public class Home {
    private List<Animal> nodeList = new ArrayList<Animal>();

    public void action(Person person) {
        for (Animal node : nodeList) {
            node.accept(person);
        }
    }

    //添加操作
    public void add(Animal animal) {
        nodeList.add(animal);
    }
}
  1. 测试类
public class Client {
    public static void main(String[] args) {
        Home home = new Home();
        home.add(new Dog());
        home.add(new Cat());
        Owner owner = new Owner();
        home.action(owner);
        System.out.println("-------------");
        Someone someone = new Someone();
        home.action(someone);
    }
}

image-20230715134247425

6.9.4 优缺点

优点:

  • 扩展性好

    在不修改对象结构中的元素的情况下,为对象结构中的元素添加新的功能。

  • 复用性好

    通过访问者来定义整个对象结构通用的功能,从而提高复用程度。

  • 分离无关行为

    通过访问者来分离无关的行为,把相关的行为封装在一起,构成一个访问者,这样每一个访问者的功能都比较单一。

缺点:

  • 对象结构变化很困难

    在访问者模式中,每增加一个新的元素类,都要在每一个具体访问者类中增加相应的具体操作,这违背了“开闭原则”。

  • 违反了依赖倒置原则

    访问者模式依赖了具体类,而没有依赖抽象类。

6.9.5 使用场景

  • 对象结构相对稳定,但其操作算法经常变化的程序。

  • 对象结构中的对象需要提供多种不同且不相关的操作,而且要避免让这些操作的变化影响对象的结构。

6.9.6 分派

访问者模式用到了一种双分派的技术。

6.9.6.1分派

变量被声明时的类型叫做变量的静态类型,有些人又把静态类型叫做明显类型;而变量所引用的对象的真实类型又叫做变量的实际类型。比如 Map map = new HashMap()map变量的静态类型是 Map ,实际类型是 HashMap根据对象的类型而对方法进行的选择,就是分派(Dispatch),比如是执行Map类型当中的方法,还是HashMap类型当中的方法。分派(Dispatch)又分为两种,即静态分派动态分派

Map map = new HashMap(),编译看左边,运行看右边。

静态分派(Static Dispatch) 发生在编译时期,分派根据静态类型信息发生。静态分派对于我们来说并不陌生,方法重载就是静态分派。

动态分派(Dynamic Dispatch) 发生在运行时期,动态分派动态地置换掉某个方法。Java通过方法的重写支持动态分派。

6.9.6.2动态分派

通过方法的重写支持动态分派。

public class Animal {
    public void execute() {
        System.out.println("Animal");
    }
}

public class Dog extends Animal {
    @Override
    public void execute() {
        System.out.println("dog");
    }
}

public class Cat extends Animal {
     @Override
    public void execute() {
        System.out.println("cat");
    }
}

public class Client {
   	public static void main(String[] args) {
        Animal a = new Dog();
        a.execute();
        
        Animal a1 = new Cat();
        a1.execute();
    }
}

上面代码的结果大家应该直接可以说出来,这不就是多态吗!运行执行的是子类中的方法。

Java编译器在编译时期并不总是知道哪些代码会被执行,因为编译器仅仅知道对象的静态类型,而不知道对象的真实类型;而方法的调用则是根据对象的实际类型,而不是静态类型

6.9.6.3静态分派

通过方法重载支持静态分派。

public class Animal {
}

public class Dog extends Animal {
}

public class Cat extends Animal {
}

public class Execute {
    public void execute(Animal a) {
        System.out.println("Animal");
    }

    public void execute(Dog d) {
        System.out.println("dog");
    }

    public void execute(Cat c) {
        System.out.println("cat");
    }
}

public class Client {
    public static void main(String[] args) {
        Animal a = new Animal();
        Animal a1 = new Dog();
        Animal a2 = new Cat();

        Execute exe = new Execute();
        exe.execute(a);
        exe.execute(a1);
        exe.execute(a2);
    }
}

运行结果:

重载方法的分派是根据静态类型进行的,这个分派过程在编译时期就完成了。因为他们的静态类型都是animal,只有在调用方法的时候才会使用实际类型。

6.9.6.4双分派

双分派(Double Dispatch)是一种多态性的概念,它要求在运行时根据两个对象的类型来确定要调用的方法或函数。

public class Animal {
    public void accept(Execute exe) {
        exe.execute(this);
    }
}

public class Dog extends Animal {
    public void accept(Execute exe) {
        exe.execute(this);
    }
}

public class Cat extends Animal {
    public void accept(Execute exe) {
        exe.execute(this);
    }
}

public class Execute {
    public void execute(Animal a) {
        System.out.println("animal");
    }

    public void execute(Dog d) {
        System.out.println("dog");
    }

    public void execute(Cat c) {
        System.out.println("cat");
    }
}

测试类

public class Client {
    public static void main(String[] args) {
        Animal a = new Animal();
        Animal d = new Dog();
        Animal c = new Cat();

        Execute exe = new Execute();
        a.accept(exe);
        d.accept(exe);
        c.accept(exe);
    }
}

在上面代码中,客户端将Execute对象做为参数传递给Animal类型的变量调用的方法,这里完成第一次分派,这里是方法重写,所以是动态分派,也就是执行实际类型中的方法,同时也将自己this作为参数传递进去,这里就完成了第二次分派,这里的Execute类中有多个重载的方法,而传递进行的是this,就是具体的实际类型的对象。

说到这里,我们已经明白双分派是怎么回事了,但是它有什么效果呢?就是可以实现方法的动态绑定,我们可以对上面的程序进行修改。

运行结果如下:

双分派实现动态分派的本质,就是在重载方法委派的前面加上了继承体系中覆盖的环节,由于第一次分派是动态的,所以第二次分派的重载就是动态的了。

6.10 备忘录模式

6.10.1 概述

备忘录模式提供了一种状态恢复的实现机制,使得用户可以方便地回到一个特定的历史步骤,当新的状态无效或者存在问题时,可以使用暂时存储起来的备忘录将状态复原,很多软件都提供了撤销(Undo)操作,如 Word、记事本、Photoshop、IDEA等软件在编辑时按 Ctrl+Z 组合键时能撤销当前操作,使文档恢复到之前的状态;还有在 浏览器中的后退键、数据库事务管理中的回滚操作、玩游戏时的中间结果存档功能、数据库与操作系统的备份操作、棋类游戏中的悔棋功能等都属于这类。

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

6.10.2 结构

备忘录模式的主要角色如下:

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

备忘录有两个等效的接口:

  • 窄接口:管理者(Caretaker)对象(其他发起人对象之外的任何对象)看到的是备忘录的窄接口(narror Interface),这个窄接口只允许他把备忘录对象传给其他的对象。
  • 宽接口:与管理者看到的窄接口相反,发起人对象可以看到一个宽接口(wide Interface),这个宽接口允许它读取所有的数据,以便根据这些数据恢复这个发起人对象的内部状态。

6.10.3 案例实现

【例】游戏挑战BOSS

游戏中的某个场景,一游戏角色有生命力、攻击力、防御力等数据,在打Boss前和后一定会不一样的,我们允许玩家如果感觉与Boss决斗的效果不理想可以让游戏恢复到决斗之前的状态。

要实现上述案例,有两种方式:

  • “白箱”备忘录模式
  • “黑箱”备忘录模式

6.10.3.1 “白箱”备忘录模式

备忘录角色对任何对象都提供一个接口,即宽接口,备忘录角色的内部所存储的状态就对所有对象公开。类图如下:

代码如下:

  1. GameRole(发起人
public class GameRole {
    private int vit; // 生命力
    private int atk; // 攻击力
    private int def; // 防御力

//    初始化内部状态
    public void initState(){
        this.vit = 100;
        this.atk = 100;
        this.def = 100;
    }

//    战斗
    public void fight(){
        this.vit = 0;
        this.atk = 0;
        this.def = 0;
    }

//    保存角色状态
    public RoleStateMemento saveState(){
        return new RoleStateMemento(vit,atk,def);
    }

//    恢复角色状态
    public void recoverState(RoleStateMemento roleStateMemento){
        this.vit = roleStateMemento.getVit();
        this.atk = roleStateMemento.getAtk();
        this.def = roleStateMemento.getDef();
    }

//    显示角色状态
    public void displayState(){
        System.out.println("角色当前状态:");
        System.out.println("生命力:" + this.vit);
        System.out.println("攻击力:" + this.atk);
        System.out.println("防御力:" + this.def);
    }

    public int getVit() {
        return vit;
    }

    public void setVit(int vit) {
        this.vit = vit;
    }

    public int getAtk() {
        return atk;
    }

    public void setAtk(int atk) {
        this.atk = atk;
    }

    public int getDef() {
        return def;
    }

    public void setDef(int def) {
        this.def = def;
    }
}
  1. RoleStateMemento(备忘录
public class RoleStateMemento {
    private int vit; // 生命力
    private int atk; // 攻击力
    private int def; // 防御力

    public RoleStateMemento() {
    }

    public RoleStateMemento(int vit, int atk, int def){
        this.vit = vit;
        this.atk = atk;
        this.def = def;
    }

    public int getVit() {
        return vit;
    }

    public void setVit(int vit) {
        this.vit = vit;
    }

    public int getAtk() {
        return atk;
    }

    public void setAtk(int atk) {
        this.atk = atk;
    }

    public int getDef() {
        return def;
    }

    public void setDef(int def) {
        this.def = def;
    }
}
  1. RoleStateCaretaker(管理者
public class RoleStateCaretaker {

    private RoleStateMemento memento;

    public RoleStateMemento getMemento() {
        return memento;
    }

    public void setMemento(RoleStateMemento memento) {
        this.memento = memento;
    }
}
  1. 测试类
public class Client {
    public static void main(String[] args) {
//        1.创建发起人
        GameRole gameRole = new GameRole();
//        2.初始化游戏角色
        gameRole.initState();
//        3.查看游戏角色状态
        gameRole.displayState();
//        4.创建管理者,保存游戏角色状态
        RoleStateCaretaker roleStateCaretaker = new RoleStateCaretaker();
//          4.1设置备忘录对象,内部是创建备忘录对象
        roleStateCaretaker.setMemento(gameRole.saveState());
//        5.战斗
        gameRole.fight();
//        6.查看游戏角色状态
        gameRole.displayState();
//        7.发起人恢复备忘录对象保存的状态
        gameRole.recoverState(roleStateCaretaker.getMemento());
//        8.查看游戏角色状态
        gameRole.displayState();
    }
}

image-20230715155141967

分析:白箱备忘录模式是破坏封装性的。但是通过程序员自律,同样可以在一定程度上实现模式的大部分用意。

6.10.3.2 “黑箱”备忘录模式

备忘录角色对发起人对象提供一个宽接口,而为其他对象提供一个窄接口。在Java语言中,实现双重接口的办法就是将备忘录类设计成发起人类的内部成员类。

RoleStateMemento 设为 GameRole 的内部类,从而将 RoleStateMemento 对象封装在 GameRole 里面;在外面提供一个标识接口 MementoRoleStateCaretaker 及其他对象使用。这样 GameRole 类看到的是 RoleStateMemento 所有的接口,而RoleStateCaretaker 及其他对象看到的仅仅是标识接口 Memento 所暴露出来的接口,从而维护了封装型。类图如下:

代码如下:

  1. 窄接口Memento,这是一个标识接口,因此没有定义出任何的方法
public interface Memento {
}
  1. 定义发起人类 GameRole,并在内部定义备忘录内部类 RoleStateMemento(该内部类设置为私有的)
public class GameRole {
    private int vit; // 生命力
    private int atk; // 攻击力
    private int def; // 防御力

    //    初始化内部状态
    public void initState() {
        this.vit = 100;
        this.atk = 100;
        this.def = 100;
    }

    //    战斗
    public void fight() {
        this.vit = 0;
        this.atk = 0;
        this.def = 0;
    }

    //    保存角色状态
    public Memento saveState() {
        return new RoleStateMemento(vit, atk, def);
    }

    //    恢复角色状态
    public void recoverState(Memento memento) {
        RoleStateMemento roleStateMemento = (RoleStateMemento) memento;
        this.vit = roleStateMemento.getVit();
        this.atk = roleStateMemento.getAtk();
        this.def = roleStateMemento.getDef();
    }

    //    显示角色状态
    public void displayState() {
        System.out.println("角色当前状态:");
        System.out.println("生命力:" + this.vit);
        System.out.println("攻击力:" + this.atk);
        System.out.println("防御力:" + this.def);
    }

    private class RoleStateMemento implements Memento{
        private int vit; // 生命力
        private int atk; // 攻击力
        private int def; // 防御力

        public RoleStateMemento() {
        }

        public RoleStateMemento(int vit, int atk, int def) {
            this.vit = vit;
            this.atk = atk;
            this.def = def;
        }

        public int getVit() {
            return vit;
        }

        public void setVit(int vit) {
            this.vit = vit;
        }

        public int getAtk() {
            return atk;
        }

        public void setAtk(int atk) {
            this.atk = atk;
        }

        public int getDef() {
            return def;
        }

        public void setDef(int def) {
            this.def = def;
        }
    }

    public int getVit() {
        return vit;
    }

    public void setVit(int vit) {
        this.vit = vit;
    }

    public int getAtk() {
        return atk;
    }

    public void setAtk(int atk) {
        this.atk = atk;
    }

    public int getDef() {
        return def;
    }

    public void setDef(int def) {
        this.def = def;
    }
}
  1. 负责人角色类 RoleStateCaretaker 能够得到的备忘录对象是以 Memento 为接口的,由于这个接口仅仅是一个标识接口,因此负责人角色不可能改变这个备忘录对象的内容
public class RoleStateCaretaker {

    private Memento memento;

    public Memento getMemento() {
        return memento;
    }

    public void setMemento(Memento memento) {
        this.memento = memento;
    }
}
  1. 客户端测试类
public class Client {
    public static void main(String[] args) {
//        1.创建发起人
        GameRole gameRole = new GameRole();
//        2.初始化游戏角色
        gameRole.initState();
//        3.查看游戏角色状态
        gameRole.displayState();
//        4.创建管理者,保存游戏角色状态
        RoleStateCaretaker roleStateCaretaker = new RoleStateCaretaker();
//          4.1设置备忘录对象,内部是创建备忘录对象
        roleStateCaretaker.setMemento(gameRole.saveState());
//        5.战斗
        gameRole.fight();
//        6.查看游戏角色状态
        gameRole.displayState();
//        7.发起人恢复备忘录对象保存的状态
        gameRole.recoverState(roleStateCaretaker.getMemento());
//        8.查看游戏角色状态
        gameRole.displayState();
    }
}

6.10.4 优缺点

优点:

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

缺点

资源消耗大。如果要保存的内部状态信息过多或者特别频繁,将会占用比较大的内存资源。

6.10.5 使用场景

  • 需要保存与恢复数据的场景,如玩游戏时的中间结果的存档功能。

  • 需要提供一个可回滚操作的场景,如 Word、记事本、Photoshop,idea等软件在编辑时按 Ctrl+Z 组合键,还有数据库中事务操作。

6.11 解释器模式

6.11.1 概述

如上图,设计一个软件用来进行加减计算。我们第一想法就是使用工具类,提供对应的加法和减法的工具方法。

//用于两个整数相加
public static int add(int a,int b){
    return a + b;
}

//用于两个整数相加
public static int add(int a,int b,int c){
    return a + b + c;
}

//用于n个整数相加
public static int add(Integer ... arr) {
    int sum = 0;
    for (Integer i : arr) {
        sum += i;
    }
    return sum;
}

上面的形式比较单一、有限,如果形式变化非常多,这就不符合要求,因为加法和减法运算,两个运算符与数值可以有无限种组合方式。比如 1+2+3+4+5、1+2+3-4等等。

显然,现在需要一种翻译识别机器,能够解析由数字以及 + - 符号构成的合法的运算序列。如果把运算符和数字都看作节点的话,能够逐个节点的进行读取解析运算,这就是解释器模式的思维。

解释器模式给定一个语言,定义它的文法表示,并定义一个解释器,这个解释器使用该标识来解释语言中的句子。

在解释器模式中,我们需要将待解决的问题,提取出规则,抽象为一种“语言”。比如加减法运算,规则为:由数值和+-符号组成的合法序列,“1+3-2” 就是这种语言的句子。

解释器就是要解析出来语句的含义。但是如何描述规则呢?

文法(语法)规则:

文法是用于描述语言的语法结构的形式规则。

expression ::= value | plus | minus
plus ::= expression ‘+’ expression   
minus ::= expression ‘-’ expression  
value ::= integer

注意: 这里的符号“::=”表示“定义为”的意思,竖线 | 表示或,左右的其中一个,引号内为字符本身,引号外为语法。

上面规则描述为 :

表达式可以是一个值,也可以是plus或者minus运算,而plus和minus又是由表达式结合运算符构成,值的类型为整型数。

抽象语法树:

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

用树形来表示符合文法规则的句子。

6.11.2 结构

解释器模式包含以下主要角色。

  • 抽象表达式(Abstract Expression)角色:定义解释器的接口,约定解释器的解释操作,主要包含解释方法 interpret()。

  • 终结符表达式(Terminal Expression)角色:是抽象表达式的子类,用来实现文法中与终结符相关的操作,文法中的每一个终结符都有一个具体终结表达式与之相对应。

  • 非终结符表达式(Nonterminal Expression)角色:也是抽象表达式的子类,用来实现文法中与非终结符相关的操作,文法中的每条规则都对应于一个非终结符表达式。

  • 环境(Context)角色:通常包含各个解释器需要的数据或是公共的功能,一般用来传递被所有解释器共享的数据,后面的解释器可以从这里获取这些值。

  • 客户端(Client):主要任务是将需要分析的句子或表达式转换成使用解释器对象描述的抽象语法树,然后调用解释器的解释方法,当然也可以通过环境角色间接访问解释器的解释方法。

6.11.3 案例实现

【例】设计实现加减法的软件

代码如下:

//抽象角色AbstractExpression
public abstract class AbstractExpression {
    public abstract int interpret(Context context);
}

//终结符表达式角色
public class Value extends AbstractExpression {
    private int value;

    public Value(int value) {
        this.value = value;
    }

    @Override
    public int interpret(Context context) {
        return value;
    }

    @Override
    public String toString() {
        return new Integer(value).toString();
    }
}

//非终结符表达式角色  加法表达式
public class Plus extends AbstractExpression {
    private AbstractExpression left;
    private AbstractExpression right;

    public Plus(AbstractExpression left, AbstractExpression right) {
        this.left = left;
        this.right = right;
    }

    @Override
    public int interpret(Context context) {
        return left.interpret(context) + right.interpret(context);
    }

    @Override
    public String toString() {
        return "(" + left.toString() + " + " + right.toString() + ")";
    }
}

///非终结符表达式角色 减法表达式
public class Minus extends AbstractExpression {
    private AbstractExpression left;
    private AbstractExpression right;

    public Minus(AbstractExpression left, AbstractExpression right) {
        this.left = left;
        this.right = right;
    }

    @Override
    public int interpret(Context context) {
        return left.interpret(context) - right.interpret(context);
    }

    @Override
    public String toString() {
        return "(" + left.toString() + " - " + right.toString() + ")";
    }
}

//终结符表达式角色 变量表达式
public class Variable extends AbstractExpression {
    private String name;

    public Variable(String name) {
        this.name = name;
    }

    @Override
    public int interpret(Context ctx) {
        return ctx.getValue(this);
    }

    @Override
    public String toString() {
        return name;
    }
}

//环境类
public class Context {
    private Map<Variable, Integer> map = new HashMap<Variable, Integer>();

    public void assign(Variable var, Integer value) {
        map.put(var, value);
    }

    public int getValue(Variable var) {
        Integer value = map.get(var);
        return value;
    }
}

//测试类
public class Client {
    public static void main(String[] args) {
        Context context = new Context();

        Variable a = new Variable("a");
        Variable b = new Variable("b");
        Variable c = new Variable("c");
        Variable d = new Variable("d");
        Variable e = new Variable("e");
        //Value v = new Value(1);

        context.assign(a, 1);
        context.assign(b, 2);
        context.assign(c, 3);
        context.assign(d, 4);
        context.assign(e, 5);

        AbstractExpression expression = new Minus(new Plus(new Plus(new Plus(a, b), c), d), e);

        System.out.println(expression + "= " + expression.interpret(context));
    }
}

6.11.4 优缺点

优点:

  • 易于改变和扩展文法。

    由于在解释器模式中使用类来表示语言的文法规则,因此可以通过继承等机制来改变或扩展文法。每一条文法规则都可以表示为一个类,因此可以方便地实现一个简单的语言。

  • 实现文法较为容易。

    在抽象语法树中每一个表达式节点类的实现方式都是相似的,这些类的代码编写都不会特别复杂。

  • 增加新的解释表达式较为方便。

    如果用户需要增加新的解释表达式只需要对应增加一个新的终结符表达式或非终结符表达式类,原有表达式类代码无须修改,符合 “开闭原则”。

缺点

  • 对于复杂文法难以维护。

    在解释器模式中,每一条规则至少需要定义一个类,因此如果一个语言包含太多文法规则,类的个数将会急剧增加,导致系统难以管理和维护。

  • 执行效率较低。

    由于在解释器模式中使用了大量的循环和递归调用,因此在解释较为复杂的句子时其速度很慢,而且代码的调试过程也比较麻烦。

6.11.5 使用场景

  • 当语言的文法较为简单,且执行效率不是关键问题时。

  • 当问题重复出现,且可以用一种简单的语言来进行表达时。

  • 当一个语言需要解释执行,并且语言中的句子可以表示为一个抽象语法树的时候。


Design patterns
https://xhablog.online/2023/07/13/Design patterns/
作者
Xu huaiang
发布于
2023年7月13日
许可协议