厚积薄发,静水流深,今天介绍设计模式
设计模式分类
-
创建型模式
用于描述如何创建对象,它的主要特点是“将对象的创建与使用分离”。有: 单例、原型、工厂方法、抽象工厂、建造者等 5 种。
-
结构型模式
用于描述如何将类或对象按某种布局组成更大的结构,G有: 代理、适配器、桥接、装饰、外观、享元、组合等 7 种结构型模式。
-
行为型模式
用于描述类或对象之间怎样相互协作共同完成单个对象无法单独完成的任务,以及怎样分配职责。有: 模板方法、策略、命令、职责链、状态、观察者、中介者、迭代器、访问者、备忘录、解释器等 11 种行为型模式。
创建者模式
创建型模式分为:
单例模式
工厂方法模式
抽象工程模式
原型模式
建造者模式
单例模式
一个类,该类自己负责创建自己的对象,且确保整个系统中自身实例只有一个。这个类提供了访问其该唯一实例的方法
特点:
1, 自身实例作为静态属性, 类加载时就初始化
2, 构造方法私有, 禁止外部创建实例
3, 本类自己负责创建本类对象
4, 对外提供公共的、静态的获取实例的方式, 获取本类对象
饿汉式-线程安全
饿汉式单例模式: 类加载时, 该类对象就会被创建
public class Singleton {
// 饿汉式单例模式, 自身实例作为静态属性, 类加载时就初始化
// 1. 构造方法私有, 禁止外部创建实例
private Singleton() {
}
// 2, 本类自己负责创建本类对象
private static Singleton INSTANCE = new Singleton();
// 3. 对外提供公共的静态方式, 获取本类对象
public static Singleton getInstance() {
return INSTANCE;
}
}
测试:
public class Client {
public static void main(String[] args) {
for (int i = 0; i < 3; i++) {
System.out.println(Singleton.getInstance());
}
}
}
无限次获取到的对象是同一个
懒汉式-线程不安全
懒汉式单例模式: 外界首次需要使用该类对象时才创建对象
public class Singleton {
// 懒汉式单例模式, 自身实例作为静态属性, 外界首次需要使用该类对象时才创建对象
// 1. 构造方法私有, 禁止外部创建实例
private Singleton() {
}
// 2, 本类中创建本类对象
private static Singleton INSTANCE;
// 3. 对外提供公共的静态方式, 获取本类对象
public static Singleton getInstance() {
// 核心实现: 判断是否已经创建对象, 如果没有则创建, 如果有则直接返回
if (INSTANCE == null) {
INSTANCE = new Singleton();
}
return INSTANCE;
}
}
测试:
public class Client {
public static void main(String[] args) {
for (int i = 0; i < 3; i++) {
System.out.println(Singleton.getInstance());
}
}
}
注意: 高并发下, 通过getInstance()方法获取实例, 可能会出现创建出多个实例对象, 这样就违反了单例模式的基本原则,即实例的唯一性
懒汉式-线程安全-Synchronized
通过Synchronized关键字保证并发下依旧只创建一个实例
说明:该方式也实现了懒加载效果,同时又解决了线程安全问题。但是在getInstance()方法上添加了synchronized关键字,导致该方法的执行效率低。其实只有在初始化instance的时候才会出现线程安全问题,一旦初始化完成就不存在了。
懒汉式-线程安全-双重检查锁
public class Singleton {
private static Singleton INSTANCE;
private Singleton() {
}
public static Singleton getInstance() {
// 第一次检查, 如果INSTANCE不为null, 不进入同步代码块, 直接返回实例
if (INSTANCE == null) {
synchronized (Singleton.class) {
// 抢到锁后, 第二次检查, 如果实例为null, 创建实例
if (INSTANCE == null) {
INSTANCE = new Singleton();
}
}
}
return INSTANCE;
}
}
双重检查锁模式是一种非常好的单例实现模式,解决了单例、性能、线程安全问题,上面的双重检测锁模式看上去完美无缺,其实还是存在问题,在多线程的情况下,可能会出现空指针问题(JVM在实例化对象的时候会进行优化和指令重排序操作)
指令重排序是指为了优化性能,Java编译器和处理器可以对指令的执行顺序进行调整,只要不改变单线程程序的语义。
对于new Singleton()的操作,实际上可以分为以下三个步骤:
1, 分配内存 :为Singleton实例分配内存。
2, 初始化对象 :调用Singleton构造函数,初始化对象。
3, 设置引用 :将INSTANCE指向分配的内存空间。
然而,由于指令重排序的存在,上述三个步骤的实际执行顺序可能会发生改变,比如可能的顺序是:
1, 分配内存
2, 设置引用
3, 初始化对象
当这种重排序发生时,在多线程环境下可能会导致空指针异常, 就是说后续线程第一次检查时INSTANCE已经不为null了(因为指向了内存空间), 返回INSTANCE, 但是此时的INSTANCE可能是一个未完全初始化的对象, 如果试图使用这个对象, 就可能抛出空指针异常
要解决双重检查锁模式带来空指针异常的问题,只需要使用 volatile 关键字, volatile 关键字可以保证可见性和有序性:
1, 禁止指令重排序 :volatile可以确保对于被修饰的变量的读写操作不会与其他内存操作一起被重排序,确保INSTANCE的赋值操作(即new Singleton())在对象初始化完成之后才可见。
2, 保证可见性 :volatile保证了所有线程在读取INSTANCE时都会看到最新的内存值。
最完美的双重检查锁示范:
public class Singleton {
// volatile关键字, 保证可见性和有序性
private static volatile Singleton INSTANCE;
private Singleton() {
}
public static Singleton getInstance() {
// 第一次检查, 如果INSTANCE不为null, 不进入同步代码块, 直接返回实例
if (INSTANCE == null) {
synchronized (Singleton.class) {
// 抢到锁后, 第二次检查, 如果实例为null, 创建实例
if (INSTANCE == null) {
INSTANCE = new Singleton();
}
}
}
return INSTANCE;
}
}
懒汉式-线程安全-静态内部类
public class Singleton {
private Singleton() {
}
// 静态内部类,只有在调用静态内部类的属性和方法时,才会加载静态内部类
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}
静态内部类单例模式中, 实例由内部类创建,由于 JVM 在加载外部类的过程中, 是不会加载静态内部类的, 只有内部类的属性/方法被调用时才会被加载, 并初始化其静态属性。且因为静态属性由于被 static 修饰,保证只被实例化一次,并且严格保证实例化顺序。
静态内部类单例模式是一种优秀的单例模式,是开源项目中比较常用的一种单例模式。在没有加任何锁的情况下,保证了多线程下的安全,并且没有任何性能影响和空间的浪费。
懒汉式-线程安全-枚举类
public enum Singleton {
INSTANCE;
// 可以添加需要的其他方法
public void otherMethod() {
System.out.println("其他一些方法...");
}
}
测试:
public class Client {
public static void main(String[] args) {
Singleton instance = Singleton.INSTANCE;
Singleton anotherInstance = Singleton.INSTANCE;
System.out.println(instance == anotherInstance);
// 执行其他一些方法
instance.otherMethod();
}
}
1, 枚举类实现单例模式是极力推荐的单例实现模式,因为枚举类型是线程安全的,并且只会装载一次,设计者充分的利用了枚举的这个特性来实现单例模式,枚举的写法非常简单,而且枚举类型是所用单例实现中唯一一种不会被破坏的单例实现模式。
2, 使用枚举(enum)来实现单例模式是最简单且最安全的方式之一。枚举类型不仅天生具有单例特性,而且还能防止反序列化和反射攻击,是一种非常推荐的单例模式实现方式。
3, 如果不考虑空间的浪费, 枚举类是最好的实现
4, 枚举单例的天然安全特性使其特别适用于那些可能通过反射或序列化攻击的场景
防止反射攻击 :使用反射创建枚举实例会抛出IllegalArgumentException,因此它也可以防止通过反射来破坏单例模式。
防止反序列化破坏单例 :默认情况下,从序列化中恢复的枚举实例是同一个实例。因此,枚举类型的单例是防止反序列化破坏单例模式的一种优雅的解决方案。
序列化破坏单例模式
public class Singleton implements Serializable {
// 私有构造方法
private Singleton() {}
// 定义一个静态内部类
private static class SingletonHolder {
// 在内部类中声明并初始化外部类的对象
private static final Singleton INSTANCE = new Singleton();
}
// 提供公共的访问方式
public static Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}
public class Test {
public static void main(String[] args) throws Exception {
// 向文件中写数据(对象)
writeObject2File();
// 从文件读取数据(对象), 比较两次对象的地址是否一致
readObjectFromFile();
readObjectFromFile();
}
// 向文件中写数据(对象)
public static void writeObject2File() throws Exception {
// 1,获取Singleton对象
Singleton instance = Singleton.getInstance();
// 2,创建对象输出流对象
ObjectOutputStream oos = new ObjectOutputStream(Files.newOutputStream(Paths.get("E:/test/a.txt")));
// 3,写对象
oos.writeObject(instance);
// 4,释放资源
oos.close();
System.out.println("对象已写入文件...");
}
// 从文件读取数据(对象)
public static void readObjectFromFile() throws Exception {
// 1,创建对象输入流对象
ObjectInputStream ois = new ObjectInputStream(Files.newInputStream(Paths.get("E:/test/a.txt")));
// 2,读取对象
Singleton instance = (Singleton) ois.readObject();
System.out.println("读取结束, 对象: " + instance);
// 释放资源
ois.close();
}
}
两次得到的对象不是同一个, 单例模式被破坏
序列化破坏单例模式-解决
在Singleton类中添加readResolve()方法,在反序列化时会被反射调用,如果定义了这个方法,就返回这个方法的值(序列化时保存的内部类静态对象),如果没有定义,则返回新new出来的对象。
反射破坏单例模式
public class Singleton implements Serializable {
// 私有构造方法
private Singleton() {}
// 定义一个静态内部类
private static class SingletonHolder {
//在内部类中声明并初始化外部类的对象
private static final Singleton INSTANCE = new Singleton();
}
// 提供公共的访问方式
public static Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}
public class Test {
public static void main(String[] args) throws Exception {
// 1,获取Singleton的字节码对象
Class<Singleton> clazz = Singleton.class;
// 2,获取无参构造方法对象
Constructor<Singleton> cons = clazz.getDeclaredConstructor();
// 3,取消访问检查
cons.setAccessible(true);
// 4,创建Singleton对象
Singleton s1 = cons.newInstance();
System.out.println(s1);
Singleton s2 = cons.newInstance();
System.out.println(s2);
}
}
两次得到的对象不是同一个, 单例模式被破坏
反射破坏单例模式-解决
使用单例模式的案例
public class Runtime {
private static Runtime currentRuntime = new Runtime();
public static Runtime getRuntime() {
return currentRuntime;
}
private Runtime() {}
// ...
}
Runtime类就是使用的单例设计模式(饿汉式)
简单工厂模式(不属于23种设计模式)
概述
简单工厂不是一种设计模式,反而比较像是一种编程习惯。
结构
简单工厂模式包含如下角色:
抽象产品 :定义了产品的规范,描述了产品的主要特性和功能(案例中的咖啡)
具体产品 :实现或者继承抽象产品的子类(案例中的美式咖啡和拿铁等)
具体工厂 :提供了创建产品的方法,调用者通过该方法来获取产品(简单工厂需要的角色)
实现步骤
1, 定义产品接口或抽象类:定义产品的接口或抽象类,规定产品的共同行为。
2, 创建具体产品类:实现产品接口或继承抽象类的具体产品类,每个具体产品类实现不同的行为。
3, 创建工厂类:定义一个工厂类,该类包含一个静态方法,根据传入的参数决定创建哪一个具体产品类的实例。
代码示例
点咖啡案例:
可以发现, 如果咖啡店想要推出新的咖啡类型, 不仅要新增咖啡子类外, 所有创建咖啡对象的地方全部都要修改(继续添加 if 语句的数量), 代码耦合性非常高
现在使用简单工厂对上面案例进行改进,类图如下:
咖啡类只允许咖啡工厂生产, 各个业务类想要什么咖啡, 传入具体咖啡类型, 由咖啡工厂创建咖啡实例并返回, 实现各个业务类对咖啡对象创建的控制(解耦)
后续咖啡新品类的维护只需要修改咖啡工厂添加新品类咖啡的创建支持就行
工厂(factory)处理创建对象的细节,一旦有了咖啡工厂,后期如果需要Coffee对象直接从工厂中获取即可。但同时又产生了新的耦合:需要咖啡的类 和 咖啡工厂对象 的耦合、咖啡工厂 和 咖啡对象的 耦合
后期如果再加新品种的咖啡,我们需求修改咖啡工厂创建咖啡实现类的代码,违反了开闭原则。但相比较不用简单工厂的代码要容易拓展维护的多
静态工厂
工厂创建咖啡的方法改为静态方法, 这样一来所有使用工厂的地方就不用创建工厂对象了
工厂方法模式
概述
简单工厂一定程度优化了代码, 降低了耦合性, 但是还是违反了开闭原则(允许拓展, 不允许修改)
结构
工厂方法模式包含以下角色:
抽象工厂:具体工厂(美式咖啡工厂/拿铁咖啡工厂)的抽象, 定义创建产品(咖啡)的方法
具体工厂:实现抽象工厂中的抽象方法,完成具体产品(咖啡)的创建
抽象产品:定义了产品(咖啡)的规范
具体产品:实现了抽象产品(咖啡),由具体工厂来创建,它同具体工厂之间一一对应(美式咖啡由美式咖啡工厂创建, 拿铁咖啡由拿铁咖啡工厂创建)
实现步骤
1, 定义产品接口:创建一个接口,定义所有具体产品必须实现的方法。
2, 创建具体产品类:实现产品接口的具体类,每个类代表一种具体的产品。
3, 定义工厂接口或抽象类:创建一个包含工厂方法的接口或抽象类,工厂方法用于返回产品对象。
4, 实现具体工厂类:实现工厂接口或继承抽象工厂类,并实现工厂方法,返回具体产品的实例。
代码示例
优缺点
优点:
用户只需要知道具体工厂的名称就可得到所要的产品,无须知道产品的具体创建过程
如果需要新增咖啡品类的话, 只需要添加新咖啡和生产该新咖啡的工厂就行了, 不需要修改原有的代码
缺点:
每增加一个产品就要增加一个具体产品类和一个对应的具体工厂类,这增加了系统的复杂度
如果咖啡子类特别多, 对应的工厂类也会特别多
抽象工厂模式
概述
前面介绍的工厂方法模式中考虑的是同一产品级的生产,如咖啡工厂只生产咖啡产品。但现实生活中许多工厂是综合型的工厂,如小米工厂既生产手机, 又生产电脑, 还生产手表手环。
抽象工厂模式将考虑多等级产品的生产,同一个具体工厂所生产的位于不同等级的一组产品称为一个产品族。
产品族和产品等级介绍:
小米手机、小米电脑、小米汽车属于同一产品族
小米手机、苹果手机、三星手机属于同一等级的产品
结构
抽象工厂模式包含如下角色:
定义各个产品级的产品抽象类(手机抽象类, 电脑抽象类)
各个产品级的实现产品(小米手机, 小米电脑, 苹果手机, 苹果电脑)
定义生产同一产品族的抽象工厂接口(数码产品工厂接口)
同一产品族的工厂实现类(小米工厂, 苹果工厂), 实现生产方法, 生产该产品族下各个级别的产品(小米工厂生产各类产品)
实现步骤
1, 定义抽象产品接口:为每种产品类型创建一个接口,定义产品的行为。
2, 创建具体产品类:实现抽象产品接口的具体类,每个类代表一种具体的产品。
3, 定义抽象工厂接口:创建一个接口,声明所有产品创建的方法。
4, 实现具体工厂类:实现抽象工厂接口,创建具体的产品实例。
代码示例
测试:
运行结果:
优缺点
优点:
抽象工厂模式将具体的产品类与客户端代码解耦。
添加新的产品族时只需要增加新的具体工厂类,而无需更改现有代码。
可以动态选择需要的具体工厂来创建同一产品族的对象。
缺点:
每增加一个产品族就需要增加相应的具体工厂类。
如果产品族中需要增加新产品类型,则需要修改抽象工厂接口及其所有具体工厂,这违背了开闭原则。
使用场景
当一个产品族中的多个对象需要在一起工作时:
例如新家装修接入米家智能家居设备
输入法皮肤需要一整套同一产品族的组件(图标, 提示文字等)
原型模式
概述
用一个已经创建的实例作为原型,复制该对象来创建一个和原型对象相同的新对象
结构
原型模式包含如下角色:
抽象原型类:规定了具体原型对象必须实现的的 clone() 方法
具体原型类:实现抽象原型类的 clone() 方法,它是可被复制的对象
访问类:使用具体原型类中的 clone() 方法来复制新的对象
实现步骤
定义一个原型接口: 包含 clone() 方法。
实现具体的原型类: 实现原型接口,并提供 clone() 方法的具体逻辑。
使用原型: 客户端通过调用具体原型类的 clone() 方法来创建新的对象。
代码示例
浅克隆和深克隆
浅克隆:
浅克隆会复制对象的基本类型字段(如 int、float 等),但对于对象中的引用类型字段(如对象、数组),浅克隆只复制它们的引用,不复制实际的数据。这意味着克隆后的对象和原始对象共享引用类型的数据,这可能会导致不期望的修改行为。
实现浅克隆:
在 Java 中,浅克隆通常通过实现 Cloneable 接口并覆盖 clone() 方法来实现。Cloneable 接口是一个标记接口,表示对象是可以被克隆的。Object 类的 clone() 方法提供了浅拷贝的实现。
深克隆:
复制对象时,不仅复制对象本身,还复制对象所引用的对象,即完全独立的副本。
实现深克隆, 深克隆的实现方式较为复杂,可以通过以下两种方式实现:
手动递归克隆: 逐个克隆所有引用类型字段。
序列化与反序列化: 通过将对象序列化成字节流,然后再反序列化生成新对象。
使用场景
比较适用于创建对象的过程比较复杂或代价较高
建造者模式
概述
将一个复杂对象(例如主机/自行车)的构建与最终产品(主机)分离,使得同样的构建过程可以创建不同的表示(不同品牌不同性能的主机)
结构
建造者模式有以下角色:
产品类(Product): 构建的最终产品(自行车/主机)
抽象建造者类(Builder):这个接口规定了复杂对象(自行车)各个零件的组装过程,不涉及具体零件的创建。
具体建造者类(ConcreteBuilder):实现 Builder 接口,完成复杂产品的各个部件的具体创建方法。在构造过程完成后,返回产品的实例。
指挥者类(Director):调用具体建造者来创建复杂对象的各个部分,在指导者中不涉及具体产品的信息,只负责保证对象各部分完整创建或按某种顺序创建。
实现步骤
1:创建产品类
2:创建建造者接口或抽象类
3:实现具体的创造者实现类
4:编写指挥者类
5:客户端使用建造者创建产品对象
代码案例-建造自行车
自行车类:
@Getter
@Setter
public class Bike {
// 品牌
private String brand;
//车架
private String frame;
//车座
private String seat;
@Override
public String toString() {
return "这是一辆" + frame + ", " + seat + "的" + brand + "自行车";
}
}
构建者抽象类:
// 自行车的构建者, 需要提供构建自行车的各种零件的方法
public abstract class BikeBuilder {
// 构建的目标对象
protected Bike bike = new Bike();
// 构建对象的零件-车架
public abstract BikeBuilder buildFrame();
// 构建对象的零件-车座
public abstract BikeBuilder buildSeat();
// 构建对象(自行车)
public abstract BikeBuilder setBrand();
// 返回对象(构建好的自行车)
public abstract Bike build();
}
具体的构建者类-摩拜单车:
public class MobileBuilder extends BikeBuilder {
public MobileBuilder buildFrame() {
bike.setFrame("碳纤维车架");
return this;
}
public MobileBuilder buildSeat() {
bike.setSeat("真皮车座");
return this;
}
public MobileBuilder setBrand() {
bike.setBrand("摩拜");
return this;
}
public Bike build() {
return bike;
}
}
具体的构建者类-ofo单车:
public class OfoBuilder extends BikeBuilder {
public OfoBuilder buildFrame() {
bike.setFrame("铝合金车架");
return this;
}
public OfoBuilder buildSeat() {
bike.setSeat("橡胶车座");
return this;
}
public OfoBuilder setBrand() {
bike.setBrand("ofo");
return this;
}
public Bike build() {
return bike;
}
}
指挥者类:
public class Director {
// 声明用什么组装器来组装自行车
private BikeBuilder builder;
public Director(BikeBuilder builder) {
this.builder = builder;
}
// 组装自行车, 该方法里定义组装的顺序等细节
public Bike construct() {
return builder.buildFrame().buildSeat().setBrand().build();
}
}
测试:
public class Client {
public static void main(String[] args) {
// 创建摩拜单车的指挥者对象
Director director = new Director(new MobileBuilder());
// 让指挥者组装自行车
Bike bike = director.construct();
System.out.println(bike);
}
}
运行结果:
代码案例-建造电脑(优雅的链式调用)
产品类(笔记本):
public class Computer {
private String CPU;
private String RAM;
private String storage;
private String graphicsCard;
private String operatingSystem;
// 构造函数是私有的,防止其他类直接创建对象
protected Computer(ComputerBuilder builder) {
this.CPU = builder.CPU;
this.RAM = builder.RAM;
this.storage = builder.storage;
this.graphicsCard = builder.graphicsCard;
this.operatingSystem = builder.operatingSystem;
}
@Override
public String toString() {
return "这是一台电脑,CPU是" + CPU + ", 内存是" + RAM + ", 存储是" + storage
+ ", 显卡是" + graphicsCard + ", 操作系统是" + operatingSystem;
}
}
建造者接口(根据需要可以选择是否省略):
public interface Builder {
// 设置CPU
Builder setCPU(String CPU);
// 设置内存
Builder setRAM(String RAM);
// 设置存储
Builder setStorage(String storage);
// 设置显卡
Builder setGraphicsCard(String graphicsCard);
// 设置操作系统
Builder setOperatingSystem(String operatingSystem);
// 构建最终的产品对象
Computer build();
}
具体的建造者类:
public class ComputerBuilder implements Builder {
protected String CPU;
protected String RAM;
protected String storage = "256GB";
protected String graphicsCard = "Integrated";
protected String operatingSystem = "Windows 10";
/**
* 设置CPU
*
* @param CPU CPU
* @return this
*/
public ComputerBuilder setCPU(String CPU) {
this.CPU = CPU;
// 返回当前对象,支持链式调用
return this;
}
/**
* 设置内存
*
* @param RAM 内存
* @return this
*/
public ComputerBuilder setRAM(String RAM) {
this.RAM = RAM;
// 返回当前对象,支持链式调用
return this;
}
/**
* 设置存储
*
* @param storage 存储
* @return this
*/
public ComputerBuilder setStorage(String storage) {
this.storage = storage;
// 返回当前对象,支持链式调用
return this;
}
/**
* 设置显卡
*
* @param graphicsCard 显卡
* @return this
*/
public ComputerBuilder setGraphicsCard(String graphicsCard) {
this.graphicsCard = graphicsCard;
// 返回当前对象,支持链式调用
return this;
}
/**
* 设置操作系统
*
* @param operatingSystem 操作系统
* @return this
*/
public ComputerBuilder setOperatingSystem(String operatingSystem) {
this.operatingSystem = operatingSystem;
// 返回当前对象,支持链式调用
return this;
}
/**
* 构建最终的产品对象并返回
*
* @return Computer
*/
public Computer build() {
return new Computer(this);
}
}
指挥者类:
public class ComputerDirector {
public Computer construct() {
return new ComputerBuilder()
.setCPU("Intel")
.setRAM("8GB")
.setStorage("256GB")
.setGraphicsCard("Integrated")
.setOperatingSystem("Windows 10")
.build();
}
}
测试类(调用者类):
public class Client {
public static void main(String[] args) {
ComputerDirector ComputerDirector = new ComputerDirector();
Computer computer = ComputerDirector.construct();
System.out.println(computer);
}
}
运行结果:
可以将指挥者去掉, 调用者类中直接使用建造者获取对象:
public class Client {
public static void main(String[] args) {
Computer computer = new ComputerBuilder()
.setCPU("Intel")
.setRAM("8GB")
.setStorage("256GB")
.setGraphicsCard("Integrated")
.setOperatingSystem("Windows 10")
.build();
System.out.println(computer);
}
}
使用场景
需要创建的对象非常复杂,包含多个组件,并且这些组件的创建步骤需要按照一定顺序进行。
同样的构建过程可以创建不同表示的对象。
构造函数参数过多或者参数之间依赖关系复杂
优缺点
优点:
有效的封装变化,在使用建造者模式的场景中,一般产品类和建造者类是比较稳定的,因此,将主要的业务逻辑封装在指挥者类中对整体而言可以取得比较好的稳定性。
客户端不必知道产品内部组成的细节,将产品本身与产品的创建过程解耦,使得相同的创建过程可以创建不同的产品对象。
可以更加精细地控制产品的创建过程 。将复杂产品的创建步骤分解在不同的方法中,使得创建过程更加清晰,也更方便使用程序来控制创建过程。
很容易进行扩展。如果有新的需求,通过实现一个新的建造者类就可以完成,基本上不用修改之前已经测试通过的代码,因此也就不会对原有功能引入风险。符合开闭原则。
缺点:
造者模式所创建的产品一般具有较多的共同点,其组成部分相似,如果产品之间的差异性很大,则不适合使用建造者模式,因此其使用范围受到一定的限制。
对比
工厂方法模式VS建造者模式
工厂方法模式注重的是整体对象的创建方式;而建造者模式注重的是部件构建的过程,意在通过一步一步地精确构造创建出一个复杂的对象。
举个简单例子来说明两者的差异,如要制造一个超人,如果使用工厂方法模式,直接产生出来的就是一个力大无穷、能够飞翔、内裤外穿的超人;而如果使用建造者模式,则需要组装手、头、脚、躯干等部分,然后再把内裤外穿,于是一个超人就诞生了。
抽象工厂模式VS建造者模式
抽象工厂模式实现对产品家族的创建,一个产品家族是这样的一系列产品:具有不同分类维度的产品组合,采用抽象工厂模式则是不需要关心构建过程,只关心什么产品由什么工厂生产即可。
建造者模式则是要求按照指定的蓝图建造产品,它的主要目的是通过组装零配件而产生一个新产品。
如果将抽象工厂模式看成汽车配件生产工厂,生产一个产品族的产品,那么建造者模式就是一个汽车组装工厂,通过对部件的组装可以返回一辆完整的汽车。
结构型模式
代理模式
静态代理
概述
Java中的代理模式(Proxy Pattern)是一种结构型设计模式,主要用于为其他对象提供一种代理,以控制对该对象的访问。
代理模式可以为原对象的操作提供额外的功能或控制,而不改变其接口。
代理模式分为静态代理和动态代理,静态代理在编译时生成代理类,动态代理在运行时生成代理类。
以火车站卖票为例, 现实中有很多卖票的代理点, 可以从代理点买票
结构
代理模式的主要结构包括以下几个角色:
抽象接口:定义了目标对象与代理对象的公共接口(卖票),这样客户端可以通过该接口与目标对象或代理对象进行交互
真实对象类:实现了抽象接口(卖票)的真实对象(火车站),代理对象最终会将请求转发给该对象(卖票代理点调用火车站的卖票接口)
代理类:实现了抽象接口(卖票),并包含对真实对象(火车站)的引用,代理类在处理请求时可以进行额外的操作,然后将请求转发给真实对象(卖票代理点调用火车站的卖票接口)
实现步骤
1, 定义抽象接口,声明公共方法
2, 真实类实现接口中的方法
3, 代理类同样实现接口,并通过持有真实对象的引用来转发请求给真实类
代码示例
抽象接口-定义卖票方法
public interface SellTickets {
// 卖票
void sell();
}
真实对象类(火车站):
public class TrainStation implements SellTickets {
public void sell() {
System.out.println("火车站卖票");
}
}
代理类(卖火车票的代理点):
public class ProxyPoint implements SellTickets {
// 声明火车站类对象
private TrainStation trainStation = new TrainStation();
public void sell() {
System.out.println("代售点收取一些服务费用");
trainStation.sell();
}
}
访问类(买票的用户):
public class Client {
public static void main(String[] args) {
//创建代售点类对象
ProxyPoint proxyPoint = new ProxyPoint();
//调用方法进行买票
proxyPoint.sell();
}
}
运行结果:
使用场景
1. 远程代理:为远程对象提供本地代理,以控制对该远程对象的访问。
2. 虚拟代理:在需要时才创建和访问真实对象,用于节省开销。
3. 安全代理:控制对原始对象的访问,用于权限控制。
4. 增强属性和方法:在访问真实对象时执行一些附加操作,如日志记录、性能监测等。
优缺点
优点:
- 控制访问:可以控制对原始对象的访问
- 增强功能:在不修改原始类的情况下添加额外功能
- 松耦合:客户端通过代理对象与真实对象交互,不直接接触真实对象
缺点:
- 增加复杂性:引入代理类会增加系统的复杂性
- 性能开销:代理模式会带来一些性能上的开销,特别是涉及动态代理时
JDK动态代理
接口:
public interface SellTickets {
// 卖票
void sell();
}
实现类(正式对象类):
public class TrainStation implements SellTickets {
public void sell() {
System.out.println("火车站卖票");
}
}
动态代理类:
public class DynamicProxy {
/**
* 动态代理
*
* @param realSubject 需要被代理的对象
* @return 代理对象
*/
public static Object getProxyObject(Object realSubject) {
// 使用Proxy的newProxyInstance()获取代理对象
/*
newProxyInstance()方法参数说明:
ClassLoader loader : 类加载器,用于加载代理类,使用真实对象的类加载器即可
Class<?>[] interfaces : 真实对象所实现的接口,代理模式真实对象和代理对象实现相同的接口
InvocationHandler h : 代理对象的调用处理程序
*/
return Proxy.newProxyInstance(
realSubject.getClass().getClassLoader(),
realSubject.getClass().getInterfaces(),
new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("调用前增强...");
// 调用真实主题对象的方法
Object result = method.invoke(realSubject, args);
System.out.println("调用后增强...");
// 返回方法调用的结果
return result;
}
}
);
}
}
测试:
public class Client {
public static void main(String[] args) {
// 注意, 动态代理拿到的代理类是接口的实例(相当于得到了非动态代理的情况下, 同样实现了接口的代理类)
SellTickets proxyObject = (SellTickets) DynamicProxy.getProxyObject(new TrainStation());
proxyObject.sell();
}
}
运行结果:
思考:
1, 上述动态代理的方式: DynamicProxy.getProxyObject(new TrainStation()), 传入了火车站的实例, 拿到的代理类是实现了接口(SellTickets)的匿名实例
2, 这个匿名实例, 相当于得到了非动态代理的情况下, 同样实现了接口的那个代理类(ProxyPoint)的实例
3, 如果没有定义SellTickets接口,只定义了TrainStation(火车站类)。很显然JDK代理是无法使用了,因为JDK动态代理要求必须定义接口,对接口进行代理
执行流程
下面是摘取的重点代码块(不重要的代码块删除了, 便于查看):
// $Proxy0就是程序运行过程中动态生成的代理类
// 就是SellTickets proxyObject = (SellTickets) DynamicProxy.getProxyObject(new TrainStation()) 的 proxyObject
public final class $Proxy0 extends Proxy implements SellTickets {
private static Method m3;
public $Proxy0(InvocationHandler invocationHandler) {
super(invocationHandler);
}
static {
m3 = Class.forName("com.itheima.proxy.dynamic.jdk.SellTickets").getMethod("sell", new Class[0]);
}
public final void sell() {
this.h.invoke(this, m3, null);
}
}
// Java提供的动态代理相关类
public class Proxy implements java.io.Serializable {
protected InvocationHandler h;
protected Proxy(InvocationHandler h) {
this.h = h;
}
}
// 代理工厂类
public class DynamicProxy {
// 获取动态代理对象
public static Object getProxyObject(Object realSubject) {
return Proxy.newProxyInstance(
realSubject.getClass().getClassLoader(),
realSubject.getClass().getInterfaces(),
new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("调用前增强...");
// 调用真实主题对象的方法
Object result = method.invoke(realSubject, args);
System.out.println("调用后增强...");
// 返回方法调用的结果
return result;
}
}
);
}
}
// 测试访问类
public class Client {
public static void main(String[] args) {
// 注意, 动态代理拿到的代理类是接口的实例(相当于得到了非动态代理的情况下, 同样实现了接口的代理类)
SellTickets proxyObject = (SellTickets) DynamicProxy.getProxyObject(new TrainStation());
proxyObject.sell();
}
}
执行流程如下:
1. 在测试类中通过代理对象调用sell()方法
2. 根据多态的特性,执行的是代理类($Proxy0)中的sell()方法
3. 代理类($Proxy0)中的sell()方法中又调用了InvocationHandler接口的子实现类对象的invoke方法
4. invoke方法通过反射执行了真实对象所属类(TrainStation)中的sell()方法
CGLIB动态代理
真实对象(被代理类):
public class TrainStation {
// 火车站卖票
public void sell() {
System.out.println("火车站卖票");
}
// 火车站改签
public void change() {
System.out.println("火车站改签");
}
// 火车站退票
public void back() {
System.out.println("火车站退票");
}
}
动态代理类:
public class ProxyFactory implements MethodInterceptor {
// 声明火车站对象
private TrainStation station = new TrainStation();
public TrainStation getProxyObject() {
// 创建Enhancer对象,类似于JDK代理中的Proxy类
Enhancer enhancer = new Enhancer();
// 设置父类的字节码对象。指定父类
enhancer.setSuperclass(TrainStation.class);
// 设置回调函数, 指定代理逻辑, 其他类拿到代理对象调用方法时, 就会调用intercept方法
enhancer.setCallback(this);
// 创建代理对象
return (TrainStation) enhancer.create();
}
/**
* CGLIB动态代理的核心方法, 代理逻辑
*
* @param o 代理对象, 几乎不用, 代码逻辑中更关注的是真实的目标对象
* @param method 目标对象被代理的方法
* @param objects 方法调用时的传参
* @param methodProxy CGLib 提供的一个用于执行父类方法的代理类。它比直接使用反射调用(如 method.invoke())效率更高,因为它可以直接调用到被代理的方法而不经过反射。
* 你可以使用 methodProxy.invokeSuper() 调用父类的原始方法,而不必直接通过反射调用。
* 这个参数的主要目的是在拦截方法中需要调用父类的方法时提供一个快捷方式。
* @return 方法的执行结果
* @throws Throwable 异常
*/
public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
System.out.println("代售点收取一定的服务费用(CGLib代理)");
// 调用目标对象的方法(也可以使用methodProxy.invokeSuper()调用父类即被代理类的方法)
return method.invoke(station, objects);
}
}
测试类:
public class Client {
public static void main(String[] args) {
// 创建代理工厂对象
ProxyFactory factory = new ProxyFactory();
// 获取代理对象
TrainStation proxyObject = factory.getProxyObject();
// 调用代理对象中的sell方法卖票
proxyObject.sell();
proxyObject.change();
proxyObject.back();
}
}
运行结果:
三种代理的对比
-
jdk代理和CGLIB代理
使用CGLib实现动态代理,CGLib底层采用ASM字节码生成框架,使用字节码技术生成代理类,在JDK1.6之前比使用Java反射效率要高。唯一需要注意的是,CGLib不能对声明为final的类或者方法进行代理,因为CGLib原理是动态生成被代理类的子类。
在JDK1.6、JDK1.7、JDK1.8逐步对JDK动态代理优化之后,在调用次数较少的情况下,JDK代理效率高于CGLib代理效率,只有当进行大量调用的时候,JDK1.6和JDK1.7比CGLib代理效率低一点,但是到JDK1.8的时候,JDK代理效率高于CGLib代理。所以如果有接口使用JDK动态代理,如果没有接口使用CGLIB代理。
-
动态代理和静态代理
动态代理与静态代理相比较,最大的好处是接口中声明的所有方法都被转移到调用处理器一个集中的方法中处理(InvocationHandler.invoke)。这样,在接口方法数量比较多的时候,我们可以进行灵活处理,而不需要像静态代理那样每一个方法进行中转。
如果接口增加一个方法,静态代理模式除了所有实现类需要实现这个方法外,所有代理类也需要实现此方法。增加了代码维护的复杂度。而动态代理不会出现该问题
优缺点
优点:
- 代理模式在客户端与目标对象之间起到一个中介作用和保护目标对象的作用
- 代理对象可以扩展目标对象的功能
- 代理模式能将客户端与目标对象分离,在一定程度上降低了系统的耦合度
缺点:
- 增加了系统的复杂度
使用场景
-
远程(Remote)代理
本地服务通过网络请求远程服务。为了实现本地到远程的通信,我们需要实现网络通信,处理其中可能的异常。为良好的代码设计和可维护性,我们将网络通信部分隐藏起来,只暴露给本地服务一个接口,通过该接口即可访问远程服务提供的功能,而不必过多关心通信部分的细节。
-
防火墙(Firewall)代理
当你将浏览器配置成使用代理功能时,防火墙就将你的浏览器的请求转给互联网;当互联网返回响应时,代理服务器再把它转给你的浏览器。
-
保护(Protect or Access)代理
控制对一个对象的访问,如果需要,可以给不同的用户提供不同级别的使用权限。
适配器模式
概述
笔记本可以读取SD卡, 但是无法读取TF卡, 如果想要读取TF卡的数据, 就需要适配器(TF卡插入适配器, 适配器插入电脑)
又或者中国的电器插头到了欧洲, 电器插头插不了欧洲的插座, 这个时候就需要电源适配器了
适配器就是将原先不能用的资源(接口/方法), 通过适配器的介入, 可以正常的获取资源(调用接口/方法)
适配器模式分为类适配器和对象适配器
结构
适配器模式有以下角色:
目标接口:当前系统期待的接口(可以正常使用的接口方法, 如SD卡),它可以是抽象类或接口。
适配者类:当前系统想要访问却无法访问的资源(TF卡), 需要适配的类(适配成满足可以读取资源的)。
适配器类:转换器,通过继承或引用适配者的对象,把适配者接口转换成目标接口(SD卡),让系统以访问目标接口的方式访问适配者类(TF卡)
对象适配器-常用
实现步骤:
新增适配器类, 实现目标接口, 引用适配者类
代码案例
案例: 电脑可以读取SD卡却无法读取TF卡
SD卡接口规范:
public interface SDCard {
// 从SD卡中读取数据
String readSD();
// 往SD卡中写数据
void writeSD(String msg);
}
SD卡实现类:
public class SDCardImpl implements SDCard {
public String readSD() {
String msg = "这是SD卡保存的数据";
System.out.println("正在读取SD卡数据...");
return msg;
}
public void writeSD(String msg) {
System.out.println("正在往SD卡写数据 :" + msg);
}
}
电脑类:
public class Computer {
// 从SD卡中读取数据
public String readSD(SDCard sdCard) {
return sdCard.readSD();
}
// 往SD卡中写数据
public void writeSD(SDCard sdCard, String msg) {
sdCard.writeSD(msg);
}
}
测试类:
public class Client {
public static void main(String[] args) {
// 创建计算机对象
Computer computer = new Computer();
// 读取SD卡中的数据
SDCardImpl sdCard = new SDCardImpl();
String msg = computer.readSD(sdCard);
System.out.println(msg);
// 使用该电脑写入SD卡
computer.writeSD(sdCard, "这是写入SD卡的数据");
}
}
运行结果:
现在有TF卡, 想要电脑读取
TF卡接口规范:
public interface TFCard {
// 从TF卡中读取数据
String readTF();
// 往TF卡中写数据
void writeTF(String msg);
}
TF卡实现类:
public class TFCardImpl implements TFCard {
public String readTF() {
String msg = "这是TF卡保存的数据";
System.out.println("正在读取TF卡数据...");
return msg;
}
public void writeTF(String msg) {
System.out.println("正在往TF卡写数据:" + msg);
}
}
发现代码编译报错, 因为电脑只能接收SD卡, 不能接收TF卡:
适配器的加入:
适配器, 持有TF卡对象(可以读写TF卡), 并且实现了SD卡接口(可以作为SD卡插入电脑)可以读取TF卡
// 适配器, 持有TF卡对象(可以读写TF卡), 并且实现了SD卡接口(可以作为SD卡插入电脑)可以读取TF卡
public class SDAdapterTF implements SDCard {
// 持有一个TFCard的对象
private TFCard tfCard;
public SDAdapterTF(TFCard tfCard) {
System.out.println("适配器插入TF卡...");
this.tfCard = tfCard;
}
public String readSD() {
System.out.println("通过适配器读取TF卡数据...");
// 调用TFCard中的readTF()
return tfCard.readTF();
}
public void writeSD(String msg) {
System.out.println("通过适配器往TF卡写数据...");
// 调用TFCard中的writeTF()
tfCard.writeTF(msg);
}
}
测试及结果:
类适配器
SD卡接口, SD卡实现类, TF卡接口, TF卡实现类, computer类与对象适配器都一致, 不同的是适配器需要继承TF卡实现类(TF卡功能), 并实现SD卡接口(能被电脑使用)
适配器类:
测试及结果:
接口适配器
适用于适配个别接口...
装饰器模式
概述
装饰器模式(Decorator Pattern)是一种结构型设计模式,允许向现有对象添加新的功能,同时又不改变其结构。装饰器模式是对继承关系的一种替代方案,它使用组合而非继承, 来扩展对象的功能。
下述通过咖啡及其子类(具体咖啡例如拿铁/美式等), 以及咖啡的加料(加牛奶/加糖等)的案例
装饰器模式的结构
装饰器模式的核心结构包括以下几个部分:
对象接口(抽象类也行):定义一个对象(咖啡)接口,定义对象的一些规范
具体对象:实现了对象接口(拿铁/美式),装饰器就是装饰这些具体对象
装饰器接口(抽象类也行):同样实现对象接口,核心就是引用了对象接口的对象
具体装饰器:实现了装饰器接口,会将请求委托给被装饰的对象,并可以在请求前后增加额外的行为
实现步骤
定义组件接口:定义一个接口,用于表示将要被装饰的对象规范
实现具体组件:实现组件接口, 即被装饰的
创建装饰器接口:同样实现组件接口,同时包含一个组件类型的引用,用于持有将要被装饰的对象
实现具体装饰器:继承自装饰器基类,实现具体的装饰功能: 将请求委托给被装饰的对象,并可以在请求前后增加额外的行为
使用场景
需要在不修改现有代码的情况下给对象添加额外的功能
需要动态地组合对象的功能
代码案例
通过咖啡及其子类(具体咖啡例如拿铁/美式等), 以及咖啡的加料(加牛奶/加糖等)的案例, 通过装饰器模式来实现对不同咖啡类型和配料的组合。
// 对象抽象类(接口也行)
public abstract class Coffee {
// 获取描述
abstract String getDescription();
// 获取价格
abstract double getCost();
}
// 实现对象抽象类, 具体的对象, 也就是被装饰的
public class LatteCoffee extends Coffee{
@Override
String getDescription() {
return "拿铁";
}
@Override
double getCost() {
return 10.0;
}
}
// 实现对象抽象类, 具体的对象, 也就是被装饰的
public class AmericanCoffee extends Coffee {
@Override
String getDescription() {
return "美式咖啡";
}
@Override
double getCost() {
return 8.0;
}
}
// 抽象装饰器, 核心就是持有被装饰的对象
public abstract class CoffeeDecorator extends Coffee {
// 引用被装饰的对象
protected Coffee coffee;
// 通过有参构造函数传入具体的咖啡对象
public CoffeeDecorator(Coffee coffee) {
this.coffee = coffee;
}
}
// 实现抽象装饰器, 具体的装饰器, 将请求委托给被装饰的对象,并可以在请求前后增加额外的行为
public class MilkDecorator extends CoffeeDecorator {
// 调用父类构造函数传入具体的咖啡对象
public MilkDecorator(Coffee coffee) {
super(coffee);
}
// 增加牛奶的描述
@Override
public String getDescription() {
return coffee.getDescription() + ", Milk";
}
// 增加牛奶的价格
@Override
public double getCost() {
return coffee.getCost() + 2.0;
}
}
// 实现抽象装饰器, 具体的装饰器, 将请求委托给被装饰的对象,并可以在请求前后增加额外的行为
public class SugarDecorator extends CoffeeDecorator {
// 调用父类构造函数传入具体的咖啡对象
public SugarDecorator(Coffee coffee) {
super(coffee);
}
// 增加牛奶的描述
@Override
public String getDescription() {
return coffee.getDescription() + ", Sugar";
}
// 增加牛奶的价格
@Override
public double getCost() {
return coffee.getCost() + 1.0;
}
}
// 测试类
public class Client {
public static void main(String[] args) {
// 创建一个拿铁(具体对象)
Coffee coffee = new LatteCoffee();
System.out.println(coffee.getDescription() + " Cost: $" + coffee.getCost()); // 输出:拿铁 Cost: $10.0
// 增加牛奶(具体装饰器)
coffee = new MilkDecorator(coffee);
System.out.println(coffee.getDescription() + " Cost: $" + coffee.getCost()); // 输出:拿铁, Milk Cost: $12.0
// 再增加糖(具体装饰器)
coffee = new SugarDecorator(coffee);
System.out.println(coffee.getDescription() + " Cost: $" + coffee.getCost()); // 输出:拿铁, Milk, Sugar Cost: $13.0
// 再增加糖(具体装饰器)
coffee = new SugarDecorator(coffee);
System.out.println(coffee.getDescription() + " Cost: $" + coffee.getCost()); // 输出:拿铁, Milk, Sugar, Sugar Cost: $14.0
}
}
优缺点
优点:
- 开闭原则:可以在不修改原有类的情况下扩展功能。
- 灵活性:可以动态组合不同的
装饰器,从而实现多种功能的叠加。 - 细粒度控制:可以单独控制每个装饰器的功能,并根据需要组合。
缺点:
- 增加复杂性:每个具体装饰器都需要创建一个类,类的数量可能急剧增加。
- 排查困难:由于是动态组合对象,有时可能难以排查某个特定功能是由哪个装饰器添加的。
桥接模式
桥接模式(Bridge Pattern)是结构型设计模式之一,旨在将抽象部分与实现部分分离,使它们可以独立变化。这种模式通过引入一个桥接接口将抽象与实现分离,从而实现解耦。桥接模式的主要目标是通过组合而非继承的方式来扩展系统的功能,从而避免因为多重继承带来的复杂性问题。
概述
桥接模式非常适合用于多个维度相互交叉搭配的场景, 例如画图(图形和颜色交叉搭配), 分离各个维度, 使得每个维度可以独立地扩展和修改。
桥接模式的主要目标是通过组合而非继承的方式来扩展系统的功能,从而避免因为多重继承带来的复杂性问题。
结构
桥接模式涉及以下四个角色(将画图的图形拆分为主体部分, 颜色作为抽象部分):
主体部分抽象类:主体部分(图形)的抽象类,并包含一个对抽象部分对象(颜色)的引用
主体部分抽象类的实现:主体部分的实现(圆形/正方形),持有抽象部分对象(红色/蓝色), 可以调用抽象部分对象的方法
抽象部分接口:抽象部分(颜色)接口
抽象部分的具体实现:实现抽象部分接口的具体类(红色/蓝色)
实现步骤
实现桥接模式的一般步骤如下:
- 定义主体部分抽象类
- 创建主体部分抽象类的实现
- 定义抽象部分接口
- 抽象部分的具体实现
使用场景
桥接模式适用于以下场景:
- 需要两个维度独立变化的系统:例如,图形界面中的形状和颜色的独立变化。
- 系统不希望使用继承或因继承导致类爆炸的情况:例如,不同数据库类型和不同操作类型的组合。
- 需要在运行时切换不同的实现:例如,通过配置文件决定使用不同的日志系统。
代码案例
假设我们有一个绘图工具,它可以绘制不同的形状(如圆形、正方形),并且这些形状可以使用不同的颜色来绘制。我们希望能够独立地扩展形状和颜色,而不通过继承来创建大量子类
// 将形状作为实体部分, 形状抽象类
public abstract class Shape {
// 持有一个实现部分接口的引用,即颜色
protected Color color;
// 通过构造函数将实现部分注入
protected Shape(Color color) {
this.color = color;
}
// 定义抽象方法,用于绘制形状
abstract void draw();
}
// 形状的实现部分, 圆形
public class Circle extends Shape {
// 通过构造函数将颜色注入
protected Circle(Color color) {
super(color);
}
// 实现抽象方法,绘制圆形并设置颜色
@Override
void draw() {
System.out.print("画了个圆, ");
// 调用颜色的具体实现
color.applyColor();
}
}
// 形状的实现部分, 正方形
public class Square extends Shape{
// 通过构造函数将颜色注入
protected Square(Color color) {
super(color);
}
// 实现抽象方法,绘制圆形并设置颜色
@Override
void draw() {
System.out.print("画了个正方形, ");
// 调用颜色的具体实现
color.applyColor();
}
}
// 将颜色作为抽象部分, 颜色接口
public interface Color {
// 使用什么颜色绘制
void applyColor();
}
// 颜色的实现部分, 红色
public class RedColor implements Color {
@Override
public void applyColor() {
System.out.println("使用红色绘制");
}
}
// 颜色的实现部分, 蓝色
public class BlueColor implements Color {
@Override
public void applyColor() {
System.out.println("使用蓝色绘制");
}
}
// 测试类
public class Client {
public static void main(String[] args) {
// 通过构造函数将颜色注入
Square square = new Square(new BlueColor());
// 实现抽象方法,绘制圆形并设置颜色
square.draw(); // 输出: 画了个正方形, 使用蓝色绘制
// 通过构造函数将颜色注入
Circle circle = new Circle(new RedColor());
// 实现抽象方法,绘制圆形并设置颜色
circle.draw(); // 输出: 画了个圆, 使用红色绘制
}
}
优缺点
优点:
1. 解耦主体和抽象:主体和抽象(两个维度)分离,可以独立扩展而不影响对方。
2. 提高可扩展性:更易于扩展和维护,可以在不修改现有代码的情况下添加新功能。
3. 减少类的数量:通过组合减少了因继承带来的大量子类。
缺点:
1. 增加复杂度:引入额外的桥接接口和类,使得系统结构更复杂。
2. 维护成本:抽象与实现的分离可能导致维护成本的增加,需要理解两个独立部分的设计和实现。
细节补充
- 与适配器模式的区别:适配器模式用于接口不兼容的情况,通过适配器将一个类的接口转换为客户端希望的接口。而桥接模式则是为了分离抽象和实现,提供独立的扩展能力。
- 与策略模式的区别:策略模式的目的是将算法或行为独立封装,通过组合的方式在运行时动态选择算法或行为。桥接模式则更关注于将抽象与实现分离,并不是专注于行为封装。
- 与装饰者模式的区别:装饰者模式通过动态地为对象添加行为,而桥接模式则是两个维度,让它们可以独立变化。
桥接模式在实际应用中非常有用,特别是需要将多个维度的变化独立处理时,可以极大地减少代码的复杂度并提高扩展性。
外观模式
概述
外观模式是一种结构型设计模式,它提供了一些暴露的公共接口,在这些接口中,访问子系统中的一群接口。外观模式定义了一个高层接口,这个接口使得这一子系统更加容易使用。
主要目的是为用户提供一个简单的公共接口来间接访问复杂的子系统。它对外隐藏子系统的访问,通过外观类提供的简单接口来实现对系统中多个复杂部分的访问。用户不用知道对子系统的操作细节(这些都有外观者控制)
结构
外观模式包含以下几个部分:
- 外观类: 对外提供一个简单的接口(该接口中外观类根据要实现的功能控制子系统的各个接口),供客户端调用。
- 子系统类: 处理用户实际请求的类,它们各自执行各自的功能,并不知道外观类的存在。
- 客户端: 通过调用外观类的暴露接口来无感的与子系统交互(与子系统的实际交互由外观者控制),不直接接触子系统。
实现步骤
- 识别子系统中的一组相关类: 这些类构成了需要被简化的复杂子系统。
- 定义外观类: 创建一个新的外观类,它需要对外提供用户需要子系统单个或配合实现的所有功能接口,供客户端调用。
- 客户端调用: 客户端调用外观类提供的方法,间接来操作子系统,而无需直接与子系统类交互。
使用场景
- 当你需要简化一个复杂子系统的使用时。
- 当你需要将子系统与客户端代码解耦时。
- 在构建一个大型软件系统时,将系统划分为多个子系统,通过外观模式来简化交互。
代码案例
用一个家庭影院系统的例子来解释外观模式。在这个例子中,有一个复杂的家庭影院系统,它包含了多个子系统:灯光、音响、投影仪、幕布、DVD 播放器等。为了让用户能够方便地使用这个系统,设计一个外观类 HomeTheaterFacade来控制这些子系统。
子系统类
// 灯光(子系统类)
public class Light {
public void on() {
System.out.println("灯光已打开");
}
public void off() {
System.out.println("灯光已关闭");
}
public void dim() {
System.out.println("灯光已调暗");
}
}
// 音响(外观类)
public class SoundSystem {
public void on() {
System.out.println("音响已打开");
}
public void off() {
System.out.println("音响已关闭");
}
public void setVolume(int level) {
System.out.println("音量已设为 " + level);
}
}
// 投影仪(子系统类)
public class Projector {
public void on() {
System.out.println("投影仪已打开");
}
public void off() {
System.out.println("投影仪已关闭");
}
public void wideScreenMode() {
System.out.println("投影仪已设为宽屏模式");
}
}
// 幕布(外观类)
public class Screen {
public void down() {
System.out.println("幕布已放下");
}
public void up() {
System.out.println("幕布已收起");
}
}
// 播放器(外观类)
public class DVDPlayer {
public void on() {
System.out.println("DVD 播放器已打开");
}
public void off() {
System.out.println("DVD 播放器已关闭");
}
public void play(String movie) {
System.out.println("正在播放电影: " + movie);
}
}
外观类
// 家庭影院(外观类)
public class HomeTheaterFacade {
private Light light;
private SoundSystem soundSystem;
private Projector projector;
private Screen screen;
private DVDPlayer dvdPlayer;
// 构造函数初始化所有子系统
public HomeTheaterFacade(Light light, SoundSystem soundSystem, Projector projector, Screen screen, DVDPlayer dvdPlayer) {
this.light = light;
this.soundSystem = soundSystem;
this.projector = projector;
this.screen = screen;
this.dvdPlayer = dvdPlayer;
}
// 方法封装了子系统的复杂操作
public void watchMovie(String movie) {
System.out.println("准备看电影...");
light.dim();
screen.down();
projector.on();
projector.wideScreenMode();
soundSystem.on();
soundSystem.setVolume(10);
dvdPlayer.on();
dvdPlayer.play(movie);
}
// 结束电影的方法
public void endMovie() {
System.out.println("关闭家庭影院系统...");
light.on();
screen.up();
projector.off();
soundSystem.off();
dvdPlayer.off();
}
}
客户端代码
// 用户(客户端类)
public class Client {
public static void main(String[] args) {
// 创建家庭影院的各个组成, 如灯光、音响、投影仪、屏幕、DVD播放器(子系统实例)
Light light = new Light();
SoundSystem soundSystem = new SoundSystem();
Projector projector = new Projector();
Screen screen = new Screen();
DVDPlayer dvdPlayer = new DVDPlayer();
// 创建家庭影院中控系统(外观类)
HomeTheaterFacade homeTheater = new HomeTheaterFacade(light, soundSystem, projector, screen, dvdPlayer);
// 看电影, 用户只操作家庭影院中控系统(外观类)
homeTheater.watchMovie("《阿凡达》");
System.out.println("=======================");
// 结束电影, 用户只操作家庭影院中控系统(外观类)
homeTheater.endMovie();
}
}
优缺点
优点:
- 简化了客户端代码: 外观模式提供了一个简单的接口,让客户端可以很方便地使用复杂的子系统。
- 降低了耦合度: 外观模式将客户端代码与子系统解耦,客户端不需要了解子系统的细节。
- 更好的分层结构: 外观模式可以帮助划分系统的层次结构,减少子系统之间的依赖。
缺点:
- 可能会掩盖子系统的细节: 如果外观类封装得过于彻底,可能会导致客户端无法直接访问子系统的某些功能。
- 不能完全阻止子系统类被直接使用: 外观模式并不能完全杜绝对子系统类的直接访问,还是需要注意子系统类的合理封装。
组合模式
概述
组合模式(Composite Pattern)是一种结构型设计模式,用于将对象组合成树形结构以表示“部分-整体”的层次结构。通过组合模式,客户端可以以一致的方式处理单个对象和对象组合,使得用户对单个对象和组合对象的使用具有一致性。这个模式使得客户端可以忽略对象组合与对象单个的区别,客户端将统一地使用组合结构中的所有对象。
结构
组合模式包含以下几个核心角色:
- 组件:组合模式的核心接口,定义了叶子节点和组合节点的公共操作。
- 组合:表示组合节点,包含子节点,可以是叶子节点或者是其他组合节点。
- 叶子:表示叶子节点,没有子节点
实现步骤
- 定义一个组件接口:声明所有对象(无论是单个对象还是组合对象)需要实现的方法。
- 实现组合节点类:实现组件接口的类,可以包含子节点(叶子节点或者其他组合节点)。
- 实现叶子节点类:实现组件接口的类,没有子节点。
- 构建组合结构:客户端通过组件接口与组合结构进行交互,不关心是叶子节点还是组合节点。
使用场景
- 文件系统:文件夹可以包含文件和其他文件夹,形成树形结构。
- 组织结构:公司结构中可以有员工和子部门,部门本身也可以包含子部门或员工。
- UI组件:用户界面中的组件,如按钮、面板等,可以包含其他组件。
代码案例
下面是一个通俗易懂的例子:公司组织架构,其中每个部门可能包含其他子部门或员工。
// 定义组织规范, 可以是部门或员工
public abstract class OrganizationComponent {
protected String name;
protected String type;
public OrganizationComponent(String name, String type) {
this.name = name;
this.type = type;
}
// 添加子组件的方法, 叶子节点不用重写(不支持的操作)
public void add(OrganizationComponent component) {
throw new UnsupportedOperationException("不支持的操作");
}
// 添加子组件的方法, 叶子节点不用重写(不支持的操作)
public void remove(OrganizationComponent component) {
throw new UnsupportedOperationException("不支持的操作");
}
// 显示组件信息的方法
public abstract void showInfo();
}
public class Department extends OrganizationComponent {
private List<OrganizationComponent> components = new ArrayList<>();
public Department(String name, String type) {
super(name, type);
}
@Override
public void add(OrganizationComponent component) {
components.add(component);
}
@Override
public void remove(OrganizationComponent component) {
components.remove(component);
}
@Override
public void showInfo() {
System.out.println(type + ": " + name);
for (OrganizationComponent component : components) {
component.showInfo();
}
}
}
// 员工(叶子节点类)
public class Employee extends OrganizationComponent {
public Employee(String name, String type) {
super(name, type);
}
@Override
public void showInfo() {
System.out.println(type + ": " + name);
}
}
public class Client {
public static void main(String[] args) {
// 创建员工
Employee emp1 = new Employee("张三", "员工");
Employee emp2 = new Employee("李四", "员工");
Employee emp3 = new Employee("王五", "员工");
Employee emp4 = new Employee("赵六", "员工");
Employee emp5 = new Employee("孙七", "员工");
// 创建部门并添加员工
Department department1 = new Department("研发部", "部门");
department1.add(emp1);
department1.add(emp2);
Department department11 = new Department("研发一部", "部门");
department11.add(emp4);
department1.add(department11);
Department department12 = new Department("研发二部", "部门");
department12.add(emp5);
department1.add(department12);
Department department2 = new Department("市场部", "部门");
department2.add(emp3);
// 创建公司并添加部门
Department company = new Department("公司", "公司");
company.add(department1);
company.add(department2);
// 显示公司信息
company.showInfo();
}
}
输出:
公司: 公司
部门: 研发部
员工: 张三
员工: 李四
部门: 研发一部
员工: 赵六
部门: 研发二部
员工: 孙七
部门: 市场部
员工: 王五
优缺点
优点:
- 高层模块调用简单:客户端可以一致地使用组合结构中的所有对象,而不需要关注具体的对象类型。
缺点:
- 设计较复杂:如果设计不当,可能导致类层次结构变得复杂。
- 难以限制组合中的组件:无法对组件中包含哪些对象进行约束,比如强制组合节点必须包含叶子节点。
思考
目前感觉组合模式不大实用
享元模式
概述
享元模式(Flyweight Pattern)是一种结构型设计模式,旨在减少大量对象的创建和维护成本。其核心思想是通过共享对象来减少内存使用和提高性能,特别适用于需要创建大量相似对象的场景。
结构
享元模式的核心思想是将状态分为内在状态 和外在状态 :
- 内在状态 :存储在享元对象内部的状态,通常是固定不变的,可以共享的部分。
- 外在状态 :由客户端传递给享元对象的外部状态,因上下文不同而变化,不能共享的部分。
享元模式的结构通常包括以下几个部分:
- 享元接口 :为具体享元对象定义接口,声明需要实现的公共方法,外部状态会通过这些方法传入。
- 具体享元类 :实现享元接口,负责处理内部的内在状态,并接收外部状态。
- 享元工厂 :管理享元对象的创建和共享,保证相同的内在状态只创建一次享元对象。
- 客户端 :客户端负责传递外在状态,并调用享元对象的方法。
实现步骤
- 定义享元接口:创建一个接口,声明所有享元对象的共有操作。
- 实现具体享元类:实现享元接口,提供具体的共享行为。
- 创建享元工厂:负责创建和管理享元对象,根据需要决定是返回一个已存在的享元对象还是创建一个新的。
使用场景
- 需要大量对象:例如图形处理应用中的字符、单词或图形。
- 对象的重复性高:相同对象的创建和销毁频繁, 典型案例: 线程池。
- 内存消耗大:需要减少内存使用,提升系统性能。
代码案例
围棋播放案例
// 享元接口,定义了棋子的操作
public interface ChessPiece {
// 外部状态,如位置
void display(String position);
}
// 具体享元角色
public class ConcreteChessPiece implements ChessPiece {
// 共享的状态,如颜色
private String color;
// 构造函数接收共享的状态
public ConcreteChessPiece(String color) {
this.color = color;
}
// 实现享元接口的方法
@Override
public void display(String position) {
System.out.println("播报: " + color + "棋, 位置:" + position);
}
}
// 享元工厂角色
public class ChessPieceFactory {
private Map<String, ChessPiece> chessPieceMap = new HashMap<>();
// 获取棋子对象
public ChessPiece getChessPiece(String color) {
ChessPiece piece = chessPieceMap.get(color);
if (piece == null) {
piece = new ConcreteChessPiece(color);
chessPieceMap.put(color, piece);
}
return piece;
}
// 获取棋子对象的数量
public int getChessPieceCount() {
return chessPieceMap.size();
}
}
public class Client {
public static void main(String[] args) {
ChessPieceFactory factory = new ChessPieceFactory();
// 获取棋子对象,并设置不同的位置
ChessPiece whitePawn = factory.getChessPiece("白");
// 位置由客户端管理
whitePawn.display("A2");
// 在相同的棋子对象上设置不同的位置
whitePawn.display("A4");
ChessPiece blackPawn = factory.getChessPiece("黑");
blackPawn.display("B7");
System.out.println("生成的棋子总数: " + factory.getChessPieceCount());
}
}
运行结果:
播报: 白棋,位置:A2
播报: 白棋,位置:A4
播报: 黑棋,位置:B7
生成的棋子总数: 2
优缺点
优点:
- 减少内存使用:通过共享对象,降低了内存占用。
- 提高性能:重用相同对象,减少了对象的创建和销毁开销。
- 灵活性:可以动态管理和配置享元对象。
缺点:
- 增加复杂度:需要管理享元对象的共享和状态,可能会增加系统的复杂度。
- 可能影响性能:当享元对象的创建和管理过于复杂时,可能会对性能产生负面影响。
行为型模式
模板方法模式
概述
对于一些业务, 执行流程(步骤)有着高度的相似
例如去银行办理业务(存钱/取钱/转账)一般要经过以下4个流程:取号、排队、办理具体业务、对银行工作人员进行评分等,其中取号、排队和对银行工作人员进行评分的业务对每个客户是一样的,可以在父类中实现,但是办理具体业务却因人而异,它可能是存款、取款或者转账等,可以延迟到子类中实现。
又或者是炒菜, 都是起锅、 烧油、 放菜、放调料、翻炒, 不同的是炒不同的菜需要放不同的菜和不同的调料
模板方法模式就是一个定义操作中的算法骨架,而对于算法骨架中, 不同业务的不同步骤延迟到子类中实现,使得子类可以不改变该算法骨架的情况下重定义该算法的某些特定步骤。
结构
模板方法模式包含以下主要角色:
抽象类:负责给出算法的轮廓和骨架。它由一个模板方法和若干个基本方法构成
模板方法:定义了算法骨架,也就是根据业务骨架按照指定顺序调用其包含的基本方法
基本方法:算法骨架中按照顺序被调用的方法, 是模板方法的组成部分
抽象方法:不同业务需要执行特定的业务, 由具体子类实现
具体方法:不同业务某些步骤可能一模一样, 在父类中实现,子类可以直接继承也可以进行覆盖重写
钩子方法:用于判断的逻辑方法和需要子类重写的空方法两种, 一般钩子方法是用于判断的逻辑方法,这类方法名一般为isXxx,返回值类型为boolean类型
具体子类:根据具体业务, 实现抽象类中所定义的抽象方法和钩子方法
案例实现
// 银行业务模板(抽象类)
public abstract class BankBusiness {
protected User user;
// 办理业务的用户
public BankBusiness(User user) {
this.user = user;
}
// 办理业务(取钱/存钱/转账等)的流程模板
public final void doBusiness() {
// 取号
getTheNumber();
// 排队
waitInLine();
// 执行业务
doBankBusiness();
// 是否需要评价服务
if (shouldEvaluateService()) {
// 评价服务
evaluateService();
}
}
// 取号, 在不同业务中都一样, 在父类中实现
public void getTheNumber() {
System.out.println(user.getName() + "取号成功, 请您在5分钟内完成本次业务");
}
// 排队, 在不同业务中都一样, 在父类中实现
public void waitInLine() {
System.out.println(user.getName() + "排队成功");
}
// 执行业务, 不同的业务不同实现, 定义成抽象, 由子类自己实现
public abstract void doBankBusiness();
// 排队, 在不同业务中都一样, 在父类中实现
public void evaluateService() {
System.out.println(user.getName() + "本次业务已完成, 请您评价本次服务");
}
// 钩子方法: 是否需要评价服务, 提供一个默认实现, 子类可选择重写
public boolean shouldEvaluateService() {
return true;
}
}
// 取钱业务
public class WithdrawMoney extends BankBusiness {
private Double money;
public WithdrawMoney(User user, Double money) {
super(user);
this.money = money;
}
@Override
public void doBankBusiness() {
System.out.println(user.getName() + "正在取钱...");
System.out.println(user.getName() + "取钱" + money + "元");
System.out.println(user.getName() + "取钱结束...");
}
}
// 存钱业务
public class DepositMoney extends BankBusiness {
private Double money;
public DepositMoney(User user, Double money) {
super(user);
this.money = money;
}
@Override
public void doBankBusiness() {
System.out.println(user.getName() + "正在存钱...");
System.out.println(user.getName() + "存钱" + money + "元");
System.out.println(user.getName() + "存钱结束...");
}
}
// 转账业务
public class TransferMoney extends BankBusiness {
private User toUser;
private Double money;
public TransferMoney(User user, User toUser, Double money) {
super(user);
this.toUser = toUser;
this.money = money;
}
@Override
public void doBankBusiness() {
System.out.println(user.getName() + "正在转账...");
System.out.println(user.getName() + "转账" + money + "元到" + toUser.getName() + "账户上");
System.out.println(user.getName() + "转账结束...");
}
}
// 用户
public class User {
private String name;
public User(String name) {
this.name = name;
}
public String getName() {
return this.name;
}
}
// 测试
public class Client {
public static void main(String[] args) {
// 张三办理存钱
User user1 = new User("张三");
BankBusiness bankBusiness1 = new DepositMoney(user1, 2000.0);
bankBusiness1.doBankBusiness();
System.out.println("====================");
// 李四办理取钱
User user2 = new User("李四");
BankBusiness bankBusiness2 = new WithdrawMoney(user2, 3000.36);
bankBusiness2.doBankBusiness();
System.out.println("====================");
// 王五给张三转账1000元
User user3 = new User("王五");
BankBusiness bankBusiness3 = new TransferMoney(user3, user1, 1000.0);
bankBusiness3.doBankBusiness();
}
}
运行结果:
张三正在存钱...
张三存钱2000.0元
张三存钱结束...
====================
李四正在取钱...
李四取钱3000.36元
李四取钱结束...
====================
王五正在转账...
王五转账1000.0元到张三账户上
王五转账结束...
实现步骤
模板方法模式适用于以下场景:
- 多个子类有相同的行为逻辑,但实现细节不同。
- 需要把算法的步骤固定下来,但允许子类对某些步骤进行定制。
- 需要防止代码重复,通过定义通用模板来复用代码。
优缺点
优点:
-
提高代码复用性
将相同部分的代码放在抽象的父类中,而将不同的代码放入不同的子类中。
-
实现了反向控制
通过一个父类调用其子类的操作,通过对子类的具体实现扩展不同的行为,实现了反向控制 ,并符合“开闭原则”。
缺点:
- 对每个不同的实现都需要定义一个子类,这会导致类的个数增加,系统更加庞大,设计也更加抽象。
- 父类中的抽象方法由子类实现,子类执行的结果会影响父类的结果,这导致一种反向的控制结构,它提高了代码阅读的难度。
策略模式
概述
做同一件事, 但是用到的方法(策略/算法)不一样, 但是都达到了最终效果, 比如支付方式
相同的前提条件, 传入不同的策略, 得到不同的结果, 如: 相同的集合, 根据传入排序策略的不同, 得到不同的结果
结构
策略模式的主要角色如下:
- 抽象策略类:由一个接口或抽象类实现。给出定义策略的接口
- 具体策略类:实现了抽象策略类定义的接口,提供具体的算法实现或行为
- 持有策略引用的类:持有一个策略类的引用,给客户端调用(可有可无)
代码案例
// 定义策略接口
interface PaymentStrategy {
// 支付方法
void pay(int amount);
}
// 支付宝支付(实现策略类)
public class AliPay implements PaymentStrategy {
@Override
public void pay(int amount) {
System.out.println("使用支付宝支付 " + amount + " 元");
}
}
// 微信支付(实现策略类)
public class WeChatPay implements PaymentStrategy {
@Override
public void pay(int amount) {
System.out.println("使用微信支付 " + amount + " 元");
}
}
// 上下文类, 持有策略接口的引用
public class PaymentContext {
// 持有策略的引用
private PaymentStrategy paymentStrategy;
// 设置策略
public void setPaymentStrategy(PaymentStrategy paymentStrategy) {
this.paymentStrategy = paymentStrategy;
}
// 执行支付
public void pay(int amount) {
if (paymentStrategy == null) {
throw new IllegalStateException("未设置支付策略");
}
paymentStrategy.pay(amount);
}
}
// 测试类
public class Client {
public static void main(String[] args) {
PaymentContext context = new PaymentContext();
// 使用微信支付
context.setPaymentStrategy(new WeChatPay());
context.pay(100);
// 切换到支付宝支付
context.setPaymentStrategy(new AliPay());
context.pay(200);
}
}
运行结果:
使用微信支付 100 元
使用支付宝支付 100 元
优缺点
1,优点:
- 策略类之间可以自由切换: 由于策略类都实现同一个接口,所以使它们之间可以自由切换。
- 易于扩展: 增加一个新的策略只需要添加一个具体的策略类即可,基本不需要改变原有的代码,符合“开闭原则“
2,缺点:
- 客户端必须知道所有的策略类,并自行决定使用哪一个策略类。
- 策略模式将造成产生很多策略类,可以通过使用享元模式在一定程度上减少对象的数量。
使用场景
- 一个系统需要动态地在几种算法中选择一种时,可将每个算法封装到策略类中。
- 系统中各算法彼此完全独立,且要求对客户隐藏具体算法的实现细节时。
命令模式
概述
命令模式通过创建命令对象来实现请求的封装。每个命令对象都包含一个执行请求的操作。这些命令对象遵循统一的接口,使得调用者可以通过执行命令对象的方法来请求操作,而无需了解具体的执行细节。
结构(以开关灯为例)
命令模式主要涉及以下几个角色:
- 命令抽象接口:定义一个接口(一般是执行命令excute() )
- 接收者类:执行命令的接收者(灯), 接收者里定义了自己的各种方法(开灯, 关灯方法)
- 具体命令类:实现命令接口(开灯具体类/关灯具体类),命令中调用接收者(灯)完成相应的操作(开灯/关灯)。
- 调用者类:(遥控器)持有一个命令对象(开灯命令类/关灯命令类)的引用, 要求该命令(开灯命令类/关灯命令类)执行这个请求(开灯/关灯)。
- 客户端:将接收者对象绑定到具体命令对象上。然后将命令对象传递给调用者。
代码案例
// 命令接口
public interface Command {
// 执行命令
void execute();
}
// 接收者类
public class Light {
public void turnOn() {
System.out.println("灯已打开");
}
public void turnOff() {
System.out.println("灯已关闭");
}
}
// 具体命令类:开灯命令
public class LightOnCommand implements Command {
private Light light;
public LightOnCommand(Light light) {
this.light = light;
}
@Override
public void execute() {
light.turnOn();
}
}
// 具体命令类:关灯命令
public class LightOffCommand implements Command {
private Light light;
public LightOffCommand(Light light) {
this.light = light;
}
@Override
public void execute() {
light.turnOff();
}
}
// 调用者类:遥控器
public class RemoteControl {
private Command command;
public void setCommand(Command command) {
this.command = command;
}
// 按下按钮, 执行命令
public void pressButton() {
command.execute();
}
}
// 客户端
public class Client {
public static void main(String[] args) {
// 创建接收者(卧室灯)
Light livingRoomLight = new Light();
// 创建具体命令对象(开灯命令)
Command lightOn = new LightOnCommand(livingRoomLight);
// 创建具体命令(关灯命令)
Command lightOff = new LightOffCommand(livingRoomLight);
// 创建调用者(遥控器)
RemoteControl remote = new RemoteControl();
// 设置命令为开灯
remote.setCommand(lightOn);
// 执行开灯命令
remote.pressButton();
// 设置命令为关灯
remote.setCommand(lightOff);
// 执行关灯命令
remote.pressButton();
}
}
运行结果:
灯已打开
灯已关闭
优缺点
优点:
- 降低系统的耦合度。命令模式能将调用操作的对象与实现该操作的对象解耦。
- 增加或删除命令非常方便。采用命令模式增加与删除命令不会影响其他类,它满足“开闭原则”,对扩展比较灵活。
- 可以实现宏命令。命令模式可以与组合模式结合,将多个命令装配成一个组合命令,即宏命令。
- 方便实现 Undo 和 Redo 操作。命令模式可以与后面介绍的备忘录模式结合,实现命令的撤销与恢复。
缺点:
- 使用命令模式可能会导致某些系统有过多的具体命令类。
- 系统结构更加复杂。
使用场景
- 系统需要将请求调用者和请求接收者解耦,使得调用者和接收者不直接交互。
- 系统需要在不同的时间指定请求、将请求排队和执行请求(调用者持有命令的集合对象, 并依次执行)。
- 系统需要支持命令的撤销(Undo)操作和恢复(Redo)操作(后面备忘录模式介绍)。
责任链模式
概述
责任链模式(Chain of Responsibility Pattern)是一种行为设计模式,它允许将请求沿着处理链传递,让链上的每个处理者都有机会参与处理(要不要在某个处理者中断传递根据业务来)。
结构
责任链模式有以下角色:
处理者抽象接口:可以是抽象类或接口, 定义一个处理请求的接口。有一个指向下一个处理者的引用。
具体处理者(实现类):实现Handler接口的具体处理者,通过引用的处理者对象调用接口实现链的传递。
代码示例
// 处理者接口
public abstract class Handler {
// 下一个处理者
protected Handler nextHandler;
public void setNextHandler(Handler nextHandler) {
this.nextHandler = nextHandler;
}
// 处理请求的方法
public abstract void handleRequest(int requestDays);
}
// 直接领导
public class LineManager extends Handler {
@Override
public void handleRequest(int requestDays) {
if (requestDays > 0 && requestDays <= 3) {
System.out.println("直接领导批准了请假申请,天数:" + requestDays);
} else {
System.out.println("请假天数大于3天,转给部门经理处理");
if (nextHandler != null) {
nextHandler.handleRequest(requestDays);
}
}
}
}
// 部门领导
public class DepartmentManager extends Handler {
@Override
public void handleRequest(int requestDays) {
if (requestDays > 3 && requestDays <= 5) {
System.out.println("部门经理批准了请假申请,天数:" + requestDays);
} else {
System.out.println("请假天数大于5天,转给人力资源部处理");
if (nextHandler != null) {
nextHandler.handleRequest(requestDays);
}
}
}
}
// 人力资源部处理者
public class HRDepartment extends Handler {
@Override
public void handleRequest(int requestDays) {
if (requestDays > 3 && requestDays <= 7) {
System.out.println("人力资源部批准了请假申请,天数:" + requestDays);
} else {
System.out.println("请假天数大于7天,不允许请假");
if (nextHandler != null) {
nextHandler.handleRequest(requestDays);
}
}
}
}
// 测试类
public class Client {
public static void main(String[] args) {
// 创建处理者
Handler handler1 = new LineManager();
Handler handler2 = new DepartmentManager();
Handler handler3 = new HRDepartment();
// 设置责任链
handler1.setNextHandler(handler2);
handler2.setNextHandler(handler3);
System.out.println("请假2天的情况==========>>>>>>");
handler1.handleRequest(2);
System.out.println("==============================");
System.out.println("请假4天的情况==========>>>>>>");
handler1.handleRequest(4);
System.out.println("==============================");
System.out.println("请假6天的情况==========>>>>>>");
handler1.handleRequest(6);
System.out.println("==============================");
System.out.println("请假8天的情况==========>>>>>>");
handler1.handleRequest(8);
}
}
运行结果:
请假2天的情况==========>>>>>>
直接领导批准了请假申请,天数:2
==============================
请假4天的情况==========>>>>>>
请假天数大于3天,转给部门经理处理
部门经理批准了请假申请,天数:4
==============================
请假6天的情况==========>>>>>>
请假天数大于3天,转给部门经理处理
请假天数大于5天,转给人力资源部处理
人力资源部批准了请假申请,天数:6
==============================
请假8天的情况==========>>>>>>
请假天数大于3天,转给部门经理处理
请假天数大于5天,转给人力资源部处理
请假天数大于7天,不允许请假
优缺点
优点:
- 降低耦合度:请求发送者和处理者之间的耦合度降低,使得修改处理者链中的处理者或添加新处理者变得更加容易。
- 增强灵活性:处理者的职责可以动态地更改或添加,这使得系统更加灵活。
- 职责链结构清晰:每个处理者都只处理自己关心的部分,职责分离清晰。
缺点:
- 可能导致请求传递时间过长:如果链条很长,处理请求可能需要经过多个处理者,可能会导致性能问题。
- 链条过于复杂:如果处理者之间的关系复杂,链条可能会变得难以维护和理解。
使用场景
- 需要在多个对象中选择一个处理者来处理请求时:例如,日志记录系统中,根据日志级别选择适当的处理器(如控制台输出、文件输出等)。
- 请求处理者的数量和顺序可能会动态变化时:例如,审批流程中,处理者可以根据情况动态增加或调整。
- 需要将请求处理责任分布到多个处理者上时:例如,订单处理系统中,不同的处理者负责不同类型的订单(如普通订单、急单订单等)。
思考
可以优化结构, 比如将处理者加到队列里, 按照顺序执行, 这样就不用每个处理者指定指定自己的下个处理者了, 代码更加优雅
状态模式
概述
状态模式,允许对象在多种状态之间切换,且每种状态下切换到其他状态下, 行为会有所不同。
状态模式的核心思想是将不同状态的行为封装到不同的状态类中,状态转换由这些类进行管理。
使用场景
状态模式适用于当一个对象在其生命周期中会经历多种不同状态,并且在不同状态下其行为有所不同的情况。生活中有很多类似的例子,例如:电梯的运行状态
电梯有多个状态,如“运行状态”、“停止状态”、“开门状态”和“关门状态”。在不同状态下,电梯的行为是不同的:
- 在“开门状态”下,电梯可以关门, 不能上行下行。
- 在“关门状态”下,电梯可以开门, 不能上行下行。
- 在“上行状态”下,电梯不可以开门。
- 在“下行状态”下,电梯不可以开门。
每个状态都有不同的行为,电梯会根据状态的变化,表现出不同的行为,这就是状态模式的典型应用场景。
结构
状态模式的主要组成部分如下:
- 抽象状态类 :定义所有切换状态的抽象方法(开门/关门/上行/下行)。
- 具体状态类 :实现抽象状态接口或抽象类,并实现
当前状态下的切换状态(开门/关门/上行/下行)的逻辑。 - 上下文类 :(电梯)持有一个
当前状态的引用,并且负责状态的切换。
代码案例
电梯的一些细节, 如上行/下行到指定楼层等省略...
// 电梯状态接口
public interface ElevatorState {
// 开门
void openDoor(Elevator elevator);
// 关门
void closeDoor(Elevator elevator);
// 上行
void moveUp(Elevator elevator);
// 下行
void moveDown(Elevator elevator);
}
// 关门状态下
public class DoorCloseState implements ElevatorState {
@Override
public void openDoor(Elevator elevator) {
System.out.println("电梯门打开了...");
// 切换到开门状态
elevator.setState(new DoorOpenState());
}
@Override
public void closeDoor(Elevator elevator) {
// 待机状态下, 关门不进行操作
System.out.println("电梯已经是关门状态了...");
}
@Override
public void moveUp(Elevator elevator) {
System.out.println("电梯开始上行...");
// 切换到上行状态
elevator.setState(new MovingUpState());
}
@Override
public void moveDown(Elevator elevator) {
System.out.println("电梯开始下行...");
// 切换到下行状态
elevator.setState(new MovingDownState());
}
}
// 开门状态下
public class DoorOpenState implements ElevatorState {
@Override
public void openDoor(Elevator elevator) {
System.out.println("电梯已经是开门状态了...");
}
@Override
public void closeDoor(Elevator elevator) {
System.out.println("电梯门关闭了...");
// 切换到关门状态
elevator.setState(new DoorCloseState());
}
@Override
public void moveUp(Elevator elevator) {
System.out.println("开门状态无法上行...");
}
@Override
public void moveDown(Elevator elevator) {
System.out.println("开门状态无法下行...");
}
}
// 上行状态下
public class MovingUpState implements ElevatorState {
@Override
public void openDoor(Elevator elevator) {
System.out.println("电梯上行中,无法开门...");
}
@Override
public void closeDoor(Elevator elevator) {
System.out.println("电梯上行中,无法关门...");
}
@Override
public void moveUp(Elevator elevator) {
System.out.println("电梯已经是上行状态了...");
}
@Override
public void moveDown(Elevator elevator) {
System.out.println("电梯上行中,无法下行,,,");
}
}
// 下行状态下
public class MovingDownState implements ElevatorState {
@Override
public void openDoor(Elevator elevator) {
System.out.println("电梯下行中,无法开门...");
}
@Override
public void closeDoor(Elevator elevator) {
System.out.println("电梯下行中,无法关门...");
}
@Override
public void moveUp(Elevator elevator) {
System.out.println("电梯下行中,无法上行...");
}
@Override
public void moveDown(Elevator elevator) {
System.out.println("电梯已经是下行状态了...");
}
}
// 电梯类(上下文)
public class Elevator {
// 当前状态
private ElevatorState currentState;
public Elevator() {
// 初始状态为关门状态
currentState = new DoorCloseState();
}
// 设置电梯的状态
public void setState(ElevatorState state) {
currentState = state;
}
// 电梯操作方法,调用当前状态的行为
public void openDoor() {
currentState.openDoor(this);
}
public void closeDoor() {
currentState.closeDoor(this);
}
public void moveUp() {
currentState.moveUp(this);
}
public void moveDown() {
currentState.moveDown(this);
}
}
// 测试类
public class Client {
public static void main(String[] args) {
// 电梯初始状态为待命状态
Elevator elevator = new Elevator();
// 电梯开门
elevator.openDoor();
// 电梯关门
elevator.closeDoor();
// 电梯上行
elevator.moveUp();
// 电梯下行
elevator.moveDown();
}
}
运行结果:
电梯默认是关门状态...
电梯门打开了...
电梯门关闭了...
电梯开始上行...
电梯上行中,无法下行,,,
优缺点
优点:
- 将所有与某个状态有关的行为放到一个类中,并且可以方便地增加新的状态。
- 简化复杂条件判断 :避免了大量的
if-else或switch-case语句,代码更加清晰易读。
缺点:
- 状态模式的使用必然会增加系统类和对象的个数。
- 状态模式的结构与实现都较为复杂,如果使用不当将导致程序结构和代码的混乱。
- 状态模式对"开闭原则"的支持并不太好(增加一个状态需要修改其他所有的状态)。
观察者模式(发布订阅模式)
概述
观察者模式是一种行为型设计模式,它定义了一种一对多的依赖关系,使得当一个对象状态发生改变时,所有依赖它的对象都会得到通知并自动更新。这种模式适用于需要事件通知的场景,例如GUI框架的事件处理、订阅-发布系统、数据变动通知等。
观察者模式允许对象之间建立一种发布-订阅的关系。一个对象(称为“主题”或“被观察者”)维护一组依赖它的对象(称为“观察者”),并在其状态发生变化时通知这些观察者。可以独立地增加观察者或改变主题。
使用场景
观察者模式适用于以下场景:
- 数据变化通知:数据变动通知, 例如,当股票价格变化时,所有关注该股票的应用程序都能立即收到更新。
- 新闻发布系统:例如,新闻发布系统发布新内容时,所有订阅的用户会收到通知。
生活案例:
案例1:新闻订阅系统
新闻发布平台是“主题”,用户是“观察者”。当平台发布新文章时,所有订阅的用户都会收到通知。
案例2:天气预报系统
天气预报中心是“主题”,用户设备或显示屏是“观察者”。当天气数据更新时,所有订阅的设备都会收到更新信息。
案例3:电商促销系统
电商平台是“主题”,用户是“观察者”。当有新的促销活动时,所有订阅的用户会收到通知。
结构
观察者模式包括以下几个部分:
- 主题(发布者)接口:被观察/被订阅的对象。引用一个观察者列表,当状态发生变化时通知所有观察者。
- 具体主题:实现主题接口,当状态变化时通知所有观察者。
- 观察者(订阅者)接口:定义一个更新(查看推送内容)的接口,用于接收主题的通知。
- 具体观察者:实现观察者接口,接收到通知后处理相关业务。
代码案例
以下是基于新闻订阅系统的观察者模式示例代码:
// 新闻接口(发布者)
public interface NewsPublisher {
// 注册观察者
void registerSubscriber(Subscriber subscriber);
// 移除观察者
void removeSubscriber(Subscriber subscriber);
// 通知观察者
void notifySubscribers(String news);
}
// 新闻联播(具体的发布者)
public class CCTVNewsChannel implements NewsPublisher {
private List<Subscriber> subscribers = new ArrayList<>();
@Override
public void registerSubscriber(Subscriber subscriber) {
System.out.println("新闻联播新增订阅者: " + subscriber.getName());
subscribers.add(subscriber);
}
@Override
public void removeSubscriber(Subscriber subscriber) {
System.out.println("新闻联播移除订阅者: " + subscriber.getName());
subscribers.remove(subscriber);
}
@Override
public void notifySubscribers(String news) {
for (Subscriber subscriber : subscribers) {
subscriber.update(news);
}
}
// 发布新闻
public void publishNews(String news) {
System.out.println("新闻联播播报新闻: " + news);
notifySubscribers(news);
}
}
// 订阅新闻的对象(订阅者)
public interface Subscriber {
// 获取订阅者的名字
String getName();
// 接收主题的通知
void update(String news);
}
// 三好市民(具体的订阅者)
public class UserSubscriber implements Subscriber {
private String name;
public UserSubscriber(String name) {
this.name = name;
}
@Override
public String getName() {
return name;
}
@Override
public void update(String news) {
System.out.println(name + " 接收到新闻: " + news);
}
}
// 测试类
public class Client {
public static void main(String[] args) {
// 创建主题
CCTVNewsChannel newsPublisher = new CCTVNewsChannel();
// 创建观察者
Subscriber user1 = new UserSubscriber("张三");
Subscriber user2 = new UserSubscriber("李四");
// 张三和李四订阅了新闻(注册一个观察者)
newsPublisher.registerSubscriber(user1);
newsPublisher.registerSubscriber(user2);
// 新闻联播发布新闻
newsPublisher.publishNews("发布2024节假日通知");
// 张三取消订阅新闻(移除一个观察者)
newsPublisher.removeSubscriber(user1);
// 再次发布新闻
newsPublisher.publishNews("经济数据发布");
}
}
运行结果:
新闻联播新增订阅者: 张三
新闻联播新增订阅者: 李四
新闻联播播报新闻: 发布2024节假日通知
张三 接收到新闻: 发布2024节假日通知
李四 接收到新闻: 发布2024节假日通知
新闻联播移除订阅者: 张三
新闻联播播报新闻: 经济数据发布
李四 接收到新闻: 经济数据发布
优缺点
优点:
- 低耦合:主题和观察者之间是低耦合的,可以独立演变。
- 支持广播通信:一个主题可以通知多个观察者。
- 动态添加观察者:可以在运行时动态添加或移除观察者。
缺点:
- 可能引起性能问题:如果观察者较多,那么通知所有的观察者可能会比较耗时。
- 可能导致循环依赖:特别是当观察者本身也会作为主题被观察时,可能会形成循环依赖或通知风暴。
中介者模式
概述
中介者模式定义了一个中介者对象来封装对象之间的交互。对象不再直接相互引用,只需与中介者打交道即可, 减少冗余,使得系统的可维护性和可扩展性更好。
使用场景
中介者模式常用于处理对象之间复杂的交互场景。以下是几个通俗易懂的生活案例:
- 机场塔台管理飞机起飞降落:在机场中,每架飞机不能直接与其他飞机沟通以协调起飞或降落,而是通过塔台这个中介者来进行沟通和调度,确保不会发生冲突。
- 聊天室中的消息分发:在聊天室中,用户之间不会直接通信,而是通过服务器(中介者)进行消息转发。服务器负责接收和转发用户消息,使得用户之间可以通信。
- 智能家居控制中心:在智能家居系统中,各种设备(灯、空调、电视等)通过一个控制中心(中介者)来实现相互的联动和控制,而不需要设备之间直接通信。
结构
中介者模式的结构,以聊天室中的消息分发为例:
- 中介者接口:定义了同事对象之间的通信接口。
- 具体中介者:实现中介者接口,协调各个同事对象之间的通信。
- 同事类接口:定义同事类的接口。
- 具体同事类:实现同事类接口,通过中介者与其他同事类进行通信。
代码案例
以下是聊天室中消息分发的代码示例:
// 抽象中介者
public interface ChatRoom {
/**
* 发送消息
*
* @param message 消息
* @param user 发送人
*/
void sendMessage(String message, Colleague user);
/**
* 增加用户
*
* @param user 用户
*/
void addUser(Colleague user);
}
// 聊天室(具体中介者)
public class ChatRoomImpl implements ChatRoom {
// 维护用户列表, 用于群发消息
private List<Colleague> users = new ArrayList<>();
@Override
public void addUser(Colleague user) {
users.add(user);
}
@Override
public void sendMessage(String message, Colleague user) {
for (Colleague u : users) {
// 消息不发给发送者自己
if (u != user) {
u.receiveMessage(message);
}
}
}
}
// 同事类
public abstract class Colleague {
protected ChatRoom chatRoom;
protected String name;
public Colleague(ChatRoom chatRoom, String name) {
this.chatRoom = chatRoom;
this.name = name;
}
/**
* 发送消息
*
* @param message 消息
*/
public abstract void sendMessage(String message);
/**
* 接收消息
*
* @param message 消息
*/
public abstract void receiveMessage(String message);
}
// 用户(具体同事类)
public class User extends Colleague {
public User(ChatRoom chatRoom, String name) {
super(chatRoom, name);
}
@Override
public void sendMessage(String message) {
System.out.println(name + " 发送消息: " + message);
chatRoom.sendMessage(message, this);
}
@Override
public void receiveMessage(String message) {
System.out.println(name + " 接收到消息: " + message);
}
}
// 测试类
public class Client {
public static void main(String[] args) {
ChatRoom chatRoom = new ChatRoomImpl();
User user1 = new User(chatRoom, "小明");
User user2 = new User(chatRoom, "小华");
User user3 = new User(chatRoom, "小红");
chatRoom.addUser(user1);
chatRoom.addUser(user2);
chatRoom.addUser(user3);
user1.sendMessage("大家好, 我是小明");
System.out.println("============================");
user2.sendMessage("大家好, 我是小明!");
System.out.println("============================");
user3.sendMessage("大家好, 我是小华!");
}
}
运行结果:
小明 发送消息: 大家好, 我是小明
小华 接收到消息: 大家好, 我是小明
小红 接收到消息: 大家好, 我是小明
============================
小华 发送消息: 大家好, 我是小明!
小明 接收到消息: 大家好, 我是小明!
小红 接收到消息: 大家好, 我是小明!
============================
小红 发送消息: 大家好, 我是小华!
小明 接收到消息: 大家好, 我是小华!
小华 接收到消息: 大家好, 我是小华!
优缺点
优点:
- 减少了对象之间的直接依赖,使得类之间的耦合性降低,系统更灵活、更易维护。
- 通过集中控制复杂的对象交互逻辑,可以更容易地修改和扩展系统行为。
缺点:
- 中介者本身的复杂性可能会随着对象交互的复杂性增加而变得复杂,导致中介者成为一个“上帝类”。
- 如果不小心设计,中介者可能会变成系统的单点故障,影响整体的可靠性。
迭代器模式
概述
迭代器模式用于在不暴露集合内部结构的情况下,遍历集合中的元素。
使用场景
迭代器模式在以下场景中非常有用:
- 需要访问一个集合对象的内容而无需暴露其内部细节:比如在一个书架上逐本取书,不需要知道书架的内部结构。
- 需要为不同的集合对象提供一种统一的遍历方式:比如有不同类型的书架或收纳柜,使用迭代器模式可以让它们提供统一的取物方式。
- 需要遍历的集合有多种类型,且需要支持多种遍历方式:比如对一本书可以从头到尾读,也可以按章节随机阅读。
结构
迭代器模式主要包括以下几个角色:
- 迭代器接口(Iterator) :需要定义了遍历元素的方法,例如
hasNext()和next()。 - 具体迭代器(Concrete Iterator) :实现迭代器接口,需要提供遍历集合中的元素的方法。
- 集合接口(Aggregate) :需要定义了创建迭代器的方法。
- 具体集合(Concrete Aggregate) :实现集合接口,需要实现返回具体迭代器实例的方法。
代码示例
以下是一个关于书架的迭代器模式的代码示例:
// 书的类,表示书架上的书
public class Book {
// 书名
private String name;
public Book(String name) {
// 构造函数
this.name = name;
}
public String getName() {
// 获取书名的方法
return name;
}
}
// 迭代器接口,定义遍历书的基本方法
public interface BookIterator {
// 判断是否有下一本书
boolean hasNext();
// 获取下一本书
Book next();
}
// 具体迭代器,实现书架上书的遍历
public class ConcreteBookIterator implements BookIterator {
// 书架的引用
private BookShelf bookShelf;
// 当前书的索引
private int index;
public ConcreteBookIterator(BookShelf bookShelf) {
// 初始化书架
this.bookShelf = bookShelf;
// 初始索引设为0
this.index = 0;
}
// 判断书架上是否还有书
public boolean hasNext() {
// 如果索引小于书的数量,则还有下一本书
return index < bookShelf.getLength();
}
// 获取书架上的下一本书
public Book next() {
// 根据索引获取书
Book book = bookShelf.getBookAt(index);
// 索引加1
index++;
// 返回当前的书
return book;
}
}
// 书架接口,定义创建迭代器的方法
interface BookShelf {
// 创建迭代器的方法
BookIterator iterator();
// 获取书的数量
int getLength();
// 根据索引获取书
Book getBookAt(int index);
}
// 具体书架,实现书架接口
class ConcreteBookShelf implements BookShelf {
// 书的数组
private Book[] books;
// 书架上最后一本书的索引
private int last = 0;
public ConcreteBookShelf(int maxSize) {
// 初始化书的数组
this.books = new Book[maxSize];
}
// 添加书到书架上
public void addBook(Book book) {
// 如果书架未满
if (last < books.length) {
// 将书放入数组中
books[last] = book;
last++; // 书的数量加1
} else {
throw new RuntimeException("书架已满, 无法存放!");
}
}
// 获取书的数量
public int getLength() {
// 返回书的数量
return last;
}
// 根据索引获取书
public Book getBookAt(int index) {
// 返回对应索引的书
return books[index];
}
// 创建书架的迭代器
public BookIterator iterator() {
// 返回具体的迭代器
return new ConcreteBookIterator(this);
}
}
// 测试类
public class Client {
public static void main(String[] args) {
// 创建一个可容纳5本书的书架
ConcreteBookShelf bookShelf = new ConcreteBookShelf(5);
// 添加书
bookShelf.addBook(new Book("三国演义"));
bookShelf.addBook(new Book("红楼梦"));
bookShelf.addBook(new Book("西游记"));
bookShelf.addBook(new Book("水浒传"));
// 获取书架的迭代器
BookIterator iterator = bookShelf.iterator();
// 遍历书架
while (iterator.hasNext()) {
// 获取下一本书
Book book = iterator.next();
// 打印书名
System.out.println("正在阅读: " + book.getName());
}
}
}
优缺点
优点:
- 简化集合遍历:使用迭代器模式可以统一不同集合的遍历方式,简化代码。
- 隐藏集合内部实现:不需要了解集合内部的实现细节,只需通过迭代器接口访问即可。
- 支持多种遍历方式:可以根据需求实现多种不同的迭代方式,比如顺序遍历、反向遍历、跳跃遍历等。
缺点:
- 开销增加:每个集合需要实现自己的迭代器类,可能会增加类的数量和复杂性。
- 访问速度较慢:相比于直接访问集合元素,迭代器可能会有性能开销。
- 并发问题:在多线程环境下使用迭代器需要特别注意同步问题,否则可能导致数据不一致或异常。
访问者模式
概述
访问者模式的核心思想是将对象结构和对该对象的操作分离,操作封装在访问者类中,而数据结构在各自的类中。对象可以接受访问者,将其自身作为参数传递给访问者,从而在访问者中实现拓展的功能
使用场景
访问者模式适用于以下场景:
- 当你需要对一些对象执行很多不同且不相关的操作时,可以使用访问者模式来避免操作逻辑的扩散。
- 当对象结构比较稳定,但需要在对象结构上定义新的操作时,访问者模式非常适用。
- 适用于数据结构和操作分开演变的场景。
结构
访问者模式的主要结构包括:
- 访问者:为需要访问(拓展方法)的对象定义一个访问操作。
- 具体访问者:实现访问者接口, 实现访问操作。
- 被访问对象:实现接受操作。
代码案例
// 访问者接口,定义访问不同商品的操作
public interface ProductVisitor {
// 访问水果
void visit(Fruit fruit);
// 访问蔬菜
void visit(Vegetable vegetable);
}
// 具体的商品访问者
public class ConcreteProductVisitor implements ProductVisitor {
// 实现访问水果的功效推广逻辑
public void visit(Fruit fruit) {
// 水果功效
System.out.println("水果: " + fruit.getName() + " 富含维C, 是身体的必须");
}
// 实现访问蔬菜的折扣逻辑
public void visit(Vegetable vegetable) {
// 蔬菜有20%的折扣
System.out.println("蔬菜: " + vegetable.getName() + " 折扣价格是" + (vegetable.getPrice() * 0.8));
}
}
// 具体元素 - 水果类
public class Fruit {
// 水果名称
private String name;
// 水果价格
private double price;
// 构造函数
public Fruit(String name, double price) {
this.name = name;
this.price = price;
}
// 核心: 定义接收访问者的方法, 并使用访问者调用访问方法
public void accept(ProductVisitor visitor) {
// 将自己传给访问者
visitor.visit(this);
}
// 获取水果名称
public String getName() {
return name;
}
// 获取水果价格
public double getPrice() {
return price;
}
}
// 具体元素 - 蔬菜类
public class Vegetable {
// 蔬菜名称
private String name;
// 蔬菜价格
private double price;
// 构造函数
public Vegetable(String name, double price) {
this.name = name;
this.price = price;
}
// 核心: 定义接收访问者的方法, 并使用访问者调用访问方法
public void accept(ProductVisitor visitor) {
// 将自己传给访问者
visitor.visit(this);
}
// 获取蔬菜名称
public String getName() {
return name;
}
// 获取蔬菜价格
public double getPrice() {
return price;
}
}
// 测试类
public class Client {
public static void main(String[] args) {
// 创建商品实例
Fruit apple = new Fruit("苹果", 10.0);
Vegetable cabbage = new Vegetable("卷心菜", 5.0);
// 创建商品的访问者
ProductVisitor productVisitor = new ConcreteProductVisitor();
// 接收访问者, 访问商品
apple.accept(productVisitor);
cabbage.accept(productVisitor);
}
}
运行结果:
水果: 苹果 富含维C, 是身体的必须
蔬菜: 卷心菜 折扣价格是4.0
优缺点
优点:
- 易于添加新的操作:只需增加新的访问者类即可。
- 分离无关的行为:将数据结构和作用于结构上的操作进行分离,避免代码逻辑混乱。
缺点:
- 难以增加新的元素:每当增加一个新的元素时,需要修改所有访问者类,违反了开闭原则。
- 较复杂:使得系统中的类和关系较为复杂,尤其是数据结构较为庞大时。
备忘录模式
概述
备忘录模式的主要目的是在不破坏封装的前提下捕获一个对象的内部状态,并在以后需要时能恢复该状态。这个模式主要涉及三个角色:
使用场景
备忘录模式非常适合需要保存对象的某个状态,以便在以后恢复的需求场景
生活案例:
- 文本编辑器的撤销功能:在编辑文本时,可以撤销最后的操作。每次操作前,文本编辑器会保存当前状态到一个备忘录,当需要撤销时,从备忘录中恢复到之前的状态。
- 游戏存档:玩家在游戏中可以随时存档和读档,存档时保存游戏的当前状态,读档时恢复到某个存档点的状态。
- 手机系统备份与恢复:在手机系统更新前,系统会备份当前数据,更新失败时可以恢复到更新前的状态。
结构
以“文本编辑器的撤销功能”为例,备忘录模式的结构可以如下:
- 发起人:
TextEditor,文本编辑器,它可以创建备忘录并恢复到备忘录保存的状态。 - 备忘录:
TextMemento,保存文本编辑器的状态。 - 管理者:
HistoryManager,负责保存多个备忘录对象。
代码案例
以下是文本编辑器撤销功能的完整代码示例:
// 发起人:文本编辑器
public class TextEditor {
// 当前文本内容
private String text;
// 设置文本内容
public void setText(String text) {
this.text = text;
}
// 获取当前文本内容
public String getText() {
return text;
}
// 创建备忘录,保存当前文本状态
public TextMemento createMemento() {
return new TextMemento(text);
}
// 从备忘录中恢复文本状态
public void restoreMemento(TextMemento memento) {
if (memento==null){
System.out.println("提示: 没有备忘录可以恢复!");
return;
}
this.text = memento.getText();
}
}
// 备忘录:保存文本状态
public class TextMemento {
// 备忘录中的文本内容
private final String text;
// 构造器,初始化时保存文本状态
public TextMemento(String text) {
this.text = text;
}
// 获取备忘录中的文本内容
public String getText() {
return text;
}
}
// 管理者:管理备忘录
public class HistoryManager {
// 保存多个备忘录对象
private final List<TextMemento> history = new ArrayList<>();
// 添加备忘录
public void saveMemento(TextMemento memento) {
history.add(memento);
}
// 获取最近的备忘录并移除
public TextMemento getLastMemento() {
if (!history.isEmpty()) {
return history.remove(history.size() - 1);
}
// 若没有历史记录,返回null
return null;
}
}
public class Client {
public static void main(String[] args) {
// 创建文本编辑器
TextEditor editor = new TextEditor();
// 创建历史记录管理者
HistoryManager history = new HistoryManager();
// 编辑文本
editor.setText("第一次编辑");
// 保存备忘录
history.saveMemento(editor.createMemento());
System.out.println("当前内容: " + editor.getText());
// 第二次编辑
editor.setText("第二次编辑");
// 保存备忘录
history.saveMemento(editor.createMemento());
System.out.println("当前内容: " + editor.getText());
// 第三次编辑
editor.setText("第三次编辑");
System.out.println("当前内容: " + editor.getText());
// 撤销一次(将上一次的副本塞到文本编辑器中)
editor.restoreMemento(history.getLastMemento());
System.out.println("撤销后内容: " + editor.getText());
// 再撤销一次(将上一次的副本塞到文本编辑器中)
editor.restoreMemento(history.getLastMemento());
System.out.println("再次撤销后内容: " + editor.getText());
// 再撤销一次(将上一次的副本塞到文本编辑器中)
editor.restoreMemento(history.getLastMemento());
System.out.println("再次撤销后内容: " + editor.getText());
}
}
优缺点
优点
- 封装性好:发起人的状态可以隐藏在备忘录中,不会被外界直接访问。
- 易于扩展:可以在不修改原有类的情况下增加管理备忘录的功能。
缺点
- 资源消耗:如果需要保存的状态很多且状态本身非常复杂,会占用较多的内存。
- 管理复杂:需要小心管理备忘录的生命周期,防止内存泄漏。
解析器模式
概述
解释器模式是一种行为型设计模式,用于定义语言的文法,并提供一个解释器来处理这些文法的解释。它的核心思想是为语言创建一个解释器,解释器可以解释语言的表达式,从而执行某些特定的操作。通常用于需要频繁对某种表达式求值的场景。
使用场景
解释器模式适用于以下场景:
- 需要自定义语言或语法, 解释形成大家能理解的东西, 例如自定义语言解释成标准的SQL查询语句
- 需要对某种语言或表达式进行解释的场景。
- 例如编译器、计算器、正则表达式解析等。
- 简单的语法分析场景,如简化的脚本语言、查询语言等。
结构
- 抽象表达式(Expression) :定义一个解释操作的接口。所有的终结符表达式和非终结符表达式都需要实现该接口。
- 终结符表达式:终结符表达式是最基本的表达式,它直接处理输入数据并返回结果,不依赖其他表达式。不能再被分解的最小单位。
- 非终结符表达式:终结符表达式是由多个其他表达式(包括终结符和非终结符)组合而成的表达式。它们表示更复杂的语法规则,并且通常需要递归地调用其他表达式的解释方法来完成自身的解释。
终结符表达式 vs 非终结符表达式
| 特性 | 终结符表达式 | 非终结符表达式 |
|---|---|---|
| 定义 | 处理基本的输入单元,直接返回结果。 | 通过组合其他表达式(包括终结符和非终结符)实现更复杂的操作。 |
| 在解析树中的角色 | 叶子节点,无法再分解。 | 内部节点,可以递归调用其他表达式。 |
| 复杂度 | 简单,通常只处理单一的输入。 | 复杂,需要递归地处理多个子表达式。 |
| 示例 | 数字、变量等单一元素。 | 加法、乘法等组合运算。 |
总结
终结符表达式和非终结符表达式是解释器模式中的核心组成部分。终结符表达式用于直接处理最基本的语法元素,而非终结符表达式则用于处理更复杂的语法结构,通过递归和组合来解释更复杂的表达式。通过这两种类型的表达式,我们可以构建一个强大的解析器,能够解释复杂的文法规则并执行相应的操作。
代码示例
案例: 自定义语言转化为标准的SQL查询语句
// 抽象表达式接口,定义解释方法
public interface Expression {
// 解释方法,将自定义语言的片段转换为 SQL 语句片段, 所有的解释器最终都解释为字符串返回
String interpret();
}
// 终结符表达式类:解析列名
public class ColumnExpression implements Expression {
private String column;
// 构造方法,接收列名
public ColumnExpression(String column) {
this.column = column;
}
// 实现解释方法,返回 SQL 格式的列名
@Override
public String interpret() {
// 直接返回列名
return column;
}
}
// 终结符表达式类:解析表名
public class TableExpression implements Expression {
private String table;
// 构造方法,接收表名
public TableExpression(String table) {
this.table = table;
}
// 实现解释方法,返回 SQL 格式的表名
@Override
public String interpret() {
// 直接返回表名
return table;
}
}
// 终结符表达式类:解析条件
public class ConditionExpression implements Expression {
private String condition;
// 构造方法,接收条件
public ConditionExpression(String condition) {
this.condition = condition;
}
// 实现解释方法,返回 SQL 格式的条件
@Override
public String interpret() {
// 直接返回条件
return condition;
}
}
// 非终结符表达式类:SELECT 表达式
public class SelectExpression implements Expression {
// 只允许列名表达式
private ColumnExpression columnExpression;
// 构造方法,接收列名表达式
public SelectExpression(ColumnExpression columnExpression) {
this.columnExpression = columnExpression;
}
// 实现解释方法,返回 SQL 格式的 SELECT 语句
@Override
public String interpret() {
// 返回 SELECT 部分的 SQL
return "SELECT " + columnExpression.interpret();
}
}
// 非终结符表达式类:FROM 表达式
public class FromExpression implements Expression {
// 只允许表名表达式
private TableExpression tableExpression;
// 构造方法,接收表名表达式
public FromExpression(TableExpression tableExpression) {
this.tableExpression = tableExpression;
}
// 实现解释方法,返回 SQL 格式的 FROM 语句
@Override
public String interpret() {
// 返回 FROM 部分的 SQL
return "FROM " + tableExpression.interpret();
}
}
// 非终结符表达式类:WHERE 表达式
public class WhereExpression implements Expression {
// 只允许条件表达式
private ConditionExpression conditionExpression;
// 构造方法,接收条件表达式
public WhereExpression(ConditionExpression conditionExpression) {
this.conditionExpression = conditionExpression;
}
// 实现解释方法,返回 SQL 格式的 WHERE 语句
@Override
public String interpret() {
// 返回 WHERE 部分的 SQL
return "WHERE " + conditionExpression.interpret();
}
}
// 测试:使用解释器将自定义查询语言转换为 SQL 语句
public class Client {
public static void main(String[] args) {
// 构造自定义查询的各个部分
// 列名:name
ColumnExpression column = new ColumnExpression("name");
// 表名:users
TableExpression table = new TableExpression("users");
// 条件:age > 30
ConditionExpression condition = new ConditionExpression("age > 30");
// 构造非终结符表达式:SELECT、FROM、WHERE
// SELECT name
Expression select = new SelectExpression(column);
// FROM users
Expression from = new FromExpression(table);
// WHERE age > 30
Expression where = new WhereExpression(condition);
// 拼接最终的 SQL 语句
String sqlQuery = select.interpret() + " " + from.interpret() + " " + where.interpret();
System.out.println("生成的 SQL 语句: " + sqlQuery);
}
}
运行结果:
生成的 SQL 语句: SELECT users FROM name WHERE age > 30
案例: 与或非解释器
// 抽象表达式类,定义逻辑表达式解释的接口
public interface BooleanExpression {
// 解释方法,返回布尔值结果
boolean interpret();
}
// 终结符表达式类:表示布尔值
public class BooleanValueExpression implements BooleanExpression {
private boolean value;
// 构造方法,接收布尔值
public BooleanValueExpression(boolean value) {
this.value = value;
}
// 实现解释方法,直接返回布尔值
@Override
public boolean interpret() {
return value;
}
}
// 非终结符表达式类:逻辑与(AND)运算
public class AndExpression implements BooleanExpression {
// 左侧表达式
private BooleanExpression leftExpression;
// 右侧表达式
private BooleanExpression rightExpression;
// 构造方法,接收两个布尔表达式
public AndExpression(BooleanExpression leftExpression, BooleanExpression rightExpression) {
this.leftExpression = leftExpression;
this.rightExpression = rightExpression;
}
// 实现解释方法,返回两个表达式的逻辑与结果
@Override
public boolean interpret() {
return leftExpression.interpret() && rightExpression.interpret();
}
}
// 非终结符表达式类:逻辑或(OR)运算
public class OrExpression implements BooleanExpression {
// 左侧表达式
private BooleanExpression leftExpression;
// 右侧表达式
private BooleanExpression rightExpression;
// 构造方法,接收两个布尔表达式
public OrExpression(BooleanExpression leftExpression, BooleanExpression rightExpression) {
this.leftExpression = leftExpression;
this.rightExpression = rightExpression;
}
// 实现解释方法,返回两个表达式的逻辑或结果
@Override
public boolean interpret() {
return leftExpression.interpret() || rightExpression.interpret();
}
}
// 非终结符表达式类:逻辑非(NOT)运算
public class NotExpression implements BooleanExpression {
// 单一表达式
private BooleanExpression expression;
// 构造方法,接收一个布尔表达式
public NotExpression(BooleanExpression expression) {
this.expression = expression;
}
// 实现解释方法,返回表达式的逻辑非结果
@Override
public boolean interpret() {
return !expression.interpret();
}
}
// 测试类
public class Client {
public static void main(String[] args) {
// 创建布尔值表达式
BooleanExpression trueExp = new BooleanValueExpression(true);
BooleanExpression falseExp = new BooleanValueExpression(false);
// 创建逻辑与表达式:true AND false
BooleanExpression andExpression = new AndExpression(trueExp, falseExp);
System.out.println("true AND false: " + andExpression.interpret());
// 创建逻辑或表达式:true OR (true AND false)
BooleanExpression orExpression = new OrExpression(trueExp, andExpression);
System.out.println("true OR (true AND false): " + orExpression.interpret());
// 创建逻辑非表达式:NOT (true AND false)
BooleanExpression notExpression = new NotExpression(andExpression);
System.out.println("NOT (true AND false): " + notExpression.interpret());
}
}
运行结果:
true AND false: false
true OR (true AND false): true
NOT (true AND false): true
优缺点
优点:
- 清晰的表达和扩展性:逻辑操作被清晰地分离成不同的类,可以轻松添加新的操作
- 灵活性:可以方便地组合不同的表达式来构建复杂的逻辑运算
- 可组合:可以将简单的表达式组合成复杂的表达式,递归地解析。
缺点:
- 类数量增加:每个新的逻辑操作都需要新类,可能导致类的数量迅速增加。
- 复杂性增加:当语法规则复杂时,类的数量会迅速增加,代码维护变得困难。
思考
可以使用解释器模式, 完成对ES的自定义框架搭建, 将自定义语言转化为标准的ES查询DSL语言