设计模式(上)

79 阅读49分钟

这一章节的内容了解为主,直接看图吧

设计模式背景

这里我们要注意的是,设计模式并不是什么新的技术,他是前人的设计经验的一种总结

我的设计模式可以总分为三大类,分别是创建型模式、结构性模式和行为型模式,他们具体的不同就自己看图吧

UML

然后我们来了解下UML中的类图,UML下有多种不同的图,但是我们这里主要了解类图

接着来看一下在UML中我们的类以及类中的方法和变量的表示方式

在我们的实际编程中,我们的各种类中是存在各种各样的关系的,我们就一个个来学习,首先我们来学习最普遍的关联关系

关联关系的表示方式

关联关系又分为三种,分别是单向关联和双向关联以及自关联,单向关联简单来说就是一个类里引用了另一个类,这个我们非常熟悉了,就不再赘述了,直接看图吧

第二种情况是双向关联,简单来说就是你中有我,我中有你,最简单的例子就是顾客和商品,一个商品可以被多个顾客购买,一个顾客也可以购买多个商品,所以一个顾客对象里会有商品的成员变量,而商品对象里也会有顾客的成员变量

而自关联则使用得比较少,这种情况常见于我们的链表结构中,自己指向下一个自己,然后形成一个链表结构

我们上面重点要记住我们的各种关系在图上是如何表示的,以及他们各种关系在代码中所代表的实际意义就可以了

聚合关系和组合关系

然后我们来讲我们的聚合关系和组合关系,他们的本质其实都是一种关联关系,首先聚合关系是一种强关联关系,在聚合关系中,成员对象是整体对象的一部分,但是其成员可以脱离该整体而独立存在,比如学校和老师,即使学校停办了,老师也仍然存在

接着我们来讲组合关系,其本质是一种更强烈的聚合关系。其与聚合关系不同的是,其成员变量不能脱离整体而存在,比如头和嘴,头没了,那么嘴也不复存在了

依赖关系、继承关系和实现关系

然后我们来看看依赖关系,依赖关系是耦合度最弱的一种关联方式,也是我们最常用的一种关联关系。其是指某个类依赖另一个类,但是这个一类是只有在调用某一种方法或者是使用某一个参数时才会依赖该类的情况,比如说下图中的情况,Driver类中只有调用drive方法的时候,才会依赖car类,否则是不会有依赖的,这就是依赖关系

然后是继承关系,这个就不必多说了,懂的都懂,这里我们值得一提的是泛化关系是继承关系的另一种称呼

最后是实现关系,其实就是接口和实现类的关系,自己看图吧

开闭原则

然后我们来学习软件的设计原则,其下的设计原则有很多种,我们先来学习第一种原则,开闭原则

开闭原则的最简单的例子就是以搜狗输入法为例,我们可以将皮肤设计定义为一个抽象类,然后任何一个不同的皮肤都可以以实现类的形式接入到我们的软件中,用户修改皮肤,就不需要更改源代码,只需要自己增加一个实现类即可(当然,我们会有工具来帮忙生成这个实现类)

我们下面的类图表示的意思是,我们有两个类继承了一个抽象的父类,同时一个类实现了这个抽象的父类

开闭原则案例实现

这个案例非常简单,我就不写了,我们记住我们的开闭原则的本质是要对修改关闭,对扩展开放。也就是说,我们允许别人添加代码,但是我们不允许别人修改我们的代码,就是这么简单。

里氏代换原则

接着我们来学习里氏代换原则,其是面向对象涉及的基本原则之一,其基本原则就是任何父类可以出现的地方,子类一定可以出现。通俗的来说,子类可以扩展父类的功能,但是不可以改变父类原有的功能。

比方说我们可以举一个长方形和例子,首先我们构建长方形类的代码如下

public class Rectangle {
    private double length;
    private double width;
​
    public double getLength() {
        return length;
    }
​
    public void setLength(double length) {
        this.length = length;
    }
​
    public double getWidth() {
        return width;
    }
​
    public void setWidth(double width) {
        this.width = width;
    }
}

我们顺理成章的会让我们的正方形类继承我们的长方形类,那么我们还可以构造我们的正方形类的代码如下

public class Square extends Rectangle {
​
    public void setWidth(double width) {
        super.setLength(width);
        super.setWidth(width);
    }
    public void setLength(double length) {
        super.setLength(length);
        super.setWidth(length);
    }
}

接着我们写入我们的测试类的代码如下

public class RectangleDemo {
​
    public static void resize(Rectangle rectangle) {
        while (rectangle.getWidth() <= rectangle.getLength()) {
            rectangle.setWidth(rectangle.getWidth() + 1);
        }
    }
​
    //打印长方形的长和宽
    public static void printLengthAndWidth(Rectangle rectangle) {
        System.out.println(rectangle.getLength());
        System.out.println(rectangle.getWidth());
    }
​
    public static void main(String[] args) {
        Rectangle rectangle = new Rectangle();
        rectangle.setLength(20);
        rectangle.setWidth(10);
        resize(rectangle);
        printLengthAndWidth(rectangle);
​
        System.out.println("============");
​
        Rectangle rectangle1 = new Square();
        rectangle1.setLength(10);
        resize(rectangle1);
        printLengthAndWidth(rectangle1);
    }
}

这里我们的测试类提供了更改对应的长方形的边长的代码,我们更改长方形的边长的方法就是更改长方形对应的长宽,而正方形的话则是直接改其长和宽,那么我们的这行测试代码就是产生不断重复进入死循环的问题,为了解决这个问题,我们的一个方法就是我们用一个四边形的接口来实现正方形和长方形类,这样正方形和长方形不存在继承关系,那么就不会违背里氏代换原则了

最后我们来说一下图里的类图的关系,我们有两个类实现了一个四边形的父类,然后我们的测试类在执行某些方法的时候会依赖这个四边形接口的实现类,不过这里应该是依赖的关系画错了,画成了实线,应该是虚线的,而且这个线我觉得也不该对这个接口有指向,因为其依赖的是他的实现类而不是其四边形类

依赖倒转原则

接着我们来学习依赖倒转原则,所谓的依赖倒转原则,其实就是我们的开闭原则的ProMax版。这个原则讲起来很麻烦,但是其实本质很好理解

比方说我们要做一台电脑,电脑下有许多的配件,如果我们直接定义对应的实现类为成员变量的话,那么我们要更换成员变量的时候,我们就必须要重新修改原来的代码,那样就很麻烦,但如果我们将每个实现类的特点抽取出一个接口,然后用这些接口去代替我们的实现类,这样的话就可以达到我们所需要的更换效果,用新的实现类直接换就行了,不需要修改原来的代码

接口隔离原则

接着我们来学习接口隔离原则,所谓的接口隔离原则,简单理解就是如果子类继承父类时,同时继承了父类中自己不需要的方法,那么就应该将方法抽离成接口,以接口的形式赋予给父类和子类。

我们可以举一个安全门的例子,比方说我们要建造一个安全门,其拥有防火防盗防水的功能,于是我们就将这三个功能抽离成一个接口,但是如果我们还要建造一个安全门,其拥有防火防盗但没有防水的功能,此时我们如果还是用原来的接口,那么就会有一个功能没有用到,直接继承原来的类,也会是同样的情况,所以此时我们的改进方法就是将这三个功能都抽离出来,然后在对应的类中实现这三个技能即可,因为java是支持多继承的,因此创建多个接口是可行的

迪米特法则

迪米特法则简单来说,就是创建一个中间类,这个中间类用于帮助两个类的协作(当然,如果有一些必须要直接沟通,那么就不必创建中间类),这就类似于是明星和经纪人之间的关系,具体可以看图

最后我们要提一下的是就是迪米特法则里的朋友指的是当前对象本身、当前对象的成员变量、当前对象所创建的对象、当前对象的方法参数等

合成复用原则

接着我们来学习我们的最后一个原则,合成复用原则。合成复用原则指的是,我们的类如何和其他的类有关系,那么我们尽量使用组合或者是聚合等关联关系来实现,其次再考虑使用继承关系来实现。简单来说,如果我们可以将另一类令其成为一个类的成员变量来达到我们的使用的目的,那么就尽量用这种方法,而不要去使用继承关系,至于原因就请看图吧

我们可以举一个汽车分类管理程序的例子来帮助我们的理解,假如我们采用继承的方式来添加我们的汽车组合,那么我们要重新定义一个新的汽车类,然后还要令其继承原来的新汽车类,再定义颜色类,这样太麻烦了

但如果我们将其改为聚合复用,将颜色改为汽车的一个属性,那么我们一个就可以省略定义不同颜色的类了,依葫芦画瓢我们还可以将不同的汽车类也定义为属性,这样就可以省略很多类的构建了

单例模式

接着我们来学习第一种模式,创建型模式。创建型模式的主要特点是将对象的创建与使用分离,使用者不需要关注对象的创建细节,可以降低系统耦合度

创建型模式一共有五种,分别是单例模式、工厂方法模式、抽象工程模式、原型模式、建造者模式。

单例设计模式介绍

单例设计模式主要包含以下两个角色

1、单例类。只能创建一个实例的类

2、访问类。使用单例类

单例设计模式的设计分类总分为两种:

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

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

饿汉式

我们先来将饿汉式设计,饿汉式的设计又可以再细分为两种方式,一种是静态变量方式,另一种是静态代码块方式。我们先来讲第一种,我们可以写入其代码如下

/**
 * 饿汉式
 *      静态变量创建类的对象
 */
public class Singleton {
    //私有构造方法
    private Singleton() {}
​
    //在成员位置创建该类的对象
    private static Singleton instance = new Singleton();
​
    //对外提供静态方法获取该对象
    public static Singleton getInstance() {
        return instance;
    }
}

