两万字盘点9种被玩烂了的设计模式以及在开源项目中的运用(上)

简介: 大家好,我是三友~~之前有小伙伴私信我说看源码的时候感觉源码很难,不知道该怎么看,其实这有部分原因是因为没有弄懂一些源码实现的套路,也就是设计模式,所以本文我就总结了9种在源码中非常常见的设计模式,并列举了很多源码的实现例子,希望对你看源码和日常工作中有所帮助。

大家好,我是三友~~

之前有小伙伴私信我说看源码的时候感觉源码很难,不知道该怎么看,其实这有部分原因是因为没有弄懂一些源码实现的套路,也就是设计模式,所以本文我就总结了9种在源码中非常常见的设计模式,并列举了很多源码的实现例子,希望对你看源码和日常工作中有所帮助。

单例模式

单例模式是指一个类在一个进程中只有一个实例对象(但也不一定,比如Spring中的Bean的单例是指在一个容器中是单例的)

单例模式创建分为饿汉式和懒汉式,总共大概有8种写法。但是在开源项目中使用最多的主要有两种写法:

1、静态常量

静态常量方式属于饿汉式,以静态变量的方式声明对象。这种单例模式在Spring中使用的比较多,举个例子,在Spring中对于Bean的名称生成有个类AnnotationBeanNameGenerator就是单例的。

AnnotationBeanNameGenerator

2、双重检查机制

除了上面一种,还有一种双重检查机制在开源项目中也使用的比较多,而且在面试中也比较喜欢问。双重检查机制方式属于懒汉式,代码如下:

public class Singleton {
   

    private volatile static Singleton INSTANCE;

    private Singleton() {
   
    }

    public static Singleton getInstance() {
   
        if (INSTANCE == null) {
   
            synchronized (Singleton.class) {
   
                if (INSTANCE == null) {
   
                    INSTANCE = new Singleton();
                }
            }
        }
        return INSTANCE;
    }

}

之所以这种方式叫双重检查机制,主要是在创建对象的时候进行了两次INSTANCE == null的判断。

疑问讲解

这里解释一下双重检查机制的三个疑问:

  • 外层判断null的作用
  • 内层判断null的作用
  • 变量使用volatile关键字修饰的作用

外层判断null的作用:其实就是为了减少进入同步代码块的次数,提高效率。你想一下,其实去了外层的判断其实是可以的,但是每次获取对象都需要进入同步代码块,实在是没有必要。

内层判断null的作用:防止多次创建对象。假设AB同时走到同步代码块,A先抢到锁,进入代码,创建了对象,释放锁,此时B进入代码块,如果没有判断null,那么就会直接再次创建对象,那么就不是单例的了,所以需要进行判断null,防止重复创建单例对象。

volatile关键字的作用:防止重排序。因为创建对象的过程不是原子,大概会分为三个步骤

  • 第一步:分配内存空间给Singleton这个对象
  • 第二步:初始化对象
  • 第三步:将INSTANCE变量指向Singleton这个对象内存地址

假设没有使用volatile关键字发生了重排序,第二步和第三步执行过程被调换了,也就是先将INSTANCE变量指向Singleton这个对象内存地址,再初始化对象。这样在发生并发的情况下,另一个线程经过第一个if非空判断时,发现已经为不为空,就直接返回了这个对象,但是此时这个对象还未初始化,内部的属性可能都是空值,一旦被使用的话,就很有可能出现空指针这些问题。

双重检查机制在dubbo中的应用

在dubbo的spi机制中获取对象的时候有这样一段代码:

虽然这段代码跟上面的单例的写法有点不同,但是不难看出其实是使用了双重检查机制来创建对象,保证对象单例。

建造者模式

将一个复杂对象的构造与它的表示分离,使同样的构建过程可以创建不同的表示,这样的设计模式被称为建造者模式。它是将一个复杂的对象分解为多个简单的对象,然后一步一步构建而成。

上面的意思看起来很绕,其实在实际开发中,其实建造者模式使用的还是比较多的,比如有时在创建一个pojo对象时,就可以使用建造者模式来创建:

PersonDTO personDTO = PersonDTO.builder()
        .name("三友的java日记")
        .age(18)
        .sex(1)
        .phone("188****9527")
        .build();

