设计模式相关内容介绍

1、设计模式相关内容介绍

1.1、 设计模式概述

  • 创建型模式--------买建筑材料

    用于描述------怎样创建对象,它的主要特点是----------将对象的创建与使用分离,这样可以降低系统的耦合度,使用者不需要关心对象的创建细节。

    单例、原型、工厂、抽象工厂、建造者模式

  • 结构型模式--------用建筑材料搭房

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

    代理、适配器、桥接、装饰、外观、享元、组合

  • 行为型模式

    用于描述类和对象之间怎样相互协作共同完成单个对象无法单独完成的任务

    模板方法、策略、命令、责任链、状态、观察者、中介者、访问者、备忘录、解释器

1.2 、U ML图

1.3、软件设计原则

2、创建者模式

2.1、单例设计模式

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

单例模式的优点:

  • 控制资源的使用,通过线程同步来控制资源的并发访问;
  • 控制实例产生的数量,达到节约资源的目的;
  • 作为通信媒介使用,也就是数据共享,它可以在不建立直接关联的条件下,让多个不相关的线程或者进程之间实现通信

2.1.1 、单例模式的结构

单例模式主要有一下角色

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

2.1.2、单例模式的实现

单例设计模式分两类:

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

1、饿汉式----方式1(静态变量方式)

//类加载到内存后,就实例化一个单例,JVM保证线程安全
//缺点:无论是否用到,类加载时就完成实例化
public class SingletonStaticVar {

    // 1.私有构造方法 --------- 为什么私有构造方法 ----> 当私有构造方法以后,外界就访问不到这个构造方法
    private SingletonStaticVar() { }
    //外界访问不到之后,就一个方法都创建不了------> 那如何实例化对象呢 -----> 自己在该类中创建一个该类的对象,供外界使用

    // 2.在本类中创建该类对象, ------> 因为只能创建一次,所以用static修饰
    private static SingletonStaticVar singletonStaticVar = new SingletonStaticVar();

    // 3.提供一个公共的访问方式,让外界去访问该对象------>getInstance是可以换名字的,不过是一个返回对象的方法----
    // 不过最好用getInstance,业界都这样写
    public static SingletonStaticVar getInstance(){
        return singletonStaticVar;
    }

    /*
    * 其他的业务方法,无所谓,随便加
    * */
    public void printM() {
        System.out.println("M");
    }

    public void printQ() {
        System.out.println("Q");
    }
    
}

public class Client {
    public static void main(String[] args) {
        //创建singleton类的对象
        SingletonStaticVar instance = SingletonStaticVar.getInstance();
        System.out.println("创建的实例对象是:"+instance);

        SingletonStaticVar instance2 = SingletonStaticVar.getInstance();
        System.out.println("创建的实例对象2是:"+instance2);

        //判断创建的两个对象是不是同一个对象    == 判断的是两个对象的  内存地址 是否一样
        System.out.println(instance == instance2);

        /*
        * 然后就可以调用该类的方法了
        * */
        instance.printM();
        instance2.printQ();
    }
}

2.饿汉式----方式2-----(静态代码块)

public class SingletonStaticBlock {

    //1.私有构造方法
    private SingletonStaticBlock() {}

    //2.声明SingletonStaticBlock类型的变量------>初始值为null
    private static SingletonStaticBlock singletonStaticBlock;

    //3.在静态代码块中赋值
    static {
        singletonStaticBlock = new SingletonStaticBlock();
    }

    //4.对外提供一个获取该类对象的方法,自然也就是public
    public static SingletonStaticBlock getInstance(){
        return singletonStaticBlock;
    }
    
    /*
     * 其他的业务方法,无所谓,随便加
     * */
    public void printM() {
        System.out.println("M");
    }

    public void printQ() {
        System.out.println("Q");
    }
}

3.懒汉式

public class SingleLazyman {
    //私有构造方法
    private SingleLazyman() {}

    //声明单例类型的变量---->只是声明了该类型的变量,并没有进行赋值--->null
    private static SingleLazyman singleLazyman;

    //对外提供访问方式     当多线程的时候,可能存在线程安全的问题
    public static synchronized SingleLazyman getInstance() {
        //懒汉式和饿汉式的区别--->懒汉式是首次使用该类的对象的时候,才会被创建---->为了只创建一次---->所以判断该类对象是否被创建了
        //如果该类为null,那么便是第一次,创建(static很关键)
        if( singleLazyman == null){
            singleLazyman = new SingleLazyman();
            return singleLazyman;
        }
        //如果已经创建了该类,那么便 不再创建,
        return singleLazyman;
    }
    /*
    * 该类的一些方法
    * */
    public void printM() {
        System.out.println("M");
    }
    public void printA() {
        System.out.println("A");
    }

}

4.懒汉式-双重检查锁

讨论:懒汉模式加锁的问题

对于getInstance()方法来说,绝大部分的操作都是读操作,读操作是线程安全的,所以我们没有必要让每个线程必须持有锁才能调用该方法,我们需要调整加锁的时机========》由此产生了一种新的实现模式:双重检查锁!!!!!

package com.njust.singleton.lazyman_double_check_lock;

/**
 * @program: ruoyi
 * @description: 单例模式-----懒汉式-----双重检查锁方式
 * @author: tjj
 * @create: 2021-09-24 20:57
 **/
//双重检查锁是一种很好的单例模式,解决了单例,性能,线程安全的问题
//存在问题:在多线程情况下,可能会出现空指针异常的问题--->原因:JVM在实例化对象的时候会进行优化和指令重排序操作
// ------->解决,使用volatile关键字!!!
public class SingleLazymanDoubleLock {
    //私有构造方法
    private SingleLazymanDoubleLock() {}

    //声明该单例模式对象的变量
    private static volatile SingleLazymanDoubleLock singleLazymanDoubleLock;

    //对外提供公共的访问方式
    public static SingleLazymanDoubleLock getInstance() {
        //第一次判断,如果singleLazymanDoubleLock的值不为null,那么就不要抢占锁,直接返回对象
        //如果为null,那么给它加一把锁
        if (singleLazymanDoubleLock == null) {
            synchronized (SingleLazymanDoubleLock.class) {
                //第二次判断
                if (singleLazymanDoubleLock == null) {
                    singleLazymanDoubleLock = new SingleLazymanDoubleLock();
                }
            }
        }
        return singleLazymanDoubleLock;
    }
    /*
    * 该类的一些方法
    * */
    public void printR() {
        System.out.println("R");
    }
}
public class Client {
    public static void main(String[] args) {
        SingleLazymanSync instance = SingleLazymanSync.getInstance();
    }
}

