设计模式-Java
软件设计原则
在软件开发中,为了提高软件系统的可维护性和可复用性,增加软件的可扩展性和灵活性,程序员要尽量根据 6 条原则来开发程序,从而提高软件开发效率、节约软件开发成本和维护成本
开闭原则
对扩展开放,对修改关闭。在程序需要进行拓展的时候,不能去修改原有的代码,实现一个热插拔的效果。简言之,是为了使程序的扩展性好,易于维护和升级。
想要达到这样的效果,我们需要使用接口和抽象类。因为抽象灵活性好,适应性广,只要抽象的合理,可以基本保持软件架构的稳定。而软件中易变的细节可以从抽象派生来的实现类来进行扩展,当软件需要发生变化时,只需要根据需求重新派生一个实现类来扩展就可以了
假如现在有一款输入法,用户可以随意切换皮肤,那么在设计软件结构时,就可以定义一个皮肤的抽象类,在类中定义输入法对外暴露的公共接口,如下代码
/**
* 定义皮肤的抽象类
*
* @author dhj
* @date 2022/9/4
*/
public abstract class AbstractSkin {
/**
* 定义显示皮肤的抽象方法
*/
public abstract void display();
}
在
display()中的逻辑即输入法皮肤的渲染逻辑,不同的输入法需要实现自己的渲染逻辑
提供默认皮肤
/**
* 默认的皮肤实现类,继承自抽象皮肤类 {@link AbstractSkin}
*
* @author dhj
* @date 2022/9/4
*/
public class DefaultSkin extends AbstractSkin {
@Override
public void display() {
System.out.println("default skin");
}
}
提供红色背景的皮肤
/**
* 红色背景的皮肤实现类,继承自 {@link AbstractSkin}
*
* @author dhj
* @date 2022/9/4
*/
public class RedSkin extends AbstractSkin {
@Override
public void display() {
System.out.println("red background skin");
}
}
RedSkin 和 DefaultSkin它们实现的都是抽象的AbstractSkin,这能保证不论有多少种不同皮肤实现类,在实际应用时,都只需要统一调用display(),如下代码
/**
* 搜狗输入法具体皮肤的应用类,主要用于将不同的皮肤实现类渲染出来
*
* @author dhj
* @date 2022/9/4
*/
public class SougouInput {
// 定义一个类型为 AbstractSkin 的属性,目的在兼容其任意实现类
private AbstractSkin skin;
public void setSkin(AbstractSkin skin) {
this.skin = skin;
}
public void display() {
skin.display();
}
}
在实际应用输入法时,只需要声明
private AbstractSkin skin;即可兼容所有的皮肤实现类
最终在客户端使用SougoInput
/**
* 定义客户端,使用搜狗输入法
*
* @author dhj
* @date 2022/9/4
*/
public class Client {
public static void main(String[] args) {
// 创建搜狗输入法的实例
SougouInput sougouInput = new SougouInput();
// 传入 AbstractSkin 的具体实现类(首次使用默认皮肤)
sougouInput.setSkin(new DefaultSkin());
// 调用公共的 display 接口,即可实现任意子类的皮肤渲染逻辑
sougouInput.display();
// 后续客户端如果要更换皮肤,只需要重新设置对应皮肤的实现类即可
sougouInput.setSkin(new RedSkin());
sougouInput.display();// 重新渲染皮肤
}
}
如果说还需要其他类型的皮肤,则不应该修改现有的DefaultSkin和RedSkin两个类,应该继续实现AbstractSkin类,重写其中display() 方法来自定义扩展其他类型的皮肤;这就是开闭原则的体现(对扩展开放,对修改关闭)
里氏替换原则
里氏代换原则是面向对象设计的基本原则之一
里氏代换原则:任何基类可以出现的地方,子类一定可以出现。通俗理解:子类可以扩展父类的功能, 但不能改变父类原有的功能。换句话说,子类继承父类时,除添加新的方法完成新增功能外,尽量不要重写父类的方法。 如果通过重写父类的方法来完成新的功能,这样写起来虽然简单,但是整个继承体系的可复用性会比较差,特别是运用多态比较频繁时,程序运行出错的概率会非常大。
下面来看一个例子
现在要定义一个正方向和长方形,从数学角度来说,正方向也是长方形的一种,那么为了方便代码复用,可以定义长方形类,正方形类只需要实现即可,不论是从数学常识还是代码的实现角度来说,都很符合逻辑
长方形定义如下
/**
* 长方形类
*
* @author dhj
* @date 2022/9/4
*/
public class Rectangle {
private double length;
private double width;
public double getWidth() {
return width;
}
public void setWidth(double width) {
this.width = width;
}
public double getLength() {
return length;
}
public void setLength(double length) {
this.length = length;
}
}
正方向定义如下,其继承了Rectangle
/**
* 正方形类
*
* @author dhj
* @date 2022/9/4
*/
public class Square extends Rectangle {
@Override
public void setLength(double length) {
super.setLength(length);
super.setWidth(length);
}
@Override
public void setWidth(double width) {
super.setLength(width);
super.setWidth(width);
}
}
注意,这里子类
Square重写了父类Rectangle中的setLength()和setWidth(),这是不遵循里氏替换原则的,如果在后续父类Rectangle出现的地方出现了Square,可能会出现问题,这在后续代码中会体现出来
调用测试类,创建一个Rectangle实例进行测试
/**
* @author dhj
* @date 2022/9/4
*/
public class RectangleDemo {
public static void main(String[] args) {
Rectangle rectangle = new Rectangle();
rectangle.setWidth(5);
rectangle.setLength(10);
resize(rectangle);
printWidthAndRectangle(rectangle);
}
/**
* 当检查到长方形的 width <= length 时,对 width 进行增长
*
* @param rectangle 接收一个 Rectangle 实例
*/
public static void resize(Rectangle rectangle) {
while (rectangle.getWidth() <= rectangle.getLength()) {
rectangle.setWidth(rectangle.getWidth() + 1);
}
}
/**
* 打印 Rectangle 的 width 和 length
*
* @param rectangle 接收一个 Rectangle 实例
*/
public static void printWidthAndRectangle(Rectangle rectangle) {
System.out.println("width:" + rectangle.getWidth() + "\n" + "length:" + rectangle.getLength());
}
}
注意,在
main方法中,这里传入到resize()中的rectangle本身就是一个Rectangle类型的
下面将Square实例传入到resize()中,对main方法进行改造,如下代码
public static void main(String[] args) {
Square square = new Square();
square.setWidth(10);
resize(square);
printWidthAndRectangle(square);
}
执行后会发现,程序无法停止运行,这里建议再回过头去观察
Square中对父类Rectangle中相关方法的重写就能得出程序无法停止的原因了并且,
resize()方法在设计之初,也是针对Rectangle中相关方法的逻辑来实现的,子类Square既然修改了Rectangle中的逻辑,那就会存在问题出现的风险
以上问题是不遵循里氏替换原则的一个经典体现,即:在resize(Rectangle rectangle)的方法参数中,也就是基类Rectangle出现的地方,子类Square不一定可以出现,原因在于子类重写了父类中的相关方法,而不是去扩展新方法
要想解决这个问题,需要定义一个更抽象的接口,将Rectangle和Square之间的继承关系断开,二者应该实现公共的抽象接口,公共接口定义如下
/**
* 定义一个四边形接口
*
* @author dhj
* @date 2022/9/4
*/
public interface Quadrilatera {
/**
* 定义四边形获取长度的接口方法,只要是四边形就有长度
*
* @return 返回长度
*/
double getLength();
/**
* 定义四边形获取宽度的接口方法,只要是四边形就有宽度
*
* @return 返回宽度
*/
double getWidth();
}
定义长方形
/**
* 定义长方形,实现 Quadrilatera 接口
*
* @author dhj
* @date 2022/9/4
*/
public class Rectangle implements Quadrilatera {
private double length;
private double width;
public void setLength(double length) {
this.length = length;
}
public void setWidth(double width) {
this.width = width;
}
@Override
public double getLength() {
return this.length;
}
@Override
public double getWidth() {
return this.width;
}
}
定义正方形
/**
* @author dhj
* @date 2022/9/4
*/
public class Square implements Quadrilatera {
private double side;
@Override
public double getLength() {
return this.side;
}
@Override
public double getWidth() {
return this.side;
}
}
注意,这里的正方形不再需要再去重写父类中的某些方法了,因为
Square和Rectangle都实现了公共的接口Quadrilatera,在Quadrilatera中定义的都是更为抽象的接口方法,Square和Rectangle都适用,二者只需要根据自己的情况实现对应接口方法即可例如这里的
Square,因为正方形无论长宽都相等,因此可以直接定义一个side变量,但是getLength()和getWidth()这两个接口方法却不受影响,因为这两个接口方法的定义足够抽象【任何四边形都有长宽】
下面是测试类
public class RectangleDemo {
public static void main(String[] args) {
Rectangle rectangle = new Rectangle();
rectangle.setWidth(10);
rectangle.setLength(20);
resize(rectangle);
printWidthAndRectangle(rectangle);
}
/**
* 当检查到长方形的 width <= length 时,对 width 进行增长
*
* @param rectangle 接收一个 Rectangle 实例
*/
public static void resize(Rectangle rectangle) {
while (rectangle.getWidth() <= rectangle.getLength()) {
rectangle.setWidth(rectangle.getWidth() + 1);
}
}
/**
* 打印 Rectangle 的 width 和 length
*
* @param rectangle 接收一个 Rectangle 实例
*/
public static void printWidthAndRectangle(Rectangle rectangle) {
System.out.println("width:" + rectangle.getWidth() + "\n" + "length:" + rectangle.getLength());
}
}
经过上述结构性的变化,resize()方法的参数将不再支持传入Square类型的示例,因为resize()方法的参数明确声明了需要Rectangle类型,而单论Square与Rectangle之间并无派生或继承的关系;除非方法的参数为Quadrilatera类型,因为Quadrilatera是二者的公共接口
既然resize()无法传入Square类型的实例,也就不存在程序无法停止的问题了
依赖倒转原则
高层模块不应该依赖低层模块,两者都应该依赖其抽象;抽象不应该依赖细节,细节应该依赖抽象,通俗来说,就是依赖关系之间,具备通用性,例如电脑主板上的 cpu 插槽和具体的不同品牌的 cpu 之间就具备依赖关系,而主板的插槽也需要具备一定的通用性,也就是说插槽 ---> CPU而不是插槽 ---> Intel、AMD等具体的 cpu,这就是依赖其抽象
下面来看一个文件上传的例子
在实际的开发中,对于一些静态资源的存储,多会选择云存储,优点在于简单高效,易于维护,但可能有时候项目中不只使用一种云存储,某些情况下可能会使用百度云,也可能会使用阿里云,这就需要设计一个抽象接口,在其中定义文件上传存储的抽象方法,用于适配不同的云存储方案,如下
/**
* @author dhj
* @date 2022/9/5
*/
public interface UploadService {
/**
* 上传文件
*
* @param file 需要上传的目标文件
* @return 返回上传的 url
*/
String uploadFile(File file);
}
有了抽象接口,不同的云存储方案只需要实现此接口,然后编写各自的云存储逻辑即可,如下代码
阿里云文件上传
/**
* @author dhj
* @date 2022/9/5
*/
public class AliyunUploadService implements UploadService {
@Override
public String uploadFile(File file) {
return RandomUtil.randomString(12);
}
}
百度云文件上传
/**
* @author dhj
* @date 2022/9/5
*/
public class BaiduUploadService implements UploadService {
@Override
public String uploadFile(File file) {
return RandomUtil.randomString(10);
}
}
在具体使用时,需要注意,应该依赖抽象的UploadService接口
/**
* @author dhj
* @date 2022/9/5
*/
public class Demo {
public static void main(String[] args) {
// 依赖接口, 也就是依赖其抽象的体现
UploadService uploadService = new BaiduUploadService();
String url = uploadService.uploadFile(new File(""));
System.out.println(url);
}
}
依赖抽象的好处在于,后续如果需要更换云存储,只需要替换为对应的实例即可,又因为接口方法uploadFile()的统一定义,就无需再去修改具体的方法签名
但这样通过new方式来指定具体实现的方式依然具备一定耦合度,后续如果要更换对应接口的实现,需要在每个new关键字处去修改调用的构造方法,例如需要将每个new BaiduUploadService()替换为new AliyunUploadService()
这实际上就深刻的体现出了Spring中控制反转的优点,在Spring中,只需要申明对应的依赖关系,例如
// 申明一个 UploadService 依赖
UploadService uploadService;
至于依赖关系的初始化等操作,都交由Spring管理,而无需自己通过new去创建和指定对应的依赖实例,这就达到将所有依赖关系全都抽象化,高度解耦的效果,是依赖倒转原则的深刻体现
接口隔离原则
一个类对另一个类的依赖应该建立在最小的接口上,而不应该被迫依赖其不使用的方法,如下图

迪米特法则
迪米特法则又叫最少知识原则。 只和你的【朋友】交谈,不跟【陌生人】说话,其含义是:如果两个软件实体无须直接通信,那么就不应当发生直接的相互调用,可以通过第三方转发该调用。其目的是降低类之间的耦合度,提高模块的相对独立性
迪米特法则中的【朋友】是指:当前对象本身、当前对象的成员对象、当前对象所创建的对象、当前对象的方法参数等,这些对象同当前对象存在关联、聚合或组合关系,可以直接访问这些对象的方法。
迪米特法则就好比租客、房东、中介之间的关系,如下图