上面这段代码就是通过建造者模式构建了一个PersonDTO对象,所以建造者模式又被称为Budiler模式。

这种模式在创建对象的时候看起来比较优雅,当构造参数比较多的时候,适合使用建造者模式。

接下来就来看看建造者模式在开源项目中是如何运用的

1、在Spring中的运用

我们都知道,Spring在创建Bean之前,会将每个Bean的声明封装成对应的一个BeanDefinition,而BeanDefinition会封装很多属性,所以Spring为了更加优雅地创建BeanDefinition,就提供了BeanDefinitionBuilder这个建造者类。

BeanDefinitionBuilder

2、在Guava中的运用

在项目中,如果我们需要使用本地缓存,会使用本地缓存的实现的框架来创建一个,比如在使用Guava来创建本地缓存时,就会这么写

Cache<String, String> cache = CacheBuilder.newBuilder()
         .expireAfterAccess(1, TimeUnit.MINUTES)
         .maximumSize(200)
         .build();

这其实也就是建造者模式。

建造者模式不仅在开源项目中有所使用,在JDK源码中也有使用到,比如StringBuilder类。

最后上面说的建造者模式其实算是在Java中一种简化的方式,如果想了解一下传统的建造者模式,可以看一下这篇文章

https://m.runoob.com/design-pattern/builder-pattern.html?ivk_sa=1024320u

工厂模式

工厂模式在开源项目中也使用的非常多,具体的实现大概可以细分为三种:

  • 简单工厂模式
  • 工厂方法模式
  • 抽象工厂模式

简单工厂模式

简单工厂模式,就跟名字一样,的确很简单。比如说,现在有个动物接口Animal,具体的实现有猫Cat、狗Dog等等,而每个具体的动物对象创建过程很复杂,有各种各样地步骤,此时就可以使用简单工厂来封装对象的创建过程,调用者不需要关心对象是如何具体创建的。

public class SimpleAnimalFactory {
   

    public Animal createAnimal(String animalType) {
   
        if ("cat".equals(animalType)) {
   
            Cat cat = new Cat();
            //一系列复杂操作
            return cat;
        } else if ("dog".equals(animalType)) {
   
            Dog dog = new Dog();
            //一系列复杂操作
            return dog;
        } else {
   
            throw new RuntimeException("animalType=" + animalType + "无法创建对应对象");
        }
    }

}

当需要使用这些对象,调用者就可以直接通过简单工厂创建就行。

SimpleAnimalFactory animalFactory = new SimpleAnimalFactory();
Animal cat = animalFactory.createAnimal("cat");

需要注意的是,一般来说如果每个动物对象的创建只需要简单地new一下就行了,那么其实就无需使用工厂模式,工厂模式适合对象创建过程复杂的场景。

工厂方法模式

上面说的简单工厂模式看起来没啥问题,但是还是违反了七大设计原则的OCP原则,也就是开闭原则。所谓的开闭原则就是对修改关闭,对扩展开放。

什么叫对修改关闭?就是尽可能不修改的意思。就拿上面的例子来说,如果现在新增了一种动物兔子,那么createAnimal方法就得修改,增加一种类型的判断,那么就此时就出现了修改代码的行为,也就违反了对修改关闭的原则。

所以解决简单工厂模式违反开闭原则的问题,就可以使用工厂方法模式来解决。

/**
 * 工厂接口
 */
public interface AnimalFactory {
   
    Animal createAnimal();
}

/**
 * 小猫实现
 */
public class CatFactory implements AnimalFactory {
   
    @Override
    public Animal createAnimal() {
   
        Cat cat = new Cat();
        //一系列复杂操作
        return cat;
    }
}

/**
 * 小狗实现
 */
public class DogFactory implements AnimalFactory {
   
    @Override
    public Animal createAnimal() {
   
        Dog dog = new Dog();
        //一系列复杂操作
        return dog;
    }
}

这种方式就是工厂方法模式。他将动物工厂提取成一个接口AnimalFactory,具体每个动物都各自实现这个接口,每种动物都有各自的创建工厂,如果调用者需要创建动物,就可以通过各自的工厂来实现。