5.恶汉式-----枚举

枚举类实现单例模式是极力推荐的单例模式,因为枚举类型是线程安全的,并且只会装在一次,设计者充分利用了枚举的这个特性来实现单例模式

// 不考虑内存浪费空间的时候首选枚举类型
public enum SingletonEvilEnum {
    INSTANCE;
}
public class Client {
    public static void main(String[] args) {
        SingletonEvilEnum instance = SingletonEvilEnum.INSTANCE;
        SingletonEvilEnum instance2 = SingletonEvilEnum.INSTANCE;

        System.out.println(instance == instance2);
        //结果为true,可以看出枚举类的单例模式两次获取到的对象是同一个对象
    }
}

2.1.3 存在的问题

2.2.3.1 问题演示

破坏单例模式

使上面定义的单例类可以创建对各对象,枚举方式除外。--------->两种方式

  • 序列化

  • 反射

  • 序列化反序列化

Singleton类

2.2 、工厂模式

2.2.1、概述

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

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

image-20210928135055401
public class CoffeeStore {

    public Coffee orderCoffee(String type) {
        //声明Cofee类型的变量,根据不同的类型创建不同coffee子类对象
        Coffee coffee = null;
        //如果type传递的是美式咖啡
        if ("american".equals(type)) {
            coffee = new AmericaCoffee();
        } else if ("latte".equals(type)) {
            coffee = new LatteCoffee();
        } else {
            throw new RuntimeException("对不起,您所点的咖啡没有");
        }

        //加配料
        coffee.addMilk();
        coffee.addSuger();
        return coffee;
    }
}

美式咖啡和拿铁咖啡是咖啡类(Coffee)的子类,咖啡店在点咖啡的时候,只需要new

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

也就是咖啡店和美式咖啡、拿铁咖啡的解耦合。

介绍三种工厂模式的使用

  • 简单工厂模式(不属于GOF的23种经典设计模式)
  • 工厂方法模式
  • 抽象工厂模式

2.2.2、简单工厂模式

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

2.2.2.1、结构

简单工厂模式包含以下角色

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

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LYJpaT7U-1640244901390)(C:\Users\ASVS\AppData\Roaming\Typora\typora-user-images\image-20210929092312447.png)]

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

CoffeeStore对象和simplecoffeeFactory工厂对象的耦合,工厂对象和商品对象的耦合。

//SimpleCoffeeFactory和new AmericaCoffee()耦合//以后要是想要修改或者添加新的产品,那么势必会修改工厂对象里面的代码public class SimpleCoffeeFactory {   Coffee coffee = null;    //要生产哪一种咖啡呢,所以应该要传入一个字符串    public Coffee creatCoffee(String type) {        if ("american".equals(type)) {            coffee = new AmericaCoffee();        } else if ("latte".equals(type)) {            coffee = new LatteCoffee();        } else {            throw new RuntimeException("对不起,您所点的咖啡没有");        }        return coffee;    }}

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

2.2.2.3、优缺点

优点:

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

缺点:

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

2.2.2.4、扩展

静态工厂

在开发中也有一部分人将创建工厂的方法定义为静态的,这就是静态工厂模式但这也不是23种设计模式的其中一种

-------这样做在咖啡店就可以不用再去new一个SimpleCoffeeFactory,可以直接调用

public class CoffeeStore {    public Coffee orderCoffee(String type) {       /* SimpleCoffeeFactory simpleCoffeeFactory = new SimpleCoffeeFactory();        Coffee coffee = simpleCoffeeFactory.creatCoffee(type);*/        Coffee coffee = SimpleCoffeeFactory.creatCoffee(type);        //加配料        coffee.addMilk();        coffee.addSuger();        return coffee;    }}
public class SimpleCoffeeFactory {
    //要生产哪一种咖啡呢,所以应该要传入一个字符串
    public static Coffee creatCoffee(String type) {
        Coffee coffee = null;
        if ("american".equals(type)) {
            coffee = new AmericaCoffee();
        } else if ("latte".equals(type)) {
            coffee = new LatteCoffee();
        } else {
            throw new RuntimeException("对不起,您所点的咖啡没有");
        }
        return coffee;
    }
}

2.2.3、工厂方法模式

针对上例中的缺点,使用工厂方法模式就可以完美解决,避开开闭原则

2.2.3.1 、概念

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

2.2.3.2、 结构

工厂方法模式的主要角色

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

使用工厂模式对上述进行改进

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dnn4lrmu-1640244901393)(C:\Users\ASVS\AppData\Roaming\Typora\typora-user-images\image-20210929104713734.png)]

​ 要增加产品类时,只需要增加对应产品的工厂类,然后继承工厂类,这样就不用去修改工厂类的方法,也就解决了简单工厂的缺点。

  • 工厂方法模式时简单工厂模式进一步的抽象。

优点:

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

缺点:

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

2.2.4、抽象工厂模式

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

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

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

image-20210929152515503
2.2.4.1、概念

​ 是一种为访问类提供一个创建一组相关或相互依赖对象的接口,且访问类无须指定所要产品的具体类,就能得到同族不同等级产品的模式结构。

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

2.2.4.2、结构

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

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

​ 现在咖啡店业务发生了改变,不仅要生产咖啡还要生产甜点(提拉米苏、抹茶慕斯),要是按照工厂方法模式,需要定义提拉米苏类,抹茶慕斯类,提拉米苏工厂类,抹茶慕斯工厂类、甜点工厂类,很容易发生类爆炸情况,其中拿铁咖啡、美式咖啡是一个产品等级 ,提拉米苏、抹茶慕斯是一个产品等级,拿铁咖啡和提拉米苏是同一产品族(意大利风味)。

美式咖啡和抹茶慕斯是同一产品族(美式风味)。所以这个案例可以使用抽象工厂模式实现,类图如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GUatRjOO-1640244901394)(C:\Users\ASVS\AppData\Roaming\Typora\typora-user-images\image-20210929163651786.png)]

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

2.2.4.4、优缺点

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

缺点:当产品族需要修改一个产品时,所有的工厂类都需要修改

2.2.4.5、使用场景
  • 当需要创建的对象是一系列相互关联或相互依赖的产品族时,如电器工厂中的电视机,洗衣机,空调等
  • 系统中有多个产品族,但是每次只使用其中的某一族产品,比如某人只喜欢穿某一个品牌的衣服和鞋子
  • 系统中提供了产品的类库,且所有产品的接口相同,客户端不依赖产品实例的创建细节和内部构造

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

2.2.5、模式扩展

