[设计模式]设计模式

工厂方法模式

  • 将需要频繁出现的对象创建,封装到一个工厂类中。
  • 当我们需要对象时,直接调用工厂类中工厂方法来为我们生成对象。
  • 这样,就算类出现了变动,我们也只需要修改工厂中的代码即可。

简单工厂模式

1
2
3
4
5
6
7
8
9
10
11
12
public abstract class Fruit {   //水果抽象类
private final String name;

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

@Override
public String toString() {
return name+"@"+hashCode(); //打印一下当前水果名称,还有对象的hashCode
}
}
1
2
3
4
5
6
public class Apple extends Fruit{   //苹果,继承自水果

public Apple() {
super("苹果");
}
}
1
2
3
4
5
public class Orange extends Fruit{  //橘子,也是继承自水果
public Orange() {
super("橘子");
}
}
  • 正常情况下,我们直接new就可以得到对象了:
    1
    2
    3
    4
    5
    6
    public class Main {
    public static void main(String[] args) {
    Apple apple = new Apple();
    System.out.println(apple);
    }
    }
  • 现在我们将对象的创建封装到工厂中:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    public class FruitFactory {
    /**
    * 这里就直接来一个静态方法根据指定类型进行创建
    * @param type 水果类型
    * @return 对应的水果对象
    */
    public static Fruit getFruit(String type) {
    switch (type) {
    case "苹果":
    return new Apple();
    case "橘子":
    return new Orange();
    default:
    return null;
    }
    }
    }
  • 现在我们就可以使用此工厂来创建对象了:

    1
    2
    3
    4
    5
    6
    public class Main {
    public static void main(String[] args) {
    Fruit fruit = FruitFactory.getFruit("橘子"); //直接问工厂要,而不是我们自己去创建
    System.out.println(fruit);
    }
    }
  • 但这样不太符合开闭原则:一个软件实体,比如类、模块和函数应该对扩展开放,对修改关闭。

  • 但如果我们现在需要新增一种水果,比如桃子,那么这时我们就的去修改工厂提供的工厂方法了,这样就不符合开闭原则了,因为工厂实际上是针对于调用方提供的,所以我们应该尽可能对修改关闭。

工厂方法模式

1
2
3
public abstract class FruitFactory<T extends Fruit> {   //将水果工厂抽象为抽象类,添加泛型T由子类指定水果类型
public abstract T getFruit(); //不同的水果工厂,通过此方法生产不同的水果
}
1
2
3
4
5
6
public class AppleFactory extends FruitFactory<Apple> {  //苹果工厂,直接返回Apple,一步到位
@Override
public Apple getFruit() {
return new Apple();
}
}
  • 这样,我们就可以使用不同类型的工厂来生产不同类型的水果了。
  • 如果新增了水果类型,直接创建一个新的工厂类就行,不需要修改之前已经编写好的内容。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    public class Main {
    public static void main(String[] args) {
    test(new AppleFactory()::getFruit); //比如我们现在要吃一个苹果,那么就直接通过苹果工厂来获取苹果
    }

    //此方法模拟吃掉一个水果
    private static void test(Supplier<Fruit> supplier){
    System.out.println(supplier.get()+" 被吃掉了,真好吃。");
    }
    }

抽象工厂模式

  • 前面我们介绍了工厂方法模式,通过定义顶层抽象工厂类,通过继承的方式,针对于每一个产品都提供一个工厂类用于创建。
  • 不过这种模式只适用于简单对象,当我们需要生产许多个产品族的时候,这种模式就有点乏力了,比如:

  • 果按照我们之前工厂方法模式来进行设计,那就需要单独设计9个工厂来生产上面这些产品,显然这样就比较浪费时间的。

  • 将多个产品都放在一个工厂中进行生成,按不同功能的产品族进行划分。

  • 比如小米,那么我就可以安排一个小米工厂,而这个工厂里面就可以生产整条产品线上的内容,包括小米手机、小米平板、小米路由等。
  • 只需要建立一个抽象工厂即可:

    1
    2
    public class Router {
    }
    1
    2
    public class Table {
    }
    1
    2
    public class Phone {
    }
    1
    2
    3
    4
    5
    public abstract class AbstractFactory {
    public abstract Phone getPhone();
    public abstract Table getTable();
    public abstract Router getRouter();
    }
  • 一个工厂可以生产同一个产品族的所有产品,这样按族进行分类,显然比之前的工厂方法模式更好。

  • 不过,缺点还是有的,如果产品族新增了产品,那么我就不得不去为每一个产品族的工厂都去添加新产品的生产方法,违背了开闭原则。

