设计模式 | Java后端
SOLID 原则
单一职责原则 SRP
概念
一个类或者一个方法只做一件事
本质是接口单一职责原则,软件项目里几乎不可能实施类单一职责原则
单一职责原则不只是面向对象编程思想所特有的,只要是模块化的程序设计,都适用单一职责原则
优点
- 类的复杂性降低,代码可读性高
- 变更风险低,代码维护性高
- 提高代码的扩展性
开闭原则 OCP
概念
类、模块和函数等软件实体应该对扩展开放,对修改关闭
意为一个实体独立之后就不应该去修改它,而是以扩展的方式适应新需求
优点
- 单元测试可不变,代码可靠性高
- 代码复用性强
- 代码维护性高
里式替换原则 LSP
概念
所有基类出现的地方都可以用派生类替换而不会程序产生错误
里氏替换原是继承复用的基础,它反映了基类与子类之间的关系,是对开闭原则的补充,是对实现抽象化的具体步骤的规范
实现
- 子类可以实现父类的抽象方法,但不能覆盖父类的非抽象方法
- 子类中可以增加自己特有的方法
- 当子类的方法重载父类的方法时,方法的前置条件(即方法的输入参数)要比父类的方法更宽松
- 当子类的方法实现父类的方法时(重写/重载或实现抽象方法),方法的后置条件(即方法的的输出/返回值)要比父类的方法更严格或相等
优点
- 克服了继承中重写父类造成的可复用性变差的缺点,为良好的继承定义了一个规范
- 提高代码的健壮性,降低程序出错的可能性
- 是实现开闭原则的重要方式之一
迪米特法则 LoD/LKP
概念
一个对象应该对其他对象保持最少的了解
迪米特法则要求限制软件实体之间通信的宽度和深度:如果两个软件实体无须直接通信,那么就不应当发生直接的相互调用,可以通过第三方转发该调用
类与类之间的关系越密切,耦合度越大,当一个类发生改变时,对另一个类的影响也越大
实现
依赖者只依赖应该依赖的对象,被依赖者只暴露应该暴露的方法
在类的划分上,应该创建弱耦合的类。类与类之间的耦合越弱,就越有利于实现可复用的目标
在类的结构设计上,尽量降低类成员的访问权限
在类的设计上,优先考虑将一个类设置成不变类
在对其他类的引用上,将引用其他对象的次数降到最低
不暴露类的属性成员,而应该提供相应的访问器(set 和 get 方法)
谨慎使用序列化 (Serializable) 功能
优点
降低了类之间的耦合度,提高了模块的相对独立性
提高了类的可复用率和系统的扩展性
缺点
过度使用迪米特法则会使系统产生大量的中介类,从而增加系统的复杂性,使模块之间的通信效率降低
在釆用迪米特法则时需要反复权衡,确保高内聚和低耦合的同时,保证系统的结构清晰
接口隔离原则 ISP
概念
客户端不应该依赖它不需要的接口
一个类对另一个类的依赖应该建立在最小的接口上
即要为各个类建立它们需要的专用接口,而不要试图去建立一个很庞大的接口供所有依赖它的类去调用
接口隔离原则和单一职责原则都是为了提高类的内聚性、降低它们之间的耦合性,体现了封装的思想,但两者是不同的:
单一职责原则注重的是职责,而接口隔离原则注重的是对接口依赖的隔离
单一职责原则主要是约束类,它针对的是程序中的实现和细节;接口隔离原则主要约束接口,主要针对抽象和程序整体框架的构建
实现
- 接口尽量小,但是要有限度。一个接口只服务于一个子模块或业务逻辑
- 为依赖接口的类定制服务。只提供调用者需要的方法,屏蔽不需要的方法
- 了解环境,拒绝盲从。每个项目或产品都有选定的环境因素,环境不同,接口拆分的标准就不同深入了解业务逻辑
- 提高内聚,减少对外交互。使接口用最少的方法去完成最多的事情
依赖倒置原则 DIP
概念
高层模块不应该依赖低层模块,二者都应该依赖其抽象;抽象不应该依赖细节,细节应该依赖抽象
其核心就是面向接口编程
实现
- 每个类尽量提供接口或抽象类,或者两者都具备
- 变量的声明类型尽量是接口或者是抽象类
- 任何类都不应该从具体类派生
- 使用继承时尽量遵循里氏替换原则
优点
降低类间的耦合性
提高系统的稳定性
减少并行开发引起的风险
提高代码的可读性和可维护性
设计模式
单例设计模式 Singleton
概念
单例模式确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例
这个类称为单例类,它提供全局访问的方法
条件
- 构造方法私有
- 提供一个自身的静态私有成员变量
- 提供一个公有的静态工厂方法
优点
单例模式设置全局访问点,可以优化和共享资源的访问
减少内存开销,节约系统资源
允许可变数目的实例
缺点
单例模式一般没有接口,扩展困难。如果要扩展,则除了修改原来的代码,没有第二种途径,违背开闭原则
单例类的职责过重,在一定程度上违背了“单一职责原则”
单例类既充当了工厂角色,提供了工厂方法,同时又充当了产品角色,包含一些业务方法,将产品的创建和产品的本身的功能融合到一起
实现
- 懒加载
- 线程不安全:在静态工厂方法里实例化
- 线程安全:① 给静态工厂方法加锁;② 在静态内部类中的静态代码块里实例化(真正的静态工厂方法设置在静态内部类中)
- 立即加载
- 线程安全:① 作为静态私有变量声明时就实例化;② 在静态代码块里实例化
- 懒加载
应用场景
- 需要频繁创建的一些类,使用单例可以降低系统的内存压力,减少 GC
- 某类只要求生成一个对象时
- 某些类创建实例时占用资源较多,或实例化耗时较长,且经常使用
- 某类需要频繁实例化,而创建的对象又频繁被销毁的时候,如多线程的线程池、网络连接池等
- 频繁访问数据库或文件的对象
- 当对象需要被共享的场合
扩展
单例模式可扩展为有限的多例 (Multitcm) 模式,这种模式可生成有限个实例并保存在 ArrayList 中,客户需要时可随机获取
工厂设计模式 Factory
按实际业务场景划分,工厂模式有 3 种不同的实现方式:简单工厂模式、工厂方法模式和抽象工厂模式
简单工厂 Simple Factory
概念
只有一个工厂类
创建实例的方法通常为静态方法,因此又称为静态工厂方法模式
要点在于:当你需要什么,只需要传入一个正确的参数,就可以获取你所需要的对象,而无须知道其创建细节
模式结构
- 工厂:负责实现创建所有实例的内部逻辑
- 抽象产品:所创建的所有对象的父类,负责描述所有实例所共有的公共接口
- 具体产品:创建目标。所有创建的对象都充当这个角色的某个具体类的实例
优点
- 实现对象的创建和对象的使用分离,将对象的创建交给专门的工厂类负责
- 客户端无须知道所创建的具体产品类的类名,只需要知道具体产品类所对应的参数即可,对于一些复杂的类名,通过简单工厂模式可以减少使用者的记忆量
- 通过引入配置文件,可以在不修改任何客户端代码的情况下更换和增加新的具体产品类,在一定程度上提高了系统的灵活性
缺点
- 工厂类的职责相对过重,增加新的产品需要修改工厂类的判断逻辑,这一点与开闭原则相违背
- 工厂类集中了所有产品创建逻辑,一旦不能正常工作,整个系统都要受到影响
- 会增加系统中类的个数,在一定程序上增加了系统的复杂度和理解难度
- 产品类型较多时,有可能造成工厂逻辑过于复杂,不利于系统的扩展和维护
- 由于使用了静态工厂方法,造成工厂角色无法形成基于继承的等级结构
应用场景
- 工厂类负责创建的对象比较少
- 客户端只知道传入工厂类的参数,对于如何创建对象不关心
工厂方法 Factory Method
概念
又称为工厂模式,也叫虚拟构造器模式或者多态工厂模式
是对简单工厂模式的进一步抽象化,工厂父类负责定义创建产品对象的公共接口,而工厂子类则负责生成具体的产品对象
这样做的目的是将产品类的实例化操作延迟到工厂子类中完成,即通过工厂子类来确定究竟应该实例化哪一个具体产品类
模式结构
- 抽象产品
- 具体产品
- 抽象工厂
- 具体工厂
优点
由于使用了面向对象的多态性,工厂方法模式保持了简单工厂模式的优点,而且克服了它的缺点
在系统中加入新产品时,无须修改抽象工厂和抽象产品提供的接口,无须修改客户端,也无须修改其他的具体工厂和具体产品,而只需添加一个具体工厂和具体产品即可,这样系统的可扩展性也就变得非常好,完全符合“开闭原则”
缺点
类的个数容易过多,增加复杂度
在添加新产品时,需要编写新的具体产品类,而且还要提供与之对应的具体工厂类,系统中类的个数将成对增加,在一定程度上增加了系统的复杂度,有更多的类需要编译和运行,给系统带来额外开销
增加了系统的抽象性和理解难度
由于考虑到系统的可扩展性,需要引入抽象层,在客户端代码中均使用抽象层进行定义,增加了系统的抽象性和理解难度,且在实现时可能需要用到 DOM、反射等技术,增加了系统的实现难度
应用场景
一个类不知道它所需要的对象的类(客户不关心创建产品的细节,只关心产品的品牌)
在工厂方法模式中,客户端不需要知道具体产品类的类名,只需要知道所对应的工厂即可
一个类通过其子类来指定创建哪个对象(多态 + 里式替换)
抽象工厂 Abstract Factory
概念
为访问类提供一个创建一组相关或相互依赖对象的接口,且访问类无须指定所要产品的具体类就能得到同族的不同等级的产品的模式结构
是工厂方法模式的升级版本,工厂方法模式只生产一个等级的产品,而抽象工厂模式可生产多个等级的产品
产品等级结构:即产品的继承结构
产品族 :由同一个抽象工厂生产的,位于不同产品等级结构中的一组产品
条件
- 系统中有多个产品族,每个具体工厂创建同一族但属于不同等级结构的产品
- 系统一次只可能消费其中某一族产品,即同族的产品一起使用
模式结构
- 抽象产品
- 具体产品
- 抽象工厂
- 具体工厂
优点
- 可以在类的内部对产品族中相关联的多等级产品共同管理,而不必专门引入多个新的类来进行管理
- 隔离了具体类的生成,使得客户并不需要知道什么被创建
- 当一个产品族中的多个对象被设计成一起工作时,能够保证客户端始终只使用同一个产品族中的对象
- 增强了程序的可扩展性,当增加一个新的产品族时,不需要修改原代码,满足开闭原则
缺点
- 当产品族中需要增加一个新的产品时,所有的工厂类都需要进行修改
- 增加了系统的抽象性和理解难度
应用场景
在很多软件系统中需要更换界面主题,要求界面中的按钮、文本框、背景色等一起发生改变时,可以使用抽象工厂模式进行设计
代理设计模式 Proxy
概念
给某一个对象提供一个代理,并由代理对象控制对原对象的引用
模式结构
- 抽象主题 Subject:通过接口或抽象类声明业务方法
- 真实主题 RealSubject:委托类,实现了抽象主题中的具体业务
- 代理主题 Proxy:代理类,可以访问、控制或扩展委托类的功能
在委托类的方法的执行的前提下,代理类再额外去做增强
优点
- 在客户端与目标对象之间起到一个中介作用和保护目标对象的作用
- 代理对象可以扩展目标对象的功能
- 代理模式能将客户端与目标对象分离,在一定程度上降低了系统的耦合度,增加了程序的可扩展性
- 虚拟代理通过使用一个小对象来代表一个大对象,可以减少系统资源的消耗,对系统进行优化并提高运行速度
- 保护代理可以控制对真实对象的使用权限
缺点
- 会造成系统设计中类的数量增加
- 在客户端和目标对象之间增加一个代理对象,会造成请求处理速度变慢
- 增加了系统的复杂度
可以使用动态代理方式克服以上缺点
根据代理的创建时期(代理类是否是我们自己写的),代理模式分为静态代理和动态代理
- 静态:由程序员创建代理类或特定工具自动生成源代码再对其编译,在程序运行前代理类的 .class 文件就已经存在了。
- 动态:在程序运行时,运用反射机制动态创建而成
静态代理
- 写法一:将委托类作为代理类中的成员变量
- 写法二:代理类继承委托类
JDK 动态代理
JDK 提供的方法,可生成提供和委托类具有相同方法的代理对象,不仅执行了委托类的方法,而且也执行了增强的代码
其中委托类需要实现接口 ,生成的代理对象和接口相关 ,代理对象实现了和委托类相同的接口
使用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35//委托类对象
Jinghuayuan jinghuayuan = new Jinghuayuan();
//通过Jdk动态代理获得代理对象
//参数1:classloader
//参数2:委托类所实现的接口
//参数3:invocationHandler 👉 以回调函数的形式提供了代理类需要去做的内容 👉 委托类方法的执行 + 增强
//使用接口来接收Jdk动态代理的对象
BuyBreakFast proxy = (BuyBreakFast) Proxy.newProxyInstance(Jinghuayuan.class.getClassLoader(), Jinghuayuan.class.getInterfaces(), new InvocationHandler() {
/**
* @param proxy 👉 第20行生成的代理对象
* @param method 👉 代理对象正在执行的方法
* @param args 👉 代理对象正在执行的方法携带的参数
* @return Object 👉 委托类方法的返回值
*/
//invoke方法,代理对象去执行方法时需要做的事情
//代理类的方法 👉 调用了invocationHandler的invoke 👉 委托类方法的执行 + 增强
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//委托类方法的执行 👉 缺少委托类对象
//jinghuayuan method args 👉 反射
System.out.println("step2:" + method.getName());
Object invoke = method.invoke(jinghuayuan, args);//委托类方法的调用
//增强
System.out.println("一碗豆浆");
return invoke;
}
});
//代理对象都对哪一些方法做了增强 👉 默认是委托类的全部方法
//proxy.buyBreakFast(); //先执行代理类的方法 👉 调用到invocationHandler的invoke
System.out.println("step1");
proxy.buyBreakFast("炒米粉");
//代理对象去执行对应的方法执行结果:
1
2
3
4step1
step2:buyBreakFast
一份炒米粉
一碗豆浆
生成动态代理类
默认不生成 class 字节码文件,需要自行反编译
设置
1
2//key来源于ProxyGenerator这个类
System.setProperty("sun.misc.ProxyGenerator.saveGeneratedFiles","true"); //生成的字节码文件保存在working directory生成目录
字节码文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15public final class $Proxy0 extends Proxy implements BuyBreakFast { //实现了抽象主题
......
......
public final void buyBreakFast() throws {
try {
super.h.invoke(this, m3, (Object[])null); //h: invocationHandler实例
} catch (RuntimeException | Error var2) {
throw var2;
} catch (Throwable var3) {
throw new UndeclaredThrowableException(var3);
}
}
......
......
}
Cglib 动态代理
代理类继承委托类
使用
- 引入新依赖
1
2
3<groupId>cglib</groupId>
<artifactId>cglib</artifactId>
<version>3.2.12</version>- 使用代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20//委托类对象
Jinghuayuan jinghuayuan = new Jinghuayuan();
//保存cglib动态代理生成的字节码文件 👉 将字节码文件存储在指定路径
System.setProperty(DebuggingClassWriter.DEBUG_LOCATION_PROPERTY,
"D:\\WorkSpace\\j29_workspace\\codes\\day01-design-pattern\\demo5-cglib-dynamic-proxy");
//生成cglib动态代理的对象
//参数1:委托类的class对象
//参数2:callBack 👉 invocationHandler 和Jdk的不是同一个
//Cglib动态代理可以以委托类来接收,也可以以委托类的接口来接收
Jinghuayuan proxy = (Jinghuayuan) Enhancer.create(Jinghuayuan.class, new InvocationHandler() {
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Object invoke = method.invoke(jinghuayuan, args);//注意不要传入proxy作为第一个参数
System.out.println("一碗豆浆");
return invoke;
}
});
proxy.buyBreakFast();- 字节码文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21public class Jinghuayuan$$EnhancerByCGLIB$$c6d6c217 extends Jinghuayuan implements Factory {
......
......
public final void buyBreakFast() {
try {
InvocationHandler var10000 = this.CGLIB$CALLBACK_0;
if (var10000 == null) {
CGLIB$BIND_CALLBACKS(this);
var10000 = this.CGLIB$CALLBACK_0;
}
var10000.invoke(this, CGLIB$buyBreakFast$0, new Object[0]);
} catch (Error | RuntimeException var1) {
throw var1;
} catch (Throwable var2) {
throw new UndeclaredThrowableException(var2);
}
}
......
......
}
建造者设计模式 Builder
概念
将一个复杂对象的构造与它的表示分离,使同样的构建过程可以创建不同的表示
它是将一个复杂的对象分解为多个简单的对象,然后一步一步构建而成
它将变与不变相分离,即产品的组成部分是不变的,但每一部分是可以灵活选择的
模式结构
- 产品角色 Product:包含多个组成部件的复杂对象,由具体建造者来创建其各个零部件
- 抽象建造者 Builder:包含创建产品各个子部件的抽象方法的接口,通常还包含一个返回复杂产品的方法 getResult()
- 具体建造者 Concrete Builder :实现 Builder 接口,完成复杂产品的各个部件的具体创建方法
- 指挥者 Director:调用建造者对象中的部件构造与装配方法完成复杂对象的创建,在指挥者中不涉及具体产品的信息
优点
- 封装性好,构建和表示分离
- 扩展性好,各个具体的建造者相互独立,有利于系统的解耦
- 客户端不必知道产品内部组成的细节,建造者可以对创建过程逐步细化,而不对其它模块产生任何影响,便于控制细节风险
缺点
- 产品的组成部分必须相同,这限制了其使用范围
- 如果产品的内部变化复杂,如果产品内部发生变化,则建造者也要同步修改,后期维护成本较大
建造者模式和工厂模式的关注点不同:建造者模式注重零部件的组装过程,而工厂方法模式更注重零部件的创建过程,但两者可以结合使用
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 ZERO!