简单工厂 + 配置文件解除耦合

可以通过工厂模式+配置文件的方式解除工厂对象和产品对象的耦合,在工厂类中加载配置文件中的全类名,并创建对象进行存储,客户端如果需要对象,直接进行获取即可。

第一步:定义配置文件

american=com.njust.factory.config_factory.AmericanCoffeelatte=com.njust.factory.config_factory.LatteCoffee

第二步:改进工厂类


2.3、原型模式

2.3.1、概述

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

2.3.2、结构

原型模式包含以下角色:

  • 抽象原型类:规定了具体原型对象必须创建的方法
  • 具体原型类:实现了抽象原型的clone()方法,它是可被复制的对象。
  • 访问类:使用具体原型对象的clone()方法来复制新的对象。

接口类图如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-a19VvgGy-1640244901396)(C:\Users\ASVS\AppData\Roaming\Typora\typora-user-images\image-20210930151010468.png)]

2.3.3、实现

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

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

java中提供了Object类中提供了clone()方法来实现浅克隆。cloneable接口是是上面类图中的抽象原型类,而实现了cloneable接口的子实现类,就是具体的原型类。代码如下:

2.3.4、实例

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ERuIlybk-1640244901397)(C:\Users\ASVS\AppData\Roaming\Typora\typora-user-images\image-20210930161302525.png)]

2.3.5、使用场景

  • 对象的创建非常复杂,可以使用原型模式快捷创建对象-----------记住必须实现Cloneable方法
  • 性能和安全性要求比较高

2.3.6、扩展----深克隆

将上述Citation中的name属性修改为Student属性(引用数据类型)------>(引用数据类型的特点和基本数据类型的特点是一样的)

2.4、建造者模式

2.4.1、概述

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UHqOnlPc-1640244901407)(C:\Users\ASVS\AppData\Roaming\Typora\typora-user-images\image-20211003123914220.png)]

  • 分离了部件的构造(由Builder来负责)和装配(由Director负责)。从而可以构造出复杂的对象。这个模式适用于:某个对象的构建过程复杂的情况。
  • 由于实现了构建和装配的解耦。不同的构建器,相同的装配,也可以做出不同的对象;相同的构建器,不同的装配顺序也可以做出不同的对象。也就是实现了构建算法、装配算法的解耦,实现了更好的复用。
  • 建造者模式可以将部件和其组装过程分开,一步一步创建一个复杂的对象。用户只需要指定复杂对象的类型就可以得到该对象,而无须知道其内部的具体构造细节。

2.4.2、结构

建造者模式包含以下角色:

  • 抽象建造者类(Builder):这个接口规定要实现复杂对象的哪些部分的创建,并不涉及具体的对象部件的创建。
  • 具体建造者类(ConcreteBuilder):实现Builder接口,完成复杂产品的各个部件的具体创建方法。在构造过程完成后,提供产品的实例。
  • 产品类(Product):要创建的复杂对象
  • 指挥者类(Director):调用具体建造者 来创建复杂对象的各个部件,在指挥者中,不涉及具体产品的信息,只负责保证对象完整创建,或按照某种顺序创建

类图:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8KEJHcdE-1640244901410)(C:\Users\ASVS\AppData\Roaming\Typora\typora-user-images\image-20211003130603410.png)]

2.4.3、实例

生产共享单车

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

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

image-20211003131112820

注意:

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

是不是和工厂模式有点像,其实不是---->

  • 一个是应对对象生成的解耦,一个是用来应对复杂对象的生成,有点区别
  • 一个是对象的复杂生产,一个是对象的量产

2.4.4、优缺点

优点:

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

缺点:

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

2.4.5、使用场景

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

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

2.4.6、模式扩展

建造者模式除了上述的用途以外,在开发中还有一个常用的使用方式。

当类构造器需要传入很多参数时,如果创建这个类的实例,代码的可读性非常差,而且容易引入很多错误,此时就可以利用建造者模式重构。

重构前代码如下:

public class Phone {

    private String screen;

    private String cpu;

    private String memory;

    private String mainboard;

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

    public String getScreen() {
        return screen;
    }

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

    public String getCpu() {
        return cpu;
    }

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

    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{" +
                "screen='" + screen + '\'' +
                ", cpu='" + cpu + '\'' +
                ", memory='" + memory + '\'' +
                ", mainboard='" + mainboard + '\'' +
                '}';
    }
}
public class Client {    public static void main(String[] args) {        Phone phone = new Phone("lcd", "intel", "金斯顿", "amd");        System.out.println(phone);    }}

重构后代码

public class Phone {

    private String screen;

    private String cpu;

    private String memory;

    private String mainboard;


    //私有构造方法,需要传递一个构建器
    //把Builder中的成员变量传递给Phone手机的成员变量
    private Phone(Builder builder) {
        this.screen = builder.screen;
        this.cpu = builder.cpu;
        this.memory = builder.memory;
        this.mainboard = builder.mainboard;

    }

    public static final class Builder {
        private String screen;

        private String cpu;

        private String memory;

        private String mainboard;

        /**
         * 进行组件的构建--------lombok注解可以完成以下工作,@Builder
         */
        public Builder screen(String screen) {
            this.screen = screen;
            //返回Builder对象,也就是当前对象
            return this;
        }

        public Builder cpu(String cpu) {
            this.cpu = cpu;
            //返回Builder对象,也就是当前对象
            return this;
        }
        public Builder memory(String memory) {
            this.memory = memory;
            //返回Builder对象,也就是当前对象
            return this;
        }
        public Builder mainboard(String mainboard) {
            this.mainboard = mainboard;
            //返回Builder对象,也就是当前对象
            return this;
        }
        //使用构建这创建Phone对象
        public Phone build() {
            //把当前的builder对象传递过去
            return new Phone(this);
        }

    }

    @Override
    public String toString() {
        return "Phone{" +
                "screen='" + screen + '\'' +
                ", cpu='" + cpu + '\'' +
                ", memory='" + memory + '\'' +
                ", mainboard='" + mainboard + '\'' +
                '}';
    }
}
public class Client {
    public static void main(String[] args) {
        //这里不能在使用 Phone phone = new Phone("lcd", "intel", "金斯顿", "amd");
        //而是应该使用构建者进行创建手机对象
        Phone phone = new Phone.Builder()
                .cpu("intel")
                .screen("三星屏幕")
                .mainboard("惠普")
                .memory("银士顿")
                .build();
        /**
         * 这样去做,比起对于构建了什么组件,一目了然
         */
        System.out.println(phone);
    }
}

2.5、创建者模式 各模式对比