单例模式

  • 只有一个实例对象
  • 在我们的整个程序中,同一个类始终只会有一个对象来进行操作。
  • 比如数据库连接类,实际上我们只需要创建一个对象或是直接使用静态方法就可以了,没必要去创建多个对象。

  • 这里还是还原一下我们之前使用的简单单例模式:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    public class Singleton {
    private final static Singleton INSTANCE = new Singleton(); //用于引用全局唯一的单例对象,在一开始就创建好

    private Singleton() {} //不允许随便new,需要对象直接找getInstance

    public static Singleton getInstance(){ //获取全局唯一的单例对象
    return INSTANCE;
    }
    }
  • 这样,当我们需要获取此对象时,只能通过getInstance()来获取唯一的对象:
    1
    2
    3
    public static void main(String[] args) {
    Singleton singleton = Singleton.getInstance();
    }

    饿汉单例

  • 在一开始类加载时创建好了。

懒汉单例

  • 延迟创建
  • 当需要时才创建
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    public class Singleton {
    private static Singleton INSTANCE; //在一开始先不进行对象创建

    private Singleton() {} //不用多说了吧

    public static Singleton getInstance(){ //将对象的创建延后到需要时再进行
    if(INSTANCE == null) { //如果实例为空,那么就进行创建,不为空说明已经创建过了,那么就直接返回
    INSTANCE = new Singleton();
    }
    return INSTANCE;
    }
    }
  • 懒汉式就真的是条懒狗,你不去用它,它是不会给你提前准备单例对象的(延迟加载,懒加载)。
  • 当我们需要获取对象时,才会进行检查并创建。虽然饿汉式和懒汉式写法不同,但是最后都是成功实现了单例模式。

  • 再多线程环境下,可能会出现问题,多个线程同时调用了getInstace()方法,会出现问题:

  • 在多线程环境下,如果三条线程同时调用getInstance()方法,会同时进行INSTANCE == null的判断。

  • 那么此时由于确实还没有进行任何实例化,所以导致三条线程全部判断为true(而饿汉式由于在类加载时就创建完成,不会存在这样的问题)
  • 此时问题就来了,我们既然要使用单例模式,那么肯定是只希望对象只被初始化一次的,但是现在由于多线程的机制,导致对象被多次创建。

  • 为了避免线程安全问题,针对于懒汉式单例,我们还得进行一些改进:

    1
    2
    3
    4
    5
    6
    public static synchronized Singleton getInstance(){   //方法必须添加synchronized关键字加锁
    if(INSTANCE == null) {
    INSTANCE = new Singleton();
    }
    return INSTANCE;
    }
  • 既然多个线程要调用,那么我们就直接加一把锁,在方法上添加synchronized关键字即可,这样同一时间只能有一个线程进入了。
  • 虽然这样简单粗暴,但是在高并发的情况下,效率肯定是比较低的,我们来看看如何进行优化:只需要对赋值这一步进行加锁即可
    1
    2
    3
    4
    5
    6
    7
    8
    public static Singleton getInstance(){
    if(INSTANCE == null) {
    synchronized (Singleton.class) { //实际上只需要对赋值这一步进行加锁即可
    INSTANCE = new Singleton();
    }
    }
    return INSTANCE;
    }
  • 不过这样还不完美,因为这样还是有可能多个线程同时判断为null而进入等锁的状态,所以,我们还得加一层内层判断:
    1
    2
    3
    4
    5
    6
    7
    8
    public static Singleton getInstance(){
    if(INSTANCE == null) {
    synchronized (Singleton.class) {
    if(INSTANCE == null) INSTANCE = new Singleton(); //内层还要进行一次检查,双重检查锁定
    }
    }
    return INSTANCE;
    }
  • 为什么需要双重锁定:

    • 多线程环境下:
      • 第一个if会对instance进行非空判断,如果instance不为空,则直接返回对象,不会计入同步代码块中,避免了不必要的开销。
      • 第二个if避免了重复创建单例对象的问题。
    • 只有第一个锁:
      • 两个线程同时判断isnatance为空,同时进入同步代码块中。
        • 第一个线程创建了单例并释放了锁。
        • 第二个线程进入,也会创建一个新的对象覆盖掉前一个线程创建的单例对象。
  • 不过我们还少考虑了一样内容,其实IDEA此时应该是给了黄标了:

  • IDEA会要求我们添加一个volatile给INSTANCE.

  • volatile保证可见性,添加后INSTANCE最新值就可以被其他线程拿到了。
  • 不用加锁的方式也能实现延迟加载的写法呢?我们可以使用静态内部类:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    public class Singleton {
    private Singleton() {}

    private static class Holder { //由静态内部类持有单例对象,但是根据类加载特性,我们仅使用Singleton类时,不会对静态内部类进行初始化
    private final static Singleton INSTANCE = new Singleton();
    }

    public static Singleton getInstance(){ //只有真正使用内部类时,才会进行类初始化
    return Holder.INSTANCE; //直接获取内部类中的
    }
    }
  • 由于类加载机制,不会对Holder类进行初始化,只有真正使用时才会初始化。
  • 这种方式显然是最完美的懒汉式解决方案,没有进行任何的加锁操作,也能保证线程安全。
  • 不过要实现这种写法,跟语言本身也有一定的关联,并不是所有的语言都支持这种写法。