AnimalFactory animalFactory = new CatFactory();
Animal cat = animalFactory.createAnimal();

此时假设需要新增一个动物兔子,那么只需要实现AnimalFactory接口就行,对于原来的猫和狗的实现,其实代码是不需要修改的,遵守了对修改关闭的原则,同时由于是对扩展开放,实现接口就是扩展的意思,那么也就符合扩展开放的原则。

抽象工厂模式

工厂方法模式其实是创建一个产品的工厂,比如上面的例子中,AnimalFactory其实只创建动物这一个产品。而抽象工厂模式特点就是创建一系列产品,比如说,不同的动物吃的东西是不一样的,那么就可以加入食物这个产品,通过抽象工厂模式来实现。

public interface AnimalFactory {
   

    Animal createAnimal();

    Food createFood();

}

在动物工厂中,新增了创建食物的接口,小狗小猫的工厂去实现这个接口,创建狗粮和猫粮,这里就不去写了。

1、工厂模式在Mybatis的运用

在Mybatis中,当需要调用Mapper接口执行sql的时候,需要先获取到SqlSession,通过SqlSession再获取到Mapper接口的动态代理对象,而SqlSession的构造过程比较复杂,所以就提供了SqlSessionFactory工厂类来封装SqlSession的创建过程。

SqlSessionFactory及默认实现DefaultSqlSessionFactory

对于使用者来说,只需要通过SqlSessionFactory来获取到SqlSession,而无需关心SqlSession是如何创建的。

2、工厂模式在Spring中的运用

我们知道Spring中的Bean是通过BeanFactory创建的。

BeanFactory就是Bean生成的工厂。一个Spring Bean在生成过程中会经历复杂的一个生命周期,而这些生命周期对于使用者来说是无需关心的,所以就可以将Bean创建过程的逻辑给封装起来,提取出一个Bean的工厂。

策略模式

策略模式也比较常见,就比如说在Spring源码中就有很多地方都使用到了策略模式。

在讲策略模式是什么之前先来举个例子,这个例子我在之前的《写出漂亮代码的45个小技巧》文章提到过。

假设现在有一个需求,需要将消息推送到不同的平台。

最简单的做法其实就是使用if else来做判断就行了。

public void notifyMessage(User user, String content, int notifyType) {
   
    if (notifyType == 0) {
   
        //调用短信通知的api发送短信
    } else if (notifyType == 1) {
   
        //调用app通知的api发送消息
    }
}

根据不同的平台类型进行判断,调用对应的api发送消息。

虽然这样能实现功能,但是跟上面的提到的简单工厂的问题是一样的,同样违反了开闭原则。当需要增加一种平台类型,比如邮件通知,那么就得修改notifyMessage的方法,再次进行else if的判断,然后调用发送邮件的邮件发送消息。

此时就可以使用策略模式来优化了。

首先设计一个策略接口:

public interface MessageNotifier {
   

    /**
     * 是否支持改类型的通知的方式
     *
     * @param notifyType 0:短信 1:app
     * @return
     */
    boolean support(int notifyType);

    /**
     * 通知
     *
     * @param user
     * @param content
     */
    void notify(User user, String content);

}

短信通知实现:

@Component
public class SMSMessageNotifier implements MessageNotifier {
   
    @Override
    public boolean support(int notifyType) {
   
        return notifyType == 0;
    }

    @Override
    public void notify(User user, String content) {
   
        //调用短信通知的api发送短信
    }
}

app通知实现:

public class AppMessageNotifier implements MessageNotifier {
   
    @Override
    public boolean support(int notifyType) {
   
        return notifyType == 1;
    }

    @Override
    public void notify(User user, String content) {
   
       //调用通知app通知的api
    }
}

最后notifyMessage的实现只需要要循环调用所有的MessageNotifier的support方法,一旦support方法返回true,说明当前MessageNotifier支持该类的消息发送,最后再调用notify发送消息就可以了。

@Resource
private List<MessageNotifier> messageNotifiers;

public void notifyMessage(User user, String content, int notifyType) {
   
    for (MessageNotifier messageNotifier : messageNotifiers) {
   
        if (messageNotifier.support(notifyType)) {
   
            messageNotifier.notify(user, content);
        }
    }
}