建造者侧重于 零件—>产品

工厂方法侧重于 一步到位生成产品

抽象工厂 一步到位生产个系列的产品

2.5.1、工厂方法模式VS建造者模式

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

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

2.5.2、抽象工厂模式VS建造者模式

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

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

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

3、结构型模式

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

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

结构型模式分为七种:

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

3.1、代理模式

3.1.1 概述

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

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

3.1.2、结构

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

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

3.1.3、静态代理

【案例】火车站卖票

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

3.1.4、JDK动态代理

使用动态代理来实现上述案例。

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

程序运行时在内存中动态生成。

public interface SellTickets {
    void sell();
}
public class TrainStation implements SellTickets {
    @Override
    public void sell() {
        System.out.println("火车站卖票了");
    }
}
public class ProxyFactory {
    //声明目标对象-----即火车站对象TrainStation
    private TrainStation trainStation = new TrainStation();
    //获取代理对象的方法
    public SellTickets getProxyObject() {
        //返回代理对象即可
        /**Proxy.newProxyInstance()的三个参数
         * ClassLoader loader,类加载器--->用于加载代理类。   可以通过目标对象获取类加载器
         * Class<?>[] interfaces,代理类实现的接口的字节码对象
         *InvocationHandler h,  代理对象的调用处理程序
         */
        SellTickets proxyObject = (SellTickets) Proxy.newProxyInstance(
                trainStation.getClass().getClassLoader(),
                trainStation.getClass().getInterfaces(),
                new InvocationHandler() {
                    /**
                     *
                        Object proxy:代理对象 。  和proxyObject是同一个对象,在invoke中基本不用
                        Method method: 对接口中的方法进行封装的method对象
                        Object[] args: 调用方法的实际参数----->调用的sell方法的参数
                        返回值:方法的返回值---->是通过代理对象调用的sell方法
                     */
                    @Override
                    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                        System.out.println("invoke方法执行了");
                        System.out.println("JDK动态代理点收服务费了,虽然更贵,但是更方便了,也不用直接去火车站排队买票了");
                        //执行目标对象的方法   先采用反射的方式.执行的是trainStation里面的sell方法
                        Object obj = method.invoke(trainStation, args);
                        return obj;//因为sell方法是没有返回值的,所以obj就是null
                    }
                }
        );
        return proxyObject;
    }
}
public class Client {
    public static void main(String[] args) {
        //获取代理对象-----通过代理工厂来获取代理对象
        ProxyFactory proxyFactory = new ProxyFactory();
        //使用工厂对象的方法获取代理对象
        SellTickets proxyObject = proxyFactory.getProxyObject();
        //调用卖票的方法,其根本的调用方法是 内部类实现的invoke()方法。
        proxyObject.sell();
    }
}

思考以下问题

ProxyFactory是代理类吗?

ProxyFactory不是代理模式中的代理类,而代理类是程序运行过程中动态在内存中生成的类。

JDK动态代理执行流程:

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

3.1.5、CGLIB动态代理

同样是上述案例,再次使用CGLIB实现

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

CGLIB是一个强大的代码生成包,他为没有实现接口的类提供代理,为JDK动态代理提供了很好的补充

3.1.6、三种代理对比

  • JDK与CGLIB

    接口,就是用JDK动态代理。没有接口就使用CGLIB

  • 动态代理与静态代理

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

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

3.1.7、优缺点

优点

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

缺点

  • 增加了系统的复杂度

3.1.7、使用场景

  • 远程代理

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

  • 防火墙代理

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

  • 保护代理

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

3.2、适配器模式

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tsmwu8oY-1640244901412)(C:\Users\ASVS\AppData\Roaming\Typora\typora-user-images\image-20211004114253141.png)]

定义:

  • 将一个类的接口转换成客户希望的另外一个接口,使得原本由于接口不兼容而不能一起工作的那些类能一起工作。
  • 适配器模式分为类适配器模式和对象适配器模式,前者类之间的耦合度比后者高,且要求程序员了解现有组件库中的相关组件的内部结构,所以应用相对较少些。

3.2.1、结构

适配器主要包含一下角色:

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

3.2.2、类适配器模式

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

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6A9foIrK-1640244901414)(C:\Users\ASVS\AppData\Roaming\Typora\typora-user-images\image-20211004141229052.png)]

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

3.2.3、对象适配器模式

实现方式:对象适配器模式可以采用将现有组件库中已经实现了的组件引入到适配器类中,该类同时实现当前系统的业务接口

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YRguZr6w-1640244901416)(C:\Users\ASVS\AppData\Roaming\Typora\typora-user-images\image-20211004144951369.png)]

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

3.2.4、 应用场景

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

Attention:静态代理是扩展功能,而适配器模式是改变接口

3.2.5、JDK源码解析

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

InputStreamReader继承自java.io中的Reader,对他抽象未实现的方法进行了实现。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qbYsRfSr-1640244901417)(C:\Users\ASVS\AppData\Roaming\Typora\typora-user-images\image-20211004162210215.png)]

3.3、 装饰者模式

3.3.1、概述

看一个快餐店的例子

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

如果按照传统,那么就是去继承

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DGHoIC0v-1640244901418)(C:\Users\ASVS\AppData\Roaming\Typora\typora-user-images\image-20211004162733670.png)]

但是:使用继承存在以下问题:

  • 扩展性不好

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

  • 产生过多的子类。

装饰者模式定义:指在不改变现有对象结构的情况下,动态的给该对象增加一些职责(即增加其额外的功能)的模式

3.3.2、结构

装饰者(Decarator)模式中的角色

  • 抽象构件(Component)角色:定义一个抽象接口 以规范 准备接收附加责任的对象。
  • 具体构件(Concrete Component)角色:实现抽象构件,通过装饰角色为其添加一些职责
  • 抽象装饰(Decorate)角色:继承或实现抽象构件,并包含具体构件的实例,可以通过其子类扩展具体构件的功能。
  • 具体装饰(Concrete Decorate)角色:实现抽象装饰的相关方法,并给具体构件对象添加附加的责任

3.3.3、案例

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yyfCsL5l-1640244901419)(C:\Users\ASVS\AppData\Roaming\Typora\typora-user-images\image-20211004165038977.png)]

Garnish继承了FastFood,又聚合了FastFood

3.3.4、优缺点

优点:

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

3.3.5、使用场景

  • 当不能采用继承的方式对系统进行扩充或者采用继承不利于系统扩展和维护时。不能采用继承的情况主要有两类:

    不能采用继承的情况有两种

    • 第一类是系统中存在大量的扩展,为支持每一种组合将产生大量的子类,使得子类呈爆炸性增长;

    • 第二类是类定义不能继承(final修饰)

  • 在不影响其他对象的情况下,以动态、透明的方式给单个对象添加职责

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

