更多相关内容可查看
在一个狂风骤雨的下午,有人突然问了我一句,单例模式是什么,我愣了,相信看完这篇就不会愣了,本文以通俗易懂的方式写的,可能有不严谨的地方🙏🙏
单例模式
一个类只有一个实例对象
(例外:Spring中的Bean的单例是指在一个容器中是单例)
那是怎么实现的呢?我不能new很多吗?看看如下代码
public class Singleton {
// 创建 Singleton 类的一个对象
private static Singleton instance = new Singleton();
// 让构造函数为 private,这样该类就不会被实例化
private Singleton(){}
// 获取唯一可用的对象
public static Singleton getInstance(){
return instance;
}
public void showMessage(){
System.out.println("Hello World!");
}
}
看了如上代码,因为构造函数私有
,想用这个对象只能Singleton.getInstance()
来获取这个对象,是不是就很一目了然了,所以引出单例模式的核心:
无论在哪里,无论何时,只要你调用 Singleton.getInstance()
,你都将得到同一个 Singleton 对象
。
那我什么时候用他比较优雅呢?
如果你有一个配置类,它读取配置文件并提供配置设置,你可能只想在程序中创建一个这样的对象。使用单例模式,你可以确保所有需要访问配置设置的代码都使用同一个配置对象
,这样可以避免在内存中存储多个相同的配置对象,从而节省内存
以上这种方式为饿汉式(静态常量方式):在类加载的时候就完成了实例化,我不用他他就已经存在了
有饿汉式就有懒汉式(双重检查机制),看看如下代码
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变量
,在调用getInstance()
,这两个if会确保INSTANCE只生成一个对象
第二个if是干嘛的?为什么还要判断一遍?
假设AB同时走到同步代码块,A先抢到锁,进入代码,创建了对象,释放锁,此时B进入代码块,如果没有判断null,那么就会直接再次创建对象,那么就不是单例的了
那为什么变量上用一个volatile?
volatile:可以保证变量的可见性
和禁止指令重排序
那什么是重排序?
在多线程环境下,为了保证性能,编译器和处理器可能会对代码进行重排序
例如原来的执行顺序是:
- 第一步:分配内存空间给Singleton这个对象
- 第二步:初始化对象
- 第三步:将INSTANCE变量指向Singleton这个对象内存地址
假设没有使用volatile
关键字发生了重排序,第二步和第三步执行过程被调换了,也就是先将INSTANCE变量指向Singleton这个对象内存地址,再初始化对象。这样在发生并发的情况下,另一个线程经过第一个if非空判断时,发现已经为不为空,就直接返回了这个对象,但是此时这个对象还未初始化,内部的属性可能都是空值,一旦被使用的话,就很有可能出现空指针这些问题。
建造者模式
优雅的构建一个对象
public class BuilderPatternDemo {
public static void main(String[] args) {
Car car = new Car.CarBuilder()
.buildEngine("Ferrari V8 Engine")
.buildWheels("Ferrari Sports Wheels")
.buildColor("Red")
.build();
System.out.println("Car built: " + car);
}
}
class Car {
private String engine;
private String wheels;
private String color;
// ... getter and setter methods ...
static class CarBuilder {
private Car car;
public CarBuilder() {
car = new Car();
}
public CarBuilder buildEngine(String engine) {
car.setEngine(engine);
return this;
}
public CarBuilder buildWheels(String wheels) {
car.setWheels(wheels);
return this;
}
public CarBuilder buildColor(String color) {
car.setColor(color);
return this;
}
public Car build() {
return car;
}
}
}
注意每一个build方法(例如:buildEngine)
返回的this
都是CarBuilder
对象,所以他可以继续调用CarBuilder的其他build方法,最后返回一个Car
对象,这样是不是以后我们在创建实体类的时候也可以如此呢,是不是比new一个对象,然后不断set显得更优雅一点了
工厂模式
- 简单工厂模式
- 工厂方法模式
- 抽象工厂模式
简单工厂模式
调用者不需要关心对象是如何具体创建的
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 一个Cat的对象,需要这样折腾一下?
对,是不需要,这种情况直接new就可以,但如果你想想,这个if语句是进行类型判断,是不是字符串,是不是数值类型,根据类型去创建对象的业务场景时,你的工厂类可能穿过来个json串,进行解析,解析完进行createAnimal创建对象就可以使用这种方式
工厂方法模式
上面说的简单工厂模式看起来没啥问题,但是还是违反了七大设计原则的OCP原则
,也就是开闭原则。也就是在加一个动物,是不是就要去修改代码的问题
所以解决简单工厂模式违反开闭原则的问题,就可以使用工厂方法模式来解决。
/**
* 工厂接口
*/
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 = new CatFactory();
Animal cat = animalFactory.createAnimal();
此时假设需要新增一个动物兔子,那么只需要实现AnimalFactory接口就行,对于原来的猫和狗的实现,其实代码是不需要修改的,遵守了对修改关闭的原则,同时由于是对扩展开放,实现接口就是扩展的意思,那么也就符合扩展开放的原则。
应用(应用一说,offer到手):
1.在Mybatis中,当需要调用Mapper接口执行sql的时候,需要先获取到SqlSession,通过SqlSession再获取到Mapper接口的动态代理对象,而SqlSession的构造过程比较复杂,所以就提供了SqlSessionFactory工厂类来封装SqlSession的创建过程。
2.BeanFactory就是Bean生成的工厂。一个Spring Bean在生成过程中会经历复杂的一个生命周期,而这些生命周期对于使用者来说是无需关心的,所以就可以将Bean创建过程的逻辑给封装起来,提取出一个Bean的工厂。
抽象工厂模式
工厂方法模式其实是创建一个产品的工厂,比如上面的例子中,AnimalFactory其实只创建动物这一个产品。而抽象工厂模式特点就是创建一系列产品,比如说,不同的动物吃的东西是不一样的,那么就可以加入食物这个产品,通过抽象工厂模式来实现。
public interface AnimalFactory {
Animal createAnimal();
Food createFood();
}
策略模式
假设现在有一个需求,需要将消息推送到不同的平台。
最简单的做法其实就是使用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通知等其实都是发送消息一种策略,而策略模式就是需要将这些策略进行封装,抽取共性,使这些策略之间相互替换。
应用:
1、策略模式在SpringMVC中提供了一个策略接口HandlerMethodArgumentResolver对接口方法参数的处理
比如说,我们经常在写接口的时候,会使用到了@PathVariable、@RequestParam、@RequestBody等注解,一旦我们使用了注解,SpringMVC会处理注解,从请求中获取到参数,然后再调用接口传递过来,而这个过程,就使用到了策略模式。
2、SpringBoot对于配置文件的加载PropertySourceLoader也是策略模式的运用
本篇小结
本文通过对设计模式的讲解加源码举例的方式介绍了4种在代码设计中常用的设计模式:
- 单例模式
- 建造者模式
- 工厂模式
- 策略模式
写到这就开始搬砖去了,看完这篇相信再问你单例模式是什么你已经能侃侃而谈了