那么如果现在需要支持通过邮件通知,只需要实现MessageNotifier接口,注入到Spring容器就行,其余的代码根本不需要有任何变动。

到这其实可以更好的理解策略模式了。就拿上面举的例子来说,短信通知,app通知等其实都是发送消息一种策略,而策略模式就是需要将这些策略进行封装,抽取共性,使这些策略之间相互替换。

策略模式在SpringMVC中的运用

1、对接口方法参数的处理

比如说,我们经常在写接口的时候,会使用到了@PathVariable、@RequestParam、@RequestBody等注解,一旦我们使用了注解,SpringMVC会处理注解,从请求中获取到参数,然后再调用接口传递过来,而这个过程,就使用到了策略模式。

对于这类参数的解析,SpringMVC提供了一个策略接口HandlerMethodArgumentResolver

HandlerMethodArgumentResolver

这个接口的定义就跟我们上面定义的差不多,不同的参数处理只需要实现这个解决就行,比如上面提到的几个注解,都有对应的实现。

比如处理@RequestParam注解的RequestParamMethodArgumentResolver的实现。

RequestParamMethodArgumentResolver

当然还有其它很多的实现,如果想知道各种注解处理的过程,只需要找到对应的实现类就行了。

2、对接口返回值的处理

同样,SpringMVC对于返回值的处理也是基于策略模式来实现的。

HandlerMethodReturnValueHandler

HandlerMethodReturnValueHandler接口定义跟上面都是同一种套路。

比如说,常见的对于@ResponseBody注解处理的实现RequestResponseBodyMethodProcessor。

ResponseBody注解处理的实现RequestResponseBodyMethodProcessor

同样,HandlerMethodReturnValueHandler的实现也有很多,这里就不再举例了。

策略模式在Spring的运用远不止这两处,就比如我在《三万字盘点Spring/Boot的那些常用扩展点》文章提到过对于配置文件的加载PropertySourceLoader也是策略模式的运用。

往期热门文章推荐

如何去阅读源码,我总结了18条心法

如何写出漂亮代码,我总结了45个小技巧

三万字盘点Spring/Boot的那些常用扩展点

三万字盘点Spring 9大核心基础功能

万字+20张图剖析Spring启动时12个核心步骤

1.5万字+30张图盘点索引常见的11个知识点

两万字盘点那些被玩烂了的设计模式

搜索关注公众号 三友的java日记 ,及时干货不错过,公众号致力于通过画图加上通俗易懂的语言讲解技术,让技术更加容易学习,回复 面试 即可获得一套面试真题。

相关文章
|
设计模式
110.【十万字带你深入学习23种设计模式】(二十二)
110.【十万字带你深入学习23种设计模式】
62 1
110.【十万字带你深入学习23种设计模式】(二十二)
|
设计模式 算法 Java
110.【十万字带你深入学习23种设计模式】(十九)
110.【十万字带你深入学习23种设计模式】
68 1
|
设计模式 Java
110.【十万字带你深入学习23种设计模式】(二十一)
110.【十万字带你深入学习23种设计模式】
67 1
|
设计模式 Java
110.【十万字带你深入学习23种设计模式】(二十六)
110.【十万字带你深入学习23种设计模式】
83 1
|
设计模式 应用服务中间件 智能硬件
110.【十万字带你深入学习23种设计模式】(十七)
110.【十万字带你深入学习23种设计模式】
67 0
|
设计模式 Java Linux
110.【十万字带你深入学习23种设计模式】(十五)
110.【十万字带你深入学习23种设计模式】
67 0
|
设计模式 算法
110.【十万字带你深入学习23种设计模式】(十二)
110.【十万字带你深入学习23种设计模式】
49 0
|
设计模式 存储 Java
110.【十万字带你深入学习23种设计模式】(十一)
110.【十万字带你深入学习23种设计模式】
64 0
|
设计模式 安全 Java
110.【十万字带你深入学习23种设计模式】(五)
110.【十万字带你深入学习23种设计模式】
65 0
|
设计模式 安全 Java
110.【十万字带你深入学习23种设计模式】(六)
110.【十万字带你深入学习23种设计模式】
56 0