3.3.6、JDK源码解析

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Plu0haZc-1640244901422)(C:\Users\ASVS\AppData\Roaming\Typora\typora-user-images\image-20211004215525781.png)]

3.3.7、静态代理和装饰者的区别

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JnQCv2vw-1640244901424)(C:\Users\ASVS\AppData\Roaming\Typora\typora-user-images\image-20211004215723384.png)]

3.4、桥接模式

3.4.1、概述

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-eJTic1lq-1640244901425)(C:\Users\ASVS\AppData\Roaming\Typora\typora-user-images\image-20211004220430616.png)]

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

试想,在一个有多种可能会变化的维度的系统中,用继承会造成类爆炸,扩展不灵活,每次在一个维度上新增一个具体实现都要增加多个子类。 这种情况就可以使用桥接模式。

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

3.4.2、结构

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

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

3.4.3、案例

视频播放器

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CEmACL4Q-1640244901426)(C:\Users\ASVS\AppData\Roaming\Typora\typora-user-images\image-20211004225355005.png)]

3.4.4、优点

  • 桥接模式提高了系统的可扩充性,在两个变化维度中任意扩展一个维度,都不会影响议原有系统

    比如:如果现在还有一种wmv格式的视频,我们只需要一个类去实现VideoFile接口即可,其他类不需要发生变化

  • 实现细节对客户透明

3.4.5、使用场景

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

3.5、外观模式

3.5.1、概述

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

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-19Awc1D2-1640244901427)(C:\Users\ASVS\AppData\Roaming\Typora\typora-user-images\image-20211005160512784.png)]

3.5.2、结构

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

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

3.5.3、案例

【智能家电控制】

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-uKpSzXyM-1640244901429)(C:\Users\ASVS\AppData\Roaming\Typora\typora-user-images\image-20211005161309475.png)]

3.5.4、优缺点

好处

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

缺点

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

3.5.4、使用场景

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

3.5.5、源码分析

3.6、组合模式

3.6.1、概述

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-o5FfSI91-1640244901430)(C:\Users\ASVS\AppData\Roaming\Typora\typora-user-images\image-20211005202631582.png)]

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

组合模式就是为了解决这个问题

组合模式定义:又名部分整体模式,是用于一组相似的对象当作一个单一的对象。组合模式依照树形结构来组合对象,用来表示部分以及整体层次。 它创建了对象组的树形结构。

3.6.2、结构

组合模式主要包含三种结构:

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

3.6.3、案例

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6nhh2w3b-1640244901432)(C:\Users\ASVS\AppData\Roaming\Typora\typora-user-images\image-20211005204841223.png)]

类图如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SiBZiTkI-1640244901433)(C:\Users\ASVS\AppData\Roaming\Typora\typora-user-images\image-20211005204954878.png)]

代码实现:

不管是菜单还是菜单项,都应继承自统一的接口,这里姑且将这个统一的接口称为菜单组件

3.6.3、优点

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

3.6.4、使用场景

组合模式正是应树形结构而生,哪儿有树形结构那儿就有组合模式。

3.7、享元模式

3.7.1、概述

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

3.7.2、结构

享元模式存在两种状态:

  • 内部状态:不会随环境的改变而改变的可共享部分
  • 外部状态:环境的改变而改变的不可共享部分

享元模式的主要要领:区分应用中的这两种状态,并将外部状态外部化。

享元模式角色:

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

3.7.3、案例实现

俄罗斯方块

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jlre7ZqF-1640244901435)(C:\Users\ASVS\AppData\Roaming\Typora\typora-user-images\image-20211006163046379.png)]

类图:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pZsTVK5C-1640244901436)(C:\Users\ASVS\AppData\Roaming\Typora\typora-user-images\image-20211006163245280.png)]

3.7.4、优缺点

优点:

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

缺点:

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

3.7.5、使用场景

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

3.7.6、JDK源码解析

Integer类使用了享元模式

4、行为型模式

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

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

行为型模式:

  • 类行为模式

    • 模板方法模式
    • 解释器模式
  • 对象行为型模式

    • 策略模式
    • 命令模式
    • 责任链模式
    • 状态模式
    • 观察者模式
    • 中介者模式
    • 迭代器模式
    • 访问者模式
    • 备忘录模式

4.1、模板方法模式

4.1.1、概述

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

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

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

4.1.2、结构

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

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

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

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

      • 抽象方法(Abstract Method):由抽象类声明、由其具体子类实现
      • 具体方法(Concrete Method):由一个抽象类或者具体类声明 并实现,其子类也可以进行覆盖也可以直接继承
      • 钩子方法(Hook Method):在抽象类中已经实现,包括用于判断的逻辑方法需要子类重写的空方法两种

    一般钩子方法都是用于判断的逻辑方法,该类的方法名一般为isXxxx,返回值类型为Boolean

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

4.1.3、案例实现

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VmZoUw0c-1640244901438)(C:\Users\ASVS\AppData\Roaming\Typora\typora-user-images\image-20211006200611695.png)]

4.1.4、优缺点

优点

  • 提高了代码的复用性

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

  • 实现了反向控制

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

缺点

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

4.1.5、使用场景

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

4.1.6、JDK源码分析

InputStream使用了模板模式

4.2、策略模式

4.2.1、概述

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TCygnOKW-1640244901439)(C:\Users\ASVS\AppData\Roaming\Typora\typora-user-images\image-20211006223310355.png)]

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

4.2.2、结构

策略模式的主要角色:

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

4.2.3、案例实现

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HpjtDLF0-1640244901439)(C:\Users\ASVS\AppData\Roaming\Typora\typora-user-images\image-20211006232448582.png)]

4.2.4、优缺点

优点:

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

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

  • 增加一个新的策略只需要添加一个具体的策略类即可,基本不需要改变原有的代码,符合"开闭原则”避免使用多重条件选择语句(if else),充分体现面向对象设计思想。

缺点:

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

4.2.5、使用场景

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

4.2.6、源码分析

Comparator中的策略模式,

4.3、命令模式

4.3.1、概述

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PIe01UkL-1640244901441)(C:\Users\ASVS\AppData\Roaming\Typora\typora-user-images\image-20211007125420762.png)]

定义:

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

4.3.2、结构

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

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

4.3.3、案例实现

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

厨师:接收者角色,真正命令的执行对象