原型模式

  • 原型模式实际上与对象的拷贝息息相关,原型模式适用原型实例化指定待创建对象的类型,并且通过赋值这个原型来创建的对象。
  • 也就是说,原型对象作为模版,通过克隆操作。来产生更多的对象,就像细胞复制一样。

浅拷贝

  • 对于类中基本数据类型,会直接复制值给拷贝对象。
  • 对于引用类型,只会复制对象的地址,而实际上指向的还是原来的那个对象,拷贝个寂寞。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    public static void main(String[] args) {
    int a = 10;
    int b = a; //基本类型浅拷贝
    System.out.println(a == b);

    Object o = new Object();
    Object k = o; //引用类型浅拷贝,拷贝的仅仅是对上面对象的引用
    System.out.println(o == k);
    }

深拷贝

  • 无论是基本类型还是引用类型,深拷贝会将引用类型的所有内容,全部拷贝作为一个新的对象。
  • 包括对象内部的所有成员变量,也会进行拷贝。

  • 在Java中,我们可以使用Cloneeable接口提供的拷贝机制,来实现原型模式:

    1
    2
    3
    4
    5
    6
    public class Student implements Cloneable{   //注意需要实现Cloneable接口
    @Override
    public Object clone() throws CloneNotSupportedException { //提升clone方法的访问权限
    return super.clone();
    }
    }
  • 接着我们来看看克隆的对象是不是原来的对象:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    public static void main(String[] args) throws CloneNotSupportedException {
    Student student0 = new Student();
    Student student1 = (Student) student0.clone();
    System.out.println(student0);
    System.out.println(student1);
    }
    out:
    com.test.Student@7d417077
    com.test.Student@7dc36524
  • 可以看到,通过clone()方法克隆的对象并不是原来的对象,我们来看看如果对象内部有属性会不会一起进行克隆:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    public class Student implements Cloneable{

    String name;

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

    public String getName() {
    return name;
    }

    @Override
    public Object clone() throws CloneNotSupportedException {
    return super.clone();
    }
    }
    1
    2
    3
    4
    5
    6
    7
    public static void main(String[] args) throws CloneNotSupportedException {
    Student student0 = new Student("小明");
    Student student1 = (Student) student0.clone();
    System.out.println(student0.getName() == student1.getName());
    }
    out:
    true
  • 可以看到,虽然Student对象成功拷贝,但是其内层对象并没有进行拷贝,依然只是对象引用的复制。
  • 所以Java为我们提供的clone方法只会进行浅拷贝。那么如何才能实现深拷贝呢?
    1
    2
    3
    4
    5
    6
    @Override
    public Object clone() throws CloneNotSupportedException { //这里我们改进一下,针对成员变量也进行拷贝
    Student student = (Student) super.clone();
    student.name = new String(name);
    return student; //成员拷贝完成后,再返回
    }

装饰模式

  • 不改变一个对象本身功能的基础上,给对象添加额外的行为。
  • 通过组合的形式完成,而不是传统的继承关系。