除此之外【明星 => 经纪人】也能够很好的解释迪米特法则,明星拥有粉丝,明星还需要和传媒公司签约,但明星和粉丝之间的见面以及与公司的签约都不会由明星自己去安排,一般会交由经纪人,【经纪人】就是迪米特法则中的第三方,如下代码
明星类
/**
* 明星类
*
* @author dhj
* @date 2022/9/7
*/
public class Star {
private String name;
public Star(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
粉丝类
/**
* 粉丝类
*
* @author dhj
* @date 2022/9/7
*/
public class Fans {
private String name;
public Fans(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
传媒公司
/**
* 媒体公司类
*
* @author dhj
* @date 2022/9/7
*/
public class Company {
private String name;
public Company(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
/**
* 经纪人类
*
* @author dhj
* @date 2022/9/7
*/
public class Agent {
private Star star;
private Fans fans;
private Company company;
public Agent(Star star, Fans fans, Company company) {
this.star = star;
this.fans = fans;
this.company = company;
}
/**
* 安排明星和粉丝见面
*/
public void meeting() {
System.out.println("【" + star.getName() + "】" + "和【" + fans.getName() + "】见面");
}
/**
* 与媒体公司的商业洽谈
*/
public void business() {
System.out.println("【" + star.getName() + "】" + "和【" + company.getName() + "】签约");
}
}
关键在于Agent类,它将Star Fans Compay这三个不相关的类作为自身的依赖属性,然后在对应的方法中,调用这些属性,完成具体的业务逻辑,至始至终,Star Fans Compay 之间没有直接的关联,达到解耦的效果
可以想象在实际业务开发中,涉及到不同业务或模块之间的交互但又不想产生耦合,或许可以参考参考迪米特法则
合成复用原则
合成复用原则是指:尽量先使用组合或者聚合等关联关系来实现,其次才考虑使用继承关系来实现。
尽管继承的方式能够提高代码的复用性,但也存在一些缺点
- 继承复用破坏了类的封装性。因为继承会将父类的实现细节暴露给子类,父类对子类是透明的,所以这种复用又称为
白箱复用 - 子类与父类的耦合度高。父类实现的任何改变都会导致子类的实现发生变化,这不利于类的扩展与维护
- 它限制了复用的灵活性。从父类继承而来的实现是静态的,在编译时已经定义,所以在运行时不可能发生变化,也就是不可能在代码运行时,改变这种继承关系
采用组合或聚合复用时,可以将已有对象纳入新对象中,使之成为新对象的一部分,新对象可以调用已有对象的功能,它有以下优点
- 维持了类的封装性。因为成分对象的内部细节是新对象看不见的,所以这种复用又称为
黑箱复用 - 对象间的耦合度低。可以在类的成员位置声明抽象。
- 复用的灵活性高。这种复用可以在运行时动态进行,新对象可以动态地重新引用与组合对象类型相同的对象
比如现在有一个Car类,按照类型,可以分为【汽油汽车】和【电动汽车】,按照颜色可划分为【白色】和【黑色】
按照继承的方式,可能会有如下结构的类

以上结构,每新增一种类型的汽车或颜色,都需要定义新的类
如果采用合成复用的原则,如下图

在Car类中,直接将汽车的颜色定义成接口并作为一个属性组合到Car中,使用时直接传入对应的颜色属性实例,Car的子类PetrolCar 和 ElectricCar就不用再单独创建对应颜色的子类
设计模式之创建型模式
创建型模式的主要关注点是 怎样创建对象?,它的主要特点是 将对象的创建与使用分离, 这样可以降低系统的耦合度,使用者不需要关注对象的创建细节
创建型模式分为
- 单例模式
- 工厂方法模式
- 抽象工程模式
- 原型模式
- 建造者模式
下面将一一分析
单例模式
单例模式(Singleton Pattern)是最简单的设计模式之一,这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式
这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建
这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象
单例模式分为两种
饿汉式:类加载就会导致该单实例对象被创建
懒汉式:类加载不会导致该单实例对象被创建,而是首次使用该对象时才会创建
饿汉式
首先是饿汉式,饿汉式中,将单实例成员变量申明为静态私有的,并且单例类的构造方法也被申明为私有,通过公共的静态get方法来获取单实例成员变量,static类型的成员变量只会在类加载时初始化一次,符合饿汉单例的特性,如下代码
/**
* 饿汉单例模式
*
* @author dhj
* @date 2022/9/8
*/
public class SingletonHungry {
private static final SingletonHungry INSTANCE = new SingletonHungry();
private SingletonHungry() {
}
public static SingletonHungry getINSTANCE() {
return INSTANCE;
}
}
饿汉单例静态代码块形式,如下代码
/**
* 饿汉单例模式
*
* @author dhj
* @date 2022/9/8
*/
public class SingletonHungry2 {
private static final SingletonHungry2 INSTANCE;
// 在静态代码块中对单实例初始化
static {
INSTANCE = new SingletonHungry2();
}
private SingletonHungry2() {
}
public static SingletonHungry2 getINSTANCE() {
return INSTANCE;
}
}
饿汉式的缺点在于,就算没有使用单实例,也会被加载到内存中,造成内存空间的浪费
饿汉单例模式(枚举)
枚举类实现单例模式是极力推荐的单例实现模式,因为枚举类型是线程安全的,并且只会装载一次
枚举单例是所有单例实现中唯一一种不会被破坏的单例实现模式
/**
* 饿汉单例模式(枚举实现)
*
* @author dhj
* @date 2022/9/8
*/
public enum SingleEnumHungry {
INSTANCE
}
懒汉式
线程不安全的懒汉单例
/**
* 懒汉单例模式
*
* @author dhj
* @date 2022/9/8
*/
public class SingletonLazy1 {
private static SingletonLazy1 INSTANCE;
private SingletonLazy1() {
}
// 多线程访问时, 可能出现同时判断到 INSTANCE 为 null 的情况, 造成多次创建单实例, 破坏了单例原则
public static SingletonLazy1 getINSTANCE() {
if (INSTANCE == null) {
INSTANCE = new SingletonLazy1();
}
return INSTANCE;
}
}
线程安全的懒汉单例模式
/**
* 懒汉单例模式
*
* @author dhj
* @date 2022/9/8
*/
public class SingletonLazy1 {
private static SingletonLazy1 INSTANCE;
private SingletonLazy1() {
}
// 加锁则可以保证线程安全
public static synchronized SingletonLazy1 getINSTANCE() {
if (INSTANCE == null) {
INSTANCE = new SingletonLazy1();
}
return INSTANCE;
}
}
懒汉单例模式(双检锁)
/**
* @author dhj
* @date 2022/9/8
*/
public class DoubleCheckLockLazySingleton {
// volatile 修饰, 禁指令重排序, 避免因指令重排序可能导致的因【变量初始化指令乱序】引发的线程安全问题(空指针)
private volatile static DoubleCheckLockLazySingleton INSTANCE;
public static DoubleCheckLockLazySingleton getINSTANCE() {
// 无锁检查, 不为空可直接返回单实例, 不影响多线程读取的性能
if (INSTANCE == null) {
// 单实例为 null, 进入单实例的创建逻辑, 需要加锁, 保证线程安全
synchronized (DoubleCheckLockLazySingleton.class) {
/*
可能存在第一次检查都为 true 的情况, 造成多个线程竞争锁
首次竞争到锁的线程创建初始化单实例成功后, 后续竞争到锁的线程需要再次检查
*/
if (INSTANCE == null) {
INSTANCE = new DoubleCheckLockLazySingleton();
}
}
}
return INSTANCE;
}
}
懒汉单例模式(静态内部类)
/**
* @author dhj
* @date 2022/9/8
*/
public class SingletonLazyStaticInnerClass {
private SingletonLazyStaticInnerClass() {
}
// 静态内部类只会在首次调用时才会被加载且只会加载一次(由 JVM 保证),符合懒汉单例模式的原则
private static class SingletonHolder {
private static final SingletonLazyStaticInnerClass INSTANCE = new SingletonLazyStaticInnerClass();
}
// 对外提供静态方法获取该对象
public static SingletonLazyStaticInnerClass getInstance() {
return SingletonHolder.INSTANCE;
}
}
序列化破坏单例模式
定义单例
/**
* 序列化破坏单例模式
* 以下使用饿汉式(静态代码块)的单例形式来演示
*
* @author dhj
* @date 2022/9/9
*/
public class SerializationDestroySingleton implements Serializable {
private static final SerializationDestroySingleton INSTANCE;
static {
INSTANCE = new SerializationDestroySingleton();
}
private SerializationDestroySingleton() {
}
public static SerializationDestroySingleton getINSTANCE() {
return INSTANCE;
}
}
使用序列化破坏单例
/**
* @author dhj
* @date 2022/9/9
*/
public class SerializationDestroySingletonDemo {
public static void main(String[] args) {
// 序列化
writeToFile();
// 反序列化
SerializationDestroySingleton readSingleton = readFromFile();
// 输出 false, 这说明通过序列化以及反序列化可以创建多个单实例对象, 破坏了单例原则
System.out.println(SerializationDestroySingleton.getINSTANCE() == readSingleton);
}
public static SerializationDestroySingleton readFromFile() {
ObjectInputStream ois = null;
try {
ois = new ObjectInputStream(new FileInputStream("D:\\INSTANCE.txt"));
return (SerializationDestroySingleton) ois.readObject();
} catch (Exception e) {
e.printStackTrace();
} finally {
if (ois != null) {
try {
ois.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
return null;
}
public static void writeToFile() {
ObjectOutputStream oos = null;
try {
SerializationDestroySingleton instance = SerializationDestroySingleton.getINSTANCE();
oos = new ObjectOutputStream(new FileOutputStream("D:\\INSTANCE.txt"));
oos.writeObject(instance);
} catch (Exception e) {
e.printStackTrace();
} finally {
if (oos != null) {
try {
oos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
解决办法
/**
* 序列化破坏单例模式
* 以下使用饿汉式(静态代码块)的单例形式来演示
*
* @author dhj
* @date 2022/9/9
*/
public class SerializationDestroySingleton implements Serializable {
private static final SerializationDestroySingleton INSTANCE;
static {
INSTANCE = new SerializationDestroySingleton();
}
private SerializationDestroySingleton() {
}
public static SerializationDestroySingleton getINSTANCE() {
return INSTANCE;
}
/**
* 解决序列化反序列化破解单例模式
*/
private Object readResolve() {
return getINSTANCE();
}
}
readResolve()会在反序列化时被调用,如果定义了这个方法, 就返回这个方法的值,如果没有定义,则返回新 new 出来的对象
反射破坏单例模式
/**
* @author dhj
* @date 2022/9/9
*/
public class ReflectionDestorySingletonDemo {
public static void main(String[] args) {
try {
ReflectionDestorySingleton singleton = ReflectionDestorySingleton.getINSTANCE();
// 反射获取私有的构造方法
Class<ReflectionDestorySingleton> clazz = ReflectionDestorySingleton.class;
Constructor<ReflectionDestorySingleton> constructor = clazz.getDeclaredConstructor();
// 设置私有构造方法的访问权限
constructor.setAccessible(true);
// 使用反射创建新的实例
ReflectionDestorySingleton refSingleton = constructor.newInstance();
// 返回值为 false, 说明可以使用反射获取多个不同的单实例, 破坏了单例原则
System.out.println(refSingleton == singleton);
} catch (Exception e) {
e.printStackTrace();
}
}
}
解决办法
/**
* @author dhj
* @date 2022/9/9
*/
public class ReflectionDestorySingleton {
private static final ReflectionDestorySingleton INSTANCE;
static {
INSTANCE = new ReflectionDestorySingleton();
}
// 在单实例的构造函数中, 直接抛出异常, 禁止反射调用创建新的实例
private ReflectionDestorySingleton() {
throw new RuntimeException("单例模式下非法调用构造函数");
}
public static ReflectionDestorySingleton getINSTANCE() {
return INSTANCE;
}
}
工厂模式
在 Java 中,随时随地都在 new 对象,但如果每次使用到该对象时,都通过 new 方式来创建,那么后续要想更换此对象,那么在 new 此对象的位置都要修改,严重耦合
如果设计一种工厂类来生产对象,在用到此对象的地方,调用工厂方法就可以了,将对象的创建和使用分离,如果要更换对象, 直接在工厂里更换该对象即可,达到了与对象解耦的目的
所以,工厂模式最大的优点就是:解耦
简单工厂模式
简单工厂不是一种设计模式,反而比较像是一种编程习惯,简单工厂包含以下角色
抽象产品 :定义了产品的规范,描述了产品的主要特性和功能。 具体产品 :实现或者继承抽象产品的子类具体工厂 :提供了创建产品的方法,调用者通过该方法来获取产品
例如有一汽车抽象类
/**
* 抽象 Car 类
*
* @author dhj
* @date 2022/9/12
*/
public abstract class AbstractCar {
abstract String getName();
}
子类
/**
* 奔驰汽车
*
* @author dhj
* @date 2022/9/12
*/
@ToString
public class BenzCar extends AbstractCar {
private final String name;
public BenzCar(String name) {
this.name = name;
}
@Override
String getName() {
return this.name;
}
}
/**
* 法拉利汽车
*
* @author dhj
* @date 2022/9/12
*/
@ToString
public class FerrariCar extends AbstractCar {
private final String name;
public FerrariCar(String name) {
this.name = name;
}
@Override
String getName() {
return this.name;
}
}
对于这种具有多个实现类的结构,就可以定义一个简单工厂,用于创建所需的子类对象,至于创建哪个子类对象,则给定的类型参数决定,如下
/**
* @author dhj
* @date 2022/9/12
*/
public class SimpleCarFactory {
/**
* 创建 AbstractCar 类型的实例
*
* @param type 类型参数
* @return 返回指定类型参数对应的子类实例,没有则返回 null
*/
public static AbstractCar createCar(Integer type) {
switch (type) {
case 1:
return new BenzCar("奔驰");
case 2:
return new FerrariCar("法拉利");
default:
return null;
}
}
}
通过这样一种方式,在使用具体的子类对象时,只需要统一调用工厂中的创建方法即可,如要替换某一子类,也只需要修改工厂方法中对应的返回值即可
简单工厂的缺点
目前使用工厂模式,将对象的创建和具体的使用分离,减少了耦合,但是,工厂类依然与具体的创建对象耦合,如果需要增加新的实现,则需要直接修改工厂方法,违背了开闭原则
工厂方法模式
工厂方法模式解决上述简单工厂违反开闭原则的问题,工厂方法模式定义了一个抽象工厂,抽象工厂中定义了创建产品的接口,而抽象工厂的实现类叫做具体工厂,对应不同的创建产品的方式,工厂方法模式将创建产品的行为延迟到了子实现类来执行,这使得创建产品时,具有更强的灵活性,因为实现类可以新增,针对不同的产品都可以定义不同的子实现类
工厂方法主要有以下角色
- 抽象工厂,定义创建产品的抽象接口
- 具体工厂,实现抽象工厂中的抽象接口
- 抽象产品,定义产品的抽象接口
- 具体产品,不同的产品可以自定义额外的属性和方法
抽象产品类
/**
* 抽象汽车类
*
* @author dhj
* @date 2022/9/12
*/
public abstract class AbstractCar {
abstract String getName();
}
具体产品类
/**
* 奔驰汽车
*
* @author dhj
* @date 2022/9/12
*/
@ToString
public class BenzCar extends AbstractCar {
private final String name;
public BenzCar(String name) {
this.name = name;
}
@Override
String getName() {
return this.name;
}
}
/**
* 法拉利汽车
*
* @author dhj
* @date 2022/9/12
*/
@ToString
public class FerrariCar extends AbstractCar {
private final String name;
public FerrariCar(String name) {
this.name = name;
}
@Override
String getName() {
return this.name;
}
}
抽象汽车工厂
/**
* 抽象汽车工厂
*
* @author dhj
* @date 2022/9/12
*/
public abstract class AbstractCarFacotry {
abstract AbstractCar createCar();
}
具体工厂
/**
* 奔驰汽车具体工厂类
*
* @author dhj
* @date 2022/9/12
*/
public class BenzCarFactory extends AbstractCarFacotry {
@Override
AbstractCar createCar() {
return new BenzCar("奔驰汽车");
}
}
/**
* 法拉利汽车具体工厂类
*
* @author dhj
* @date 2022/9/12
*/
public class FerrariCarCarFactory extends AbstractCarFacotry {
@Override
AbstractCar createCar() {
return new FerrariCar("法拉利汽车");
}
}
工厂方法模式的好处在于,如有要新增产品类型,无需再去更改对应的具体工厂方法,只需要新增一个抽象工厂的实现类,在其中定义新产品类型的创建逻辑,这完美符合了开闭原则
但缺点在于,每新增一个产品,就需要创建新的实现类,增加了系统复杂度
抽象工厂模式
抽象工厂是一种为访问类提供一个创建一组相关或相互依赖对象的接口,且访问类无须指定所要产品的具体类就 能得到同族的不同等级的产品的模式结构
抽象工厂模式是工厂方法模式的升级版本,工厂方法模式只生产一个等级的产品,而抽象工厂模式可生产多个等级的产品。
使用场景
- 当需要创建的对象是一系列相互关联或相互依赖的产品族时,如电器工厂中的电视机、洗衣机、空 调等
- 系统中有多个产品族,但每次只使用其中的某一族产品。如有人只喜欢穿某一个品牌的衣服和鞋
- 系统中提供了产品的类库,且所有产品的接口相同,客户端不依赖产品实例的创建细节和内部结构,如输入法换皮肤,一整套一起换、为不同操作系统创建对应的程序
以家具厂为例,家具厂生产桌子、椅子、沙发,这三者可以看作一个产品族,而这些家具又分为现代风格和古典风格,那么可以将其分为两个系列
- 现代风格的家具
- 古典风格的家具
那么根据不同的系列,提供不同的工厂类,在其中定义统一的桌子、椅子、沙发这一产品族的创建方法,确保同一个工厂类创建出的家具是统一风格的,示例代码如下
抽象工厂
/**
* 抽象工厂类
*
* @author dhj
* @date 2022/9/12
*/
public abstract class AbstractFurnitureFactory {
/**
* 桌子创建接口
*
* @return 返回具体的桌子实例
*/
public abstract AbstractDesk createDesk();
/**
* 椅子创建接口
*
* @return 返回具体的椅子实例
*/
public abstract AbstractChair createChair();
/**
* 沙发创建接口
*
* @return 返回具体的沙发实例
*/
public abstract AbstractSofa createSofa();
}
抽象产品类
/**
* 抽象椅子类
*
* @author dhj
* @date 2022/9/12
*/
public abstract class AbstractChair {
public abstract String getName();
}
/**
* 抽象桌子类
*
* @author dhj
* @date 2022/9/12
*/
public abstract class AbstractDesk {
public abstract String getName();
}
/**
* 抽象沙发类
*
* @author dhj
* @date 2022/9/12
*/
public abstract class AbstractSofa {
public abstract String getName();
}
具体产品类(现代风格)
/**
* 现代风格椅子
*
* @author dhj
* @date 2022/9/12
*/
@ToString
public class ModernStyleChair extends AbstractChair {
private final String name;
public ModernStyleChair(String name) {
this.name = name;
}
@Override
public String getName() {
return this.name;
}
}
/**
* 现代风格桌子
*
* @author dhj
* @date 2022/9/12
*/
@ToString
public class ModernStyleDesk extends AbstractDesk {
private final String name;
public ModernStyleDesk(String name) {
this.name = name;
}
@Override
public String getName() {
return this.name;
}
}
/**
* 现代风格沙发
*
* @author dhj
* @date 2022/9/12
*/
@ToString
public class ModernStyleSofa extends AbstractSofa {
private final String name;
public ModernStyleSofa(String name) {
this.name = name;
}
@Override
public String getName() {
return this.name;
}
}
具体产品(古典风格)
/**
* 古典风格椅子
*
* @author dhj
* @date 2022/9/12
*/
@ToString
public class ClassicalChair extends AbstractChair {
private final String name;
public ClassicalChair(String name) {
this.name = name;
}
@Override
public String getName() {
return this.name;
}
}
/**
* 古典风格桌子
*
* @author dhj
* @date 2022/9/12
*/
@ToString
public class ClassicalDesk extends AbstractDesk {
private final String name;
public ClassicalDesk(String name) {
this.name = name;
}
@Override
public String getName() {
return this.name;
}
}
/**
* 古典风格沙发
*
* @author dhj
* @date 2022/9/12
*/
@ToString
public class ClassicalSofa extends AbstractSofa {
private final String name;
public ClassicalSofa(String name) {
this.name = name;
}
@Override
public String getName() {
return this.name;
}
}
具体工厂
/**
* 古典家具工厂
*
* @author dhj
* @date 2022/9/12
*/
public class ClassicalFurnitureFacoty extends AbstractFurnitureFactory {
@Override
public AbstractDesk createDesk() {
return new ClassicalDesk("古典风格桌子");
}
@Override
public AbstractChair createChair() {
return new ClassicalChair("古典风格椅子");
}
@Override
public AbstractSofa createSofa() {
return new ClassicalSofa("古典风格沙发");
}
}
/**
* 现代家具工厂
*
* @author dhj
* @date 2022/9/12
*/
public class ModernFurnitureFacoty extends AbstractFurnitureFactory {
@Override
public AbstractDesk createDesk() {
return new ModernStyleDesk("现代风格桌子");
}
@Override
public AbstractChair createChair() {
return new ModernStyleChair("现代风格椅子");
}
@Override
public AbstractSofa createSofa() {
return new ModernStyleSofa("现代风格沙发");
}
}
上述代码的结构关系图如下

抽象工厂能够保证使用者能够始终使用同一产品族中的对象
但抽象工厂的缺点也很明显,当一个产品族中需要新增产品时,对应的抽象工厂都需要改变
简单工厂 + 配置文件解耦
可以通过工厂模式+配置文件的方式解除工厂对象和产品对象的耦合。在工厂类中加载配置文件中的全类名,并创建对象进行存储,客户端如果需要对象,直接进行获取即可
如下代码
抽象产品类
/**
* @author dhj
* @date 2022/9/12
*/
public abstract class AbstractCar {
public abstract String getName();
}
具体产品类
/**
* @author dhj
* @date 2022/9/12
*/
@ToString
public class BenzCar extends AbstractCar {
private final String name;
public BenzCar(String name) {
this.name = name;
}
@Override
public String getName() {
return name;
}
}
/**
* @author dhj
* @date 2022/9/12
*/
public class FerrariCar extends AbstractCar{
private final String name;
public FerrariCar(String name) {
this.name = name;
}
@Override
public String getName() {
return name;
}
}
对应的配置文件
BenzCar=example.mode.creator.factory.properties_factory.BenzCar-奔驰汽车
FerrariCar=example.mode.creator.factory.properties_factory.FerrariCar-法拉利汽车
工厂类
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.lang.reflect.Constructor;
import java.nio.charset.StandardCharsets;
import java.util.Map;
import java.util.Properties;
import java.util.concurrent.ConcurrentHashMap;
/**
* @author dhj
* @date 2022/9/12
*/
public class SimpleFactory {
private static final Map<String, AbstractCar> beans = new ConcurrentHashMap<>(16);
static {
final Properties properties = new Properties();
InputStreamReader classNameFileStream = null;
try {
// 加载配置文件
InputStream propertiesResource = SimpleFactory.class.getClassLoader().getResourceAsStream("className.properties");
if (propertiesResource == null) {
throw new RuntimeException("配置文件加载错误");
}
// 加载配置文件流(StandardCharsets.UTF_8 用于处理乱码问题)
classNameFileStream = new InputStreamReader(propertiesResource, StandardCharsets.UTF_8);
properties.load(classNameFileStream);
// 遍历配置项
for (Map.Entry<Object, Object> entry : properties.entrySet()) {
// 获取类名
String className = (String) entry.getKey();
// 获取类路径以及构造方法参数
String classPathAndParam = entry.getValue().toString();
String[] values = classPathAndParam.split("-");
// 加载指定类路径的 class 实例
Class<?> beanClass = Class.forName(values[0]);
// 反射获取有参构造方法实例
Constructor<?> constructor = beanClass.getDeclaredConstructor(String.class);
// 使用配置文件中指定的参数,调用构造方法创建实例对象
AbstractCar car = (AbstractCar) constructor.newInstance(values[1]);
beans.put(className, car);
}
} catch (Exception e) {
e.printStackTrace();
} finally {
if (classNameFileStream != null) {
try {
classNameFileStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
public static AbstractCar createCar(String className) {
return beans.get(className);
}
}
测试代码
/**
* @author dhj
* @date 2022/9/12
*/
public class MainDemo {
public static void main(String[] args) {
AbstractCar benzCar = SimpleFactory.createCar("BenzCar");
System.out.println(benzCar);
}
}
JDK 中的工厂方法模式
在 jdk 中也有很多地方用到了工厂方法模式,例如集合获取迭代器的方法,如下代码
List<String> list = new ArrayList<>(10);
list.iterator(); // 通过 iterator() 创建 Iterator 实例
其中
- 抽象工厂为
java.lang.Iterable为抽象工厂类,因为iterator()方法在其中定义, - 具体工厂为
java.util.ArrayList类,因为java.util.ArrayList间接实现了java.lang.Iterable接口,实现了iterator()方法 - 抽象产品为
java.util.Iterator - 具体产品为
java.util.ArrayList.Itr
除了迭代器使用到了工厂模式,jdk 中还有
- DateForamt 类中的 getInstance() 方法使用的是工厂模式
- Calendar 类中的 getInstance() 方法使用的是工厂模式
原型模式
用一个已经创建的实例作为原型,通过复制该原型对象来创建一个和原型对象相同的新对象
原型模式包含如下角色
- 抽象原型类:规定了具体原型对象必须实现的的 clone() 方法
- 具体原型类:实现抽象原型类的 clone() 方法,它是可被复制的对象
- 访问类:使用具体原型类中的 clone() 方法来复制新的对象
原型模式的克隆又分为
- 浅克隆:创建一个新对象,新对象的属性和原来对象完全相同,对于非基本类型属性,仍指向原有属性所指向的对象的内存地址
- 深克隆:创建一个新对象,属性中引用的其他对象也会被克隆,不再指向原有对象地址
浅克隆
在 jdk 中,已经提供了抽象的原型类java.lang.Cloneable,官方文档定义如下
一个类实现了 Cloneable 接口,以向 Object.clone() 方法指示该方法可以合法地对该类的实例进行逐个字段的复制
在未实现 Cloneable 接口的实例上调用 Object 的 clone 方法会导致抛出异常 CloneNotSupportedException
在java.lang.Cloneable中并没有提供抽象的clone()方法,此方法实际上在java.lang.Object被定义,是每个对象都具备的方法,但要想合法的调用此方法,就如官方文档所说,需要实现java.lang.Cloneable接口,详细代码演示如下
直接定义具体的原型类
/**
* @author dhj
* @date 2022/9/13
*/
@ToString
public class RealizetypeClone implements Cloneable {
private final String name;
public RealizetypeClone(String name) {
this.name = name;
System.out.println("原型对象创建完成");
}
@Override
public RealizetypeClone clone() {
try {
return (RealizetypeClone) super.clone();
} catch (CloneNotSupportedException e) {
throw new AssertionError();
}
}
}
测试类
/**
* @author dhj
* @date 2022/9/13
*/
public class MainDemo {
public static void main(String[] args) {
RealizetypeClone clone = new RealizetypeClone("zhangsan");
RealizetypeClone r1 = clone.clone();
RealizetypeClone r2 = clone.clone();
System.out.println(r1);
System.out.println(r2);
}
}
简单来说,原型模式的作用在于定义一个模板对象,而不同模板对象的区别主要使用构造函数进行区分,例如某些属性不同,在调用构造函数时传入,而其他相同的属性或方法后续再使用时,则可以直接通过clone()方法复用
例如在实际业务中,有一个短信发送的模板对象,每次发送短信时都需要传入手机号,姓名,内容,而内容格式大多数情况下是固定不变的,只需要通过构造方法传入手机号和姓名,创建一个具体原型对象,复用时,直接调用其clone()方法即可
深克隆
浅克隆只会拷贝原型对象,而原型对象中的引用类型的属性则会直接将其引用地址赋给克隆对象中的对应属性,这就导致当修改其中一个克隆对象中的引用类型属性时,其余的克隆对象也会跟着发生改变
这种情况就需要使用到深克隆,将原型对象中的引用类型属性也克隆一份新的到克隆对象中,假设有一个奖状类,类中包含奖状的展示方法以及对应的学生,使用深拷贝实现的具体代码如下
奖状具体原型类
/**
* @author dhj
* @date 2022/9/13
*/
@ToString
@Getter
public class Certificate implements Serializable {
private final Student stu;
public Certificate(Student stu) {
this.stu = stu;
}
public void show() {
System.out.println(this.stu.getName() + " 同学:在 2022 学年第一学期中表现优秀,被评为三好学生。特发此状!以资鼓励。");
}
// 使用 json 序列和反序列化的方式,进行深度克隆
public Certificate deepClone() {
return JSONObject.parseObject(JSONObject.toJSONString(this), this.getClass());
}
@ToString
@Data
static class Student implements Serializable {
private String name;
public Student(String name) {
this.name = name;
}
}
}
测试类
/**
* @author dhj
* @date 2022/9/13
*/
public class MainDemo {
public static void main(String[] args) {
Certificate template = new Certificate(new Certificate.Student("zhangsan"));
Certificate certificate = template.deepClone();
Certificate certificate1 = template.deepClone();
// 某一克隆对象对自己引用属性的修改不会影响其他克隆对象
certificate1.getStu().setName("lisi");
certificate.show();
certificate1.show();
}
}
深克隆的方式除了 json 序列化的方式外,还可以使用 jdk 自带的对象序列化,这里为了方便使用的的是 json 序列化
建造者模式
建造者模式,又称为生成器模式,是一种创建型设计模式,能够分步骤创建复杂对象,该模式允许使用相同的创建代码生成不同类型和形式的对象
什么叫复杂对象?在对复杂对象进行构造时需要对诸多成员变量和嵌套对象进行繁复的初始化工作,这些初始化代码通常深藏于一个包含众多参数且让人基本看不懂的构造函数中;又或者这些代码散落在客户端代码的多个位置
建造者模式一般包含如下角色
- 抽象建造者,声明建造某一产品的通用步骤
- 具体建造者,负责具体实现抽象生成器中定义的步骤
- 产品,最终生成的对象
- 主管,使用生成器,定义不同的建造方式或步骤,用以创建不同的产品
- 客户端,客户端可以选择调用主管来创建已经定义好的建造步骤;又或者直接调用建造者,自定义建造的步骤,最终生成客户端需要的产品
可以将建造者模式想象成一个工厂
- 一条流水线上定义的机械化步骤,代表【抽象建造者】
- 流水线上的工人负责维护和操作流水线,代表【具体建造者】
- 流水线最终生产的产品,代表建造者模式中的【产品】
- 工厂里面的技术主管,负责调整或协调整个流水线的机械化步骤,代表建造者模式中的【主管】
- 客户端则可以理解为有产品生成需求的客户,例如有家具公司需要生产一批家具,家具公司就可以理解为【客户端】
假设现在以生产笔记本电脑为例,可以划分出的步骤有生产 CPU 生产内存 生产硬盘 生产主板 生产显卡 生产屏幕 生产键盘等,这其中有些步骤不是必须的,例如生产显卡,一些轻薄本不配置独立显卡;而各个步骤的具体生产逻辑也可能不一样,配件的型号不同等,适合使用建造者模式,具体代码实现如下
抽象建造者定义
/**
* 笔记本电脑抽象建造者, 定义笔记本产品建造的各项步骤
*
* @author dhj
* @date 2022/9/13
*/
public abstract class Builders {
public abstract void builderCPU(String name);
public abstract void builderDisk(String name);
public abstract void builderKeyboard(String name);
public abstract void builderMemory(String name);
public abstract void builderMontherboard(String name);
public abstract void builderScreen(String name);
// 产品的建造步骤执行完成后, 调用此方法组合最终产品
public abstract Computer finished();
}
具体建造者实现
/**
* Rog牌电脑建造者
*
* @author dhj
* @date 2022/9/13
*/
public class RogBuilder extends Builders {
private CPU cpu;
private Disk disk;
private Keyboard keyboard;
private Memory memory;
private Motherboard motherboard;
private Screen screen;
@Override
public void builderCPU(String name) {
this.cpu = new CPU(name);
}
@Override
public void builderDisk(String name) {
this.disk = new Disk(name);
}
@Override
public void builderKeyboard(String name) {
this.keyboard = new Keyboard(name);
}
@Override
public void builderMemory(String name) {
this.memory = new Memory(name);
}
@Override
public void builderMontherboard(String name) {
this.motherboard = new Motherboard(name);
}
@Override
public void builderScreen(String name) {
this.screen = new Screen(name);
}
public Computer finished() {
return new Computer(this.cpu, this.disk, this.keyboard, this.memory, this.motherboard, this.screen);
}
}
产品的各组件
/**
* 处理器
*
* @author dhj
* @date 2022/9/13
*/
@Data
@ToString
public class CPU {
private String name;
public CPU(String name) {
this.name = name;
}
}
/**
* 磁盘
*
* @author dhj
* @date 2022/9/13
*/
@Data
@ToString
public class Disk {
private String name;
public Disk(String name) {
this.name = name;
}
}
/**
* 键盘
*
* @author dhj
* @date 2022/9/13
*/
@Data
@ToString
public class Keyboard {
private String name;
public Keyboard(String name) {
this.name = name;
}
}
/**
* 内存
*
* @author dhj
* @date 2022/9/13
*/
@Data
@ToString
public class Memory {
private String name;
public Memory(String name) {
this.name = name;
}
}
/**
* 主板
*
* @author dhj
* @date 2022/9/13
*/
@Data
@ToString
public class Motherboard {
private String name;
public Motherboard(String name) {
this.name = name;
}
}
/**
* 屏幕
*
* @author dhj
* @date 2022/9/13
*/
@Data
@ToString
public class Screen {
private String name;
public Screen(String name) {
this.name = name;
}
}
具体产品类
/**
* @author dhj
* @date 2022/9/13
*/
@ToString
@Setter
public class Computer {
private final CPU cpu;
private final Disk disk;
private final Keyboard keyboard;
private final Memory memory;
private final Motherboard motherboard;
private final Screen screen;
public Computer(CPU cpu, Disk disk, Keyboard keyboard, Memory memory, Motherboard motherboard, Screen screen) {
this.cpu = cpu;
this.disk = disk;
this.keyboard = keyboard;
this.memory = memory;
this.motherboard = motherboard;
this.screen = screen;
}
}
主管(协调者)类
package example.mode.creator.builders;
/**
* @author dhj
* @date 2022/9/13
*/
public class Director {
/**
* 幻16的构建步骤
*
* @param builder Rog电脑建造者
*/
public void magical16Builder(Builders builder) {
builder.builderCPU("Intel_12_i7");
builder.builderKeyboard("rog灯效键盘");
builder.builderDisk("三星DDR5");
builder.builderMemory("镁光32G");
builder.builderScreen("2K星云屏");
builder.builderMontherboard("华硕");
}
/**
* 幻14的构建步骤
*
* @param builder Rog电脑建造者
*/
public void magical14Builder(Builders builder) {
builder.builderCPU("AMD_R7_6800H");
builder.builderKeyboard("rog灯效键盘");
builder.builderDisk("三星DDR4");
builder.builderMemory("镁光16G");
builder.builderScreen("1080P");
builder.builderMontherboard("华硕");
}
}
客户端
/**
* @author dhj
* @date 2022/9/13
*/
public class Client {
public static void main(String[] args) {
// 主管实例
Director director = new Director();
// 华硕电脑建造者实例
Builders rogBuilder = new RogBuilder();
// 通过主管实例,协调建造者执行 magical16 的构建步骤
director.magical16Builder(rogBuilder);
Computer magical16 = rogBuilder.finished();
System.out.println(magical16);
// 通过主管实例,协调建造者执行 magical14 的构建步骤
director.magical14Builder(rogBuilder);
Computer magical14 = rogBuilder.finished();
System.out.println(magical14);
}
}
以上代码,整个建造者模式的核心在于Builders Director RogBuilder,首先,Builders中定义了某一类产品的各个抽象建造步骤,RogBuilder实现了这些具体的建造步骤,但目前这些步骤应该怎样调用,缺乏一个管理者,这也是Director的主要作用,在Director中,定义了magical16Builder(Builders builder)和magical14Builder(Builders builder)这两个方法,定义了两种不同产品建造时的步骤
注意,magical16Builder()和magical14Builder()两个方法的参数中传入的是Builders类型,这也说明了Director只是起一个协调作用,具体的建造逻辑依然由Builders的实现类来执行
上面的例子是一个完整的建造者模式,包含了建造者模式的各项角色,在开发过程中,还有一种比较常用的简化版建造模式的应用;当一个类中的构造参数过多时,可能在调用构造方法时需要传入很多参数,影响代码的可读性且不易于维护,此时可以使用建造模式重构类结构,如下代码示例
以一个用户对象举例,其中有很多属性,可以看到最终的构造方法比较冗长
/**
* @author dhj
* @date 2022/9/13
*/
@Data
@ToString
public class User {
private String name;
private Integer age;
private LocalDateTime birth;
private List<String> hobbys;
private String IDCard;
private String address;
public User(String name, Integer age, LocalDateTime birth, List<String> hobbys, String IDCard, String address) {
this.name = name;
this.age = age;
this.birth = birth;
this.hobbys = hobbys;
this.IDCard = IDCard;
this.address = address;
}
}
使用建造模式重构后的代码如下,通过一个内部的建造者类,简化了User的创建形式
import lombok.Data;
import lombok.ToString;
import java.time.LocalDateTime;
import java.util.List;
/**
* @author dhj
* @date 2022/9/13
*/
@Data
@ToString
public class User {
private String name;
private Integer age;
private LocalDateTime birth;
private List<String> hobbys;
private String IDCard;
private String address;
public User(Builder builder) {
this.name = builder.name;
this.age = builder.age;
this.birth = builder.birth;
this.hobbys = builder.hobbys;
this.IDCard = builder.IDCard;
this.address = builder.address;
}
public static final Builder BUILDER = new Builder();
public static final class Builder {
private String name;
private Integer age;
private LocalDateTime birth;
private List<String> hobbys;
private String IDCard;
private String address;
public Builder name(String name) {
this.name = name;
return this;
}
public Builder age(Integer age) {
this.age = age;
return this;
}
public Builder birth(LocalDateTime birth) {
this.birth = birth;
return this;
}
public Builder hobbys(List<String> hobbys) {
this.hobbys = hobbys;
return this;
}
public Builder IDCard(String IDCard) {
this.IDCard = IDCard;
return this;
}
public Builder address(String address) {
this.address = address;
return this;
}
public User build() {
return new User(this);
}
}
}
测试类
/**
* @author dhj
* @date 2022/9/13
*/
public class MainDemo {
public static void main(String[] args) {
User user = User.BUILDER
.name("zhangsan")
.birth(LocalDateTime.now())
.IDCard(RandomUtil.randomNumbers(18))
.address("地球村-中国路-成都巷-21号")
.age(19)
.hobbys(Arrays.asList("唱", "跳", "rap", "篮球"))
.build();
System.out.println(user);
}
}
设计模式之结构型模式
结构型模式介绍如何将对象和类组装成较大的结构, 并同时保持结构的灵活和高效
适配器模式
使用适配器模式,可以让两个原本不兼容的接口协调工作,结合生活中的例子,适配器模式可以理解为一些接口转换器,例如typec转耳机接口,VGA 和 HDMI 的转接器等
适配器模式包含以下角色
- 目标(Target)接口:当前系统业务所期待的接口,它可以是抽象类或接口
- 适配者(Adaptee)类:它是被访问和适配的组件接口,通常是一些第三方接口
- 适配器(Adapter)类:它是一个转换器,通过继承或引用适配者的对象,把适配者接口转换成目标接口,让客户按目标接口的格式访问适配者
例如现在有TF卡,但接口程序却针对的是SD卡类型,此时就可以使用适配器模式来解决,如下代码
目标(SD卡)接口
/**
* SD 卡驱动程序抽象接口
*
* @author dhj
* @date 2022/9/14
*/
public abstract class SDReaderWriter {
/**
* 读取 SD 卡中的数据
*
* @return 返回 sd 卡中读取的内容
*/
public abstract String reader();
/**
* 向 sd 卡中写入内容
*
* @param content 写入的具体内容
*/
public abstract void writer(String content);
}
适配者(TF卡驱动程序)
import cn.hutool.core.util.RandomUtil;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
/**
* TF 卡驱动程序
*
* @author dhj
* @date 2022/9/14
*/
public class TFReaderWriterService {
private final Map<String, Byte[]> store = new ConcurrentHashMap<>(16);
/**
* 读取 TF 卡中的数据
*
* @return 返回 TF 卡中的字节数据
*/
public List<Byte[]> reader() {
List<Byte[]> bytesList = new ArrayList<>(store.size());
for (Map.Entry<String, Byte[]> entry : store.entrySet()) {
bytesList.add(entry.getValue());
}
return bytesList;
}
/**
* 向 TF 卡中写入数据
*
* @param data 写入的数据
*/
public void writer(Byte[] data) {
store.put(RandomUtil.randomString(16), data);
}
}
适配器
import java.nio.charset.StandardCharsets;
import java.util.*;
/**
* @author dhj
* @date 2022/9/14
*/
public class TFReaderWriterAdapter extends SDReaderWriter {
private static final TFReaderWriterService TF_READER_WRITER_SERVICE = new TFReaderWriterService();
@Override
public String reader() {
List<Byte[]> bytesList = TF_READER_WRITER_SERVICE.reader();
StringBuilder sbf = new StringBuilder();
for (Byte[] bytes : bytesList) {
byte[] tempBytes = new byte[bytes.length];
for (int i = 0; i < bytes.length; i++) {
tempBytes[i] = bytes[i];
}
sbf.append(new String(tempBytes)).append("\n");
}
return sbf.toString();
}
@Override
public void writer(String content) {
byte[] bytes = content.getBytes(StandardCharsets.UTF_8);
Byte[] temp = new Byte[bytes.length];
for (int i = 0; i < bytes.length; i++) {
temp[i] = bytes[i];
}
TF_READER_WRITER_SERVICE.writer(temp);
}
}
客户端使用演示
/**
* @author dhj
* @date 2022/9/14
*/
public class Client {
public static void main(String[] args) {
TFReaderWriterAdapter tfAdapter = new TFReaderWriterAdapter();
tfAdapter.writer("HelloWorld");
String reader = tfAdapter.reader();
System.out.println(reader);
}
}
如上代码中,TF卡的适配器TFReaderWriterAdapter,实现了SDReaderWriter接口,此接口中定义的都是与 SD 卡相关的操作,在 SD卡接口方法的具体实现中,通过引入的TFReaderWriterService实例与 TF 卡的驱动程序交互
最终,客户端使用适配器,调用 SD 卡形式的方法,就能操作 TF 卡了
适配器用于在对接一些第三方库时比较常用,因为第三方库的接口与本地接口风格不一致的可能性较大,如果想在不修改本地接口的情况下继续调用第三方库,就需要为对应的第三方接口编写适配本地接口风格的适配器
桥接模式
当一个类中,存在不同维度的变化时,如果使用继承关系,那么涉及到扩展时,将会产生类爆炸,例如一个抽象类Shape(形状),两个子类分别为Round(圆形) Square(方形),现在对于Shape来说,只有形状这一个维度,使用继承的方式,涉及到扩展时,新增子类即可
可如果现在的需求是不同的形状有红 蓝两种颜色,注意这里多了一个颜色维度,最终的结构图如下

目前只是形状 颜色两个维度,就需要 4 个子类,如果再增加维度,那么子类的数量将会是指数级的,这就是类爆炸
对于这种情况,使用桥接模式再合适不过了
桥接模式将抽象与实现分离,使它们可以独立变化。因为桥接模式主要使用【组合关系代替继承关系】来实现,从而降低了抽象和实现这两个可变维度的耦合
现在将颜色这个维度抽象出来,叫做Color,作为Shape的一个组合属性,那么类结构如下

这样,对某一个维度的扩展,就不会导致另一个维度子类数量的爆炸
桥接模式就是将其他扩展的维度作为另一个抽象类,但一定有一个主要的抽象类来组合分离出去的抽象类,这个主要的抽象类叫做【抽象部分】,例如上述例子中的Shape类,将颜色这个扩展的维度抽象为Color,然后在Shape自身内部组合Color这个属性,这是桥接模式的深刻体现
为了更好的理解桥接模式,可以想象Shape作为两座桥中的A端,Color作为两座桥中的B端,呈现出如下结构

这就好比两座桥,通过组合的方式实现了连接,体现了【桥接】这个概念
在上述例子中,Shape是作为一个高阶控制层存在的,这就好比一个水杯的手柄,手柄就是高阶控制层,也就是【抽象部分】,而杯体作为【实现部分】,通过手柄带动杯体实现喝水的动作,因此桥接模式又称为柄体(Handle and Body)模式
在桥接模式中,有如下几个角色
- 抽象部分,提供高层的控制逻辑,实现则依赖底层的具体实现对象
- 实现部分,代表对某个维度的定义,同时被组合到【抽象部分】中,例如
Shape中的Color维度,就具备红色和蓝色这两个定义 - 具体抽象,对【抽象部分】的实现,主要利用【实现部分】完成控制逻辑,可以简单理解为【抽象部分】的实现类
- 具体实现,包含实现部分中定义的接口的具体逻辑,可以简单理解为【实现部分】的实现类
假设现有一款文本阅读器,需要适配win linux macos的操作系统,并且播放的格式要支持txt epud mobi三种格式,这里面有两个维度,分别为操作系统 文本格式,如果使用继承的方式来定义系统,那么会生成 9 个实现类,任意维度的扩展都会导致整个继承体系中实现类的增加,这就可以使用桥接模式,如下代码
抽象部分(因为用户主要使用不同的操作系统来调用read()来阅读数据,因此Client可以作为一个顶层控制逻辑来定义)
/**
* 文本阅读器客户端抽象类
*
* @author dhj
* @date 2022/9/15
*/
public abstract class Client {
// 组合【文本格式这个维度】这里已经形成了桥接
protected TextDecode textDecode;
public Client(TextDecode textDecode) {
this.textDecode = textDecode;
}
/**
* 阅读书籍
*
* @param book 传入书籍二进制文件
* @return 返回参数书籍的文本表示形式
*/
public abstract String read(byte[] book);
}
具体抽象实现,在这里面,主要使用【抽象部分】中组合的其他维度,如TextDecode,实现【抽象部分】中定义的具体的顶层控制逻辑,例如这里read()方法
/**
* linux 客户端
* @author dhj
* @date 2022/9/15
*/
public class LinuxClient extends Client {
public LinuxClient(TextDecode textDecode) {
super(textDecode);
}
@Override
public String read(byte[] book) {
return textDecode.decode(book);
}
}
/**
* macos 客户端
* @author dhj
* @date 2022/9/15
*/
public class MacOSClient extends Client {
public MacOSClient(TextDecode textDecode) {
super(textDecode);
}
@Override
public String read(byte[] book) {
return textDecode.decode(book);
}
}
/**
* win 客户端
* @author dhj
* @date 2022/9/15
*/
public class WindowsClinet extends Client {
public WindowsClinet(TextDecode textDecode) {
super(textDecode);
}
@Override
public String read(byte[] book) {
return textDecode.decode(book);
}
}
实现部分,在处理【抽象部分】中定义的顶层控制逻辑时,会使用到【实现部分】的具体逻辑
/**
* 文本解析器抽象类
*
* @author dhj
* @date 2022/9/15
*/
public abstract class TextDecode {
/**
* 对传入的数据进行解码
*
* @param content 数据
* @return 返回解码后的文本内容
*/
public abstract String decode(byte[] content);
}
具体实现
/**
* epud 解码器
* @author dhj
* @date 2022/9/15
*/
public class EPUDDecode extends TextDecode {
@Override
public String decode(byte[] content) {
return "epud:" + "(" + new String(content, StandardCharsets.UTF_8) + ")";
}
}
/**
* mobi 解码器
* @author dhj
* @date 2022/9/15
*/
public class MOBIDecode extends TextDecode {
@Override
public String decode(byte[] content) {
return "mobi(" + new String(content, StandardCharsets.UTF_8) + ")";
}
}
/**
* txt 解码器
* @author dhj
* @date 2022/9/15
*/
public class TXTDecode extends TextDecode {
@Override
public String decode(byte[] content) {
return "txt(" + new String(content, StandardCharsets.UTF_8) + ")";
}
}
客户端调用
/**
* @author dhj
* @date 2022/9/15
*/
public class MainDemo {
public static void main(String[] args) {
Client winClient = new WindowsClinet(new EPUDDecode());
String read = winClient.read("红楼梦".getBytes(StandardCharsets.UTF_8));
System.out.println(read);
}
}
如果后续要扩展阅读器,例如支持
Android系统,那么也只需要再实现一个AndroidClient的子类即可,无需再针对每个格式再去实现诸如AndroidTXTDecoderAndroidEPUDDecoder等类型的解码器,因为解码器是通过【组合】的形式,桥接到Clinet中的桥接模式实际上遵循的是软件设计原则中的【合成复用原则】
组合模式
组合模式(Composite Pattern),又叫部分整体模式,它创建了对象组的树形结构,将对象组合成树状结构以表示【整体-部分】的层次关系;在组合模式构成的对象树中,顶层的节点叫做root节点,root节点下的节点叫做树枝节点(存在子节点)和树叶节点(没有子节点)
尽管由组合模式构建出的对象树具备较深的层次,但客户端总是可以 使用统一的接口来操作对象树,这是因为无论是顶层的 root 节点还是树枝节点或树叶节点,都实现了统一的【抽象构件】接口,此接口定义了组成对象树中的每个元素统一的行为方式
在组合模式中,有如下角色
- 抽象构件(Component):它的主要作用是为树叶构件和树枝构件声明公共接口,并实现它们的默认行为。抽象构件还声明访问和管理子类的接口(但抽象构件本身不允许操作)
- 树枝构件(Composite):是组合中的分支节点对象,它有子节点,用于继承和实现抽象构件,但可能不具有树叶构件的某些行为。它的主要作用是存储和管理子部件,通常包含 Add()、Remove()、GetChild() 等方法
- 树叶构件(Leaf):是组合中的叶节点对象,它没有子节点,用于继承或实现抽象构件,定义了组合内元素的行为
组合模式的最大优点在于无需了解构成树状结构的对象的具体类,也无需了解对象是简单还是复杂的;只需调用通用接口以相同的方式对其进行处理即可。 当调用该方法后,对象会将请求沿着树结构 逐层传递 下去
下面以大学 -> 系部 -> 专业为例,使用组合模式定义和实现这三者之间的关系以及操作方法
定义抽象构件
/**
* 抽象构件
*
* @author dhj
* @date 2022/9/16
*/
public abstract class Component {
/**
* 构件名称
*/
protected String name;
/**
* 构件描述
*/
protected String desc;
protected String getName() {
return name;
}
protected String getDesc() {
return desc;
}
public Component(String name, String desc) {
this.name = name;
this.desc = desc;
}
/**
* 添加构件
*
* @param component 添加的构件
*/
protected abstract void add(Component component);
/**
* 删除构件
*
* @param component 删除的构件
*/
protected abstract void remove(Component component);
/**
* 显示当前节点下的所有构件信息
*/
protected abstract void show();
}
定义树枝构件
/**
* root构件(某一大学),也就是 root 节点
*
* @author dhj
* @date 2022/9/16
*/
public class University extends Component {
/**
* 树枝构件(某一大学下某一系部)
*/
private final List<Component> departments;
public University(String name, String desc) {
super(name, desc);
this.departments = new ArrayList<>(10);
}
@Override
protected void add(Component component) {
departments.add(component);
}
@Override
protected void remove(Component component) {
departments.remove(component);
}
@Override
protected void show() {
System.out.println("名称【" + getName() + "】,描述【" + getDesc() + "】");
// 传递
for (Component department : departments) {
department.show();
}
}
}
/**
* 树枝构件(某一大学下某一系部)
*
* @author dhj
* @date 2022/9/16
*/
@ToString
public class Department extends Component {
/**
* 树叶构件(某一系部下某一所属专业)
*/
private final List<Component> disciplines;
public Department(String name, String desc) {
super(name, desc);
this.disciplines = new ArrayList<>(10);
}
@Override
protected void add(Component component) {
this.disciplines.add(component);
}
@Override
protected void remove(Component component) {
this.disciplines.remove(component);
}
@Override
protected void show() {
System.out.println("名称【" + getName() + "】,描述【" + getDesc() + "】");
// 传递
for (Component discipline : this.disciplines) {
discipline.show();
}
}
}
定义树叶构件
/**
* 树叶构件(某一大学某一系部下某一专业)
*
* @author dhj
* @date 2022/9/16
*/
public class Discipline extends Component {
public Discipline(String name, String desc) {
super(name, desc);
}
@Override
protected void add(Component component) {
throw new RuntimeException("树叶节点无法操作");
}
@Override
protected void remove(Component component) {
throw new RuntimeException("树叶节点无法操作");
}
@Override
protected void show() {
// 叶子节点无法传递, 直接处理具体业务
System.out.println("名称【" + getName() + "】,描述【" + getDesc() + "】");
}
}
测试类
/**
* @author dhj
* @date 2022/9/16
*/
public class MainDemo {
public static void main(String[] args) {
University university = new University("麻省理工", "世界一流大学");
Department department = new Department("信息工程系", "世界一流系部");
university.add(department);
department.add(new Discipline("软件工程","世界一流专业"));
university.show();
}
}
装饰模式
装饰模式的装饰二字体现在对具体组件外层的装饰,这就好比一个人(抽象组件)张三(具体组件),他本身就具备走路和跑步的功能,此时在张三的脚上再穿了一双鞋子,这个行为可以称之为【装饰】,而这个鞋子称之为【装饰者】,张三称之为【被装饰者】,通过【装饰】,张三跑得更快了,所以装饰模式适合于功能增强的场景
装饰者也不仅仅只限于装饰具体组件,也可以装饰其他装饰者,层层嵌套,装饰者的方法有一部分是自己的,这属于它的功能,然后调用被装饰者的方法实现,从而也保留了被装饰者的功能;具体组件应当是装饰层次的最低层,因为只有具体组件的方法实现不需要依赖于其它对象
装饰者的作用就在于对被装饰者原本的功能进行扩展或增强,例如在被装饰者的功能执行前后做一些事情,装饰者的结构如下图

简单来说,装饰者就是在被装饰者外面套了一层,就像穿衣服或者对一个东西进行装扮
标准的装饰者模式具备以下几种角色
- 抽象组件,申明公共接口
- 具体组件,提供组件中申明的公共接口的默认实现
- 基础装饰类,对具体组件进行装饰(关联一个【抽象组件】类型的常用变量),主要定义关联的具体组件的封装接口
- 具体装饰类,定义了可动态添加到部件的额外行为。 具体装饰类会重写装饰基类的方法, 并在调用父类方法之前或之后进行额外的行为
在 JDK 的 IO 流中,就存在对装饰模式的应用,其中InputStream作为抽象组件,定义了基本的公共接口;FileInputStream作为具体组件同时也作为基础装饰类,实现了InputStream中的公共接口;BufferedInputStream中关联了InputStream类型的常用变量,并通过构造方法的形式进行初始化,额外的扩展的 IO 操作时的缓存功能,然而底层的 IO 操作依然由其关联属性FileInputStream实例来完成,BufferedInputStream则作为一个装饰者装饰FileInputStream对其功能进行增强
下面以一个数据读取器为例,演示装饰者模式的相关代码,如下
抽象组件
/**
* 抽象组件
*
* @author dhj
* @date 2022/9/18
*/
public abstract class DataReaderWriter {
/**
* 读取数据,返回数据的读取结果
*
* @return 返回读取的结果集
*/
public abstract List<String> reader();
/**
* 写入数据
*
* @param data 被写入的数据
*/
public abstract boolean writer(String data);
}
具体组件
/**
* 具体组件,实现抽象组件中的公共接口
*
* @author dhj
* @date 2022/9/18
*/
public class BaseDataReaderWriter extends DataReaderWriter {
// 保存写入的数据
protected final List<String> store = new ArrayList<>(10);
@Override
public List<String> reader() {
return this.store;
}
@Override
public boolean writer(String data) {
return store.add(data);
}
}j
基础装饰
/**
* 基础装饰,实现抽象组件中定义的接口,但在接收到对应请求时,委派给具体组件
*
* @author dhj
* @date 2022/9/18
*/
public class DataReaderWriterDecorator extends DataReaderWriter {
protected DataReaderWriter wrapper;
public DataReaderWriterDecorator(DataReaderWriter wrapper) {
this.wrapper = wrapper;
}
@Override
public List<String> reader() {
return wrapper.reader();
}
@Override
public boolean writer(String data) {
return wrapper.writer(data);
}
}
装饰者
/**
* @author dhj
* @date 2022/9/18
*/
public class EncryDataReaderWriter extends DataReaderWriterDecorator {
private static final String key = "dwhdgy672dbshtht";
public EncryDataReaderWriter(DataReaderWriter wrapper) {
super(wrapper);
}
@Override
public List<String> reader() {
List<String> reader = super.reader();
// 获得结果后解密数据
reader.replaceAll(enstr -> SecureUtil.aes(key.getBytes(StandardCharsets.UTF_8)).decryptStr(enstr));
return reader;
}
@Override
public boolean writer(String data) {
// 在保存之前加密数据
String encryptStr = SecureUtil.aes(key.getBytes(StandardCharsets.UTF_8)).encryptBase64(data);
return super.writer(encryptStr);
}
}
测试类
/**
* @author dhj
* @date 2022/9/18
*/
public class MainDemo {
public static void main(String[] args) {
DataReaderWriter dataReaderWriter = new EncryDataReaderWriter(new BaseDataReaderWriter());
dataReaderWriter.writer("HelloWorld");
List<String> reader = dataReaderWriter.reader();
reader.forEach(System.out::println);
}
}
以上是一个普通的数据读取器,通过装饰者模式扩展加密解密功能的例子,可以看到,使用装饰者模式,既能保证抽象组件中的接口不变,又能得到功能上的扩展
后续在想要扩展功能时,也无需修改现有组件,继续创建装饰类按需关联现有的装饰者或具体组件即可,满足开闭原则
外观模式
现在有一个第三方库或者子系统,以为其中的接口或逻辑极为复杂,使得客户端调用的难度增加,影响了客户端的系统复杂性,此时就可以为这个第三方库或子系统设计一个【外观】,外观中暴露简单的客户端所需的接口,提供给客户端调用,在简单接口中则实现客户端直接调用的复杂逻辑
外观模式有如下几个角色
- 外观,提供了一种访问特定子系统功能的便捷方式
- 子系统,由若干对象或复杂逻辑构成,如果要用这些对象完成有意义的工作, 你必须深入了解子系统的实现细节,比如按照正确顺序初始化对象和为其提供正确格式的数据,子系统类不会意识到外观的存在
- 客户端,使用外观代替对子系统对象的直接调用
下面以看电影举例子,电影院在播放电影时,可能会进行一系列操作,比如搭建影台、放置胶片、调整放映机、最后放映,这一系列操作客户端不应该关系,可以创建一个【外观】,客户端通过【外观】实现直接观看电影,如下代码
子系统
/**
* 子系统
*
* @author dhj
* @date 2022/9/18
*/
public class MovieShow {
/**
* 搭建影台
*/
public MovieShow buildStage() {
System.out.println("build stage");
return this;
}
/**
* 放置胶片
*/
public MovieShow setFilm() {
System.out.println("set film");
return this;
}
/**
* 调整放映机
*/
public MovieShow adjustTheProjector() {
System.out.println("adjust the projector");
return this;
}
/**
* 开始放映
*/
public void startShow() {
System.out.println("start show");
}
}
外观
/**
* 电影放映系统的【外观】类
*
* @author dhj
* @date 2022/9/18
*/
public class MovieShowFacade {
private final MovieShow movieShow;
public MovieShowFacade(MovieShow movieShow) {
this.movieShow = movieShow;
}
/**
* 一键观看电影
*/
public void movieShow() {
movieShow.buildStage().setFilm().adjustTheProjector().startShow();
}
}
客户端
/**
* 客户端,一键观看电影
*
* @author dhj
* @date 2022/9/18
*/
public class Client {
public static void main(String[] args) {
MovieShowFacade movieShowFacade = new MovieShowFacade(new MovieShow());
movieShowFacade.movieShow();
}
}
通过外观模式,将放映电影的一系列繁琐的步骤都封装到对应的【外观】中,客户端只需要通过【外观】中的movieShow()即可完成子系统的一系列调用
如果一个外观类过于臃肿,可以按照一定规则,创建其他外观类,例如按照不同的行为区分创建外观类
外观模式符合迪米特法则,以为客户端和子系统并没有直接交互的必要,通过第三方来间接调用子系统,达到解耦的效果
享元模式
享元模式主要在大量创建不可变或可重用对象时使用,是一种运用共享的方式有效地支持大量细粒度对象创建的模式
享元模式和原型模式类似,都是在节省创建对象的成本,但原型模式通过clone的形式每次创建的都是相同内容的对象,享元模式则需要区分【内在状态】和【外在状态】,内在状态是能够在【多个对象中共享】的状态,也称之为【享元】,外在状态则通过享元对象中定义的【享元方法】传递
在jdk中,存在着许多对享元模式的应用,如Integer.valueOf(),默认会缓存-128 ~ 127的Integer实例对象,用于快速返回此范围内的Integer实例
参照Integer,模拟一个对象缓存,如下代码
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* @author dhj
* @date 2022/9/22
*/
public class StudentCache {
private static final Map<String, Student> stuCache = new ConcurrentHashMap<>(16);
public Student fetchStu(Integer id, String name) {
String key = id + name;
Student student = stuCache.get(key);
if (student == null) {
student = new Student(id, name);
stuCache.put(key, student);
return student;
}
return student;
}
static class Student {
private final Integer stuId;
private final String name;
public Student(Integer stuId, String name) {
this.stuId = stuId;
this.name = name;
}
@Override
public String toString() {
return "Student{" + "stuId=" + stuId + ", name='" + name + '\'' + '}';
}
}
}
测试类
/**
* @author dhj
* @date 2022/9/22
*/
public class MainDemo {
public static void main(String[] args) {
StudentCache studentCache = new StudentCache();
StudentCache.Student zhangsan = studentCache.fetchStu(1, "zhangsan");
System.out.println(zhangsan);
StudentCache.Student zhangsan1 = studentCache.fetchStu(1, "zhangsan");
System.out.println(zhangsan == zhangsan1); // true
}
}
代理模式
代理模式是一种结构型设计模式,能够实现在原始业务逻辑执行的前后,通过代理的方式执行额外的逻辑而无需修改原始代码,如下图

代码模式在Spring中有大量应用,Spring中各种功能增强的实现,例如aop,事务,前置处理器,后置处理器等都利用了代理模式实现
下面使用【照片查看器】演示代理模式的应用,照片查看器会在目标图片加载完成之前,使用默认图片渲染,代码如下
抽象图片类
/**
* @author dhj
* @date 2022/9/22
*/
public abstract class Image {
/**
* 返回图片是否加载完成
*
* @return true 完成,false 未完成
*/
public abstract boolean isLoad();
/**
* 显示图片
*/
public abstract void show();
}
目标图片
/**
* @author dhj
* @date 2022/9/22
*/
public class TargetImage extends Image {
private final String url;
private final long startTime;
public TargetImage(String url) {
this.url = url;
this.startTime = System.currentTimeMillis();
}
@Override
public boolean isLoad() {
long endTime = System.currentTimeMillis();
return endTime - startTime >= 3000;
}
@Override
public void show() {
System.out.println(url);
}
}
目标图片的【代理对象】
/**
* @author dhj
* @date 2022/9/22
*/
public class ProxyImage extends Image {
// 被代理的目标对象
private final TargetImage targetImage;
private static final String defaultUrl = "https://image/default.jpg";
public ProxyImage(TargetImage targetImage) {
this.targetImage = targetImage;
}
@Override
public boolean isLoad() {
return true;
}
@Override
public void show() {
// 在目标图片加载完成之前,显示默认图片
while (!targetImage.isLoad()) {
System.out.println(defaultUrl);
SleepHelper.sleep(100);
}
System.out.println("------load finish--------");
targetImage.show();
}
}
测试类
/**
* @author dhj
* @date 2022/9/22
*/
public class MainDemo {
public static void main(String[] args) {
ProxyImage proxyImage = new ProxyImage(new TargetImage("https://images/target.jepg"));
proxyImage.show();
}
}
设计模式之行为型模式
行为模式负责对象间的高效沟通和职责委派
责任链模式
责任链模式负责将目标调用所对应的一系列步骤或环节组合起来,每一个环节中,通过成员变量的形式保存对下一个环节的引用,同时由当前环节决定是否调用下一环节,请求会在所有环节构成的【执行链】上移动,直至完成整个流程或者在某一个环节中断,如下图

这类似于单向链表,通过这种结构能够完成多步骤处理的逻辑且更为灵活,责任链中的每一个环节都可以方便的替换和删除,便于扩展,下面以一个登录认证授权为例,简单使用责任链模式
import lombok.Data;
/**
* 客户端类,携带 token 和 permission 发起请求
* @author dhj
* @date 2022/9/23
*/
@Data
public class ClientRequest {
private String token;
private String permission;
public ClientRequest(String token, String permission) {
this.token = token;
this.permission = permission;
}
}
抽象处理器(定义每一个处理环节的公共接口)
/**
* @author dhj
* @date 2022/9/23
*/
public abstract class Handler {
protected Handler nextHandler;
public Handler(Handler nextHandler) {
this.nextHandler = nextHandler;
}
public abstract boolean handler(ClientRequest req);
}
具体处理器(实现抽象处理器中定义的接口,可以有多个,组成【责任链】)
/**
* 简单的登录认证处理器
* @author dhj
* @date 2022/9/23
*/
public class AuthHandler extends Handler {
public AuthHandler(Handler nextHandler) {
super(nextHandler);
}
@Override
public boolean handler(ClientRequest req) {
if (req.getToken() != null) {
System.out.println("auth check pass!");
return nextHandler.handler(req);
}
System.out.println("auth check fail");
return false;
}
}
/**
* 简单的权限认证处理器
* @author dhj
* @date 2022/9/23
*/
public class PermissionHandler extends Handler {
public PermissionHandler(Handler nextHandler) {
super(nextHandler);
}
@Override
public boolean handler(ClientRequest req) {
if ("user.list".equals(req.getPermission())) {
System.out.println("permission chech pass");
return true;
}
System.out.println("permission check fail");
return false;
}
}
客户端
/**
* @author dhj
* @date 2022/9/23
*/
public class MainDemo {
public static void main(String[] args) {
// 组装责任链
AuthHandler authHandler = new AuthHandler(new PermissionHandler(null));
ClientRequest clientRequest = new ClientRequest("4278dhxfgytef34", "user.list");
// 处理客户端请求
boolean handler = authHandler.handler(clientRequest);
if (handler) {
System.out.println("request success");
} else {
System.out.println("request fail");
}
}
}
命令模式
在命令模式中,将不同的处理逻辑对应的请求封装成命令,再通过调用者发送命令来触发对应的处理逻辑,将请求与具体的处理逻辑分离开
这样,每个具体逻辑都由指定的命令来触发,而这些命令又由调用者统一管理,这代表其他模块也能通过调用者来调用指定的处理逻辑,而不是说将这些具体逻辑的调用耦合在某个单一的模块中
这就好比遥控器与空调,用户作为请求者,只需要按下遥控器上的按钮,就可以控制空调的开关,这里的按钮就相当于【命令】,用户按下按钮,相当于发送一个【命令】,至于按下按钮后,遥控器作为一个【调用者】通过此命令调用了哪个【具体逻辑】,用户并不需要关心;最后空调会接收到命令,它作为一个接收者,负责真正的执行具体逻辑
命令模式有如下几个角色
- 抽象命令(Command)定义了命令的接口和执行命令的抽象函数
execute() - 具体命令(ConcreteCommand)实现了【抽象命令】中的所有方法,并管理一个【接收者(Receiver)】对象,通过调用接收者(Receiver)的功能来完成命令的具体操作
- 接收者(Receiver)接收者负责接收并执行命令的具体操作,是业务逻辑的处理类
- 调用者(Invoker)即发送请求的类,它管理着命令对象,并通过调用命令对象来执行请求,请注意,它不能直接访问接收者
下面通过一个简单的股票交易,演示命令模式的使用
定义抽象命令
/**
* 股票交易的抽象命令
*
* @author dhj
* @date 2022/9/24
*/
public abstract class Command {
public abstract void execute();
}
定义接收者,用于处理具体业务逻辑
/**
* 接收者,负责处理具体的业务逻辑
*
* @author dhj
* @date 2022/9/24
*/
public class Receive {
/**
* 买入股票
*
* @param orderId 股票代码
* @param amount 买入数量
*/
public void buy(String orderId, long amount) {
System.out.printf("买入【%s】,数量【%d】\n", orderId, amount);
}
/**
* 卖出股票
*
* @param orderId 股票代码
* @param amount 卖出数量
*/
public void sell(String orderId, long amount) {
System.out.printf("卖出【%s】,数量【%d】\n", orderId, amount);
}
}
定义具体命令,某一个命令可以调用接收者中的一个或多个处理逻辑
/**
* 股票买入命令
*
* @author dhj
* @date 2022/9/24
*/
public class BuyCommand extends Command {
private final Receive receive;
private final String orderId;
private final long amount;
public BuyCommand(Receive receive, String orderId, long amount) {
this.receive = receive;
this.orderId = orderId;
this.amount = amount;
}
@Override
public void execute() {
receive.buy(this.orderId, this.amount);
}
}
/**
* 股票卖出命令
*
* @author dhj
* @date 2022/9/24
*/
public class SellCommand extends Command {
private final Receive receive;
private final String orderId;
private final long amount;
public SellCommand(Receive receive, String orderId, long amount) {
this.receive = receive;
this.orderId = orderId;
this.amount = amount;
}
@Override
public void execute() {
receive.sell(this.orderId, this.amount);
}
}
定义调用者,用于统一管理以及执行命令
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.function.Consumer;
/**
* @author dhj
* @date 2022/9/24
*/
public class Invoker {
private final List<Command> commandList = Collections.synchronizedList(new ArrayList<>(10));
/**
* 创建一个交易请求
*
* @param command 交易请求【分为买入、卖出】
*/
public void createTransaction(Command command) {
commandList.add(command);
}
/**
* 执行所有的交易请求
*/
public void hanlders() {
commandList.forEach(Command::execute);
}
}
客户端通过调用者发送命令
/**
* @author dhj
* @date 2022/9/24
*/
public class MainDemo {
public static void main(String[] args) {
Receive receive = new Receive();
BuyCommand buyCommand = new BuyCommand(receive, "1013131", 10);
SellCommand sellCommand = new SellCommand(receive, "29h2dtr33", 23);
Invoker invoker = new Invoker();
invoker.createTransaction(buyCommand);
invoker.createTransaction(sellCommand);
invoker.hanlders();
}
}
在客户端中,只需要选择自己所需要的命令,交给调用者,即可完成命令对应处理逻辑的调用
迭代器模式
迭代器主要用于屏蔽不同结构的集合中获取元素的底层逻辑,向客户端暴露统一的集合迭代接口,因为集合的类型有很多种,如【数组】【图】【链表】【队列】【栈】等,不同的集合之间遍历的形式会存在一定差异,而通过定义抽象的迭代器接口,就能够消除这种差异
代码示例如下
定义抽象迭代器
/**
* @author dhj
* @date 2022/9/25
*/
public interface Iterator<T> {
/**
* 是否还有下一个元素
*
* @return true 存在下一个元素,false 没有下一个元素
*/
boolean hasNext();
/**
* 获取下一个元素
*
* @return 返回元素
*/
T next();
}
自定义动态数组
/**
* @author dhj
* @date 2022/9/25
*/
public class MyArrayList<T> {
private Object[] elements;
private int size = 0;
private static final double LOAD_FACTOR = 0.75; // 负载因子,超过该值,执行扩容逻辑
public MyArrayList() {
this.elements = new Object[10];
}
public MyArrayList(int capacity) {
this.elements = new Object[capacity];
}
public int add(T data) {
if ((double) (this.size + 1) / (double) this.elements.length >= LOAD_FACTOR) {
Object[] expanElements = new Object[this.elements.length * 2];
System.arraycopy(this.elements, 0, expanElements, 0, this.size);
this.elements = expanElements;
}
this.elements[size++] = data;
return this.size;
}
public T get(int index) {
if (index > this.size - 1) {
throw new IndexOutOfBoundsException();
}
return (T) this.elements[index];
}
// 返回具体迭代器实例,用于客户端统一调用迭代器接口遍历集合
public Iterator<T> iterator(){
return new Itr();
}
// 使用内部类的形式,实现迭代器接口,定义当前集合自己的具体迭代逻辑
private class Itr implements Iterator<T> {
private int pointer = 0;
@Override
public boolean hasNext() {
return pointer <= size - 1 && elements[pointer] != null;
}
@Override
public T next() {
return (T) elements[pointer++];
}
}
}
测试代码
/**
* @author dhj
* @date 2022/9/25
*/
public class MainDemo {
public static void main(String[] args) {
MyArrayList<String> list = new MyArrayList<>(20);
for (int i = 0; i < 30; i++) {
list.add(String.valueOf(i));
}
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
String next = iterator.next();
System.out.println(next);
}
}
}
中介者模式
中介者模式用于将对象间复杂的关系分离,将这些关系都统一交给【中介者】处理,相当于将网状结构分离为星型结构,如下

中介者模式有如下四种角色
- 抽象中介者:抽象中介者定义了统一的接口,以及一个或者多个事件方法,用于各同事类之间的通信
- 具体中介者:实现了抽象中介者所声明的事件方法,协调各同事类之间的行为,持有所有同事类对象的引用
- 抽象同事:定义了抽象同事类,持有抽象中介者对象的引用
- 具体同事:继承抽象同事类,实现自己的业务,通过中介者跟其他同事类进行通信
以一个在线点餐系统为例,顾客通过统一的在线点餐平台【中介者】点餐,再由中介者通知对应的商家出餐,据此演示中介者模式的相关代码
抽象业务类,定义基本业务接口
/**
* @author dhj
* @date 2022/9/25
*/
public abstract class Shop {
/**
* 商家接单
*
* @param foodName 菜品名称
*/
abstract void orderReceiving(String foodName);
/**
* 获取商家名称
*
* @return 商家名称
*/
abstract String getName();
}
定义具体业务类
/**
* @author dhj
* @date 2022/9/25
*/
public class ShopA extends Shop {
private final String name;
public ShopA(String name) {
this.name = name;
}
@Override
void orderReceiving(String foodName) {
System.out.printf("【%s】接单,【%s】准备出餐\n", this.name, foodName);
}
@Override
String getName() {
return this.name;
}
}
/**
* @author dhj
* @date 2022/9/25
*/
public class ShopB extends Shop {
private final String name;
public ShopB(String name) {
this.name = name;
}
@Override
void orderReceiving(String foodName) {
System.out.printf("【%s】接单,【%s】准备出餐\n", this.name, foodName);
}
@Override
String getName() {
return this.name;
}
}
抽象中介者,定义协调、调用所有业务类的基本接口
/**
* 在线点餐抽象中介者
*
* @author dhj
* @date 2022/9/25
*/
public abstract class FoodOnline {
/**
* 商家入驻在线点餐平台
*
* @param shop 入驻的商家
*/
abstract void settled(Shop shop);
/**
* 用户点餐
*
* @param shopName 商家名称
* @param foodName 菜品名称
*/
abstract void placeOrder(String shopName, String foodName);
}
具体中介者
import java.util.HashMap;
import java.util.Map;
/**
* 在线点餐系统具体中介者
*
* @author dhj
* @date 2022/9/25
*/
public class MeiTuanFoodOnline extends FoodOnline {
private final Map<String, Shop> shopMap = new HashMap<>(16);
@Override
void settled(Shop shop) {
if (shopMap.containsKey(shop.getName())) {
System.out.println("不允许重复入驻");
return;
}
shopMap.put(shop.getName(), shop);
}
@Override
void placeOrder(String shopName, String foodName) {
if (!shopMap.containsKey(shopName)) {
System.out.println("商家未入驻");
return;
}
shopMap.get(shopName).orderReceiving(foodName);
}
}
客户端通过在线点餐系统【中介者】点餐
/**
* @author dhj
* @date 2022/9/25
*/
public class MainDemo {
public static void main(String[] args) {
Shop shopA = new ShopA("老麻抄手");
Shop shopB = new ShopB("沙县小吃");
FoodOnline foodOnline = new MeiTuanFoodOnline();
foodOnline.settled(shopA);
foodOnline.settled(shopB);
foodOnline.placeOrder("隆江猪脚饭", "双拼猪肘饭");
foodOnline.placeOrder("老麻抄手", "海味抄手");
foodOnline.placeOrder("沙县小吃", "烤鸭饭");
}
}
输出
商家未入驻 【老麻抄手】接单,【海味抄手】准备出餐 【沙县小吃】接单,【烤鸭饭】准备出餐
通过在线点餐这个中介者,将所有的商家都统一管理起来,客户端只需要调用【中介者】中统一的接口便可完成对应的业务逻辑
备忘录模式
【备忘录】模式是在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态。这样以后就可以将该对象恢复到原先的状态,例如现在大多数文件编辑器都具备的撤销功能,利用的就是备忘录模式
备忘录模式有如下几个角色
备忘录(Memento)负责存储【发起者】对象的内部状态,并可以防止除【发起者】自身以外的其他对象访问备忘录
发起者(Originator)负责创建一个备忘录用以记录当前时刻它自身的内部状态,并可以使用备忘录恢复内部状态
备忘录管理者(Caretaker)负责保存所有备忘录
下面通过备忘录模式,模拟一个简单的git版本管理工具,代码如下
发起者
import lombok.Builder;
import lombok.Data;
import lombok.experimental.Accessors;
import java.util.Date;
/**
* 代码提交者【发起者】
*
* @author dhj
* @date 2022/9/25
*/
@Data
@Builder
@Accessors(chain = true)
public class CodeOriginator {
/**
* 版本号
*/
private String version;
/**
* 提交的代码内容
*/
private String codeContent;
/**
* 版本提交时间
*/
private Date commitDate;
/**
* 创建并提交当前代码版本【备忘录】
*
* @param caretaker 【备忘录】管理者
*/
public void createAndCommit(CodeCaretaker caretaker) {
caretaker.commit(new CodeMemo(this.version, this.codeContent, this.commitDate));
}
/**
* 回退到指定版本
*
* @param version 指定版本
* @param caretaker 【备忘录管理者】
* @return 返回是否回退成功
*/
public boolean goBackVersion(String version, CodeCaretaker caretaker) {
CodeMemo memoByVersion = caretaker.getMemoByVersion(version);
if (memoByVersion == null) {
System.out.println("回退失败,此版本不存在");
return false;
}
this.codeContent = memoByVersion.getCodeContent();
this.version = memoByVersion.getVersion();
this.commitDate = memoByVersion.getCommitDate();
System.out.printf("回退成功,当前版本【%s】\n", this.version);
return true;
}
}
备忘录
import lombok.Getter;
import lombok.Setter;
import java.util.Date;
/**
* 代码版本备忘录(用于记录某次代码提交版本的全部信息)
*
* @author dhj
* @date 2022/9/25
*/
@Getter
@Setter
public class CodeMemo {
/**
* 版本号
*/
private String version;
/**
* 提交的代码内容
*/
private String codeContent;
/**
* 版本提交时间
*/
private Date commitDate;
public CodeMemo(String version, String codeContent, Date commitDate) {
this.version = version;
this.codeContent = codeContent;
this.commitDate = commitDate;
}
}
管理者
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* 代码版本管理者,用于统一管理代码版本
*
* @author dhj
* @date 2022/9/25
*/
public class CodeCaretaker {
private static final Map<String, CodeMemo> codeMemoMap = new ConcurrentHashMap<>(16);
/**
* 提交指定的代码版本
*
* @param codeMemo 代码版本提交的备忘录实例
*/
public void commit(CodeMemo codeMemo) {
if (codeMemoMap.containsKey(codeMemo.getVersion())) {
System.out.println("版本冲突");
return;
}
codeMemoMap.put(codeMemo.getVersion(), codeMemo);
}
/**
* 获取指定版本的代码
*
* @param version 版本号
* @return 返回指定版本的代码备忘录实例
*/
public CodeMemo getMemoByVersion(String version) {
if (!codeMemoMap.containsKey(version)) {
return null;
}
return codeMemoMap.get(version);
}
}
客户端提交&回退代码到指定版本
import java.util.Date;
/**
* @author dhj
* @date 2022/9/25
*/
public class MainDemo {
public static void main(String[] args) {
// 先创建管理者,用于统一管理代码【备忘录】
CodeCaretaker codeCaretaker = new CodeCaretaker();
// 创建一个代码版本【发起者】
CodeOriginator codeOriginator =
CodeOriginator.builder()
.version("1.0")
.codeContent("<h1>Hello World</h1>")
.commitDate(new Date()).build();
// 提交本次代码版本【发起者】保存自身内部状态
codeOriginator.createAndCommit(codeCaretaker);
// 改动代码
codeOriginator.setCodeContent("<h1>Hello World</h1><br/><span>hello world</span>")
.setVersion("1.1")
.setCommitDate(new Date());
// 再次提交版本
codeOriginator.createAndCommit(codeCaretaker);
System.out.println(codeOriginator); // 当前最新提交为 1.1 版本
// 版本回退【发起者】恢复内部状态
if (codeOriginator.goBackVersion("1.0", codeCaretaker)) {
System.out.println(codeOriginator); // 回退到 1.0 版本
}
}
}
观察者模式
观察者模式,又称发布订阅模式,在对象之间定义了一对多的依赖,这样一来,当一个对象改变状态,依赖它的对象会收到通知并执行指定的操作
例如张三订阅了一个音乐频道,那么在音乐频道更新时,张三会收到短信或者邮箱的通知,张三可以选择收听更新的内容或者什么也不做,而没有订阅这个音乐频道的人则不会被通知到,音乐频道和订阅者之间存在一对多的关系
观察者模式有如下角色
抽象发布者(Subject):也就是一个抽象主题,它把所有对观察者对象的引用保存在一个集合中,每个主题都可以有任意数量的观察者。抽象主题提供一个接口,可以增加和删除观察者角色。一般用一个抽象类和接口来实现
具体发布者(ConcreteSubject):也就是【抽象发布者】的实现,当主题的内部状态改变时,向所有登记过的【观察者】发出通知
抽象观察者(Observer):为所有的【具体观察者】定义一个接口,此接口用于申明【观察者】在得到【发布者】的主题通知时所作的行为
具体观察者(ConcreteObserver):实现【抽象观察者】中定义的接口,执行接收到主题通知时的具体逻辑
代码演示如下
抽象观察者
/**
* 抽象观察者
*
* @author dhj
* @date 2022/9/26
*/
public abstract class Observer {
/**
* 观察者接收订阅的通知
*/
public abstract void receive(String notice);
}
抽象发布者
/**
* 抽象发布者
*
* @author dhj
* @date 2022/9/26
*/
public abstract class Publisher {
/**
* 发布主题内容,通知主题对应的观察者【订阅者】
*
* @param notice 发布的通知内容
*/
public abstract void publish(String notice);
/**
* 添加观察者订阅主题
*
* @param observer 观察者实例
*/
public abstract void addObserver(Observer observer);
/**
* 取消观察者的主题订阅
*
* @param observer 观察者对象
*/
public abstract void cancelObserver(Observer observer);
}
具体发布者
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
/**
* @author dhj
* @date 2022/9/26
*/
public class ConcretePublisher extends Publisher {
List<Observer> observerList = new CopyOnWriteArrayList<>();
@Override
public void publish(String notice) {
for (Observer observer : observerList) {
observer.receive(notice);
}
}
@Override
public void addObserver(Observer observer) {
observerList.add(observer);
}
@Override
public void cancelObserver(Observer observer) {
observerList.remove(observer);
}
}
具体观察者
/**
* @author dhj
* @date 2022/9/26
*/
public class ConcreteObserver extends Observer {
private final String name;
public ConcreteObserver(String name) {
this.name = name;
}
@Override
public void receive(String notice) {
System.out.printf("【%s】接收到订阅的通知:【%s】\n", this.name, notice);
}
}
状态模式
状态模式与 状态机 的概念紧密相连,可以参考 电子签中台认证环节的状态机实践 - 掘金 (juejin.cn)
而状态模式的主要思想在于事物任意时刻仅可处于几种有限的状态中,在任何一个特定状态中,事物的行为都不相同,且可瞬间从一个状态切换到另一个状态;不过,根据当前状态,程序可能会切换到指定一种状态,也可能会保持当前状态不变,这些数量有限且预先定义的状态切换规则被称为【转移】
以一个简单的糖果售卖机为例,有如下【状态转移表】
| 行为\状态 | 未投币 | 已投币 | 已出货 | 售空 |
|---|---|---|---|---|
| 投币 | yes | no | no | no |
| 退币 | no | yes | no | no |
| 按下按钮 | no | yes | no | no |
| 取货 | no | no | yes | no |
上述表格就规定了一个糖果售卖业务模型在不同状态下的各种行为约定,如果单纯使用if-else来处理,仅仅是这几种简单行为的状态转移,也会造成大量的分支代码,不利于维护和扩展,使用状态模式就可轻易解决
状态模式能够在发生状态切换时,改变事物的行为,以适应当前的状态或者直接进入下一状态,例如张三购买了某个商品,如果他不支付,那么当前商品会一直或在指定时间内处于【待支付状态】,一旦张三支付了该商品,那么状态就会变更为【已支付状态】,同时进入【待发货状态】,此时这个商品对象关联的行为应该变为【收货】而不是【支付】,这种行为的变化正是由【状态变更】所导致的
状态模式具备以下几个角色
- 上下文:上下文中保存了对具体状态的引用,并会将与当前状态相关的工作委派给它
- 抽象状态:定义任意状态下的公共接口,业务逻辑的通用接口通常在此声明,也就是对应【状态转移表】中的【行为】
- 具体状态:具体实现【状态】中的接口,同时【具体状态】中也可以反向引用上下文对象,用于转移到某一状态时,由当前状态自动将上下文转移到下一个状态
下面使用状态模式的相关代码演示上面的【糖果售卖机】案例
定义抽象状态,覆盖糖果机在任何状态下的可执行操作
/**
* 定义糖果售卖机的抽象状态
* 并在其中定义任意状态下的公共行为
*
* @author dhj
* @date 2022/9/28
*/
public abstract class State {
/**
* 投币
*/
public abstract void coin();
/**
* 退币
*/
public abstract void refund();
/**
* 按下按钮
*/
public abstract void down();
/**
* 取货
*/
public abstract void pickUp();
}
定义具体状态
/**
* 已投币状态
*
* @author dhj
* @date 2022/9/28
*/
public class CoinedState extends State {
private final CandyContext context;
public CoinedState(CandyContext context) {
this.context = context;
}
@Override
public void coin() {
System.out.println("已投币,不要重复投币!");
}
@Override
public void refund() {
this.context.setState(new NotCoinedState(this.context));
System.out.println("已退币,取消出货");
}
@Override
public void down() {
this.context.setState(new ShippedState(this.context));
System.out.println("正在出货,请稍等");
}
@Override
public void pickUp() {
System.out.println("请按下按钮出货");
}
}
/**
* 糖果机未投币状态【有限状态中的默认状态,也叫起始状态】
*
* @author dhj
* @date 2022/9/28
*/
public class NotCoinedState extends State {
private final CandyContext context;
public NotCoinedState(CandyContext context) {
this.context = context;
}
@Override
public void coin() {
this.context.setState(new CoinedState(this.context));
System.out.println("投币成功,按下按钮出货");
}
@Override
public void refund() {
System.out.println("未投币,无法退币!");
}
@Override
public void down() {
System.out.println("请先投币");
}
@Override
public void pickUp() {
System.out.println("未投币,无法取货!");
}
}
/**
* 糖果机已出货状态
*
* @author dhj
* @date 2022/9/28
*/
public class ShippedState extends State {
private final CandyContext context;
public ShippedState(CandyContext context) {
this.context = context;
}
@Override
public void coin() {
System.out.println("请先取货后,再次投币");
}
@Override
public void refund() {
System.out.println("已出货,无法退币");
}
@Override
public void down() {
System.out.println("已出货,请勿重复出货");
}
@Override
public void pickUp() {
context.setStock(context.getStock() - 1); // 扣减库存
context.setState(new NotCoinedState(this.context)); // 当前为【终止状态】,结束后切换上下文为初始状态
System.out.println("取货成功,可再次投币");
}
}
/**
* 糖果机售空状态【有限状态中的终止状态】
*
* @author dhj
* @date 2022/9/28
*/
public class ShortSaleState extends State {
@Override
public void coin() {
System.out.println("已售空,请等待补货");
}
@Override
public void refund() {
System.out.println("未投币,无法退币!");
}
@Override
public void down() {
System.out.println("请先投币");
}
@Override
public void pickUp() {
System.out.println("未投币,无法取货");
}
}
定义上下文对象,其引用了状态的实例,并借用此实例完成对应状态的操作
/**
* 糖果机可以包含所有状态,这里作为上下文对象
*
* @author dhj
* @date 2022/9/28
*/
public class CandyContext {
private State state;
private Integer stock;
public CandyContext(Integer stock) {
this.stock = stock;
}
public void setState(State state) {
this.state = state;
}
public void setStock(Integer stock) {
this.stock = stock;
}
public Integer getStock() {
return stock;
}
/**
* 给糖果机投币
*/
public void sell() {
System.out.println("-----投币操作------");
if (this.stock > 0) {
this.state.coin();
} else {
System.out.println("无货,请联系商家补货");
this.state = new ShortSaleState();
}
}
/**
* 糖果机退币
*/
public void refund() {
System.out.println("-----退币操作------");
this.state.refund();
}
/**
* 按下取货按钮
*/
public void down() {
System.out.println("-------出货操作-------");
this.state.down();
}
/**
* 从糖果机取货
*/
public void pickUp() {
System.out.println("------取货操作-------");
this.state.pickUp();
}
}
测试类,模拟糖果机操作
/**
* @author dhj
* @date 2022/9/28
*/
public class MainDemo {
public static void main(String[] args) {
// 初始化指定库存:10 & 初始状态:【未支付】
CandyContext context = new CandyContext(10);
context.setState(new NotCoinedState(context));
// 直接取货【失败】
context.pickUp();
// 直接退币【失败】
context.refund();
// 投币后再退币【成功】
context.sell();
context.refund();
// 投币,按下按钮,取货【成功】
context.sell();
context.down();
context.pickUp();
// 投币,退币,取货【失败】
context.sell();
context.refund();
context.pickUp();
}
}
策略模式
如果现在对同一业务的不同情况有不同的处理方式,通常情况下,可能会直接通过if条件判断来根据对应情况编写代码逻辑,如下
if(xxxx){ // 情况1
// 业务代码1
}else if(xxxx){ // 情况2
// 业务代码2
}else if(xxxx){ // 情况3
// 业务代码3
}else{ // 其他情况
// 业务代码
}
并且可能分支里面涉及到的代码逻辑又会存在其他分支情况,这使得不同情况的代码之间高度耦合,后续如果业务规则发生变化,就需要直接更改已经写好的代码逻辑,违反了开闭原则,且因为修改了旧代码,还会导致回归测试
针对这种情况,使用【策略模式】是比较合适的,策略模式中定义了多种不同的算法以适应被不同的情况所调用,同时通过一个上下文对象维护这些【策略】,而后在涉及到具体逻辑的地方,直接使用上下文对象获取对应的策略来处理,后续业务发生变化,也只需要更改对应的策略或者扩展新的策略而无需修改业务层的代码结构,如下
// 设置策略
context.setStrategy(strategyA);
// 使用指定的策略处理相应的业务逻辑
context.hanlder();
// ----- 业务情况发生变化时 ------
// 重新设置策略
context.setStrategy(strategyB);
// 处理变化后的业务逻辑
context.hanlder();
策略模式有以下角色
- 抽象策略(Strategy):策略类,通常是一个接口或者抽象类,声明了不同策略通用的接口
- 具体策略(ConcreteStrategy)角色:实现了策略类中的策略方法,封装相关的算法和行为
- 上下文(Context)角色:持有一个策略类的引用,最终给客户端调用,且根据情况切换策略的引用对象
不过,通常情况下,策略模式都会和简单工厂模式相结合,目的就是为了更方便的获取【具体策略】,同时将策略的【创建、设置】与具体的使用分离
下面以某一商品根据不同的优惠规则计算价格的业务,来演示策略模式
优惠参数配置类,用于配置不同优惠模式下计算价格所需的参数【业务相关】
import lombok.Getter;
import lombok.experimental.Accessors;
import java.math.BigDecimal;
/**
* 折扣配置类,用于配置指定类型的折扣相关的数值
* 例如【满减】类型的折扣,需要配置【满多少】、【减多少】两项配置
*
* @author dhj
* @date 2022/9/28
*/
@Getter
@Accessors(chain = true)
public class DiscountConfig {
//======================满减配置=============================
/**
* 总价满多少【满减配置1】
*/
private BigDecimal thresholdAmount;
/**
* 总价减去多少【满减配置2】
*/
public BigDecimal reductionAmount;
//======================折扣配置==============================
/**
* 折扣比【折扣配置1】
*/
public BigDecimal discount;
/**
* 最大折扣金额【折扣配置2】
*/
public BigDecimal maxDiscountAmount;
//========================无门槛配置===========================
/**
* 总价直接抵扣多少【无门槛配置】
*/
public BigDecimal deductionAmount;
/**
* 满减配置
*
* @param thresholdAmount 总价满多少
* @param reductionAmount 总价减多少
* @return 返回当前实例
*/
public DiscountConfig fullReductionConfig(String thresholdAmount, String reductionAmount) {
this.thresholdAmount = new BigDecimal(thresholdAmount);
this.reductionAmount = new BigDecimal(reductionAmount);
return this;
}
/**
* 折扣配置
*
* @param discount 折扣比【discount %】
* @param maxDiscountAmount 最大折扣金额
* @return 返回当前实例
*/
public DiscountConfig discountConfig(String discount, String maxDiscountAmount) {
this.discount = new BigDecimal(discount);
this.maxDiscountAmount = new BigDecimal(maxDiscountAmount);
return this;
}
/**
* 无门槛配置
*
* @param deductionAmount 直接抵扣的金额
* @return 返回当前实例
*/
public DiscountConfig noThresholdConfig(String deductionAmount) {
this.deductionAmount = new BigDecimal(deductionAmount);
return this;
}
}
抽象策略
import java.math.BigDecimal;
/**
* 价格计算【抽象策略】
*
* @author dhj
* @date 2022/9/28
*/
public abstract class CalculationStrategy {
/**
* 计算价格
*
* @param totalAmount 总价
* @param config 优惠参数
* @return 返回最终价格
*/
public abstract BigDecimal compute(BigDecimal totalAmount, DiscountConfig config);
}
具体策略【折扣价计算策略】
import java.math.BigDecimal;
import java.math.RoundingMode;
/**
* 折扣价格计算策略
*
* @author dhj
* @date 2022/9/28
*/
public class DiscountStrategy extends CalculationStrategy {
@Override
public BigDecimal compute(BigDecimal totalAmount, DiscountConfig config) {
BigDecimal discountAmount = totalAmount
.multiply(config.getDiscount().divide(new BigDecimal("100"), 2, RoundingMode.HALF_DOWN));
// 如果折扣超出限制,以最大折扣金额计算
if (totalAmount.subtract(discountAmount).compareTo(config.getMaxDiscountAmount()) > 0) {
return totalAmount.subtract(config.getMaxDiscountAmount());
}
// 返回折扣价
return discountAmount;
}
}
具体策略【满减计算策略】
import java.math.BigDecimal;
/**
* 满减价格计算策略
*
* @author dhj
* @date 2022/9/28
*/
public class FullReductionStrategy extends CalculationStrategy {
@Override
public BigDecimal compute(BigDecimal totalAmount, DiscountConfig config) {
// 总价小于满减金额,不计算优惠
if (totalAmount.compareTo(config.getThresholdAmount()) < 0) {
return totalAmount;
}
// 减去优惠的金额
return totalAmount.subtract(config.getReductionAmount());
}
}
具体策略【无门槛计算策略】
import java.math.BigDecimal;
/**
* 无门槛折扣价格计算策略
*
* @author dhj
* @date 2022/9/28
*/
public class NoThresholdStrategy extends CalculationStrategy {
@Override
public BigDecimal compute(BigDecimal totalAmount, DiscountConfig config) {
return totalAmount.subtract(config.getDeductionAmount());
}
}
优惠类型
/**
* 折扣类型
*
* @author dhj
* @date 2022/9/28
*/
public enum DiscountType {
/**
* 满减
*/
FULL_FEDUCTION,
/**
* 打折
*/
DISCOUNT,
/**
* 无门槛
*/
NO_THRESHOLD
}
策略工厂,方便获取指定的价格计算策略【可以理解为简化操作后的上下文对象】
import java.util.HashMap;
import java.util.Map;
/**
* 价格计算策略工厂
*
* @author dhj
* @date 2022/9/28
*/
public class PriceComputeStrategyFactory {
private final static Map<DiscountType, CalculationStrategy> strategyMap;
static {
strategyMap = new HashMap<>(3);
strategyMap.put(DiscountType.DISCOUNT, new DiscountStrategy());
strategyMap.put(DiscountType.FULL_FEDUCTION, new FullReductionStrategy());
strategyMap.put(DiscountType.NO_THRESHOLD, new NoThresholdStrategy());
}
/**
* 获取价格计算策略
*
* @param type 策略枚举类型
* @return 返回价格计算策略的实例
*/
public static CalculationStrategy getComputeStrategy(DiscountType type) {
return strategyMap.get(type);
}
}
测试类
import java.math.BigDecimal;
/**
* @author dhj
* @date 2022/9/28
*/
public class MainDemo {
public static void main(String[] args) {
// 总价
BigDecimal totalAmount = new BigDecimal("500.00");
// 多种优惠方案参数配置
DiscountConfig config = new DiscountConfig()
.discountConfig("8", "100") // 折扣参数
.noThresholdConfig("15") // 无门槛参数
.fullReductionConfig("100", "10"); // 满减参数
// 通过简单工厂模式获取对应的价格计算策略,再通过策略计算最终价格
String priceStr = PriceComputeStrategyFactory
.getComputeStrategy(DiscountType.DISCOUNT).compute(totalAmount, config).toPlainString();
System.out.println(priceStr);
}
}
策略模式和状态模式比较相像,它们都在上下文中维护了针对不同情况而创建的实例且都能够进行切换,但不同的是,状态模式的各个状态实例与上下文之间,以及各个状态实例之间都存在耦合关系,其中一个状态的变动会触发切换到另一个状态
策略模式则不一样,各个策略之间相互独立,只是视情况切换策略,策略中也无需保存对上下文的引用来判断当前上下文的情况
模板方法模式
假设有一电商平台,同时支持【微信支付】【支付宝支付】等多个第三方支付,那么在支付发起的过程中,可能还会进行一些其他步骤,例如检查订单是否超时,订单是否已支付等检查步骤,还有锁库存,构建支付参数,记录结果等一系列步骤,最后会根据支付方式发起支付请求,代码模拟如下
// 锁定库存
storeService.lock();
// 构建支付参数
buildParams();
// 一系列检查
check();
// 发起支付请求
if(WeXinPay){
// 微信支付请求
weixinPay();
}else if(AliPay){
// 支付宝支付请求
aliPay();
}
// 保存支付结果
save();
而在上面这一系列流程中,唯有发起支付请求时,具体的逻辑会有所不同,因为存在多种支付方式或者支付通道,这就可以使用模板方法模式来优化
在模板方法模式中,将一系列算法或步骤实现提供好,并通过一个统一的模板方法进行统一调用,而这一系列的算法或步骤可以是已经实现好的,也可以是抽象的,通常会将通用的步骤默认实现好,将需要自定义的步骤申明为抽象的,以便留给子类实现,如下形式
// 模板方法,统一执行定义好的步骤
void templateMethod(){
step1();
step2();
step3();
}
void step1(){
// 默认实现
}
void step2(){
// 默认实现
}
// 需要自定义的步骤,留给子类实现
abstract void step3();
模板方法模式具备如下角色
- 抽象类(AbstractClass) 会声明作为算法步骤的方法, 以及依次调用它们的实际模板方法。 算法步骤可以被声明为
抽象类型, 也可以提供一些默认实现 - 具体类(ConcreteClass) 可以重写所有步骤, 但不能重写模板方法自身
模板方法跟建造者模式很相似,也是定义了一系列步骤,但建造者模式中定义的全部是抽象步骤,具体步骤的实现和调用顺序交给子类和Director来实现;模板方法直接在抽象父类中就定义好了步骤的具体实现和调用顺序且子类无法更改调用顺序,这也是模板方法的体现:通过一个给定的模板,在此基础上进行修改但不能完全脱离模板
下面通过模拟不同支付方式的支付流程,来演示模板方法的基本使用
抽象类
/**
* 抽象的支付类
*
* @author dhj
* @date 2022/9/29
*/
public abstract class AbstractPayService {
/**
* 支付的模板方法,统一调用支付过程中的一系列步骤
*/
public final void pay() {
lockStore();
String param = buildParam();
if (check(param)) {
sendPay(param);
} else {
System.out.println("支付参数异常,请检查订单信息");
}
}
/**
* 构建支付参数
*
* @return 返回构建好的支付参数
*/
public String buildParam() {
System.out.println("构建支付参数");
return "params";
}
/**
* 检查支付参数
*
* @param params 支付参数
* @return true 检查通过
*/
public boolean check(String params) {
System.out.println("检查支付参数");
return !params.isEmpty();
}
/**
* 锁定库存
*/
public void lockStore() {
System.out.println("库存锁定");
}
/**
* 发送支付请求
*
* @param param 支付参数
*/
public abstract void sendPay(String param);
}
注意模板方法
pay()被final修饰,子类无法更改,其余不想要子类更改的步骤也可如此
实现类
/**
* 支付宝支付实现类
*
* @author dhj
* @date 2022/9/29
*/
public class AliPayService extends AbstractPayService {
@Override
public void sendPay(String param) {
System.out.println("支付宝支付请求,参数:" + param);
}
}
/**
* 微信支付实现类
*
* @author dhj
* @date 2022/9/29
*/
public class WeiXinPayService extends AbstractPayService {
@Override
public void sendPay(String param) {
System.out.println("微信支付请求,参数:" + param);
}
}
测试
/**
* @author dhj
* @date 2022/9/29
*/
public class MainDemo {
public static void main(String[] args) {
AbstractPayService payService = new WeiXinPayService();
System.out.println("---------------微信支付--------------------");
payService.pay();
payService = new AliPayService();
System.out.println("---------------支付宝支付-------------------");
payService.pay();
}
}
打印结果(符合预期)
---------------微信支付-------------------- 库存锁定 构建支付参数 检查支付参数 微信支付请求,参数:params ---------------支付宝支付------------------- 库存锁定 构建支付参数 检查支付参数 支付宝支付请求,参数:params
模板方法模式虽然能够对不同逻辑的调用实现解耦,但如果情况变化过多,会增加大量的子类,导致系统臃肿,适用于某些步骤虽然有变化,但变化的情况却比较固定的时候使用
访问者模式
现在具有某种类型的数据结构,我们需要访问此结构中的元素或内容,最能想到的简单办法,就是为结构中的元素定义某种方法,在此方法中编写访问当前元素内容的逻辑,然后获取所有的元素,依次调用该方法
但是这种方法破坏了此元素类的单一职责(定义的访问方法与元素类自身的职责没有太大关系),同时也不符合开闭原则(修改了元素类原本的结构)
可现在就是需要访问每个元素,并且元素的实际类型可能还不相同;或许可以换一种思路,将元素的【访问逻辑】分离出来,然后在元素中提供一个可接收【访问逻辑】传入的方法,然后将自身的一些信息或者直接是整个自身的引用传给【访问逻辑】,这能最大限度的减少【访问逻辑】与元素自身职责的耦合,同时能够控制向【访问逻辑】暴露的信息,这个【访问逻辑】指的就是【访问者】
【访问者】模式是一种 能将算法与其所作用的对象隔离开来 的行为型设计模式,访问者模式有如下角色
- 抽象访问者,定义对访问的目标元素的通用访问行为,其参数可以设置为要访问的元素
- 具体访问者,实现【抽象访问者】中定义的具体逻辑
- 抽象节点,定义了接收【访问者】访问的方法
- 具体节点,具体实现【抽象节点】中定义的访问方法,这说明凡实现了【抽象节点】的【具体节点】都能被访问者访问
- 结构对象,包含了多个节点,同时提供给访问者访问多个节点的方法
下面以访问不同文件夹中的文件,来简单演示访问者模式的基本代码结构
抽象节点,也就是文件
/**
* 抽象节点【文件】
*
* @author dhj
* @date 2022/10/1
*/
public abstract class SysFile {
protected String name;
public SysFile(String name) {
this.name = name;
}
public abstract void accept(Folder folder);
}
抽象访问者,通过文件夹来访问文件,文件夹就相当于文件的【访问者】
/**
* 文件夹【抽象访问者】
*
* @author dhj
* @date 2022/10/1
*/
public abstract class Folder {
public abstract void visit(SysFile file);
}
具体节点
/**
* 具体节点,指的是不同的文件【学习文件】
*
* @author dhj
* @date 2022/10/1
*/
public class LearnFile extends SysFile {
public LearnFile(String name) {
super(name);
}
@Override
public void accept(Folder folder) {
folder.visit(this);
}
}
/**
* 具体节点,指的是不同的文件【工作文件】
*
* @author dhj
* @date 2022/10/1
*/
public class WorkFile extends SysFile {
public WorkFile(String name) {
super(name);
}
@Override
public void accept(Folder folder) {
folder.visit(this);
}
}
具体访问者,针对不同的节点类型,定义不同的访问者
/**
* 针对【学习文件】的访问者
*
* @author dhj
* @date 2022/10/1
*/
public class LearnFolder extends Folder {
@Override
public void visit(SysFile file) {
if (file instanceof LearnFile) {
System.out.println("学习文件:" + file.name);
}
}
}
/**
* 针对【工作文件】的访问者
*
* @author dhj
* @date 2022/10/1
*/
public class WorkFolder extends Folder {
@Override
public void visit(SysFile file) {
if (file instanceof WorkFile) {
System.out.println("工作文件:" + file.name);
}
}
}
结构对象
/**
* 文件管理器【结构对象】
*
* @author dhj
* @date 2022/10/1
*/
public class FileManage {
private final List<SysFile> fileList = new ArrayList<>(10);
// 添加元素
public FileManage add(SysFile file) {
fileList.add(file);
return this;
}
// 提供给访问者访问元素的方法
public void openFolder(Folder folder) {
fileList.forEach(folder::visit);
}
}

浙公网安备 33010602011771号