订单:命令中包含订单

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nJeu8RPa-1640244901442)(C:\Users\ASVS\AppData\Roaming\Typora\typora-user-images\image-20211007132515729.png)]

4.3.4、优缺点

优点:

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

缺点:

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

4.3.5、使用场景

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

4.3.6、JDK源码解析

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

4.4、责任链模式

4.4.1、概述

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

定义

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

4.4.2、结构

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

4.4.3、案例实现

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NAGjc0C7-1640244901443)(C:\Users\ASVS\AppData\Roaming\Typora\typora-user-images\image-20211007152520717.png)]

4.4.4、优缺点

优点:

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

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

  • 增强了系统的可扩展性

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

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

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

  • 责任链简化了对象之间的连接
    一个对象只需保持一个指向其后继者的引用,不需保持其他所有处理者的引用,这避免了使用众多的if 或者if…else 语句。

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

缺点:

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

4.4.5、使用场景

4.4.6、JDK源码解析

FilterChain

4.5、状态模式

4.5.1、概述

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2rPYEJTz-1640244901444)(C:\Users\ASVS\AppData\Roaming\Typora\typora-user-images\image-20211007163310702.png)]

public interface ILift {    //定义四个电梯状态的常量    int OPENING_STATE = 1;    int CLOSEING_STATE = 2;    int RUNNING_STATE = 3;    int STOPING_STATE = 4;    //设置电梯状态的功能    void setState(int state);    //电梯操作的功能    void open();    void close();    void run();    void stop();}
public class Lift implements ILift{

    //声明一个记录当前电梯的状态
    private int state;

    @Override
    public void setState(int state) {
        //把局部变量赋值给成员变量即可
        this.state = state;
    }

    @Override
    public void open() {
        switch (state) {//state表示当前电梯状态
            case OPENING_STATE:
                //什么事都不做
                break;
            case CLOSEING_STATE:
                //执行打开操作
                System.out.println("电梯打开");
                //设置当前电梯状态为开启状态
                this.setState(OPENING_STATE);
                break;
            case STOPING_STATE:
                //执行打开操作
                System.out.println("电梯打开");
                // 设置当前电梯状态为开启状态
               this.setState(OPENING_STATE);
                break;
            case RUNNING_STATE:
                //什么事都不做
                break;
        }
    }

    @Override
    public void close() {
        switch (state) {//state表示当前电梯状态
            case OPENING_STATE:
                System.out.println("关闭电梯");
                setState(CLOSEING_STATE);
                break;
            case CLOSEING_STATE:

                break;
            case STOPING_STATE:

                break;
            case RUNNING_STATE:
                System.out.println("关闭电梯");
                setState(CLOSEING_STATE);
                break;
        }

    }

    @Override
    public void run() {
        switch (state) {//state表示当前电梯状态
            case OPENING_STATE:
                System.out.println("电梯处于开启状态,不可运行");
                break;
            case CLOSEING_STATE:
                System.out.println("运行电梯");
                this.setState(RUNNING_STATE);
                break;
            case STOPING_STATE:
                System.out.println("运行电梯");
                this.setState(RUNNING_STATE);
                break;
            case RUNNING_STATE:
                //什么事都不做
                break;
        }
    }

    @Override
    public void stop() {
        switch (state) {//state表示当前电梯状态
            case OPENING_STATE:
                System.out.println("停止电梯");
                setState(STOPING_STATE);
                break;
            case CLOSEING_STATE:
                System.out.println("停止电梯");
                setState(STOPING_STATE);
                break;
            case STOPING_STATE:

                break;
            case RUNNING_STATE:
                System.out.println("停止电梯");
                setState(STOPING_STATE);
                break;
        }
    }
}
public class Client {
    public static void main(String[] args) {
        Lift lift = new Lift();
        //设置当前电梯的状态
        lift.setState(ILift.OPENING_STATE);

        //开门
        lift.open();

        lift.close();

        lift.run();

        lift.close();

        lift.stop();
    }
}

问题分析:

  • 使用了大量的switch…case这样的判断(if…else也是一样),使程序的可阅读性变差。扩展性很差。

  • 如果新加了断电的状态,我们需要修改上面判断逻辑

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

4.5.2、结构

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

  • 环境角色(Context):也称上下文,它定义了客户程序需要的接口,维护一个当前的状态,并将与状态相关的操作委托给当前状态对象处理。
  • 抽象状态角色(State):

4.5.3、案例实现

对上述电梯案例使用状态模式进行改进

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TfdjNizf-1640244901445)(C:\Users\ASVS\AppData\Roaming\Typora\typora-user-images\image-20211007183219317.png)]

4.5.4、优缺点

优点:

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

缺点:

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

4.5.5、使用场景

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

4.6、观察者模式

4.6.1、概述

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

4.6.2、结构

观察者模式角色:

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

4.6.3、案例实现

微信公众号

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-EzRLbsAc-1640244901446)(C:\Users\ASVS\AppData\Roaming\Typora\typora-user-images\image-20211007193938172.png)]

4.6.4、优缺点

优点:

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

缺点:

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

4.6.5、使用场景

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

4.7、中介者模式

4.7.1、概述

​ 一般来说,同事类之间的关系是比较复杂的,多个同事类之间互相关联时,他们之间的关系会呈现为复杂的网状结构,这是一种过度耦合的架构,即不利于类的复用,也不稳定。例如在下左图中,有六个同事类对象,假如对象1发生变化,那么将会有4个对象受到影响。如果对象2发生变化,那么将会有5个对象受到影响。也就是说,同事类之间直接关联的设计是不好的。

​ 如果引入中介者模式,那么同事类之间的关系将变为星型结构,从下右图中可以看到,任何一个类的变动,只会影响的类本身,以及中介者,这样就减小了系统的耦合。一个好的设计,必定不会把所有的对象关系处理逻辑封装在本类中,而是使用一个专门的类来管理那些不属于自己的行为。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bfILbv30-1640244901447)(C:\Users\ASVS\AppData\Roaming\Typora\typora-user-images\image-20211007204943671.png)]

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

4.7.2、结构

中介者模式角色:

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

4.7.3、案例实现

【租房】

现在租房基本都是通过房屋中介,房主将房屋托管给房屋中介,而租房者从房屋中介获取房屋信息。房屋中介充当租房者与房屋所有者之的中介者。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-f4YzsaKO-1640244901450)(C:\Users\ASVS\AppData\Roaming\Typora\typora-user-images\image-20211007205856627.png)]

4.7.4、优缺点