代理模式

  • ,比如我们访问Github只需要输入网址即可访问,而添加代理之后,也是使用同样的方式去访问Github,所以操作起来是一样的。
  • 包括Spring框架其实也是依靠代理模式去实现的AOP记录日志等。
  • 比如现在有一个目标类,但是我们现在需要通过代理来使用它:
    1
    2
    3
    public abstract class Subject {
    public abstract void test();
    }
    1
    2
    3
    4
    5
    6
    7
    8
    public class SubjectImpl extends Subject{  //此类无法直接使用,需要我们进行代理

    @Override
    public void test() {
    System.out.println("我是测试方法!");
    }
    }

  • 现在建立一个代理类:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    public class Proxy extends Subject{   //为了保证和Subject操作方式一样,保证透明性,也得继承

    Subject target; //被代理的对象(甚至可以多重代理)

    public Proxy(Subject subject){
    this.target = subject;
    }

    @Override
    public void test() { //由代理去执行被代理对象的方法,并且我们还可以在前后添油加醋
    System.out.println("代理前绕方法");
    target.test();
    System.out.println("代理后绕方法");
    }
    }

    动态代理

  • 通过反射机制动态代理:
    1
    2
    3
    public interface Subject {  //JDK提供的动态代理只支持接口
    void test();
    }
    1
    2
    3
    4
    5
    6
    7
    public class SubjectImpl implements Subject{

    @Override
    public void test() {
    System.out.println("我是测试方法!");
    }
    }
  • 接着我们需要创建一个动态代理的处理逻辑:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    public class TestProxy implements InvocationHandler {    //代理类,需要实现InvocationHandler接口

    private final Object object; //这里需要保存一下被代理的对象,下面需要用到

    public TestProxy(Object object) {
    this.object = object;
    }

    @Override //此方法就是调用代理对象的对应方法时会进入,这里我们就需要编写如何进行代理了
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    //method就是调用的代理对象的哪一个方法,args是实参数组
    System.out.println("代理的对象:"+proxy.getClass()); //proxy就是生成的代理对象了,我们看看是什么类型的
    Object res = method.invoke(object, args); //在代理中调用被代理对象原本的方法,因为你是代理,还是得执行一下别人的业务,当然也可以不执行,但是这样就失去代理的意义了,注意要用上面的object
    System.out.println("方法调用完成,返回值为:"+res); //看看返回值是什么
    return res; //返回返回值
    }
    }
  • 重写invoke。
  • 创建代理类:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    public static void main(String[] args) {
    SubjectImpl subject = new SubjectImpl(); //被代理的大冤种
    InvocationHandler handler = new TestProxy(subject);
    Subject proxy = (Subject) Proxy.newProxyInstance(
    subject.getClass().getClassLoader(), //需要传入被代理的类的类加载器
    subject.getClass().getInterfaces(), //需要传入被代理的类的接口列表
    handler); //最后传入我们实现的代理处理逻辑实现类
    proxy.test(); //比如现在我们调用代理类的test方法,那么就会进入到我们上面TestProxy中invoke方法,走我们的代理逻辑
    }
    out:
    代理的对象: class com.sum.proxy.$Proxy0
    我是测试方法!
    方法调用完成,返回值为null

cglib

  • JDK提供的动态代理只能使用接口,如果换成我们一开始的抽象类,就没办法了。
  • 时我们可以使用一些第三方框架来实现更多方式的动态代理,比如Spring都在使用的CGLib框架。
  • 由于CGlib底层使用ASM框架(JVM篇视频教程有介绍)进行字节码编辑,所以能够实现不仅仅局限于对接口的代理:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    public class TestProxy implements MethodInterceptor {  //首先还是编写我们的代理逻辑

    private final Object target; //这些和之前JDK动态代理写法是一样的

    public TestProxy(Object target) {
    this.target = target;
    }

    @Override //我们也是需要在这里去编写我们的代理逻辑
    public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
    System.out.println("现在是由CGLib进行代理操作!"+o.getClass());
    return method.invoke(target, objects); //也是直接调用代理对象的方法即可
    }
    }
  • 接着我们来创建一下代理类:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    public static void main(String[] args) {
    SubjectImpl subject = new SubjectImpl();

    Enhancer enhancer = new Enhancer(); //增强器,一会就需要依靠增强器来为我们生成动态代理对象
    enhancer.setSuperclass(SubjectImpl.class); //直接选择我们需要代理的类型,直接不需要接口或是抽象类,SuperClass作为代理类的父类存在,这样我们就可以按照指定类型的方式去操作代理类了
    enhancer.setCallback(new TestProxy(subject)); //设定我们刚刚编写好的代理逻辑

    SubjectImpl proxy = (SubjectImpl) enhancer.create(); //直接创建代理类

    proxy.test(); //调用代理类的test方法
    }

与装饰模式的区别

  • 装饰模式强调的是增强自身,在被装饰之后能够在被增强的类上使用装饰的功能。
  • 代理模式则是让别人帮你去做事情,以及添加一些本身与你与物没有太多关系的事情(记录日志、设置缓存)。,重点在于让别人帮做。