JavaGuide 设计模式
JavaGuide 设计模式
1. 软件设计原则
设计原则名称 | 简单定义 |
---|---|
开闭原则 | 对扩展开放,对修改关闭 |
单一职责原则 | 一个类只负责一个功能领域中的相应职责 |
里氏替换原则 | 所有引用基类的地方必须能透明地使用其子类的对象 |
依赖倒置原则 | 依赖于抽象,不能依赖于具体实现 |
接口隔离原则 | 类之间的依赖关系应该建立在最小的接口上 |
合成/聚合复用原则 | 尽量使用合成/聚合,而不是通过继承达到复用的目的 |
迪米特法则 | 一个软件实体应当尽可能少的与其他实体发生相互作用 |
2. 设计模式分类
- 创建型: 在创建对象的同时隐藏创建逻辑,不使⽤ new 直接实例化对象,程序在判断需要创建哪些对象时更灵活。包括⼯⼚/抽象⼯⼚/单例/建造者/原型模式。
- 结构型: 通过类和接⼝间的继承和引⽤实现创建复杂结构的对象。包括适配器/桥接模式/过滤器/组合/装饰器/外观/享元/代理模式。
- ⾏为型: 通过类之间不同通信⽅式实现不同⾏为。包括责任链/命名/解释器/迭代器/中介者/备忘录/观察者/状态/策略/模板/访问者模式。
工厂模式
简单工厂模式
简单⼯⼚模式指由⼀个⼯⼚对象来创建实例,客户端不需要关注创建逻辑,只需提供传⼊⼯⼚的参数。
UML 类图如下:
适⽤于⼯⼚类负责创建对象较少的情况,缺点是如果要增加新产品,就需要修改⼯⼚类的判断逻辑,违背开闭原则,且产品多的话会使⼯⼚类⽐较复杂。
Calendar 抽象类的 getInstance ⽅法,调⽤ createCalendar ⽅法根据不同的地区参数创建不同的⽇历对象。
Spring 中的 BeanFactory 使⽤简单⼯⼚模式,根据传⼊⼀个唯⼀的标识来获得 Bean 对象。
工厂方法模式
和简单⼯⼚模式中⼯⼚负责⽣产所有产品相⽐,⼯⼚⽅法模式将⽣成具体产品的任务分发给具体的产品⼯⼚。
UML类图如下:
也就是定义⼀个抽象⼯⼚,其定义了产品的⽣产接⼝,但不负责具体的产品,将⽣产任务交给不同的派⽣类⼯⼚。这样不⽤通过指定类型来创建对象了。
抽象工厂模式
简单⼯⼚模式和⼯⼚⽅法模式不管⼯⼚怎么拆分抽象,都只是针对⼀类产品,如果要⽣成另⼀种产品,就⽐较难办了!
抽象⼯⼚模式通过在 AbstarctFactory 中增加创建产品的接⼝,并在具体⼦⼯⼚中实现新加产品的创建,当然前提是⼦⼯⼚⽀持⽣产该产品。否则继承的这个接⼝可以什么也不⼲。
UML类图如下:
从上⾯类图结构中可以清楚的看到如何在⼯⼚⽅法模式中通过增加新产品接⼝来实现产品的增加的。
单例模式
单例模式属于创建型模式,⼀个单例类在任何情况下都只存在⼀个实例,构造⽅法必须是私有的、由⾃⼰创建⼀个静态变量存储实例,对外提供⼀个静态公有⽅法获取实例。
- 优点:内存中只有⼀个实例,减少了开销,尤其是频繁创建和销毁实例的情况下并且可以避免对资源的多重占⽤
- 缺点:没有抽象层,难以扩展,与单⼀职责原则冲突。
饿汉式,线程安全
饿汉式单例模式,顾名思义,类⼀加载就创建对象,这种⽅式⽐较常⽤,但容易产⽣垃圾对象,浪费内存空间。
- 优点:线程安全,没有加锁,执⾏效率较⾼
- 缺点:不是懒加载,类加载时就初始化,浪费内存空间
饿汉式单例是如何保证线程安全的呢?它是基于类加载机制避免了多线程的同步问题,但是如果类被不同的类加载器加载就会创建不同的实例。
public class Singleton {
// 1、私有化构造⽅法
private Singleton(){}
// 2、定义⼀个静态变量指向⾃⼰类型
private final static Singleton instance = new Singleton();
// 3、对外提供⼀个公共的⽅法获取实例
public static Singleton getInstance() {
return instance;
}
}
懒汉式,线程不安全
- 优点:懒加载
- 缺点:线程不安全
这种⽅式在单线程下使⽤没有问题,对于多线程是⽆法保证单例的,这⾥列出来是为了和后⾯使⽤锁保证线程安全的单例做对⽐
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;
}
}
使用多线程破坏单例,测试代码如下:
public class Test {
public static void main(String[] args) {
for (int i = 0; i < 3; i++) {
new Thread(() -> {
System.out.println("多线程创建的单例:" +
Singleton.getInstance());
}).start();
}
}
}
输出结果如下
多线程创建的单例:
com.example.spring.demo.single.Singleton@18396bd5
多线程创建的单例:
com.example.spring.demo.single.Singleton@7f23db98
多线程创建的单例:
com.example.spring.demo.single.Singleton@5000d44
懒汉式,线程安全
- 优点:懒加载,线程安全
- 缺点:效率降低
懒汉式单例如何保证线程安全呢?通过 synchronized 关键字加锁保证线程安全, synchronized 可以添加在⽅法上⾯,也可以添加在代码块上⾯,这⾥演示添加在⽅法上⾯,存在的问题是每⼀次调⽤ getInstance 获取实例时
都需要加锁和释放锁,这样是⾮常影响性能的。
// 懒汉式单例,⽅法上⾯添加 synchronized 保证线程安全
public class Singleton {
// 1、私有化构造⽅法
private Singleton(){ }
// 2、定义⼀个静态变量指向⾃⼰类型
private static Singleton instance;
// 3、对外提供⼀个公共的⽅法获取实例
public synchronized static Singleton getInstance() {
// 判断为 null 的时候再创建对象
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
双重检查锁((DCL, 即 double-checked locking)
- 优点:懒加载,线程安全,效率较高
- 缺点:实现较复杂
这⾥的双重检查是指两次⾮空判断,锁指的是 synchronized 加锁,为什么要进⾏双重判断,其实很简单 - 第⼀重判断,如果实例已经存在,那么就不再需要进⾏同步操作,⽽是直接返回这个实例。如果没有创建,才会进⼊同步块,同步块的⽬的与之前相同,⽬的是为了防⽌有多个线程同时调⽤时,导致⽣成多个实例,有了同步块,每次只能有⼀个线程调⽤访问同步块内容,当第⼀个抢到锁的调⽤获取了实例之后,这个实例就会被创建,之后的所有调⽤都不会进⼊同步块,直接在第⼀重判断就返回了单例。
- 第⼆重空判断的作⽤,当多个线程⼀起到达锁位置时,进⾏锁竞争,其中⼀个线程获取锁,如果是第⼀次进⼊则为 null,会进⾏单例对象的创建,完成后释放锁,其他线程获取锁后就会被空判断拦截,直接返回已创建的单例对象。
// 双重检查锁(DCL, 即 double-checked locking)
public class Singleton {
// 1、私有化构造⽅法
private Singleton(){}
// 2、定义⼀个静态变量指向⾃⼰类型
private volatile static Singleton instance;
// 3、对外提供⼀个公共的⽅法获取实例
public static Singleton getInstance() {
// 第一次检查是否为 null
if (instance == null) {
// 使用 synchronized 加锁
synchronized (Singleton.class){
// 第二重检查是否为 null
if (instance == null){
// new 关键字创建对象不是原⼦操作
instance = new Singleton();
}
}
}
return instance;
}
}
静态内部类
- 优点:懒加载,线程安全,效率较高,实现简单
静态内部类单例是如何实现懒加载的呢?⾸先,我们先了解下类的加载时机。
虚拟机规范要求有且只有 5 种情况必须⽴即对类进⾏初始化(加载、验证、准备需要在此之前开始):
- 遇到 new 、getstatic 、putstatic 、invokestatic 这 4 条字节码指令时。⽣成这 4 条指令最常⻅的 Java 代码场景是:使⽤ new 关键字实例化对象的时候、读取或设置⼀个类的静态字段(final 修饰除外,被
final 修饰的静态字段是常量,已在编译期把结果放⼊常量池)的时候,以及调⽤⼀个类的静态⽅法的时候。 - 使⽤ java.lang.reflect 包⽅法对类进⾏反射调⽤的时候。
- 当初始化⼀个类的时候,如果发现其⽗类还没有进⾏过初始化,则需要先触发其⽗类的初始化。
- 当虚拟机启动时,⽤户需要指定⼀个要执⾏的主类(包含 main()的那个类),虚拟机会先初始化这个主类。
- 当使⽤ JDK 1.7 的动态语⾔⽀持时,如果⼀个java.lang.invoke.MethodHandle 实例最后的解析结果是REF_getStatic 、REF_putStatic 、REF_invokeStatic 的⽅法句柄,则需要先触发这个⽅法句柄所对应的类的初始化。
这 5 种情况被称为是类的主动引⽤,注意,这⾥《虚拟机规范》中使⽤的限定词是 "有且仅有",那么,除此之外的所有引⽤类都不会对类进⾏初始化,称为被动引⽤。静态内部类就属于被动引⽤的情况。
当 getInstance()⽅法被调⽤时,InnerClass 才在 Singleton 的运⾏时常量池⾥,把符号引⽤替换为直接引⽤,这时静态对象 INSTANCE 也真正被创建,然后再被 getInstance()⽅法返回出去,这点同饿汉模式。
枚举单例
优点:简单,⾼效,线程安全,可以避免通过反射破坏枚举单例
public enum Singleton {
INSTANCE;
public void doSomething(String str) {
System.out.println(str);
}
}
枚举在 java 中与普通类⼀样,都能拥有字段与⽅法,⽽且枚举实例创建是线程安全的,在任何情况下,它都是⼀个单例,可以直接通过如下⽅式调⽤获取实例:
Singleton singleton = Singleton.INSTANCE;
适配器模式
在我们的应⽤程序中我们可能需要将两个不同接⼝的类来进⾏通信,在不修改这两个的前提下我们可能会需要某个中间件来完成这个衔接的过程。这个中间件就是适配器。所谓适配器模式就是将⼀个类的接⼝,转换成客户期望的另⼀个接⼝。它可以让原本两个不兼容的接⼝能够⽆缝完成对接。
作为中间件的适配器将⽬标类和适配者解耦,增加了类的透明性和可复⽤性。
优点:
- 提⾼了类的复⽤
- 组合若⼲关联对象形成对外提供统⼀服务的接⼝
- 扩展性、灵活性好
缺点:
- 过多使⽤适配模式容易造成代码功能和逻辑意义的混淆
- 部分语⾔对继承的限制,可能⾄多只能适配⼀个适配者类,⽽且⽬标类必须是抽象类。
类适配器
原理:通过类继承实现适配,继承 Target 的接⼝,继承 Adaptee 的实现
对象适配器
原理:通过类对象组合实现适配
- Target: 定义 Client 真正需要使⽤的接⼝
- Adaptee: 其中定义了⼀个已经存在的接⼝,也是我们需要进⾏适配的接⼝。
- Adapter: 对 Adaptee 和 Target 的接⼝进⾏适配,保证对 target 中接⼝的调⽤可以间接转换为对 Adaptee 中接⼝进⾏调⽤。
代理模式(proxy pattern)
代理模式的本质是⼀个中间件,主要⽬的是解耦合服务提供者和使⽤者。使⽤者通过代理间接的访问服务提供者,便于后者的封装和控制。是⼀种结构性模式。
Subject: 定义 RealSubject 对外的接⼝,且这些接⼝必须被 Proxy 实现,这样外部调⽤ proxy 的接⼝最终都被转化为对 realsubject 的调⽤。
RealSubject: 真正的⽬标对象。
Proxy: ⽬标对象的代理,负责控制和管理⽬标对象,并间接地传递外部对⽬标对象的访问。
- Remote Proxy: 对本地的请求以及参数进⾏序列化,向远程对象发送请求,并对响应结果进⾏反序列化,将最终结果反馈给调⽤者;
- Virtual Proxy: 当⽬标对象的创建开销⽐较⼤的时候,可以使⽤延迟或者异步的⽅式创建⽬标对象;
- Protection Proxy: 细化对⽬标对象访问权限的控制;
静态代理和动态代理的区别
- 灵活性 :动态代理更加灵活,不需要必须实现接⼝,可以直接代理实现类,并且可以不需要针对每个⽬标类都创建⼀个代理类。另外,静态代理中,接⼝⼀旦新增加⽅法,⽬标对象和代理对象都要进⾏修改,这
是⾮常麻烦的! - JVM 层⾯ :静态代理在编译时就将接⼝、实现类、代理类这些都变成了⼀个个实际的 class ⽂件。⽽动态代理是在运⾏时动态⽣成类字节码,并加载到 JVM 中的。
观察者模式
观察者模式主要⽤于处理对象间的⼀对多的关系,是⼀种对象⾏为模式。
该模式的实际应⽤场景⽐较容易确认,当⼀个对象状态发⽣变化时,所有该对象的关注者均能收到状态变化通知,以进⾏相应的处理。
Subject: 抽象被观察者,仅提供注册和删除观察者对象的接⼝声明。
ConcreteSubject:具体被观察者对象,该对象中收集了所有需要被通知的观察者,并可以动态的增删集合中的观察者。当其状态发⽣变化时会通知所有观察者对象。
Observer: 抽象观察者,为所有观察者定义获得通知的统⼀接⼝;
ConcreteObserver: 观察者对象,其关注对象为 Subject,能接受 Subject变化时发出的通知并更新⾃身状态。
优点:
- 被观察者和观察者之间是抽象耦合的;
- 耦合度较低,两者之间的关联仅仅在于消息的通知;
- 被观察者⽆需关⼼他的观察者;
- ⽀持⼴播通信;
缺点: - 观察者只知道被观察对象发⽣了变化,但不知变化的过程和缘由;
- 观察者同时也可能是被观察者,消息传递的链路可能会过⻓,完成所有通知花费时间较多;
- 如果观察者和被观察者之间产⽣循环依赖,或者消息传递链路形成闭环,会导致⽆限循环;
装饰器模式
装饰器模式主要对现有的类对象进⾏包裹和封装,以期望在不改变类对象及其类定义的情况下,为对象添加额外功能。是⼀种对象结构型模式。需要注意的是,该过程是通过调⽤被包裹之后的对象完成功能添加的,⽽不
是直接修改现有对象的⾏为,相当于增加了中间层。
Component: 对象的接⼝类,定义装饰对象和被装饰对象的共同接⼝;
ConcreteComponent: 被装饰对象的类定义;
Decorator: 装饰对象的抽象类,持有⼀个具体的被修饰对象,并实现接⼝类继承的公共接⼝;
ConcreteDecorator: 具体的装饰器,负责往被装饰对象添加额外的功能;
责任链模式
⼀个请求沿着⼀条“链”传递,直到该“链”上的某个处理者处理它为⽌。
⼀个请求可以被多个处理者处理或处理者未明确指定时。责任链模式⾮常简单异常好理解,相信我它⽐单例模式还简单易懂,其应⽤也⼏乎⽆所不在,甚⾄可以这么说,从你敲代码的第⼀天起你就不知不觉⽤过了它最原始的裸体结构: switch-case 语句。
策略模式
策略模式(Strategy Pattern)属于对象的⾏为模式
针对⼀组算法,将每⼀个算法封装到具有共同接⼝的独⽴的类中,从⽽使得它们可以相互替换。策略模式使得算法可以在不影响到客户端的情况下发⽣变化。其主要⽬的是通过定义相似的算法,替换 if else 语句写法,并且可以随时
相互替换。
3. Spring 使用了哪些设计模式?
- ⼯⼚设计模式 : Spring 使⽤⼯⼚模式通过 BeanFactory 、ApplicationContext 创建 bean 对象。
- 代理设计模式 : Spring AOP 功能的实现。
- 单例设计模式 : Spring 中的 Bean 默认都是单例的。
- 模板⽅法模式 : Spring 中 jdbcTemplate 、hibernateTemplate 等以Template 结尾的对数据库操作的类,它们就使⽤到了模板模式。
- 包装器设计模式 : 我们的项⽬需要连接多个数据库,⽽且不同的客户在每次访问中根据需要会去访问不同的数据库。这种模式让我们可以根据客户的需求能够动态切换不同的数据源。
- 观察者模式: Spring 事件驱动模型就是观察者模式很经典的⼀个应⽤。
- 适配器模式 :Spring AOP 的增强或通知(Advice)使⽤到了适配器模式、spring MVC 中也是⽤到了适配器模式适配Controller 。