优点

  • 松散耦合

    中介者模式通过把多个同事对象之间的交互封装到中介者对象里面,从而使得同事对象之间松散耦合,基本上可以做到互补依赖。这样一来,同事对象就可以独立地变化和复用,而不再像以前那样"牵一处而动全身"了。

  • 集中控制交互

    多个同事对象的交互,被封装在中介者对象里面集中管理,使得这些交互行为发生变化的时候,只需要修改中介者对象就可以了,当然如果是已经做好的系统,那么就扩展中介者对象,而各个同事类不需要做修改

  • 一对多关联转变为一对一的关联

    没有使用中介者模式的时候,同事对象之间的关系通常是一对多的,引入中介者对象以后,中介者对象和同事对象的关系通常变成双向的一对一,这会让对象的关系更容易理解和实现。

缺点

​ 当同事类太多时,中介者的职责将很大,它会变得复杂而庞大,以至于系统难以维护。

4.7.5、使用场景

  • 系统中对象之间存在复杂的引用关系,系统结构混乱且难以理解。
  • 当想创建一个运行于多个类之间的对象,又不想生成新的子类时。

4.8、迭代器模式

4.8.1、概述

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

4.8.2、结构

迭代器模式主要包含以下角色:

  • 抽象聚合(Aggregate)角色:定义存储、添加、删除聚合元素以及创建迭代器对象的接口
  • 具体聚合(ConcreteAagcegate)角色:实现抽象聚合类,返回一个具体迭代器的实例。
  • 抽象迭代器(Iterator)角色:定义访问和遍历聚合元素的接口,通常包含hasNext ()、next ()等方法。
  • 具体迭代器(Concretelteratar)角色:实现抽象迭代器接口中所定义的方法,完成对聚合对象的遍历记录遍历的当前位置。

4.8.3、案例实现

【例】定义一个可以存储学生对象的容器对象,将遍历该容器的功能交由迭代器实现,涉及到的类如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WWI96GIQ-1640244901451)(C:\Users\ASVS\AppData\Roaming\Typora\typora-user-images\image-20211008131424933.png)]

4.8.4、优缺点

优点

  • 它支持以不同的方式遍历一个聚合对象,在同一个聚合对象上可以定义多种遍历方式。在迭代器模式中只需要用一个不同的迭代器来替换原有迭代器即可改变遍历算法,我们也可以自己定义迭代器的子类以支持新的遍历方式。
  • 迭代器简化了聚合类。由于引入了迭代器,在原有的聚合对象中不需要再自行提供数据遍历等方法,这样可以简化聚合类的设计。
  • 在迭代器模式中,由于引入了抽象层,增加新的聚合类和迭代器类都很方便,无须修改原有代码,满足“开闭原则”的要求。

缺点

  • 增加了类的个数,这在一定程度上增加了系统的复杂性

4.8.5、使用场景

  • 当需要为聚合对象提供多种遍历方式时。
  • 当需要为遍历不同的聚合结构提供一个统一的接口时。
  • 当访问一个聚合对象的内容而无须暴露其内部细节的表示时。

4.8.6、JDK源码解析

public class ListTest {
    public static void main(String[] args) {
        List<Student> list = new ArrayList<Student>();
        list.add(new Student("张安", "1231242414"));
        list.add(new Student("张sad", "1231242414"));
        list.add(new Student("张fvsda", "1231242414"));
        Iterator<Student> iterator = list.iterator();
        while (iterator.hasNext()) {
            Student student = iterator.next();
            System.out.println(student);
        }
    }
}
public class Suudent {
    private String name;
    private String num;

    public Suudent() {
    }

    public Suudent(String name, String num) {
        this.name = name;
        this.num = num;
    }

    public String getName() {
        return name;
    }

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

    public String getNum() {
        return num;
    }

    public void setNum(String num) {
        this.num = num;
    }

    @Override
    public String toString() {
        return "Suudent{" +
                "name='" + name + '\'' +
                ", num='" + num + '\'' +
                '}';
    }
}
  • List:抽象聚合类
  • ArrayList:具体的聚合类
  • Iterator:抽象迭代器
  • l.ist.iterator ()︰返回的是实现了Iterator接口的具体迭代器对象

4.9、访问者模式

4.9.1、概述

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

4.9.2、结构

访问者模式包含以下角色:

  • 抽象访问者(Visitor)角色:定义了对每一个元素(Element)访问的行为,它的参数就是可以访问的元素,它的方法个数理论上来讲与元素类个数(Element的实现类个数)是一样的,从这点不难看出,访问者模式要求元素类的个数不能改变。
  • 具体访问者(Concrete Visitor)角色:给出对每一个元素类访问时所产生的具体行为
  • 抽象元素(Element)角色:定义了一个接受访问者的方法( accept),其意义是指,每一个元素都要可以被访问者访问。
  • 具体元素(Concrete Element)角色:提供接受访问方法的具体实现,而这个具体的实现,通常情况下是使用访问者提供的访问该元素类的方法。
  • 对象结构(object structure)角色:定义当中所提到的对象结构,对象结构是一个抽象表述,具体点可以理解为一个具有容器性质或者复合对象特性的类,它会含有一组元素(Element),并且可以迭代这些元素,供访问者访问。

4.9.3、案例实现

【例】给宠物喂食

现在养宠物的人特别多,我们就以这个为例,当然宠物还分为狗,猫等,要给宠物喂食的话,主人可以喂,其他人也可以喂食。

  • 访问者角色:给宠物喂食的人
  • 具体访问者角色:主人、其他人
  • 抽象元素角色:动物抽象类
  • 具体元素角色:宠物狗、宠物猫
  • 结构对象角色:主人家

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DKhQ8Yhu-1640244901452)(C:\Users\ASVS\AppData\Roaming\Typora\typora-user-images\image-20211008150345563.png)]

4.9.4、优缺点

优点:

  • 扩展性好:

    在不修改对象结构中的元素的情况下,为对象结构中的元素添加新的功能

  • 复用性好

    通过访问者来定义整个对象结构通用的功能,从而提高复用程度。。

  • 分离无关行为

    通过访问者来分离无关的行为,把相关的行为封装在一起,构成一个访问者,这样每一个访问者的功能都比较单一。

缺点:

  • 对象结构变化很困难

    在访问者模式中,每增加一个新的元素类,都要在每一个具体访问者类中增加相应的具体操作,这违背了"开闭原则".

  • 违背了依赖倒置原则

    访问者模式依赖了具体类,而没有依赖抽象类。

4.9.5、使用场景

  • 对象结构相对稳定,但其操作算法经常变化的程序。
  • 对象结构中的对象需要提供多种不同且不相关的操作,而且要避免让这些操作的变化影响对象的结构。