我们上面的代码就是采用静态变量方式的饿汉式设计,我们先设计一个静态对象变量并令其创建我们的当前对象,然后对外提供静态方法令其可以获得该静态对象。这里有两点需要提,第一点是我们的对外提供的方法为什么要是静态方法,因为如果我们不提供静态方法,那么使用者必须创建该对象才能使用该方法,然而使用者使用该方法就是为了创建过该对象,这样就俄罗斯套娃了,所以我们一开始就要提供可以获取该对象的静态方法,这样使用者即使不创建该类的对象也可以调用该方法。同时由于我们的方法是静态方法,我们的静态方法不可以调用非静态的成员变量,因此我们的成员变量也必须设计为是静态的。

最后是关于为什么静态方法不可以调用非静态变量的解释

接着我们来讲讲静态代码块的方式,其实静态代码块的方式的原理和静态变量的原理几乎一模一样,无非是创建对象的过程放到了静态代码块中了,其作用是如果我们的一些类的创建之前需要执行一些其他的代码操作的话,我们可以使用这种方式

/**
 * 饿汉式
 *      在静态代码块中创建该类对象
 */
public class Singleton {
​
    //私有构造方法
    private Singleton() {}
​
    //在成员位置创建该类的对象
    private static Singleton instance;
​
    static {
        instance = new Singleton();
    }
​
    //对外提供静态方法获取该对象
    public static Singleton getInstance() {
        return instance;
    }
}

值得一提的是饿汉式由于其变量都是静态变量,因此无论我们调用多少次其提供的静态方法,得到的对象都会是同一个对象。同时饿汉式存在的重大问题在于,如果说这个类一开始就被加载好了,但是却没有被使用,那么就会造成内存的上的浪费。

最后我们来讲一种重量级的饿汉式,就是枚举方式,直接创建一个枚举类然后其下写入对应的变量名即可,用户想要使用就直接调用枚举类里的变量即可,现在枚举方式也是我们极力推荐的一种单例实现模式。

那么我们可以写入其代码如下

public class User {
    //私有化构造函数
    private User(){ }
​
    //定义一个静态枚举类
    static enum SingletonEnum{
        //创建一个枚举对象,该对象天生为单例
        INSTANCE;
        private User user;
        //私有化枚举的构造函数
        private SingletonEnum(){
            user=new User();
        }
        public User getInstnce(){
            return user;
        }
    }
​
    //对外暴露一个获取User对象的静态方法
    public static User getInstance(){
        return SingletonEnum.INSTANCE.getInstnce();
    }
}

同时枚举的写法不但简单,而且无法被破坏,至于什么是破坏,我们留到后面再来讲

饿汉式讲完之后,接着我们来将懒汉式,懒汉式就可以避免内存浪费的问题,因为其是只有使用时才会创建对应的对象,懒汉式同样也有多种实现方式,我们挨个讲。

懒汉式

首先我们来讲第一种懒汉式方式,我们这种的原理跟之前的也差不多,只不过我们每次进入对应方法的时候都要判断该对象是否已经创建而已,其最后的结果同样也是遵从返回的都是同一个对象的结果的

先来说说枚举类实现单例模式懒汉式的代码

class User {
    //私有化构造函数
    private User(){ }
​
    //定义一个静态枚举类
    static enum SingletonEnum{
        //创建一个枚举对象,该对象天生为单例
        INSTANCE;
        private User user;
        //私有化枚举的构造函数
        public User getInstnce(){
            if(user==null){
                user = new User();
            }
            return user;
        }
    }
​
    //对外暴露一个获取User对象的静态方法
    public static User getInstance(){
        return SingletonEnum.INSTANCE.getInstnce();
    }
}

然后来看看我们普通方式实现的单例模式懒汉式的代码

/**
 * 懒汉式
 *  线程不安全
 */
