一、设计模式分类
部分内容参考javaguide。
创建型:在创建对象的同时隐藏创建逻辑,不使用 new 直接实例化对象,程序在判断需要创建哪些对象时更灵活。包括工厂/抽象工厂/单例/建造者/原型模式。
结构型:通过类和接口间的继承和引用实现创建复杂结构的对象。包括适配器/桥接模式/过滤器/组合/装饰器/外观/享元/代理模式。
行为型:通过类之间不同通信方式实现不同行为。包括责任链/命名/解释器/迭代器/中介者/备忘录/观察者/状态/策略/模板/访问者模式。
二、创建型模式
1. 工厂模式
什么是工厂模式?
- 工厂模式是一种创建对象的设计模式,通过工厂类来创建对象实例,从而实现解耦和灵活性。
- 目的:将对象的创建过程封装起来,通过工厂类来统一创建对象,隐藏具体实现细节。
应用场景?
- 当需要创建对象的过程比较复杂时:如果对象的创建过程涉及到复杂的逻辑、依赖关系或条件判断,可以使用工厂模式将这些复杂的逻辑封装在工厂类中,简化客户端的代码。
- 创建对象的方式可能根据条件变化时:如果类的实例化方式需要根据不同的条件或配置来选择具体的实现类,可以使用工厂模式动态地根据条件来创建对象。
优点?
-
封装对象的创建:将对象的创建过程封装在工厂类中,客户端只需要关心如何获取对象,而无需关心对象的创建细节。
-
解耦对象的使用和创建:降低系统耦合度。
-
简化对象的替换和扩展:可以方便地替换具体的工厂实现或增加新的工厂实现,从而替换和扩展工厂创建出来的对象。
简单工厂模式案例:
假设我们有一个汽车制造厂,其中有多种类型的汽车需要生产,包括小轿车 Sedan 和 SUV,它们都继承 Car 接口。那么我们可以使用工厂模式来创建这些汽车对象。
首先,创建一个工厂接口(CarFactory
),定义一个用于创建汽车对象的方法:
public interface CarFactory {
Car createCar(String carName);
}
然后,创建一个工厂类,实现工厂接口,并根据传入参数 carName 创建具体的汽车实例对象:
public class DefaultCarFactory implements CarFactory{
@Override
public Car createCar(String carName) {
if (carName.equals("SUV")){
Car suv = new SUV();
// 这里可能还有一些配置SUV的操作,并且这些操作可能是比较复杂的...
return suv;
}else if (carName.equals("Sedan")){
Car sedan = new Sedan();
// 这里可能还有一些配置sedan的操作...
return sedan;
}else {
throw new RuntimeException("There is no car called: " + carName);
}
}
}
最后,我们可以使用工厂来创建具体的汽车对象:
public class Main {
public static void main(String[] args) {
String carName = Config.getCar("carName"); // 假设从配置文件中获取carName
CarFactory carFactory = new DefaultCarFactory();
Car car = carFactory.createCar(carName);
// 使用car...
}
}
通过工厂模式,我们可以将创建对象的过程封装在工厂中,客户端只需要通过工厂来获取所需的对象,而无需关心具体的对象创建细节。
工厂方法模式:上面这种方式是简单的工厂模式,可以看到有一个很明显的缺点就是当我们要扩展一些Car实现对象时(比如MPV车类型),那么就需要在 DefaultCarFactory
的 if…else 处修改代码(硬编码方式),这是不利于扩展的。所以一般会针对一个具体的 Car 实现类创建一个对应的工厂类,比如对于 SUV 就应该有一个 public class CarFactorySUV implements CarFactory
专门创建 SUV 对象。
java/Spring 中的工厂模式有哪些?
JDK 自带的 Calendar
抽象类的 getInstance
⽅法,调⽤ createCalendar
⽅法根据不同的地区参数创建不同的⽇历对象。
在Spring框架中,使用工厂模式通过 BeanFactory
、ApplicationContext
(这两者都是接口)创建和管理 bean 对象。举例如下:
public class Main {
public static void main(String[] args) {
ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
UserService userService = context.getBean("userService", UserService.class); // 获取bean
}
}
通过使用Spring框架,将对象的创建过程交由Spring容器来管理,并通过工厂模式的方式将对象的创建和使用解耦。这使得我们能够以一致的方式创建和获取对象,并且能够更灵活地切换具体的实现类或配置。
⚠️BeanFactory
使用到某个 bean 的时候才会注入,启动快;ApplicationContext
一次性创建所有 bean 。ApplicationContext
扩展了 BeanFactory
,一般使用更多。
2. 建造者模式
什么是建造者模式?
- 建造者模式是一种创建型设计模式,用于通过一步一步的构建过程来创建复杂的对象。它将对象的构建过程与其表示分离,使得同样的构建过程可以创建不同的表示。
- 目的:将复杂对象的构建过程与其表示分离,使得同样的构建过程可以创建不同的表示。
- 建造者模式包含以下几个核心角色:
- 产品(Product):表示最终构建的复杂对象。
- 抽象建造者(Builder):定义了创建产品各个部分的抽象方法,具体建造者将实现这些方法来构建具体产品。
- 具体建造者(Concrete Builder):实现抽象建造者接口,负责实际构建产品的各个部分,并提供一个方法用于返回构建好的产品。
- 指挥者(Director):负责调用具体建造者的方法来构建产品,它并不直接知道具体产品的细节。
应用场景?
- 对象创建过程复杂,包含多个步骤或参数时:通过建造者模式,可以将构建过程分解为多个独立的步骤,从而灵活地组合和构建不同的对象表示。
- 对象有多种表示或配置时:比如对象的某个属性有多种实现方案可以选择,选择不同的属性实现就能使构建出的对象有不同的表示。
优点?
- 分离构建过程与表示:建造者模式可以将一个复杂对象的构建过程与其最终的表示分离,使得同样的构建过程可以创建不同的表示。
- 隐藏构建细节:建造者模式通过将构建过程封装在具体建造者内部,客户端只需要关注设置属性的方法和最终的构建操作,而无需关心具体的构建过程。
建造者模式案例:
演示使用建造者模式构建一个包含多个部分的电脑对象:
首先,定义产品类 Computer
:
public class Computer {
// Computer应该是一个比较复杂的类,它的属性可能配置起来比较麻烦,这里只是简化表示
private String cpu;
private String memory;
private String storage;
private String graphicsCard;
public void setCpu(String cpu) {
this.cpu = cpu;
}
public void setMemory(String memory) {
this.memory = memory;
}
public void setStorage(String storage) {
this.storage = storage;
}
public void setGraphicsCard(String graphicsCard) {
this.graphicsCard = graphicsCard;
}
// 省略其他方法
}
然后,创建建造者 ComputerBuilder
:
public class ComputerBuilder {
protected Computer computer = new Computer(); // 这一步是需要留意
public ComputerBuilder addCpu(String cpu){
computer.setCpu(cpu);
return this; // 链式调用
}
public ComputerBuilder addMemory(String memory){
computer.setMemory(memory);
return this;
}
public ComputerBuilder addStorage(String storage){
computer.setStorage(storage);
return this;
}
public ComputerBuilder addGraphicsCard(String graphicsCard){
computer.setMemory(graphicsCard);
return this;
}
// build方法返回构建出来的computer对象
public Computer build(){
return computer;
}
}
使用建造者创建对象:
public class Main {
public static void main(String[] args) {
// 链式调用是建造者模式的风格之一
ComputerBuilder computerBuilder = new ComputerBuilder()
.addCpu("Intel Core i9")
.addMemory("32GB DDR4 RAM")
.addStorage("1TB NVMe SSD")
.addGraphicsCard("NVIDIA GeForce RTX 3080");
Computer computer = computerBuilder.build();
// 使用computer
}
}
java/Spring 中的建造者模式有哪些?
在 Mybatis 中,比如 SqlSessionFactoryBuilder
通过调用 builder()
方法获得的是一个 SqlSessionFactory
类。
-
JDK 自带的
StringBuilder
。 -
在 Spring 中,
BeanDefinitionBuilder
:用于构建BeanDefinition
对象,用于定义和配置 Spring 容器中的 Bean。它提供了一种链式调用的方式来设置Bean的各种属性和配置。BeanDefinition beanDefinition = BeanDefinitionBuilder.genericBeanDefinition(MyClass.class) .setScope(BeanDefinition.SCOPE_PROTOTYPE) .setLazyInit(true) .addPropertyValue("propertyName", "propertyValue") .build();
-
在 Mybatis 中,
SqlSessionFactoryBean
:用于构建SqlSessionFactory
对象,用于数据库访问。它提供了一种链式调用的方式来设置数据库连接、事务管理等属性和配置。SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBeanBuilder() .setDataSource(dataSource) .setTypeAliasesPackage("com.example.models") .setMapperLocations(new PathMatchingResourcePatternResolver() .getResources("classpath:mappers/*.xml")) .build();
建造者模式和工厂模式的区别
- 适用场景:适用于创建具有复杂构建过程的对象,且需要根据不同的配置选项来构建不同的表示;适用于创建对象的过程相对简单,或者创建对象的方式可能根据条件变化。
- 关注点:建造者模式关注对象的构建过程、不同的表示以及灵活的配置选项;工厂模式关注对象的创建过程、隐藏创建细节以及统一的创建方式。
- …
3. 单例模式
什么是单例模式?
- 单例模式是一种创建型模式,用于确保一个类只有一个实例,并提供全局访问点来获取该实例。
- 实现:单例模式通常会私有化构造器,在类的内部直接创建对象,向外暴露一个静态方法来获取对象。
应用场景?
- 在开发中有些对象只需要一个,比如说:线程池、注册表、日志记录器、数据库连接、配置信息等。如果创建多个对象可能会造成一些问题,比如:程序的行为异常、资源使用过量、或者不一致性的结果。
优点?
- 节省资源:单例模式不需要频繁去new对象,因而对系统内存的使用频率也会降低,这将减轻 GC 压力,缩短 GC 停顿时间。
- 提高效率:每次使用直接拿即可,不需要重复创建,节省时间开销。
单例模式案例:
**饿汉式,线程安全:**顾名思义,类⼀加载就创建对象,这种⽅式⽐较常⽤,但容易产⽣垃圾对象(类加载就创建,但是实际用不上),浪费内存空间。
public class Singleton {
// 1、私有化构造⽅法
private Singleton(){
}
// 2、定义⼀个静态变量指向⾃⼰类型
private final static Singleton instance = new Singleton(); // 类加载的初始化阶段赋值
// 3、对外提供⼀个公共的⽅法获取实例
public static Singleton getInstance() {
return instance;
}
}
**懒汉式,线程不安全:**只有当用户使用 getInstance()
这个方法的时候才创建对象实例。这种⽅式在多线程是⽆法保证单例的。
public class Singleton {
// 1、私有化构造⽅法
private Singleton(){
}
// 2、定义⼀个静态变量指向⾃⼰类型
private static Singleton instance;
// 3、对外提供⼀个公共的⽅法获取实例
public static Singleton getInstance() {
// 判断为 null 的时候再创建对象
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
上面的 getInstance()
这个方法会出现线程安全问题,比如多个线程同时执行到了这个方法,并且都判断了 instance == null 然后开始创建对象,这个时候就会有多个对象被创建。
懒汉式如何解决线程安全问题呢?
-
synchronized
对方法getInstance()
加锁。 -
双重检查锁。这⾥的双重检查是指两次⾮空判断,锁指的是 synchronized 加锁,如下:
public class Singleton { // 1、私有化构造⽅法 private Singleton() { } // 2、定义⼀个静态变量指向⾃⼰类型 private volatile static Singleton instance; // 双重检查锁中利用了 volatile 的可⻅性、禁⽌指令重排序 // 3、对外提供⼀个公共的⽅法获取实例 public static Singleton getInstance() { // 【第⼀重检查是否为 null】 if (instance == null) { // 使⽤ synchronized 加锁!!! synchronized (Singleton.class) { // 【第⼆重检查是否为 null】 if (instance == null) { // new 关键字创建对象不是原⼦操作 instance = new Singleton(); } } } return instance; } }
这⾥为什么要使⽤
volatile
?- 这是因为 new 关键字创建对象不是原⼦操作,创建⼀个对象会经历下⾯的步骤:
-
在堆内存开辟内存空间
-
调⽤构造⽅法,初始化对象
-
引⽤变量指向堆内存空间
为了提⾼性能,编译器和处理器常常会对既定的代码执⾏顺序进⾏指令重排序,从源码到最终执⾏指令会经历如下流程:
- 经过指令重排序之后,创建对象的执⾏顺序可能为 1 2 3 或者 1 3 2 ,因此当某个线程在乱序运⾏ 1 3 2 指令的时候,引⽤变量指向堆内存空间,这个对象不为 null,但是没有初始化,其他线程有可能这个时候进⼊了
getInstance
的第⼀个if(instance == null)
判断不为 nulll ,导致错误使⽤了没有初始化的⾮ null 实例,这样的话就会出现异常,这个就是著名的 DCL 失效问题。 - 当我们在引⽤变量上⾯添加 volatile 关键字以后,会通过在创建对象指令的前后添加内存屏障来禁⽌指令重排序,就可以避免这个问题,⽽且对 volatile 修饰的变量的修改对其他任何线程都是可⻅的。
-
静态内部类。利用了类初始化(
<clinit>
方法)是线程安全的:public class Singleton { // 私有化构造函数,防止外部实例化 private Singleton() { } // 静态内部类,在首次使用时加载 // 当外部第一次调用 LazySingleton.getInstance() 方法时,才会触发 SingletonHolder 类的加载和初始化,从而创建单例实例 INSTANCE。 private static class SingletonHolder { private static final Singleton INSTANCE = new Singleton(); } // 全局访问点,返回单例实例 public static Singleton getInstance() { return SingletonHolder.INSTANCE; } }
java/Spring 中的单例模式有哪些?
Spring 中的 Bean 默认都是单例的。