4.9.6、双分派技术------????多态??

双份派的技术

1、分派:

​ 变量被声明时的类型叫做变量的静态类型,有些人又把静态类型叫做明显类型;而变量所引用的对象的真实类型又叫做变量的实际类型。比如Map map = new Hashwap(),map变量的静态类型是Map,实际类型是HashMap,。根据对象的类型而对方法进行的选择,就是分派(Dispatch),分派(Dispatch)又分为两种,即静态分派和动态分派。

​ 静态分派(Static Dispatch)发生在编译时期,分派根据静态类型信息发生。静态分派对于我们来说并不陌生,方法重载就是静态分派。

​ 动态分派(Dynamic Dispatch)发生在运行时期,动态分派动态地置换掉某个方法。Java通过方法的重写支持动态分派。

​ 所谓双分派技术就是在选择一个方法的时候,不仅仅要根据消息接收者〈(receiver)的运行时区别,还要根据参数的运行时区别。

4.10、备忘录模式

4.10.1、概述

​ 备忘录模式提供了一种状态恢复的实现机制,使得用户可以方便地回到一个特定的历史步骤,当新的状态无效或者存在问题时,可以使用暂时存储起来的备忘录将状态复原,很多软件都提供了撤销(Undo)操作,如Mord、记事本、Photoshop、IDEa等软件在编辑时按Ctrl+Z组合键时能撤销当前操作,使文档恢复到之前的状态;还有在浏览器中的后退键、数据库事务管理中的回滚操作、玩游戏时的中间结果存档功能、数据库与操作系统的备份操作、棋类游戏中的悔棋功能等都属于这类。

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

4.10.2、结构

  • 发起人(originator)角色:记录当前时刻的内部状态信息,提供创建备忘录恢复备忘录数据的功能,实现其他业务功能,它可以访问备忘录里的所有信息。
  • ·备忘录(Memento)角色:负责存储发起人的内部状态,在需要的时候提供这些内部状态给发起人。
  • 管理者(Caretaker)角色:对备忘录进行管理,提供保存与获取备忘录的功能,但其不能对备忘录的内容进行访问与修改
备忘录有两个等效的接口:
* 窄接口:管理者(Caretaker)对象(和其他发起人对象之外的任何对象)看到的是备忘录的窄接口(narLor Interface),这个窄接口只允许他把备忘录对象传给其他的对象。
* 宽接口:与管理者看到的窄接口相反,发起人对象可以看到一个宽接口(wide Interface),这个宽接口允许它读取所有的数据,以便根据这些数据恢复这个发起人对象的内部状态。

4.10.3、案例实现

【例】游戏挑战Boss

-游戏中的某个场景,一游戏角色有生命力、攻击力、防御力等数据,在打Boss前和后一定会不一样的,我们允许玩家如果感觉与Boss决斗的效果不理想可以让游戏恢复到决斗之前的状态。

要实现上述案例,有两种方式:

  • 白箱备忘录模式
  • 黑箱备忘录模式

**白箱备忘录模式 **

备忘录角色对任何对象都提供一个接口,即宽接口,备忘录角色的内部所存储的状态就对所有对象公开。类图如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5wJILtss-1640244901454)(C:\Users\ASVS\AppData\Roaming\Typora\typora-user-images\image-20211008174427593.png)]


黑箱备忘录模式

备忘录角色对发起人对象提供一个宽接口,而为其他对象提供一个窄接口。在Java语言中,实现双重接口的办法就是将备忘录类设计成发起人类的内部成员类。

将RolestateMemento设为cameRole的内部类,从而将RolestateMemento对象封装在cameRole里面;在外面提供一个标识接口Memento给Rolestatecaretaker及其他对象使用。这样cameRo1e类看到的是RolestateMemento 所有的接口,而Rolestatecaretaker及其他对象看到的仅仅是标识接口Memento所暴露出来的接口,从而维护了封装型。类图如下:

4.10.4、优缺点

优点:

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

缺点:

  • 资源消耗大。如果要保存的内部状态信息过多或者特别频繁,将会占用比较大的内存资源。

4.10.5、使用场景

  • 需要保存与恢复数据的场景,如玩游戏时的中间结果的存档功能。
  • 需要提供一个可回滚操作的场景,如Nord、记事本、Photoshop,idea等软件在编辑时按ctx.1+Z组合键,还有数据库中事务操作。

4.11、解释器模式

4.11.1、概述

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KAGccnnB-1640244901455)(C:\Users\ASVS\AppData\Roaming\Typora\typora-user-images\image-20211008202601942.png)]

如上图,设计一个软件用来进行加减计算。我们第一想法就是使用工具类,提供对应的加法和减法的工具方法.

上面的形式比较单一、有限,如果形式变化非常多,这就不符合要求,因为加法和减法运算,两个运算符与数值可以有无限种组合方式。比如1+2+3+4+5、1+2+3-4等等。

显然,现在需要一种翻译识别机器,能够解析由数字以及+-符号构成的合法的运算序列。如果把运算符和数字都看作节点的话,能够逐个节点的进行读取解析运算,这就是解释器模式的思维。

4.11.2、结构

4.11.3、案例实现

4.11.4、优缺点

4.11.5、使用场景

5、设计模式面试随笔

5.1、Spring中用到了那些设计模式

5.1.1、控制反转(IoC)和依赖注入(DI)

IoC(Inversion of Control,控制翻转) 是Spring 中一个非常非常重要的概念,它不是什么技术,而是一种解耦的设计思想。它的主要目的是借助于“第三方”(Spring 中的 IOC 容器) 实现具有依赖关系的对象之间的解耦(IOC容易管理对象,你只管使用即可),从而降低代码之间的耦合度。IOC 是一个原则,而不是一个模式,以下模式(但不限于)实现了IoC原则

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YTvPkr6P-1640244901455)(C:\Users\ASVS\AppData\Roaming\Typora\typora-user-images\image-20211011154024435.png)]

控制反转小tips:高层建筑决定需要什么,底层去实现这样的需求,但是高层并不用管底层是怎么实现的。这样就不会出现前面的“牵一发动全身”的情况。

控制反转(Inversion of Control) 就是依赖倒置原则的一种代码设计的思路。具体采用的方法就是所谓的依赖注入(Dependency Injection)。其实这些概念初次接触都会感到云里雾里的。说穿了,这几种概念的关系大概如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lgWOtRLA-1640244901456)(C:\Users\ASVS\AppData\Roaming\Typora\typora-user-images\image-20211011154458876.png)]

https://www.zhihu.com/question/23277575/answer/169698662

img

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值