public class Singleton {
    //私有构造方法
    private Singleton() {}
​
    //在成员位置创建该类的对象
    private static Singleton instance;
​
    //对外提供静态方法获取该对象
    public static Singleton getInstance() {
​
        if(instance == null) {
            instance = new Singleton();
        }
        return 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 Singleton { 
​
    //私有构造方法
    private Singleton() {}
​
    private static Singleton instance;
​
    //对外提供静态方法获取该对象
    public static Singleton getInstance() {
        //第一次判断,如果instance不为null,不进入抢锁阶段,直接返回实例
        if(instance == null) {
            synchronized (Singleton.class) {
                //抢到锁之后再次判断是否为null
                if(instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

我们可以看到我们这里先判断我们的对象是否为空,如果不为空则直接返回对应的对象,如果为空的话我们再执行同步代码块,判断我们的对象是否为空,若为空再执行创建即可。说实话,我其实不太理解为什么这样做就可以有效提高效率,但是这里我们总之先记住就可以了。

最后我们上面这份代码在多线程的情况下可能会出现空指针异常,其原因在于JVM在实例化对象时会进行优化和指令重排序操作,要避免这种问题就就要使用volatile关键字来保证其可见性可有序性(这方面的内容和多线程有关,这里就不深入讲了)

/**
 * 双重检查方式
 */
public class Singleton {
​
    //私有构造方法
    private Singleton() {}
​
    private static volatile Singleton instance;
​
    //对外提供静态方法获取该对象
    public static Singleton getInstance() {
        //第一次判断,如果instance不为null,不进入抢锁阶段,直接返回实际
        if(instance == null) {
            synchronized (Singleton.class) {
                //抢到锁之后再次判断是否为空
                if(instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

最后我们来介绍一种重量级的方式,这种方式是我们目前最常用的懒汉式方式,就是静态内部类方式。

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

/**
 * 静态内部类方式
 */
public class Singleton {

    //私有构造方法
    private Singleton() {}

    private static class SingletonHolder {
        private static final Singleton INSTANCE = new Singleton();
    }

    //对外提供静态方法获取该对象
    public static Singleton getInstance() {
        return SingletonHolder.INSTANCE;
    }
}

我们在我们的类中再定义一个静态内部类,该类中就拥有一个外部类的私有静态属性(并直接创建),该类外则提供一个直接获得该类内部属性的方法,最终来达到我们的目的。由于静态内部类不会在类加载时创建,所以不会有内存浪费,而当我们调用了该类的方法时,该类又会被自动创建出来,此时就达到了需要时再创建的效果了

序列化破坏单例模式

接着我们来学习如何序列化破坏单例模式,所谓破坏单例模式,其实就是使上面定义的单例类(Singleton)可以创建多个对象,枚举方式除外。有两种方式,分别是序列化和反射。我们首先来学习序列化方式

要想要序列化,当然首先要将我们给我们的Singleton实现序列化接口,然后我们正式来做我们的序列化工作,我们创建两个方法,一个是将单例模式产生的对象放到我们对应的硬盘中,第二个方法是从对应的地址中读出该对象,我们先调用一次写方法,然后调用两次读方法,接着看看看两者的地址是否相同

package com.itheima.reggie.design;

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;

/**
 * 测试序列化破坏单例模式
 */
public class Client {
    public static void main(String[] args) throws Exception {
        //writeObject2File();
        readObjectFromFile();
        readObjectFromFile();
    }

    //从文件中读取数据(对象)
    public static void readObjectFromFile() throws Exception {
        //1.创建对象输入流对象
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("D:\a.txt"));
        //2.读取对象
        Singleton instance = (Singleton) ois.readObject();

        System.out.println(instance);

        //释放资源
        ois.close();
    }

    //向文件中写数据(对象)
    public static void writeObject2File() throws Exception {
        //1.获取Singleton
        Singleton instance = Singleton.getInstance();
        //2.创建对象输出流对象
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("D:\a.txt"));
        //3.写对象
        oos.writeObject(instance);
        //4.释放资源
        oos.close();
    }
}

结果会发现该两者是不同的,这里有的同学可能会疑问为什么同一个对象两次读取最后的结果会不一样,因为实际上两次读取涉及到了拷贝,这里他们的值是一样的,但是在内存中的地址是不同的,更多的解释就涉及到了深拷贝相关的知识,这里我们就不在提了

那么我们要如何解决这个问题呢?其实解决的方法很简答,直接在我们要序列化的对象上加上readResolve方法即可,我们设置了这个方法,这样我们进行反序列化时就会自动调用该方法

//当进行反序列化时,会自动调用该方法,将该方法的返回值直接返回
public Object readResolve() {
    return SingletonHolder.INSTANCE;
}

这里面的所以然是要分析源码的,有时间自己看吧,反正我们记住我们只要构造这个名字的方法,这样序列化再创建对象时就会调用该方法就可以了,使用这种方法即可解决序列化

接着我们来学习反射破坏单例模式,我们容易构造我们的代码如下,最后我们容易看到运行结果,用反射获得的两个对象并不是同一个对象,这样也破坏了我们的单例模式

/**
 * 测试反射破坏单例模式
 */
public class Client {
    public static void main(String[] args) throws Exception {
        //1.获取Singleton的字节码对象
        Class clazz = Singleton.class;
        //2.获取无参构造方法对象
        Constructor cons = clazz.getDeclaredConstructor();
        //3.取消访问检查
        cons.setAccessible(true);
        //4.创建Singleton对象
        Singleton s1 = (Singleton) cons.newInstance();
        Singleton s2 = (Singleton) cons.newInstance();

        System.out.println(s1==s2);
    }
}

问题解决

那么我们要如何解决这个问题呢?其实也很简单,由于反射每次调用的都是构造方法,所以我们要在构造方法上下功夫,我们先定义一个布尔类型的成员变量,然后我们判断其值是否为真,若为真则说明不是第一次进入,我们就抛出异常,不是则说明是第一次,我们就令其正常调用该方法,同时将布尔类型的值设置为true

同时为了防止出现线程不安全的问题,我们这里还要使用线程同步关键词将对应的代码块封锁住

package com.itheima.reggie.design;

import java.io.Serializable;

public class Singleton implements Serializable {

    private static boolean flag = false;

    //私有构造方法
    private Singleton() {
        synchronized (Singleton.class) {
            //判断flag的值是否是true,如果是true则是非首次访问,抛出异常
            if(flag){
                throw new RuntimeException("不能创建多个对象");
            }
            //将flag的值设置为true
            flag = true;
        }
    }

    //定义一个静态内部类
    private static class SingletonHolder {
        //在内部类中声明并初始化外部类的对象
        private static final Singleton INSTANCE = new Singleton();
    }

    //提供公共的访问方式
    public static Singleton getInstance() {
        return SingletonHolder.INSTANCE;
    }
}

JDK源码解析

最后我们来看看在我们的JDK中哪些类使用了单例设计模式,其中最为经典的就是我们的Runtime类,该类就是使用了单例设计模式中的饿汉式,请看源码

可以看到其自己拥有自己的一个静态属性,并且提供了无参的构造方法和获得对象的静态方法

最后再来看些科普知识,通过exec方法,可以获得含有对应信息的对象,可以将信息调出来,达到跟在控制台上获取对应信息同样的效果

package com.itheima.reggie.design;

import java.io.InputStream;

public class RuntimeDemo {
    public static void main(String[] args) throws Exception{
        //获取Runtime类的对象
        Runtime runtime = Runtime.getRuntime();

        //调用runtime的方法exec,参数需要一个代表命令的字符串
        Process process = runtime.exec("ipconfig");

        //调用process对象的获取输入流的方法
        InputStream is = process.getInputStream();
        byte[] arr = new byte[1024 * 1024 * 100];

        //读取数据
        int len = is.read(arr); //返回读到的字节的个数
        //将字节数组转换为字符串输出到控制台
        System.out.println(new String(arr,0,len,"GBK"));
    }
}

工厂模式

接着我们来学习我们的工厂模式,我们先来看一个点咖啡的案例。

我们有一个咖啡店类,其下有一个返回咖啡对象的方法,咖啡对象内容有三个方法,分别是加奶、加糖和获得名字,其下有两个子类,分别是美式咖啡和拿铁咖啡,两个子类都重写了获得名字的方法,用于判断其是美式咖啡还是拿铁咖啡

但是我们现在这个案例是有问题的,其最大的问题是,我们的这些咖啡的子类对象都是需要的手动new出来的,这样一旦我们需要新增一个咖啡对象,那么所有和new对象有关的地方就要修改一遍,这显然违背了开闭原则,因此我们需要令其解耦,此时我们需要使用到工厂模式来完成这个目的

工厂模式介绍

简单工厂模式

我们首先来学习简单工厂模式,其不属于GOF的23种经典设计模式,其实他与其说是一种设计模式,不如说是一种编程习惯,首先我们简单工厂包含如下角色:

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

那么我们可以使用简单工厂对上面的案例进行改进,我们首先创建一个简单工厂对象,然后其下可以生成对应的咖啡对象

public class SimpleCoffeeFactory {

    public Coffee createCoffee(String type){
        //声明Coffee类型的变量,根据不同类型的创建不同的coffee子类对象
        Coffee coffee = null;
        if("american".equals(type)){
            coffee = new AmericanCoffee();
        }else if("latte".equals(type)){
            coffee = new LatteCoffee();
        }else {
            throw new RuntimeException("对不起,没这种咖啡");
        }

        return coffee;
    }
}

然后我们的咖啡店对象内部就直接创建该工厂对象,调用该工厂对象的创建咖啡的方法即可获得我们所需要的咖啡,对得到的咖啡进行对应的处理再返回即可

public class CoffeeStore {

    public Coffee orderCoffee(String type) {

        SimpleCoffeeFactory factory = new SimpleCoffeeFactory();
        //调用生成咖啡的方法
        Coffee coffee = factory.createCoffee(type);

        //加配料
        coffee.addSugar();
        coffee.addMilk();

        return coffee;
    }
}

那么上面的关系用图示则是如下所示

当然,有的同学可能会说,我们即使改造成这样,那不还是违反开闭原则吗?我们添加新的咖啡对象时,仍然要修改工厂类的代码,那我们这搞得这工厂对象有啥用呢?其实,我们搞这个工厂类,就相当于是解除了咖啡店和具体咖啡类的耦合,但是又产生了两个新的耦合,分别是咖啡店和咖啡工厂的耦合以及工厂对象和商品对象的耦合。那么这样有什么用呢?其最大的作用就是我们让具体的咖啡店只与咖啡工厂耦合,而不是和具体的咖啡对象耦合,因为往往我们的生产咖啡的工厂对象只有一个,但是我们的咖啡店是可能有非常多的,我们这样做,这样我们在实际的生产环境中,如果我们想要更换咖啡店对象,就不必做非常麻烦的许多对代码上的修改了

最后我们来看看其优缺点

最后我们值得一提的是,实际开发时,也是有一部分人讲工程类中创建对象的功能定义为静态的,这就是静态工程模式,它也不是23种设计模式中的,对应到我们具体的代码上就是在我们工厂类中创建具体对象的方法上加个static关键词

工厂方法模式

上面的简单工程模式仍然没有解决其违反开闭原则的问题,如果我们要解决开闭原则的问题,那么我们就要使用工厂方法模式,我们先来看看其概念和具体的结构

然后我们来看看其整体的结构

接着我们来正式用代码来实现该结构

首先我们要创建对应的工厂接口对象,这里我们只提供最基本的生成咖啡对象的方法

public interface CoffeeFactory {
    //创建咖啡对象的方法
    Coffee createCoffee();
}

然后创建两个工厂对象,分别是美式工厂和拿铁工厂,其下对应的方法都是生成对应的咖啡对象并返回(这里展示其一),当然他们都应该要实现我们的咖啡工厂接口

package com.itheima.reggie.design;

public class AmericanCoffeeFactory implements CoffeeFactory{

    @Override
    public Coffee createCoffee() {
        return new AmericanCoffee();
    }
}

我们在对应的咖啡店内注入工厂对象属性,再提供对应的set方法和得到咖啡的方法

然后我们在对应的测试类中创建对应的咖啡店类对象和工厂对象,我们要获得对应的咖啡就直接创建具体的咖啡工程对象并设置到咖啡店对象中即可,最后我们调用咖啡店对应的点咖啡方法即可得到我们对应的咖啡对象

public class Client {
    public static void main(String[] args) {
        //创建咖啡店类对象
        CoffeeStore store = new CoffeeStore();
        //创建工厂对象
        //CoffeeFactory factory = new AmericanCoffeeFactory();
        CoffeeFactory factory = new LatteCoffeeFactory();
        store.setFactory(factory);

        //点咖啡
        Coffee coffee = store.orderCoffee("latte");

        System.out.println(coffee.getName());
    }
}

上面的工厂对象模式就是严格符合开闭原则的模式,我们要新添加咖啡对象时只需要创建对应的咖啡工厂对象和具体的咖啡对象即可,不需要对原工厂代码做任何的修改

但是其问题也很明显,就是我们每增加一个具体产品需要一并增加一个具体的工程类,这太麻烦了

抽象工厂模式

那么我们要如何解决上面的工厂模式的缺点呢?这时我们就要使用到抽象工厂模式,我们之前介绍的工厂方法模式中只考虑一类产品的生产,而现实中的工厂往往是负责一类产品的生产的,那么我们创建的工厂接口也可以令其能够生产多个级别的产品,我们将同一个公司/风格/流派的产品称为一个产品族,将同一级别的产品称为一个产品等级

接着我们来看看抽象工厂模式的概念和结构

具体到我们的实际代码中,则是我们要将我们的对应的产品分类,这里我们可以通过产品族分类,我们创建一个工厂接口对象,然后具体实现不同的产品族工厂,不同的产品族工厂都具有生成同一产品族的商品的功能

那么首先我们创建点心工厂的接口,其下具有生产咖啡功能

public interface DessertFactory {

    //生产咖啡的功能
    Coffee createCoffee();

    //生产甜品的功能
    Dessert createDessert();
}

然后我们创建具体的工厂对象的实现类,其下要实现生成咖啡和甜品的功能的方法,当然,我们要实现的都是同一产品族的产品,这里我们只展示生产美式风格的产品工厂的代码

public class AmericanDessertFactory implements DessertFactory{

    @Override
    public Coffee createCoffee() {
        return new AmericanCoffee();
    }

    @Override
    public Dessert createDessert() {
        return new MatchaMouse();
    }
}

然后由于我们这里要生产点心,因此我们这里还要创建点心对象的抽象类以及其具体的实现类(此处省略代码)

最后我们在测试类中直接创建生产对应产品族的工厂对象即可获得我们所需要的产品,如果我们要增加一个产品族的话,那么只需要增加一个对应的工厂类即可,同样符合开闭原则

其优点在于如果我们的产品族中是被设计成多个对象一起工作时,其可以保证客户端只使用同一个产品族的对象,然而其缺点在于如果我们的产品族中要增加一个新的产品时,那么所有的工厂类都需要进行修改,比如说我们的点心类要多加一个汉堡的话,那么其下的所有点心工厂都需要修改其源代码

正是因为其存在这个缺点,因此这种抽象工厂模式是具有对应的使用场景的,满足以下场景我们就推荐使用抽象工厂模式来进行开发

模式扩展

其实到现在,我们的工厂模式的全部内容就已经讲完了,但是我们这里还是多讲一个内容,这个内容是讲解我们开发中一般是如何结合配置文件和工厂模式来一起开发我们的工厂模式的

首先我们需要创建一个对应的配置文件,然后在其下左边写唯一标识的字符串,右边写要创建的实体类的类名

american=com.itheima.reggie.design.AmericanCoffee
latte=com.itheima.reggie.design.LatteCoffee

然后我们创建对应的咖啡工程对象,在其下加载对应的配置文件,将创建的对象放置于Map集合中,再提供一个根据名字获得指定对象的方法

public class CoffeeFactory {

    //加载配置文件,获取配置文件中配置的全类名,并创建该类的对象进行存储
    //1.定义容器对象存储咖啡对象
    private static HashMap<String,Coffee> map = new HashMap<>();

    //2.加载配置文件,只需要加载一次
    static {
        //2.1 创建Properties对象
        Properties p = new Properties();
        //2.2 调用p对象中的load方法键配置文件的加载
        InputStream is = CoffeeFactory.class.getClassLoader().getResourceAsStream("bean.properties");
        try {
            p.load(is);
            //从p集合中获取全类名并创建对象
            Set<Object> keys = p.keySet();
            for (Object key:keys) {
                String className = p.getProperty((String) key);
                //通过反射技术创建对象
                Class clazz = Class.forName(className);
                Coffee coffee = (Coffee) clazz.newInstance();
                //将名称和对象存储到容器中
                map.put((String) key,coffee);
            }
        }catch (Exception e){
            e.printStackTrace();
        }
    }

    //根据名称获取对象
    public static Coffee createCoffee(String name){
        return map.get(name);
    }
}

最后我们的测试方法中只要通过工厂对象的方法传入指定的字符串即可获得我们所需要的对象

public class Client {
    public static void main(String[] args) {
        Coffee coffee = CoffeeFactory.createCoffee("american");
        System.out.println(coffee.getName());
        System.out.println("=========================");
        Coffee latte = CoffeeFactory.createCoffee("latte");
        System.out.println(latte.getName());
    }
}

这其实就类似于我们的Spring中的对象管理,Map集合就类似于我们的容器,我们可以通过名字自动装配对象,我们这里也是通过名字自动获得我们需要对象

JDK源码解析

最后我们来看看JDK中的源码,看看其哪些类使用了工厂模式

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);
        }
    }
}

获取迭代器对象这个大伙们都做过,这里迭代器对象是接口,那么其返回的是哪个迭代器对象的接口,其又是怎么返回的呢?这就涉及到源码了,我们接下来就来解析源码

先来看看其结构图

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

具体的源码我们就不翻了,翻了也是一样的结果,我们这里知道就可以了

最后我们来了解一下其他de

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

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

原型模式

接着我们来学习创建者模式中的原型模式,该原型模式是通过复制原型对象来创建一个和原型相同的新对象,并通过该对象的功能来实现我们的需求

原型模式介绍

原型模式中必须拥有抽象原型类、具体原型类和访问类,更加详细的信息请看图

这里我们值得一提的是,克隆分为浅克隆和深克隆。前者指的是对于被克隆的对象的引用类型的属性,其仍然指向同一个地址,而对于后者而言,其是创建了一个新对象同时其中为引用变量的属性也会被克隆,会指向不同的地址。但是不管是哪个克隆方式,被克隆的对象都会是一个全新的对象,会存放在另一个不同的内存地址

而java中提供了clone方法来实现浅克隆,那么我们可以写入我们的代码如下

public class Realizetype implements Cloneable{

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

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

首先我们创建一个Realizetype类令其实现Cloneable接口(规定克隆的类必须实现该接口,否则无法克隆),然后我们重写其克隆方法即可,当然,我们这里的重写其实也是直接调用父类的克隆方法,实际上也没重写

最后我们在测试类中写入我们的代码如下,最后的结果为false

public class client {
    public static void main(String[] args) throws CloneNotSupportedException {
        //创建一个原型类对象
        Realizetype realizetype = new Realizetype();

        //调用Realizetype类中的clone方法进行对象的克隆
        Realizetype clone = realizetype.clone();

        System.out.println(clone == realizetype);
    }
}

从上面我们会发现,自己创建的对象的方法和克隆对象的方法并不是一个方法,克隆的方法是另外的方法,而创建对象的方法则是构造方法,这两者不存在联系,且最终创造出的两个对象的地址也是不同的

原型模式案例实现

接着我们来实现原型模式实现一个三好学生对象的案例

首先我们创建这么一个三好学生对象,然后我们给其添加一个属性对象,令其实现Cloneable接口,然后重写clone方法,接着设置对应的setandget方法,最后提供一个展示其用户名的show方法

public class Citation implements Cloneable{

    //三好学生上的姓名
    private String name;

    @Override
    protected Citation clone() throws CloneNotSupportedException {
        return (Citation) super.clone();
    }

    public String getName() {
        return name;
    }

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

    public Citation() {
    }

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

    public void show(){
        System.out.println(name+"是三好学生");
    }
}

然后我们在测试方法中创建原型对象并克隆新对象,然后给其分别设置上名字,接着调用其用于展示的show方法,然后我们可以看到这两个三好学生的对象都显示了不同的名字,此时就说明的克隆方法是成功的

public class client {
    public static void main(String[] args) throws CloneNotSupportedException {
        //1.创建原型对象
        Citation citation = new Citation();
        //2.克隆奖状对象
        Citation clone = citation.clone();

        citation.setName("张三");
        clone.setName("李四");

        //3.调用show方法展示
        citation.show();
        clone.show();
    }
}

那么我们什么时候需要使用原型模式呢?在下面的两种场景下,我们推荐使用原型模式

有人可能会说,String不是也是引用变量吗?为啥没有被一起修改啊?他们指向的内存地址不是一样的吗?那为啥我们这里修改了名字之后结果两个对象输出的名字不是一样的啊?这就涉及到String这个引用类型的特性的,具体可以见这篇文章的解释blog.csdn.net/SJB2MLN/art…,简单来说就是我们创建不一样的字符串,会产生一个全新的引用类型的对象,然后这个对象的内存地址会被赋值到字符串属性上,所以在这种情况下,这两个字符串的内存对象已经被改变了,从原来的同一个字符串对象已经改为了两个不同的字符串对象

接着我们来构造一个场景来模拟一下浅克隆会产生的问题,首先我们创建一个学生对象,并实现其对应的方法

public class Student {

    //学生姓名
    private String name;

    @Override
    public String toString() {
        return "Student{" +
                "name='" + name + ''' +
                '}';
    }

    public String getName() {
        return name;
    }

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

    public Student() {
    }

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

然后我们写入其测试类代码如下

public class client {
    public static void main(String[] args) throws CloneNotSupportedException {
        //1.创建原型对象
        Citation citation = new Citation();

        //创建学生对象
        Student student = new Student();
        student.setName("张三");
        citation.setStu(student);

        //2.克隆奖状对象
        Citation clone = citation.clone();
        Student stu = clone.getStu();
        stu.setName("李四");

        //3.调用show方法展示
        citation.show();
        clone.show();
    }
}

我们这里最终显示的结果就会是两个都是李四是三好学生,这是因为我们创建的两个对象的内部指向的是同一个学生对象,这样我们修改其内部引用对象的属性最终会导致两个对象都发生变动,此时产生的结果就不是我们想要的

要想要解决这个问题,我们就要使用深克隆,我们可以用输入输出流的方式来实现深克隆,最终我们得到的对象就会是完完全全的全新的两个对象,就连其下的引用变量的地址也是全新的地址

public class client {
    public static void main(String[] args) throws Exception {
        //1.创建原型对象
        Citation citation = new Citation();

        //创建学生对象
        Student student = new Student();
        student.setName("张三");
        citation.setStu(student);

        //创建对象输出流
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("d:/robin/a.txt"));
        //写对象
        oos.writeObject(citation);
        //释放资源
        oos.close();

        //创建对象输入流
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("d:/robin/a.txt"));
        //读取对象
        Citation clone = (Citation) ois.readObject();
        //释放资源
        ois.close();

        Student stu = citation.getStu();
        stu.setName("李四");

        //3.调用show方法展示
        citation.show();
        clone.show();
    }
}

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

建造者模式

接着我们来学习创建型模式中的最后一种模式,建造者模式,先来看看对应的概述

建造者模式介绍

光看概述好像有点云里雾里的,没关系,我们等后面实际造例子的时候我们再来加深理解,来看看其结构

案例实现

接着我们通过创建一个共享单车类来加深我们对于建造者模式的理解

首先我们创建对应的自行车类,其下带有车架和车座的私有属性

public class Bike {

    private String frame; //车架

    private String seat; //车座

    public String getFrame() {
        return frame;
    }

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

    public String getSeat() {
        return seat;
    }

    public void setSeat(String seat) {
        this.seat = seat;
    }
}

然后我们创建对应的Builder抽象类,这里我们声明的私有属性用protected修饰,这样我们的子类就可以直接使用该属性而不用特别获取了

我们还在其下定义了三个创建自行车的方法

public abstract class Builder {
    //声明Bike类型的变量,并进行赋值
    protected Bike bike = new Bike();

    public abstract void buildFrame();

    public abstract void buildSeat();

    //构建自行车的方法
    public abstract Bike createBike();
}

然后我们创建两个自行车的实现类,分别是摩拜自行车类和ofo自行车类,我们这里只展示其一

public class MobileBuilder extends Builder{
    @Override
    public void buildFrame() {
        bike.setFrame("碳纤维车架");
    }

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

    @Override
    public Bike createBike() {
        return bike;
    }
}

接着我们创建一个指挥者类,类中定义builder类型的私有属性,然后提供一个方法用于构造自行车

public class Director {

    //声明builder类型的变量
    private Builder builder;

    public Director(Builder builder) {
        this.builder = builder;
    }

    //组装自行车的功能
    public Bike construct() {
        builder.buildFrame();
        builder.buildSeat();
        return builder.createBike();
    }
}

最后我们只要在测试类中创建指挥者对象,然后其下传入具体的建造者对象,调用指挥者对象的具体方法即可建造我们的目标自行车

public class Client {
    public static void main(String[] args) {
        //创建指挥者对象
        Director director = new Director(new MobileBuilder());
        //让指挥者指挥组装自行车
        Bike bike = director.construct();

        System.out.println(bike.getFrame());
        System.out.println(bike.getSeat());
    }
}

那么学到这里,我们大概也懂建造者模式是个什么样子了,我们的建造者模式首先需要的就是我们的产品类,这是当然的,要建造一个产品首先我们应该要将指定的产品给定义出来,否则产品都没有我们怎么建造,接着我们要抽象我们的建造者类,这个建造者类只是定义我们的建造的一个特定对象时应该要执行的建造步骤,并不执行具体的建造过程,然后我们创建具体的建造者实现类,实现具体的建造过程,最后我们再创建一个指挥者类,其会调用建造者类中的方法,以此来实现按照我们指定的顺序来构造我们的所需要的产品

当然,实际上我们有时候为了简化我们的结构,我们是可以将指挥者类的功能和建造者类的功能合二为一的,但是,如果指挥者类的功能是比较复杂的话,我们还是推荐使用指挥者类

然后我们来看看建造者模式的优缺点

最后我们来看看建造者模式的适用场景,简单来说,就是如果当我们的不同产品的创建过程大部分具有共同点时,我们推荐使用建造者模式

模式扩展

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

举个例子,我们可以看看下面的原来没有使用建造者模式进行扩展的代码

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);
    }
}

可以看到上面的代码是真的非常麻烦,可读性比较差,那么此时我们就可以使用建造者模式来重构该代码

package com.itheima.reggie.login;

public class Phone {

    private String cpu;
    private String screen;
    private String memory;
    private String mainboard;

    //私有的构造方法
    private Phone(Builder builder) {
        this.cpu = builder.cpu;
        this.screen = builder.screen;
        this.memory = builder.memory;
        this.mainboard = builder.mainboard;
    }

    static final class Builder {
        private String cpu;
        private String screen;
        private String memory;
        private String mainboard;

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

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

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

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

        //使用构建者创建Phone对象
        public Phone build() {
            return new Phone(this);
        }
    }

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

我们首先定义一个不可修改的Builder类在我们的手机类内部中,其下我们提供对应的设置属性的方法让我们的Builder可以设置对应的属性到我们之前所创建的属性中,每次设置完之后我们都返回这个Builder对象,所以我们可以直接用this关键字返回结果,最后我明天工一个构建者来创建Phone对象,直接返回一个新的Phone对象即可,由于Phone类中只提供了一个构造方法,且需要传入一个builder对象,因此我们这里直接传入this即可,代表我们传入一个当前的builder对象来创建一个phone对象

然后我们的构造方法中只需要将builder中对应的属性赋予到我们的手机类中的对应属性即可,当然,有的同学可能会觉得这特么不脱裤子放屁吗?我直接用我原来的方式不简单多了?但其实我们这样做是有道理的,虽然这样我们的编程上却是麻烦了些,但是使用起来却非常方便,请看我们的测试方法

package com.itheima.reggie.login;

public class Client {
    public static void main(String[] args) {
        //创建手机对象,通过构建者对象获取手机对象
        Phone phone = new Phone.Builder()
                .cpu("intel")
                .screen("三星屏幕")
                .memory("金士顿内存条")
                .mainboard("华硕主板")
                .build();

        System.out.println(phone);
    }
}

在这里,我们可以使用使用链式编程,用户可以很直观的看到其设置的内容究竟是什么,其次链式编程也允许用户随意设置其想要的数据且不指定顺序,这是我们原来传统的方式所做不到的

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

四种模式对比

代理模式

接着我们来学习结构型模式,结构型模式分为类结构型模式和对象结构型模式,前者采用继承机制来组织接口和类,后者采用组合或聚合来组合对象。而由于组合关系或聚合关系比继承关系的耦合度低,满足合成复用原则,因此对象结构型模式比类结构型模式具有更大的灵活性

结构性模式总分为七种,分别是代理模式、适配器模式、装饰者模式、桥接模式、外观模式、组合模式、享元模式

代理模式概述

代理模式简单来说就是给某个对象提供一个中介对象,由该对象来处理其他对该对象的访问,这就好像是卖电脑的商家一般不直接把电脑卖给客户,一般都是由电脑代理商来卖出的,这里电脑代理商就可以理解为是中介对象

而在java中代理按照代理类的生成时机不同又分为静态代理和动态代理,前者在编译器时就生成,而后者则是在java运行时动态生成的,而动态代理又分为JDK代理和CGLib代理

接着我们来看看代理模式里的结构,共有抽象主题类、真实主题类、代理类三种,这个光看定义有些云里雾里的,我们来做一个火车案例加深理解

静态代理

我们首先来说说代理模式里面的静态代理的方法,在此之前我们先来看看的案例内容

我们要使用静态代理,首先我们应该要创建一个具有卖票方法的接口

public interface SellTickets {
    void sell();
}

然后我们创建一个火车站类,令其实现卖票方法

public class TrainStation implements SellTickets{
    @Override
    public void sell() {
        System.out.println("火车站卖票");
    }
}

然后我们再创建一个代售点类,同样令其实现卖票方法的接口,然后由于我们的代售点调用的火车站的卖票方法,所以此处要注入我们的火车站对象,然后在卖票方法中再次调用我们的火车站的卖票方法,当然,我们这里对原来的方法做了一些增强

public class ProxyPoint implements SellTickets{
    private TrainStation trainStation = new TrainStation();
    @Override
    public void sell() {
        System.out.println("代售点收取一些服务费用");
        trainStation.sell();
    }
}

最后我们在对应的测试类中创建对应的代售点类然后调用其卖票方法进行买票即可

public class Client {
    public static void main(String[] args) {
        //创建代售点类对象
        ProxyPoint proxyPoint = new ProxyPoint();
        //调用方法进行买票
        proxyPoint.sell();
    }
}

这里有两点需要提及,第一点是,似乎我们的代售点类即使不实现卖票方法的接口,我们也同样可以对原来的方法进行增强,并不妨碍。事实的确是如此,但是我们的规范要求我们的代理类和真实主题类都必须要实现抽象主题类接口,这是规范,我们必须这样做,这样做是有目的的,最简单的,我们后面的测试类里不是顺理成章地调用了代售点的卖票方法么?如果我们不实现这个卖票的接口,那么可能这个卖票就被定义成各种各样的名字了,到时候就失去了统一规范了,这样会徒增其他程序员的理解成本

第二点则是我们我们这里代售点和火车站的关联关系是组合,如果判断呢?有两种方式,第一种是我们可以看到我们这里是采用new对象的方式来注入火车站对象的,那么就是组合。第二点我们可以根据口诀“聚合可有可无,组合同生共死”来理解,我们分析其生命周期,容易知道代售点和被注入的火车站对象的生命周期是一致的,因此其关联关系是组合关系。这里为什么用组合而不用聚合呢?答案是因为课程就是这样做的,实际上当然是用聚合更好,毕竟代售点即使离开了火车站他也可以去卖冰红茶,并不是说一定要只能卖票,离开了火车站他就啥也做不了了

JDK动态代理

接着我们来使用动态代理来实现上面的案例,先来讲解JDK提供的动态代理,在java中提供了一个动态代理类Proxy,该类提供了一个创建代理对象的静态方法newProxyInstance方法,我们可以用该方法来获取代理对象

这个内容其实我们学习之前即已经学习过了,首先我们创建一个ProxyFactory类,在其下我们注入我们要进行代理的真实主题类,在这里是TrainStation对象,接着我们在此提供一个获取代理对象的方法,这里我们返回的代理对象本质是一个接口,这个接口就是我们抽象出来的具有卖票方法的接口

接着我们嗲用Proxy里的newProxyInstance方法来蝴蝶代理对象,由于返回的Object对象,因此此处我们需要进行强转,其下我们要传入真实主题类的类加载器以及代理类和真实主题类的共同接口,这些我们都可以通过真实主题类的一些方法获得对应的对象并传入,最后需要传入的是代理对象的调用处理程序

此处我们直接new其子类来传入对象,而其子类下又有三个变量,第一个变量proxy是要代理对象,也是我们后面要返回的代理类的对象,基本不用,忽略就行。第二个method则是我们对接口中的方法进行封装的method对象,第三个args则是我们方法中的实际参数,返回值如果有,则返回对应的返回值,若没有则返回null

最后值得一提的是,如果我们要动态代理中执行被代理对象的某些方法,我们要使用反射的方式来执行这些方法,否则是无法执行的

public class ProxyFactory {
​
    protected TrainStation station = new TrainStation();
​
    //获取代理对象的方法
    public SellTickets getProxyObject() {
        //返回代理对象
        /*
        * ClassLoader loader : 类加载器,用于加载代理类,可以通过目标对象获取类加载器
        * Class<?>[] interfaces : 代理类实现的接口的字节码对象
        * InvocationHandler h : 代理对象的调用处理程序
        */
        SellTickets proxyObject = (SellTickets) Proxy.newProxyInstance(
                station.getClass().getClassLoader(),
                station.getClass().getInterfaces(),
                new InvocationHandler() {
                    /**
                     * 代理对象的调用处理程序,此处是实现其子类对象的方式来传入所需的对象
                     * @param proxy 代理对象,和proxyObject对象是同一个对象,在invoke方法中基本不用
                     * @param method 对接口中的方法进行封装的method对象
                     * @param args 调用方法的实际参数
                     * @return 方法的返回值,如果方法无返回值,则返回null
                     * @throws Throwable
                     */
                    @Override
                    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                        System.out.println("代售点收取一定的服务费用(jdk动态代理)");
                        //执行目标对象的方法
                        Object invoke = method.invoke(station, args);
                        return invoke;
                    }
                }
        );
        return proxyObject;
    }
}
​

最后我们在测试类中写入对应的代码,可以得到我们想要的结果

public class Client {
    public static void main(String[] args) {
        //1.获取代理对象
        ProxyFactory factory = new ProxyFactory();
        //2.使用factory对象的方法获取代理对象
        SellTickets proxyObject = factory.getProxyObject();
        //3.调用卖票的方法
        proxyObject.sell();
    }
}

接着我们来学习下动态代理的原理

虽然上面说我们可以使用java诊断工具阿尔萨斯来查看代理类的结构,但是我们这里就懒得自己再查一遍了,资料里有,咱们直接拿资料里的东东拿来学吧

我们直接来看重点代码(此处的代码是省略了一部分的代码,只留下我们要学习的重点部分)

然后我们来讲解下这部分的代码,首先我们可以看到其下有对应的方法属性,这个方法属性通过static静态代码块获得对应的接口对象然后将接口的方法赋予到这个方法属性中,接着代理类$Proxy0的父类Proxy还有一个InvocationHandler属性,该属性用protected修饰,因此子类可以正常调用,此处我们可以看到其构造方法下就有一个InvocationHandler对象,该对象通过调用父类的方法将该对象的值设置到对应的属性中,该对象就是我们在构造的方法中自己手动new出来的其子类对象,然后我们在sell方法中可以看到其调用就是该对象的invoke方法,那其实就是我们自己new出的子类的invoke方法,由于我们又在该方法中进行了对代理对象的方法的再调用,所以我们最后能够正确调用我们的自己设置的方法

//程序运行过程中动态生成的代理类
public final class $Proxy0 extends Proxy implements SellTickets {
    private static Method m3;
​
    public $Proxy0(InvocationHandler invocationHandler) {
        super(invocationHandler);
    }
​
    static {
        m3 = Class.forName("com.itheima.proxy.dynamic.jdk.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;
    }
}

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

  • 代理类($Proxy0)实现了SellTickets,这也就印证了我们之前说的真实类和代理类实现同样的接口。
  • 代理类($Proxy0)将我们提供了的匿名内部类对象传递给了父类。

其总体的执行流程如下

CGLib代理

接着我们来用CGLib代理的方式来实现之前的案例,我们之前的JDK代理是面对接口的动态代理,其要执行代理要求代理对象必须实现一个接口,而我们的CGLib代理则没有这个要求,其为JDK代理提供了很好的补充

要使用CGLib,我们首先要导入其对应的包

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

接着我们的代理类必须要实现MethodInterceptor接口,实现该接口要求实现intercept方法,这个方法其实就是我们的回调方法,当然,目前我们不知道回调方法是个啥,这没关系,我们先按下不表。我们首先往其下设置注入火车站对象,也就是原始的代理类,这样方便我们之后调用对应的方法,然后我们创建一个获得代理类的方法,其下我们要先创建一个Enhance对象,然后设置其父类的字节码对象,这里我们直接设置原始主题的字节码对象,也就是火车站的字节码对象,接着我们要设置回调函数,其实回调函数就是我们之前实现的方法,我们这里只要求传入具有回调方法的对象即可,那么就直接传入当前的对象,其就有我们实现的回调方法。接着我们调用其中的创建代理对象的方法,将创建的代理对象返回即可。

最后我们在回调方法中执行我们的代理对象的售票方法,此处我们实现调用真实主题的售票的方法仍然是通过反射的方式

public class ProxyFactory implements MethodInterceptor {

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

    public TrainStation getProxyObjet() {
        //创建Enhancer对象,类似于JDK代理中的Proxy类
        Enhancer enhancer = new Enhancer();
        //设置父类的字节码对象
        enhancer.setSuperclass(TrainStation.class);
        //设置回调函数
        enhancer.setCallback(this);
        //创建代理对象
        TrainStation proxyObject = (TrainStation) enhancer.create();
        return proxyObject;
    }

    @Override
    public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
        //System.out.println("方法执行了");
        System.out.println("代售点收取一定的服务费用(CGLib代理)");
        //要调用目标对象的方法
        Object invoke = method.invoke(station, objects);
        return invoke;
    }
}

最后我们可以写入我们的测试类如下,这里我们的第八行调用代理类的售票方法其实调用的就是代理类中的回调函数,这也是为什么我们一定要设置回调函数和我们要在回调函数中实现代理对象的售票功能的原因

public class Client {
    public static void main(String[] args) {
        //创建代理工厂对象
        ProxyFactory factory = new ProxyFactory();
        //获取代理对象
        TrainStation proxyObject = factory.getProxyObjet();
        //调用代理对象中的sell方法卖票
        proxyObject.sell();
    }
}
  • 三种代理模式的对比及代理模式的优缺点

简单来说,我们推荐有接口的时候使用jdk动态代理,没有接口就使用CGLIB代理,其中能用动态代理就用动态代理,而不要用静态代理

接着我们来看看代理模式的优缺点

最后我们来看看其使用场景,我们的远程代理、防火墙代理以及保护代理都可以使用动态代理的方式来实现其功能

适配器模式

现在我们来学习结构型模式中的适配器模式,我们先来看看什么是适配器模式

适配器模式介绍

其定义就是将一个类的接口通过适配器类转换成我们所需要的另外一个接口,适配器模式又分为两类,分别是类适配器模式和对象适配器模式,前者的耦合度比后者高,且对程序员的要求也高,因此现在都是后者用得比较多

适配器模式主要包含三个主要角色,分别是目标接口(用户所期待的抽象类或者是接口)、适配器类(是一个转换器,将适配者接口转为目标接口)、适配者类(最初提供的组件接口)这三个

适配器模式案例实现

接着我们来做一个案例来加深我们对适配器类的理解,一台电脑只能读取SD卡,而如果需要读取TF卡的内容就需要用到适配器模式,我们创建一个适配者类(相当于读卡器),让我们的电脑也能够读取TF卡

我们先来用类适配器模式来实现这个案例,首先我们需要创建对应的SD卡和TF卡类的接口及其实现类

首先我们创建SD卡类的接口,其拥有最基本的读取与写入数据的方法

public interface SDCard {

    //从SD卡中读取数据
    String readSD();

    //往SD卡中写数据
    void writeSD(String msg);
}

然后我们写入其实现类

public class SDCardImpl implements SDCard{

    @Override
    public String readSD() {
        return "SD卡读取数据成功";
    }

    @Override
    public void writeSD(String msg) {
        System.out.println("往SD卡中写入数据:"+msg);
    }
}

然后我们创建TF卡的接口,其同样拥有两个方法,也是写入写出

public interface TFCard {

    //从TF卡中读取数据
    String readTF();

    //往TF卡中写数据
    void writeTF(String msg);
}

我们同样写入其实现类

public class TFCardImpl implements TFCard{

    @Override
    public String readTF() {
        String msg = "TF卡读取数据";
        return msg;
    }

    @Override
    public void writeTF(String msg) {
        System.out.println("写入数据:"+msg);
    }
}

然后我们创建一个电脑类,其提供一个往SD卡中读取数据的方法

public class Computer {

    //从SD卡中读取数据
    public String readSD(SDCard sdCard){
        if(sdCard == null){
            throw new NullPointerException("SD卡不能为空");
        }
        return sdCard.readSD();
    }
}

接着我们需要创建一个适配器类,该适配器类可以将实现让我们的电脑类能够读取TF卡的数据,相当于这个适配器类可以让TF卡的数据被只能读取SD卡数据的电脑读取,那我们应该要怎么创建这个适配器类呢?

我们注意看我们之前的图中里的实现方式,适配器类要实现当前系统的业务接口,同时又需要继承现有组件库中已经存在的组件。那么我们可以让我们的适配器类继承我们的已经存在的TFCard的实现类的组件,然后再令其实现SD卡读取的方法的系统中的业务接口

然后在对应的读写方法中调用我们的TF父类中的方法即可

public class SDAdapterTF extends TFCardImpl implements SDCard{

    @Override
    public String readSD() {
        System.out.println("适配器类读取TF卡");
        return readTF();
    }

    @Override
    public void writeSD(String msg) {
        System.out.println("适配器类往TF卡中写入数据");
        writeTF(msg);
    }
}

接着我们写入我们的测试类的代码如下

public class Client {
    public static void main(String[] args) {
        //创建计算机对象
        Computer computer = new Computer();
        //读取SD卡中的数据
        String msg = computer.readSD(new SDCardImpl());
        System.out.println(msg);

        System.out.println("========================");
        //使用该电脑读取TF卡中的数据
        //定义适配器类
        String s1 = computer.readSD(new SDAdapterTF());
        System.out.println(s1);
    }
}

可以看到我们这里计算机对象,读取SD卡的数据就传入SD卡的实现类,如果要读取TF卡的数据就往内部传入TF卡的适配器对象,最终我们就可以得到我们的所需要的读取TF卡数据的结果

在我们的上面的例子中,SDCard和SDCardImpl是目标接口,而TFCard和TFCardImpl是适配者类,而SDAdapterTF是适配器类。当然,有的同学可能会说,我们这个原来就有sd卡功能,后面我们要实现的是读取TF卡功能,显然TF类才是目标接口,SD卡才是最初的适配者类吧?其实,我也是这么觉得的,但是事实上就是我们只能按照上面的方式去理解,最终我们才能够得到我们想要的结果,否则最终我们得到的结果还是读取SD卡的数据,而非TF卡的数据,所以我们只能这么理解了,暂时先记住就得了,反正类适配器模式也不是很重要

后面我们对这个例子进行了一些尝试性的修改,看看能不能用我们的理解去完成这个例子想要的效果,最终的结果是不可以,我认为这个例子本身应该是没有问题的,但是我们的常识理解和代码设置的方式出现了问题,这个问题具体要怎么理解合适现在还不是很懂,总之先放着吧。不过可以确定的是,要实现的接口肯定是我们的目标接口,这个目标接口必然是我们的客户端已经存在的功能,我们的适配器类也必须实现这个接口,否则适配器类就无法被我们的最先的客户端类调用,而适配器类继承或者是聚合的内容必然是最初提供的组件类,也就是一开始我们拥有但是还不是我们所需要的类的类

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

对象适配器

现在我们来讲下对象适配器模式,首先我们来看看其实现方式和改造后的类图

其实我们只需要对我们的适配器类稍作改动即可,我们需要将继承我们的先有的组件库TFCard类改为聚合TFCard类,具体到代码中就是往里面注入TFCard类对象并提供一个设置对应的属性值的构造方法,然后重写的方法就调用该属性的内部的对应方法即可

public class SDAdapterTF implements SDCard{

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

    public SDAdapterTF(TFCard tfCard) {
        this.tfCard = tfCard;
    }

    @Override
    public String readSD() {
        System.out.println("适配器类读取TF卡");
        return tfCard.readTF();
    }

    @Override
    public void writeSD(String msg) {
        System.out.println("适配器类往TF卡中写入数据");
        tfCard.writeTF(msg);
    }
}

最终我们的测试类代码修改成如下形式,我们这里创建适配器类对象,然后直接往其内部传入TFCarl的子实现类,然后我们调用中的读取方法时只要将对应的适配器类对象传入即可

public class Client {
    public static void main(String[] args) {
        //创建计算机对象
        Computer computer = new Computer();
        //读取SD卡中的数据
        String msg = computer.readSD(new SDCardImpl());
        System.out.println(msg);

        System.out.println("========================");
        //使用该电脑读取TF卡中的数据
        //创建适配器类对象
        SDAdapterTF sdAdapterTF = new SDAdapterTF(new TFCardImpl());
        String s1 = computer.readSD(sdAdapterTF);
        System.out.println(s1);
    }
}

其实这个就相当于是我们构造了适配器类,这个适配器类中有SD卡的接口,我们的电脑可以通过操作这个适配器类来完成对SD卡的具体操作

注意事项和应用场景

JDK源码解析

这个直接看图吧

最后提一嘴,这一节说实话我不是很理解,我觉得这个课程这一部分讲的也是有点烂,以后有时间还是要自己去看点别的课程重新理解下

装饰者模式

现在我们来学习装饰者模式,我们先来看一个快餐店的例子

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

来看看类图

那么我们采取继承的方式来实现上面的快餐店案例存在什么问题呢?

装饰者模式介绍

接着我们来看看装饰者模式的定义和结构

光看上面的描述肯定是不足够的,我们现在来用我们的装饰模式来完善我们上面的案例来加深我们的理解

我们先来看看类图

案例实现

首先我们要创建最重要的抽象构件角色,该角色是作为我们后续接受其他对象的一个规范对象,我们在其中定义两个最基本的菜品属性,分别是名字和价格,然后提供对应的构造和getandset方法,最后我们还写入一个抽象的cost方法

//抽象构件(Component)角色
public abstract class FastFood {

    private String desc;
    private float price;

    public String getDesc() {
        return desc;
    }

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

    public float getPrice() {
        return price;
    }

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

    public FastFood() {
    }

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

    public abstract float cost();
}

然后我们来创建具体的构建角色,这里我们的具体构建角色有两个,分别是炒面和炒饭类

由于这两个类都大差不差,我们这里就只展示一个了,我们这里令其继承最初的抽象构建类,然后构造方法中我们存入自己的构造方法来创建一个快餐对象,存入的东西当然则是我们自己指定的对应快餐的名字和价格,然后我们还要实现其对应的花费方法,这里我们直接调用父类的获得价格的方法即可

//具体构件角色
public class FriedNoodles extends FastFood{

    public FriedNoodles() {
        super("炒面",12);
    }

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

接着我们构建抽象装饰者角色,该类是抽象类并且继承最初的抽象构建类,且其下还聚合了抽象构建类的实例,我们同样提供对应的构造方法,构造方法做两件事情,第一件事是将类中的属性注入对应的快餐类,第二件事情是设置该类继承父类的快餐类对应的数据,这样我们的装饰者类中就可以表示两个快餐对象,一个是我们属性中的快餐类,另外一个是我们自己注入的快餐类

//装饰者类(抽象装饰者角色)
public abstract class Garnish extends FastFood{

    //声明快餐类的变量
    private FastFood fastFood;

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

    public FastFood getFastFood() {
        return fastFood;
    }

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

最后我们则要设置具体的装饰者角色,我们这里需要设置鸡蛋和培根类两个,同样因为大差不差所以我们这里只展示一个,我们的具体装饰者角色要继承我们的抽象装饰者类,同样提供构造方法,构造方法必须要提供当前的快餐对象,用于给装饰者类注入该对象到其属性中,然后自己在传入自己要设置的快餐数据进去

同时提供计算价格和名字的方法,计算的价格就从注入对象和自己设置的快餐对象中计算总和并返回,获得全名的方法也是同理

//鸡蛋类(具体的装饰者角色)
public class Egg extends Garnish{

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

    @Override
    public float cost() {
        //计算价格
        return getPrice()+getFastFood().cost();
    }

    @Override
    public String getDesc() {
        return super.getDesc() + getFastFood().getDesc();
    }
}

最后我们写入我们的测试类代码如下

我们稍微来解释一下这些代码的工作流程,不然别以后再回来看的时候直接看不懂了。我们这里首先创建一份炒饭对象,然后我们打印炒饭的名称和钱

接着我们通过Egg的构造方法通过炒饭对象构建一个蛋炒饭对象,这里发生的过程是,我们的炒饭对象进入到Egg的构造方法中,该构造方法又会调用其继承的Garnish的构造方法,这样我们的对象就拥有了一个快餐类变量,该变量存储着我们炒饭对象,然后其自身又是一个快餐,其内部就存放着我们的鸡蛋的菜品的数据,由于其本身的究极父类还是一个快餐对象,因此我们可以用最初的快餐对象去承接这个新对象,然后我们同样调用其获得名字和花费的方法,此时我们调用的获取名字方法会从鸡蛋类中调用获取名字的方法,该对象会调用当前具有的快餐对象的名字并且拼接属性上的快餐名字然后返回,最终我们会得到我们拼接的新菜品的名字,对于获取价格的方法,也是一样的

然后我们再点一个鸡蛋,也是依葫芦画瓢,同样将对应的菜品对象设置到父类的属性中,然后将自己的数据设置到当前的菜品数据中,不过这样我们的最后构造的对象就会形成一个嵌套的形式,具体表现在于,我们的一个菜品的属性还会有下一个菜品属性,我们对方法的调用也会变成一个嵌套的调用,这有些类似于递归,递归的话就要考虑溢出问题,不过这里显然不需要考虑,因为一般没人能点一个菜品加东西加到超出我们的内存

public class Client {
    public static void main(String[] args) {
        //点一份炒饭
        FastFood friedRice = new FriedRice();

        String s1 = friedRice.getDesc() + " " + friedRice.cost();

        System.out.println(s1);


        System.out.println("====================");

        friedRice = new Egg(friedRice);
        s1 = friedRice.getDesc() + " " + friedRice.cost();
        System.out.println(s1);

        System.out.println("====================");

        friedRice = new Egg(friedRice);
        s1 = friedRice.getDesc() + " " + friedRice.cost();
        System.out.println(s1);

        System.out.println("====================");

        friedRice = new Bacon(friedRice);
        s1 = friedRice.getDesc() + " " + friedRice.cost();
        System.out.println(s1);
    }
}

可以得到如下结果,这种结果就是我们想要的动态的结果了

那么到此为止,我们的案例就算是改善完了,如果我们想要添加菜品,我们就只需要添加一个类令其继承FastFood对象即可,如果我们想要继续添加配料,那么只要创建一个类令其继承Garnish即可,这样就非常的方便,非常地好用

最后我们再来看看其好处和使用场景

JDK源码解析

IO流中的包装类就用到了装饰着模式,我们先来看看其使用方式

最后我们来看看其结构,一分析我们会发现它的确是装饰者模式的结构

最后我们来看看代理模式和装饰者模式的区别

桥接模式

现在我们来学习桥接模式,我们先来看看下面的案例

当然,有的同学可能会说,那我不可以使用装饰者模式吗?答案当然是可以的,但是我们这里主要要演示的是桥接模式,所以我们这里就用桥接模式来做

桥接模式介绍

先来看看桥接模式的定义和结构

然后我们通过桥接模式来完成下面的案例

案例实现

首先我们定义我们的实现化角色,其实就是一个具有解码功能的接口

//实现化角色
public interface VideoFile {
    //解码功能
    void decode(String fileName);
}

然后我们创建具体的实现化角色,其下具体实现了解码功能,还有一个RMVB的实现化角色,这里就不展示了

//具体的实现化角色
public class AviFile implements VideoFile{

    @Override
    public void decode(String fileName) {
        System.out.println("AVI视频文件:"+fileName);
    }
}

然后我们创建抽象化角色,其是一个抽象类,其下聚合了对实现化角色的引用,我们这里用接口来提高其扩展性,其下有一个展示的抽象方法

//抽象化角色
public abstract class OpratingSystem {

    //声明videoFile变量
    protected VideoFile videoFile;

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

    public abstract void play(String fileName);
}

接着我们创建扩展抽象话角色,我们这里以windows系统类为例,我们这里实现了其构造方法和抽象方法,实现对应的抽象方法我们的方式是调用其上存在的具有解码方法的属性

//扩展抽象化角色
public class windows extends OpratingSystem{

    public windows(VideoFile videoFile) {
        super(videoFile);
    }

    @Override
    public void play(String fileName) {
        videoFile.decode(fileName);
    }
}

最后我们可以写入我们的测试代码如下,可以看大此处我们创建对应的操作系统,其下传入具体的视频文件类,然后调用操作系统的播放方法就可以正确执行我们想要的功能了

public class Client {
    public static void main(String[] args) {
        //创建mac系统对象
        OpratingSystem mac = new Mac(new AviFile());
        //使用操作系统播放视频文件
        mac.play("复仇者联盟");
    }
}

现在我们这个案例就非常符合我们的需求,我们以后如果有了新的操作系统,我们只需要创建一个新的类令其继承操作系统抽象类即可,如果有了新的视频格式,我们也只需要令其实现VedioFile接口即可,都非常的便利

最后我们再来看看桥接模式的好处和使用场景

外观模式

现在我们来学习外观模式,这个模式其实比较简单啊,我们先举一个例子吧

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

简单来说,我们的外观模式就是给多个复杂的子系统提供一个一致的接口,外部用户可以通过访问这个接口来调用各个子系统的方法,这很好理解,给用户一个统一接口,这在我们现实生活中也是有很多对应的例子的,比如说我们会将各种功能都集成到手机中,然后用户通过手机即可调用其下各个方法,这就相当于是提供了一个统一接口

外观模式介绍

接着我们来通过实现一个只能家电案例来加深我们对外观模式的理解

案例实现

首先我们创建对应的电灯类、电视机类和空调类,其都具有开启和关闭的方法

public class Light {

    //开灯
    public void on() {
        System.out.println("打开电灯");
    }

    //关灯
    public void off() {
        System.out.println("关闭电灯");
    }
}

然后我们创建一个外观类,该类聚合了电灯、电视机、空调对象,聚合的设置就放在构造方法里,然后提供一个语音控制的方法(这个方法就是我们提供的唯一方法接口),其下还有一键打开和关闭的方法,调用该方法会执行所有的软件的关闭,我们在对应的语音控制中判断有无打开关闭关键字,有则执行对应的方法即可

public class SmartAppliancesFacade {

    //聚合电灯对象,电视机对象,空调对象
    private Light light;
    private TV tv;
    private AirCondition airCondition;

    public SmartAppliancesFacade() {
        light = new Light();
        tv = new TV();
        airCondition = new AirCondition();
    }

    //通过语音控制
    public void say(String msg){
        if(msg.contains("打开")){
            on();
        }else if(msg.contains("关闭")){
            off();
        }else {
            System.out.println("没这操作");
        }
    }

    //一键打开
    private void on() {
        light.on();
        tv.on();
        airCondition.on();
    }

    //一键关闭
    private void off() {
        light.off();
        tv.off();
        airCondition.off();
    }
}

最后我们写入测试类如下,使用该测试类即可以通过提供的唯一接口来执行我们想要的一键关闭和开启的操作

public class Client {
    public static void main(String[] args) {
        //创建智能音箱对象
        SmartAppliancesFacade facade = new SmartAppliancesFacade();
        //控制家电
        facade.say("打开家电");
        System.out.println("================");
        facade.say("关闭家电");
    }
}

最后我们来看看外观模式的好坏及其使用场景

JDK源码分析

这一段时间看图吧,图里说的挺清楚了的,没啥需要我补充的

组合模式

现在我们来学习组合模式,先来看看概述

对于这种结构的叙述,我们就不赘述了,自己看图吧,这种结构我们属于是见得太多了,我们需要知道的是,如果要让客户在使用这种结构时可以不用区分容器对象和叶子对象,此时就需要使用到我们的组合模式

组合模式介绍

现在我们来看看组合模式的定义和结构

抽象根结点要抽象出各个层次的共有方法和属性,比如说我们的容器对象和叶子对象,其都有共有的行为,我们可以将其抽象出来

接着我们来通过讲解一个案例来加深我们对组合模式的理解,先来看看我们的案例目标

然后我们来看看其类图

那么根据类图,我们首先创建抽象根结点的菜单组件类,这里我们的方法都抛出异常的原因是菜单组件只是一个抽象的类,其内部抽象的容器结点和叶子结点的共同特征,实际并不存在啥子菜单的玩意,只有名称和层级作为标识而已,因此内部的方法我们全部令其抛出异常,最后我们再定义一个打印菜单名称的抽象方法让子类实现

之所以最后一个打印方法是抽象方法而其他的不是,是因为我们的共同类中的子类其添加和移除的方法是可有可无的,不实现的类说明其本身就不支持做这个功能,此时我们抛出异常是没有问题的,最后我们设置打印方法为抽象方法,这就意味着所有继承该类的子类都必须独立实现打印方法,这个方法是我们的所有的子类都必须提供的操作,是我们自己所规定的,事实上,这个操作的提供也是必要的,因为我们的目标就是打印菜单中的所有层级

透明组合模式

//菜单组件:抽象根结点
public abstract class MenuComponent {
    //菜单组件的名称
    protected String name;
    //菜单组件的层级
    protected int level;

    //添加子菜单
    public void add(MenuComponent menuComponent){
        throw new UnsupportedOperationException();
    }

    //移除子菜单
    public void remove(MenuComponent menuComponent){
        throw new UnsupportedOperationException();
    }

    //获取指定的子菜单
    public MenuComponent getChild(int index){
        throw new UnsupportedOperationException();
    }

    //获取菜单或者菜单项的名称
    public String getName() {
        return name;
    }

    //打印菜单名称(包含子菜单和子菜单项)
    public abstract void print();
}

然后我们定义具体的菜单类,菜单类当然要继承共同抽象类,提供对应的构造方法,菜单类之所以聚合抽象菜单类集合,是因为一个菜单总是可以保存多个菜单的,这里之所以使用抽象菜单类而非具体的菜单类,是为了让我们的菜单类的存放变得更加灵活

然后我们重写其对应的添加和移除的方法,直接调用集合中对应的方法即可,重点在于我们的打印方法,我们的打印方法是打印当前的名字,并且循环打印子菜单的名字,打印直接调用子菜单的打印方法即可,参考递归

//菜单类:树枝结点
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 index) {
        return menuComponentList.get(index);
    }

    @Override
    public void print() {
        //打印菜单名称
        for (int i = 0; i < level; i++) {
            System.out.print("--");
        }
        System.out.println(name);

        //打印子菜单或者子菜单项名称
        for (MenuComponent menuComponent : menuComponentList) {
            menuComponent.print();
        }
    }
}

最后我们要定义的叶子结点的类,同样要继承共同的抽象类,这里就不用注入菜单属性了,因为叶子结点对象显然不能再拥有子菜单了,我们这里只需要提供该对象的名字和层级的构造方法以及实现其必须实现的打印方法即可

//叶子结点
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);
    }
}

最后我们在测试类中写入如下代码即可得到我们想要的结果,前面一大段代码都是在设置对应的结构,最后一行打印代码执行了我们的目标操作

public class Client {
    public static void main(String[] args) {
        //创建菜单树
        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));

        //创建一级菜单
        MenuComponent component = new Menu("系统管理",1);
        //将二级菜单添加到一级菜单中
        component.add(menu1);
        component.add(menu2);
        component.add(menu3);

        //打印菜单名称
        component.print();
    }
}

接着我们来看看组合模式的分类,我们上面使用的组合模式是透明组合模式,实际上还有安全组合模式

安全组合模式

最后我们来看看其优点和使用场景

享元模式

现在我们来学习享元模式,我们先来看看享元模式的定义和结构

享元模式介绍

光说肯定是云里雾里的,咱们还是来做个案例吧,先来看看案例内容

然后是案例的类图

案例实现

那么现在我们就来实现这个案例,首先我们创建对应的抽象享元角色,其下我们设计抽象方法为获取图形的方法,我们再设置一个显示图形和颜色的display方法

//抽象享元角色
public abstract class AbstractBox {

    //获取图形的方法
    public abstract String getShape();

    //显示图形及颜色
    public void display(String color){
        System.out.println("方块形状"+getShape()+", 颜色: "+color);
    }
}

然后我们定义具体的享元角色,当然要继承我们的抽象享元角色,内部要实现抽象方法即可,这里我们定义了三个,代码上大差不差,就只展示一个了

//I图形类(具体享元角色)
public class IBox extends AbstractBox{
    @Override
    public String getShape() {
        return "I";
    }
}

然后我们要设计我们的工厂类,工厂类的作用就是用于管理享元角色,我们这里我们维护享元角色的方式使用Map集合,构造方法中创建集合并预先创建可能需要使用到的所有对象,然后工厂类本身采取饿汉式的单例模式来结合构建,再提供一个根据名字获取对应的对象的方法

//工厂类,将该类设计为单例
public class BoxFactory {

    private HashMap<String,AbstractBox> map;

    //在构造方法中进行初始化操作
    private BoxFactory() {
        map = new HashMap<>();
        map.put("I",new IBox());
        map.put("L",new LBox());
        map.put("O",new OBox());
    }

    //提供一个方法获取该工厂类对象
    public static BoxFactory getInstance() {
        return factory;
    }

    private static final BoxFactory factory = new BoxFactory();

    //根据名称获取图形对象
    public AbstractBox getShape(String name) {
        return map.get(name);
    }
}

最后我们可以在我们的测试类中写入如下代码,最终可以得到我们预期的结果,box3和box4是同一个对象

public class Client {
    public static void main(String[] args) {
        //获取I图形对象
        AbstractBox box1 = BoxFactory.getInstance().getShape("I");
        box1.display("灰色");

        //获取I图形对象
        AbstractBox box2 = BoxFactory.getInstance().getShape("L");
        box2.display("绿色");

        //获取I图形对象
        AbstractBox box3 = BoxFactory.getInstance().getShape("O");
        box3.display("灰色");

        //获取I图形对象
        AbstractBox box4 = BoxFactory.getInstance().getShape("O");
        box4.display("红色");

        System.out.println("两次获取到的O图形对象是否是同一个对象:"+(box3==box4));
    }
}

最后我们来看看享元模式的优缺点和使用场景

JDK源码解析

JDK中的Integer类中使用了享元模式

这里我们就不具体展开说了,因为自己看也看得懂,结论总之是 Integer 默认先创建并缓存 -128 ~ 127 之间数的 Integer 对象,当调用 valueOf 时如果参数在 -128 ~ 127 之间则计算下标并从缓存中返回,否则创建一个新的 Integer 对象。