一、设计原则
1 SOLID 原则:SRP 单一职责原则
- 理解:一个类只负责完成一个职责或者功能
- 意义:(1)提高类的内聚性(2)实现代码的高内聚,低耦合
- 不满足的5种情况
(1) 类中代码行数、函数或者属性过多
(2)类依赖的其他类过多或者依赖类的其他类过多
(3)私有方法过多
(4)比较难给类起一个合适的名字
(5)类中大量的方法都是集中操作类中的某几个属性
2 SOLID 原则:OCP 开闭原则
- 理解:开闭原则并不是说完全杜绝修改,而是以最小的修改代码的代价来完成新功能的开发;同样的代码改动,在粗代码粒度下,可能被认定为“修改”;在细代码粒度下,可能又被认定为“扩展”
- 做法:提高代码扩展性的方法:
(1)多态
(2)依赖注入
(3)基于接口而非实现
(4)大部分设计模式:装饰、策略、模板、职责链、状态
3 SOLID 原则:LSP 里式替换原则
- 概念:子类对象能够替换程序中父类对象出现的任何地方,并且保证原来程序的逻辑行为不变及正确性不被破坏。
- 核心:按照协议来设计,父类定义了函数的“约定”(或者叫协议),子类可以改变函数的内部实现逻辑,但不能改变函数的原有“约定”。这里的“约定”包括:函数声明要实现的功能;对输入、输出、异常的约定;甚至包括注释中所罗列的任何特殊说明。
- 里式替换原则跟多态的区别:
(1)虽然从定义描述和代码实现上来看,多态和里式替换有点类似,但它们关注的角度是不一样的。多态是面向对象编程的一大特性,也是面向对象编程语言的一种语法,它是一种代码实现的思路。
(2)而里式替换是一种设计原则,用来指导继承关系中子类该如何设计,子类的设计要保证在替换父类的时候,不改变原有程序的逻辑及不破坏原有程序的正确性。
4 SOLID 原则:ISP 接口隔离原则
- 概念:客户端不应该强迫依赖它不需要的接口。其中的“客户端”,可以理解为接口的调用者或者使用者。理解“接口隔离原则”的重点是理解其中的“接口”二字。
- 核心:"接口"的三种不同理解。
(1)如果把“接口”理解为一组接口集合,可以是某个微服务的接口,也可以是某个类库的接口等。如果部分接口只被部分调用者使用,我们就需要将这部分接口隔离出来,单独给这部分调用者使用,而不强迫其他调用者也依赖这部分不会被用到的接口。
(2)如果把“接口”理解为单个 API 接口或函数,部分调用者只需要函数中的部分功能,那我们就需要把函数拆分成粒度更细的多个函数,让调用者只依赖它需要的那个细粒度函数。
(3)如果把“接口”理解为 OOP 中的接口,也可以理解为面向对象编程语言中的接口语法。那接口的设计要尽量单一,不要让接口的实现类和调用者,依赖不需要的接口函数。 - 单一职责原则和接口隔离原则对比
(1)单一职责原则针对的是模块、类、接口的设计。
(2)接口隔离原则提供了一种判断接口的职责是否单一的标准:通过调用者如何使用接口来间接地判定。如果调用者只使用部分接口或接口的部分功能,那接口的设计就不够职责单一。
5 SOLID 原则:DIP 依赖倒置原则
- 控制反转:是一个比较笼统的设计思想,并不是一种具体的实现方法
- 依赖注入:依赖注入和控制反转恰恰相反,它是一种具体的编码技巧。我们不通过 new 的方式在类内部创建依赖类的对象,而是将依赖的类对象在外部创建好之后,通过构造函数、函数参数等方式传递(或“注入”)给类来使用。
- 依赖注入框架:通过依赖注入框架提供的扩展点,简单配置一下所有需要的类及其类与类之间的依赖关系,就可以实现由框架来自动创建对象、管理对象的生命周期、依赖注入等原本需要程序员来做的事情。
- 依赖反转原则:也叫作依赖倒置原则。这条原则跟控制反转有点类似,主要用来指导框架层面的设计。
6 KISS、YAGNI 原则
- KISS 原则
- 概念:尽量保持简单
- 意义:保持代码可读和可维护的重要手段
- 对于如何写出满足 KISS 原则的代码,几条指导原则:
(1)不要使用同事可能不懂的技术来实现代码;
(2)不要重复造轮子,善于使用已经有的工具类库;
(3)不要过度优化。
- YAGNI 原则(英文全称是:You Ain’t Gonna Need It)。直译就是:你不会需要它。
- 概念:不要去设计当前用不到的功能;不要去编写当前用不到的代码。
- 核心思想:不要做过度设计。
- 区别:YAGNI 原则跟 KISS 原则并非一回事儿。KISS 原则讲的是“如何做”的问题(尽量保持简单),而 YAGNI 原则说的是“要不要做”的问题(当前不需要的就不要做)。
7 DRY 原则
- 概念:不要写重复的代码。
- 三种代码重复的情况:
(1)实现逻辑重复、
(2)功能语义重复、
(3)代码执行重复。
实现逻辑重复,但功能语义不重复的代码,并不违反 DRY 原则。实现逻辑不重复,但功能语义重复的代码,也算是违反 DRY 原则。而代码执行重复也算是违反 DRY 原则。 - 提高代码复用性的一些手段
(1)减少代码耦合
(2)满足单一职责原则
(3)模块化
(4)业务与非业务逻辑分离
(5)通用代码下沉
(6)继承、多态、抽象、封装、应用模板等设计模式
8 LOD 原则
“高内聚、松耦合”是一个非常重要的设计思想,能够有效提高代码的可读性和可维护性,缩小功能改动导致的代码改动范围。
“高内聚”:用来指导类本身的设计;松耦合”:用来指导类与类之间依赖关系的设计。
“高内聚”:相近的功能应该放到同一个类中,不相近的功能不要放到同一类中。相近的功能往往会被同时修改,放到同一个类中,修改会比较集中。
“松耦合”指的即使两个类有依赖关系,一个类的代码改动也不会或者很少导致依赖类的代码改动。
二、创建型
1 单例模式
定义:确保一个类最多只有一个实例,并提供一个全局访问点
1.1 饿汉(预加载)
/**
* 饿汉式
* 静态变量创建类的对象
*/
public class Singleton {
//私有构造方法
private Singleton() {}
//在成员位置创建该类的对象
private static Singleton instance = new Singleton();
//对外提供静态方法获取该对象
public static Singleton getInstance() {
return instance;
}
}
很明显,没有使用该单例对象,该对象就被加载到了内存,会造成内存的浪费。
1.2 懒汉 (延迟加载)
方式一 普通锁
/**
* 懒汉式
* 线程安全
*/
public class Singleton {
//私有构造方法
private Singleton() {}
//在成员位置创建该类的对象
private static Singleton instance;
//对外提供静态方法获取该对象
public static synchronized Singleton getInstance() {
if(instance == null) {
instance = new Singleton();
}
return instance;
}
}
如果这个单例类偶尔会被用到,那这种实现方式还可以接受。但是,如果频繁地用到,那频繁加锁、释放锁及并发度低等问题,会导致性能瓶颈,这种实现方式就不可取了。
方式二 双重检查锁
支持延迟加载、又支持高并发的单例实现方式
/**
* 双重检查方式
*/
public class Singleton {
//私有构造方法
private Singleton() {}
private static volatile Singleton instance;
//对外提供静态方法获取该对象
public static Singleton getInstance() {
//第一次判断,如果instance不为null,不进入抢锁阶段,直接返回实际
if(instance == null) {
synchronized (Singleton.class) {
//抢到锁之后再次判断是否为空
if(instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
创建一个对象分三步:
memory=allocate(); //1:初始化内存空间
ctorInstance(memory); //2:初始化对象
instance=memory(); //3:设置instance指向刚分配的内存地址
CPU 指令重排序可能导致在 Singleton 类的对象被关键字 new 创建并赋值给 instance 之后,还没来得及初始化(执行构造函数中的代码逻辑),就被另一个线程使用了。这样,另一个线程就使用了一个没有完整初始化的 Singleton 类的对象。要解决这个问题,我们只需要给 instance 成员变量添加 volatile 关键字来禁止指令重排序即可。
方式三 静态内部类
一种比双重检测更加简单的实现方法,利用 Java 的静态内部类。它有点类似饿汉式,但又能做到了延迟加载。
/**
* 静态内部类方式
*/
public class Singleton {
//私有构造方法
private Singleton() {}
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
//对外提供静态方法获取该对象
public static Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}
SingletonHolder 是一个静态内部类,当外部类 Singleton 被加载的时候,并不会创建 SingletonHolder 实例对象。只有当调用 getInstance() 方法时,SingletonHolder 才会被加载,这个时候才会创建 instance。instance 的唯一性、创建过程的线程安全性,都由 JVM 来保证。所以,这种实现方法既保证了线程安全,又能做到延迟加载。
1.3 枚举
/**
* 枚举方式
*/
public enum Singleton {
INSTANCE;
}
2 工厂模式
工厂模式分为三种:简单工厂、工厂方法和抽象工厂
2.1 简单工厂
2.1.1 基础版
// 工厂类
public class CoffeeFactory {
public Coffee create(String type) {
if ("americano".equals(type)) {
return new Americano();
}
if ("mocha".equals(type)) {
return new Mocha();
}
if ("cappuccino".equals(type)) {
return new Cappuccino();
}
return null;
}
}
// 产品基类
public interface Coffee {
}
// 产品具体类,实现产品基类接口
public class Cappuccino implements Coffee {
}
2.1.2 升级版
public class CoffeeFactory {
// 使用反射创建对象
// 加一个static变为静态工厂
public static Coffee create(Class<? extends Coffee> clazz) throws Exception {
if (clazz != null) {
return clazz.newInstance();
}
return null;
}
}
升级版就很好的解决基础版的问题,在创建的时候在传参的时候不仅会有代码提示,保证不会写错,同时在新增产品的时候只需要新增产品类即可,也不需要再在工厂类的方法里面新增代码了。
2.1.3 总结
适用场景:
工厂类负责创建的对象较少。
客户端只需要传入工厂类的参数,对于如何创建的对象的逻辑不需要关心。
优点:
只需要传入一个正确的参数,就可以获取你所需要的对象,无须知道创建的细节。
缺点:
工厂类的职责相对过重,增加新的产品类型的时需要修改工厂类的判断逻辑,违背了开闭原则。
2.2 工厂方法
定义了一个创建对象的抽象方法,由子类决定要实例化的类。工厂方法模式将对象的实例化推迟到子类。
工厂方法模式的主要角色:
- 抽象工厂(Abstract Factory):提供了创建产品的接口,调用者通过它访问具体工厂的工厂方法来创建产品。
- 具体工厂(ConcreteFactory):主要是实现抽象工厂中的抽象方法,完成具体产品的创建。
- 抽象产品(Product):定义了产品的规范,描述了产品的主要特性和功能。
- 具体产品(ConcreteProduct):实现了抽象产品角色所定义的接口,由具体工厂来创建,它同具体工厂之间一一对应。
代码实现
// 抽象工厂
public interface CoffeeFactory {
Coffee create();
}
// 具体工厂
public class AmericanCoffeeFactory implements CoffeeFactory {
@Override
public Coffee create() {
return new Cappuccino();
}
}
// 抽象产品
public interface Coffee {
}
// 具体产品
public class AmericanCoffee implements Coffee {
}
工厂方法模式中考虑的是一类产品的生产,如只生产caffee产品,如果一个工厂还需要生产别的产品(比如甜点),就需要用抽象工厂
2.3 抽象工厂
抽象工厂模式是工厂方法模式的升级版本,工厂方法模式只生产一个等级的产品,而抽象工厂模式可生产多个等级的产品。
抽象工厂模式的主要角色如下:
- 抽象工厂(Abstract Factory):提供了创建产品的接口,它包含多个创建产品的方法,可以创建多个不同等级的产品。
- 具体工厂(Concrete Factory):主要是实现抽象工厂中的多个抽象方法,完成具体产品的创建。
- 抽象产品(Product):定义了产品的规范,描述了产品的主要特性和功能,抽象工厂模式有多个抽象产品。
- 具体产品(ConcreteProduct):实现了抽象产品角色所定义的接口,由具体工厂来创建,它 同具体工厂之间是多对一的关系。
代码实现
(1)抽象工厂:
public interface DessertFactory {
Coffee createCoffee();
Dessert createDessert();
}
(2) 具体工厂
//美式甜点工厂
public class AmericanDessertFactory implements DessertFactory {
public Coffee createCoffee() {
return new AmericanCoffee();
}
public Dessert createDessert() {
return new MatchaMousse();
}
}
//意大利风味甜点工厂
public class ItalyDessertFactory implements DessertFactory {
public Coffee createCoffee() {
return new LatteCoffee();
}
public Dessert createDessert() {
return new Tiramisu();
}
}
如果要加同一个产品族的话(比如咖啡),只需要再加一个对应的工厂类即可,不需要修改其他的类。
当产品族中需要增加一个新的产品(比如方便面)时,所有的工厂类都需要进行修改。
优点:
当一个产品族中的多个对象被设计成一起工作时,它能保证客户端始终只使用同一个产品族中的对象。
缺点:
当产品族中需要增加一个新的产品时,所有的工厂类都需要进行修改。
3 建造者模式(Builder)
定义:封装一个复杂对象构造过程,并允许按步骤构造。
定义解释: 我们可以将生成器模式理解为,假设我们有一个对象需要建立,这个对象是由多个组件(Component)组合而成,每个组件的建立都比较复杂,但运用组件来建立所需的对象非常简单,所以我们就可以将构建复杂组件的步骤与运用组件构建对象分离,使用builder模式可以建立。
建造者模式包含如下角色:
(1)产品类:具体建造者要构造的复杂对象;
(2)抽象建造者类:这个接口规定要实现复杂对象的那些部分的创建,并不涉及具体的部件对象的创建。
(3)具体建造者类:实现 Builder 接口,完成复杂产品的各个部件的具体创建方法。在构造过程完成后,提供产品的实例。
(4)指挥者类:调用具体建造者来创建复杂对象的各个部分,在指导者中不涉及具体产品的信息,只负责保证对象各部分完整创建或按某种顺序创建。
3.1 常规代码:
//产品类 电脑
@Data
public class Computer {
private String motherboard;
private String cpu;
private String memory;
private String disk;
private String gpu;
private String power;
private String heatSink;
private String chassis;
}
// 抽象 builder类(接口) 组装电脑
public interface ComputerBuilder {
Computer computer = new Computer();
void buildMotherboard();
void buildCpu();
void buildMemory();
void buildDisk();
void buildGpu();
void buildHeatSink();
void buildPower();
void buildChassis();
Computer build();
}
// 具体 builder类 华硕ROG全家桶电脑(手动狗头)
public class AsusComputerBuilder implements ComputerBuilder {
@Override
public void buildMotherboard() {
computer.setMotherboard("Extreme主板");
}
@Override
public void buildCpu() {
computer.setCpu("Inter 12900KS");
}
@Override
public void buildMemory() {
computer.setMemory("芝奇幻峰戟 16G*2");
}
@Override
public void buildDisk() {
computer.setDisk("三星980Pro 2T");
}
@Override
public void buildGpu() {
computer.setGpu("华硕3090Ti 水猛禽");
}
@Override
public void buildHeatSink() {
computer.setHeatSink("龙神二代一体式水冷");
}
@Override
public void buildPower() {
computer.setPower("雷神二代1200W");
}
@Override
public void buildChassis() {
computer.setChassis("太阳神机箱");
}
@Override
public Computer build() {
return computer;
}
}
// 指挥者类 指挥该组装什么电脑
@AllArgsConstructor
public class ComputerDirector {
private ComputerBuilder computerBuilder;
public Computer construct() {
computerBuilder.buildMotherboard();
computerBuilder.buildCpu();
computerBuilder.buildMemory();
computerBuilder.buildDisk();
computerBuilder.buildGpu();
computerBuilder.buildHeatSink();
computerBuilder.buildPower();
computerBuilder.buildChassis();
return computerBuilder.build();
}
}
// 测试
public static void main(String[] args) {
ComputerDirector computerDirector = new ComputerDirector(new AsusComputerBuilder());
// Computer(motherboard=Extreme主板, cpu=Inter 12900KS, memory=芝奇幻峰戟 16G*2, disk=三星980Pro 2T, gpu=华硕3090Ti 水猛禽, power=雷神二代1200W, heatSink=龙神二代一体式水冷, chassis=太阳神机箱)
System.out.println(computerDirector.construct());
}
注意:
上面示例是 Builder模式的常规用法,指挥者类 Director 在建造者模式中具有很重要的作用,它用于指导具体构建者如何构建产品,控制调用先后次序,并向调用者返回完整的产品类,但是有些情况下需要简化系统结构,可以把指挥者类和抽象建造者进行结合
3.2 合并写法
// 把指挥者类和抽象建造者合在一起的简化建造者类
public class SimpleComputerBuilder {
private Computer computer = new Computer();
public void buildMotherBoard(String motherBoard){
computer.setMotherboard(motherBoard);
}
public void buildCpu(String cpu){
computer.setCpu(cpu);
}
public void buildMemory(String memory){
computer.setMemory(memory);
}
public void buildDisk(String disk){
computer.setDisk(disk);
}
public void buildGpu(String gpu){
computer.setGpu(gpu);
}
public void buildPower(String power){
computer.setPower(power);
}
public void buildHeatSink(String heatSink){
computer.setHeatSink(heatSink);
}
public void buildChassis(String chassis){
computer.setChassis(chassis);
}
public Computer build(){
return computer;
}
}
// 测试
public static void main(String[] args) {
SimpleComputerBuilder simpleComputerBuilder = new SimpleComputerBuilder();
simpleComputerBuilder.buildMotherBoard("Extreme主板");
simpleComputerBuilder.buildCpu("Inter 12900K");
simpleComputerBuilder.buildMemory("芝奇幻峰戟 16G*2");
simpleComputerBuilder.buildDisk("三星980Pro 2T");
simpleComputerBuilder.buildGpu("华硕3090Ti 水猛禽");
simpleComputerBuilder.buildPower("雷神二代1200W");
simpleComputerBuilder.buildHeatSink("龙神二代一体式水冷");
simpleComputerBuilder.buildChassis("太阳神机箱");
// Computer(motherboard=Extreme主板, cpu=Inter 12900K, memory=芝奇幻峰戟 16G*2, disk=三星980Pro 2T, gpu=华硕3090Ti 水猛禽, power=雷神二代1200W, heatSink=龙神二代一体式水冷, chassis=太阳神机箱)
System.out.println(simpleComputerBuilder.build());
}
可以看到,对比常规写法,这样写确实简化了系统结构,但同时也加重了建造者类的职责,也不是太符合单一职责原则,如果construct() 过于复杂,建议还是封装到 Director 中。
3.3 链式写法
// 链式写法建造者类
public class SimpleComputerBuilder {
private Computer computer = new Computer();
public SimpleComputerBuilder buildMotherBoard(String motherBoard){
computer.setMotherboard(motherBoard);
return this;
}
public SimpleComputerBuilder buildCpu(String cpu){
computer.setCpu(cpu);
return this;
}
public SimpleComputerBuilder buildMemory(String memory){
computer.setMemory(memory);
return this;
}
public SimpleComputerBuilder buildDisk(String disk){
computer.setDisk(disk);
return this;
}
public SimpleComputerBuilder buildGpu(String gpu){
computer.setGpu(gpu);
return this;
}
public SimpleComputerBuilder buildPower(String power){
computer.setPower(power);
return this;
}
public SimpleComputerBuilder buildHeatSink(String heatSink){
computer.setHeatSink(heatSink);
return this;
}
public SimpleComputerBuilder buildChassis(String chassis){
computer.setChassis(chassis);
return this;
}
public Computer build(){
return computer;
}
}
// 测试
public static void main(String[] args) {
Computer asusComputer = new SimpleComputerBuilder().buildMotherBoard("Extreme主板")
.buildCpu("Inter 12900K")
.buildMemory("芝奇幻峰戟 16G*2")
.buildDisk("三星980Pro 2T")
.buildGpu("华硕3090Ti 水猛禽")
.buildPower("雷神二代1200W")
.buildHeatSink("龙神二代一体式水冷")
.buildChassis("太阳神机箱").build();
System.out.println(asusComputer);
}
3.4 总结
适用场景:
- 适用于创建对象需要很多步骤,但是步骤顺序不一定固定。
- 如果一个对象有非常复杂的内部结构(属性),把复杂对象的创建和使用进行分离。
优点:
- 封装性好,创建和使用分离。
- 扩展性好,建造类之间独立、一定程度上解耦。
缺点:
- 产生多余的Builder对象。
- 产品内部发生变化,建造者都要修改,成本较大。
与工厂模式的区别:
- 建造者模式更注重方法的调用顺序,工厂模式更注重创建对象。
- 创建对象的力度不同,建造者模式创建复杂的对象,由各种复杂的部件组成,工厂模式创建出来的都一样。
- 关注点不同,工厂模式只需要把对象创建出来就可以了,而建造者模式中不仅要创建出这个对象,还要知道这个对象由哪些部件组成。
- 建造者模式根据建造过程中的顺序不一样,最终的对象部件组成也不一样。
4 原型模式
如果对象的创建成本比较大,而同一个类的不同对象之间差别不大(大部分字段都相同),在这种情况下,我们可以利用对已有对象(原型)进行复制(或者叫拷贝)的方式来创建新对象,以达到节省创建时间的目的。这种基于原型来创建对象的方式就叫作原型设计模式(Prototype Design Pattern),简称原型模式。
4.1 浅拷贝
Java 语言中,Object 类的 clone() 方法执行的就是我们刚刚说的浅拷贝。它只会拷贝对象中的基本数据类型的数据(比如,int、long),以及引用对象的内存地址,不会递归地拷贝引用对象本身
@Data
@AllArgsConstructor
@NoArgsConstructor
class Student {
private String name;
private String sex;
private Integer age;
}
@Data
class Clazz implements Cloneable {
private String name;
private Student student;
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
//测试
public class Test {
public static void main(String[] args) throws Exception {
Clazz clazz1 = new Clazz();
clazz1.setName("高三一班");
Student stu1 = new Student("张三", "男", 18);
clazz1.setStudent(stu1);
System.out.println(clazz1); // Clazz(name=高三一班, student=Student(name=张三, sex=男, age=18))
Clazz clazz2 = (Clazz) clazz1.clone();
Student stu2 = clazz2.getStudent();
stu2.setName("李四");
System.out.println(clazz1); // Clazz(name=高三一班, student=Student(name=李四, sex=男, age=18))
System.out.println(clazz2); // Clazz(name=高三一班, student=Student(name=李四, sex=男, age=18))
}
}
可以看到,当修改了stu2的姓名时,stu1的姓名同样也被修改了,这说明stu1和stu2是同一个对象,这就是浅克隆的特点,对具体原型类中的引用类型的属性进行引用的复制。
4.2 深拷贝
实现方式: 先将对象序列化,然后再反序列化成新的对象
public Object deepCopy(Object object) {
ByteArrayOutputStream bo = new ByteArrayOutputStream();
ObjectOutputStream oo = new ObjectOutputStream(bo);
oo.writeObject(object);
ByteArrayInputStream bi = new ByteArrayInputStream(bo.toByteArray());
ObjectInputStream oi = new ObjectInputStream(bi);
return oi.readObject();
}
具体实现
@Data
@AllArgsConstructor
@NoArgsConstructor
class Student implements Serializable {
private String name;
private String sex;
private Integer age;
}
@Data
class Clazz implements Serializable {
private String name;
private Student student;
protected Object deepClone() throws IOException, ClassNotFoundException {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bos);
oos.writeObject(this);
ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bis);
return ois.readObject();
}
}
//测试类
public class Test {
public static void main(String[] args) throws Exception {
Clazz clazz1 = new Clazz();
clazz1.setName("高三一班");
Student stu1 = new Student("张三", "男", 18);
clazz1.setStudent(stu1);
Clazz clazz3 = (Clazz) clazz1.deepClone();
Student stu3 = clazz3.getStudent();
stu3.setName("王五");
System.out.println(clazz1); // Clazz(name=高三一班, student=Student(name=张三, sex=男, age=18))
System.out.println(clazz3); // Clazz(name=高三一班, student=Student(name=王五, sex=男, age=18))
}
}
可以看到,当修改了stu3的姓名时,stu1的姓名并没有被修改了,这说明stu3和stu1已经是不同的对象了,说明Clazz中的Student也被克隆了,不再指向原有对象地址,这就是深克隆。这里需要注意的是,Clazz类和Student类都需要实现Serializable接口,否则会抛出NotSerializableException异常
4.4 总结
适用场景:
- 类初始化消耗资源较多。
- new产生的一个对象需要非常繁琐的过程(数据准备、访问权限等)。
- 构造函数比较复杂。
- 循环体中生产大量对象时。
优点:
- 性能优良,Java自带的原型模式是基于内存二进制流的拷贝,比直接new一个对象性能上提升了许多。
- 可以使用深克隆方式保存对象的状态,使用原型模式将对象复制一份并将其状态保存起来,简化了创建的过程。
缺点:
- 必须配备克隆(或者可拷贝)方法。
- 当对已有类进行改造的时候,需要修改代码,违反了开闭原则。
三、结构型
1 代理模式
代理模式是指为其他对象提供一种代理,以控制对这个对象的访问。代理对象在访问对象和目标对象之间起到中介作用。
Java中的代理按照代理类生成时机不同又分为静态代理和动态代理。
- 静态代理代理类在编译期就生成
- 动态代理代理类则是在Java运行时动态生成,动态代理又有JDK代理和CGLib代理两种。
代理(Proxy)模式分为三种角色:
- 抽象角色(Subject): 通过接口或抽象类声明真实角色和代理对象实现的业务方法。
- 真实角色(Real Subject): 实现了抽象角色中的具体业务,是代理对象所代表的真实对象,是最终要引用的对象。
- 代理角色(Proxy) : 提供了与真实角色相同的接口,其内部含有对真实角色的引用,它可以访问、控制或扩展真实角色的功能。
1.1 静态代理
静态代理就是指我们在给一个类扩展功能的时候,我们需要去书写一个静态的类,相当于在之前的类上套了一层,这样我们就可以在不改变之前的类的前提下去对原有功能进行扩展,静态代理需要代理对象和目标对象实现一样的接口。
代码实现
// 火车站接口,有卖票功能
public interface TrainStation {
void sellTickets();
}
// 广州火车站卖票
public class GuangzhouTrainStation implements TrainStation {
@Override
public void sellTickets() {
System.out.println("广州火车站卖票啦");
}
}
// 代售点卖票(代理类)
public class ProxyPoint implements TrainStation {
// 目标对象(代理火车站售票)
private TrainStation station = new GuangzhouTrainStation();
@Override
public void sellTickets() {
System.out.println("代售加收5%手续费");
station.sellTickets();
}
public static void main(String[] args) {
ProxyPoint proxyPoint = new ProxyPoint();
// 代售加收5%手续费
// 广州火车站卖票啦
proxyPoint.sellTickets();
}
}
// 测试
public static void main(String[] args) {
ProxyPoint proxyPoint = new ProxyPoint();
// 代售加收5%手续费
// 火车站卖票啦
proxyPoint.sellTickets();
}
优点:实现简单,容易理解,只要确保目标对象和代理对象实现共同的接口或继承相同的父类就可以在不修改目标对象的前提下进行扩展。
缺点:就是代理类和目标类必须有共同接口(父类),并且需要为每一个目标类维护一个代理类,当需要代理的类很多时会创建出大量代理类。一旦接口或父类的方法有变动,目标对象和代理对象都需要作出调整。
1.1 动态代理
代理类在代码运行时创建的代理称之为动态代理。动态代理中代理类并不是预先在Java代码中定义好的,而是运行时由JVM动态生成,并且可以代理多个目标对象。
1.1.2 JDK动态代理
JDK动态代理是Java JDK自带的一个动态代理实现
// 火车站接口,有卖票功能
public interface TrainStation {
void sellTickets();
}
// 广州火车站卖票
public class GuangzhouTrainStation implements TrainStation {
@Override
public void sellTickets() {
System.out.println("广州火车站卖票啦");
}
}
// 深圳火车站卖票
public class ShenzhenTrainStation implements TrainStation {
@Override
public void sellTickets() {
System.out.println("深圳火车站卖票啦");
}
}
// 代售点卖票(代理类)
public class ProxyPoint implements InvocationHandler {
private TrainStation trainStation;
public TrainStation getProxyObject(TrainStation trainStation) {
this.trainStation = trainStation;
Class<? extends TrainStation> clazz = trainStation.getClass();
return (TrainStation) Proxy.newProxyInstance(clazz.getClassLoader(), clazz.getInterfaces(), this);
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("代售火车票收取5%手续费");
return method.invoke(this.trainStation, args);
}
}
// 测试
public static void main(String[] args) {
ProxyPoint proxy = new ProxyPoint();
TrainStation guangzhouTrainStation = proxy.getProxyObject(new GuangzhouTrainStation());
// 代售火车票收取5%手续费
// 广州火车站卖票啦
guangzhouTrainStation.sellTickets();
TrainStation shenzhenTrainStation = proxy.getProxyObject(new ShenzhenTrainStation());
// 代售火车票收取5%手续费
// 深圳火车站卖票啦
shenzhenTrainStation.sellTickets();
}
优点:
- 使用简单、维护成本低。
- Java原生支持,不需要任何依赖。
- 解决了静态代理存在的多数问题。
缺点:
- 由于使用反射,性能会比较差。
- 只支持接口实现,不支持继承, 不满足所有业务场景。
1.1.2 CGLIB动态代理
CGLIB是一个强大的、高性能的代码生成库。它可以在运行期扩展Java类和接口,其被广泛应用于AOP框架中(Spring、dynaop)中, 用以提供方法拦截。CGLIB比JDK动态代理更强的地方在于它不仅可以接管Java接口, 还可以接管普通类的方法。
原理:动态生成一个要代理类的子类,子类重写要代理的类的所有不是final的方法。在子类中采用方法拦截的技术拦截所有父类方法的调用,顺势织入横切逻辑。它比使用java反射的JDK动态代理要快。
CGLIB 底层:使用字节码处理框架ASM,来转换字节码并生成新的类。不鼓励直接使用ASM,因为它要求你必须对JVM内部结构包括class文件的格式和指令集都很熟悉。
CGLIB缺点:对于final方法,无法进行代理。
<!-- 先引入cglib包 -->
<dependency>
<groupId>cglib</groupId>
<artifactId>cglib</artifactId>
<version>2.2.2</version>
</dependency>
// 深圳火车站卖票
class ShenzhenTrainStation {
public void sellTickets() {
System.out.println("深圳火车站卖票啦");
}
}
// 代售点卖票(代理类)
class ProxyPoint implements MethodInterceptor {
public Object getProxyObject(Class<?> clz) {
//创建Enhancer对象,类似于JDK动态代理的Proxy类,下一步就是设置几个参数
Enhancer enhancer = new Enhancer();
//设置父类的字节码对象
enhancer.setSuperclass(clz);
//设置回调函数
enhancer.setCallback(this);
//创建代理对象并返回
Object proxyTarget = enhancer.create();
return proxyTarget;
}
@Override
public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
System.out.println("代售火车票收取5%手续费");
return methodProxy.invokeSuper(o, objects);
}
}
public class Test {
public static void main(String[] args) {
ProxyPoint proxy = new ProxyPoint();
ShenzhenTrainStation shenzhenTrainStation = (ShenzhenTrainStation) proxy.getProxyObject(
ShenzhenTrainStation.class);
// 代售火车票收取5%手续费
// 深圳火车站卖票啦
shenzhenTrainStation.sellTickets();
}
}
1.1.3 两种动态代理的对比:
JDK动态代理的特点:
- 需要实现InvocationHandler接口, 并重写invoke方法。
0 被代理类需要实现接口, 它不支持继承。 - JDK 动态代理类不需要事先定义好, 而是在运行期间动态生成。
- JDK 动态代理不需要实现和被代理类一样的接口, 所以可以绑定多个被代理类。
- 主要实现原理为反射, 它通过反射在运行期间动态生成代理类, 并且通过反射调用被代理类的实际业务方法。
cglib的特点:
- cglib动态代理中使用的是FastClass机制。
- cglib生成字节码的底层原理是使用ASM字节码框架。
- cglib动态代理需创建3份字节码,所以在第一次使用时会比较耗性能,但是后续使用较JDK动态代理方式更高效,适合单例bean场景。
- cglib由于是采用动态创建子类的方法,对于final方法,无法进行代理。
1.3 总结
应用场景:
- 保护目标对象。
- 增强目标对象。
优点:
- 代理模式能将代理对象与真实被调用的目标对象分离。
- 一定程度上降低了系统的耦合程度,易于扩展。
- 代理可以起到保护目标对象的作用。
- 增强目标对象的职责。
缺点:
- 代理模式会造成系统设计中类的数目增加。
- 在客户端和目标对象之间增加了一个代理对象,请求处理速度变慢。
- 增加了系统的复杂度。
2.适配器模式(Adapter Class/Object)
适配器模式,它的功能是将一个类的接口变成客户端所期望的另一种接口,从而使原本因接口不匹配而导致无法在一起工作的两个类能够一起工作。
适配器模式分为类适配器模式和对象适配器模式,前者类之间的耦合度比后者高,且要求程序员了解现有组件库中的相关组件的内部结构,所以应用相对较少些。
适配器模式(Adapter)包含以下主要角色:
- 目标(Target)接口:当前系统业务所期待的接口,它可以是抽象类或接口。
- 适配者(Adaptee)类:它是被访问和适配的现存组件库中的组件接口。
- 适配器(Adapter)类:它是一个转换器,通过继承或引用适配者的对象,把适配接口转换- 成目标接口,让客户按目标接口的格式访问适配者。
2.1 类适配器
实现方式:定义一个适配器类来实现当前系统的业务接口,同时又继承现有组件库中已经存在的组件。类图如下:
// 适配者 220V电压
class AC220 {
public int output() {
System.out.println("输出220V交流电");
return 220;
}
}
// 目标 5V
interface DC5 {
public int output5();
}
// 适配器类(电源适配器)
class PowerAdapter extends AC220 implements DC5 {
@Override
public int output5() {
int output220 = super.output();
int output5 = output220 / 44;
System.out.println(output220 + "V适配转换成" + output5 + "V");
return output5;
}
}
//测试
public class Test {
public static void main(String[] args) {
PowerAdapter powerAdapter = new PowerAdapter();
powerAdapter.output(); //输出220V交流电
powerAdapter.output5();//输出220V交流电 //220V适配转换成5V
}
}
通过上面代码例子可以看出,类适配器有一个很明显的缺点,就是违背了合成复用原则。结合上面的例子,假如我不是220V的电压了,是380V电压呢?那就要多建一个380V电压的适配器了。同理,由于Java是单继承的原因,如果不断的新增适配者,那么就要无限的新增适配器,于是就有了对象适配器。
2.2 对象适配器
实现方式:对象适配器模式可釆用将现有组件库中已经实现的组件引入适配器类中,该类同时实现当前系统的业务接口。
对象适配器和类适配器使用了不同的方法实现适配,对象适配器使用组合,类适配器使用继承
//电源接口
interface Power {
int output();
}
// 适配者 220V电压
class AC220 implements Power {
@Override
public int output() {
System.out.println("输出220V交流电");
return 220;
}
}
// 目标 5V
interface DC5 {
public int output5();
}
@AllArgsConstructor
class PowerAdapter implements DC5 {
//适配者
private Power power;
@Override
public int output5() {
int output220 = power.output();
int output5 = output220 / 44;
System.out.println(output220 + "V适配转换成" + output5 + "V");
return output5;
}
}
//测试
public class Test {
public static void main(String[] args) {
PowerAdapter powerAdapter = new PowerAdapter(new AC220());
powerAdapter.output5();//输出220V交流电 //220V适配转换成5V
}
}
可以看到,上面代码中,只实现了目标接口,并没有继承适配者,而是将适配者类实现适配者接口,在适配器中引入适配者接口,当我们需要使用不同的适配者通过适配器进行转换时,就无需再新建适配器类了,如上面例子,假如我需要380V的电源转换成5V的,那么客户端只需要调用适配器时传入380V电源的类即可,就无需再新建一个380V电源的适配器了(PS:上述逻辑代码中output220 / 44请忽略,可以根据实际情况编写实际的通用逻辑代码)。
2.3 接口适配器
接口适配器主要是解决类臃肿的问题,我们可以把所有相近的适配模式的方法都放到同一个接口里面,去实现所有方法,当客户端需要哪个方法直接调用哪个方法即可。如上面例子所示,我们只是转换成了5V电压,那假如我要转换成12V,24V,30V…呢?那按照上面的写法就需要新建12V,24V,30V…的接口,这样就会导致类过于多了。那么我们就可以把5V,12V,24V,30V…这些转换方法,通通都写到一个接口里去,这样当我们需要转换哪种就直接调用哪种即可。
// 这里例子 输出不同直流电接口
public interface DC {
int output5();
int output12();
int output24();
int output30();
}
// 适配器类(电源适配器)
@AllArgsConstructor
public class PowerAdapter implements DC {
private Power power;
@Override
public int output5() {
// 具体实现逻辑
return 5;
}
@Override
public int output12() {
// 具体实现逻辑
return 12;
}
@Override
public int output24() {
// 具体实现逻辑
return 24;
}
@Override
public int output30() {
// 具体实现逻辑
return 30;
}
}
类适配器模式:当希望将一个类转换成满足另一个新接口的类时,可以使用类的适配器模式,创建一个新类,继承原有的类,实现新的接口即可。
对象适配器模式:当希望将一个对象转换成满足另一个新接口的对象时,可以创建一个Wrapper类,持有原类的一个实例,在Wrapper类的方法中,调用实例的方法就行。
接口适配器模式:当不希望实现一个接口中所有的方法时,可以创建一个抽象类Wrapper,实现所有方法,我们写别的类的时候,继承抽象类即可。
应用场景
- 以前开发的系统存在满足新系统功能需求的类,但其接口同新系统的接口不一致。
- 使用第三方提供的组件,但组件接口定义和自己要求的接口定义不同
优点:
- 能提高类的透明性和复用,现有的类复用但不需要改变。
- 目标类和适配器类解耦,提高程序的扩展性。
- 在很多业务场景中符合开闭原则。
缺点:
- 适配器编写过程需要全面考虑,可能会增加系统的复杂性。
- 增加代码阅读难度,降级代码可读性,过多使用适配器会使系统代码变得凌乱。
3.装饰模式(Decorator Pattern)
装饰模式,是指在不改变原有对象的基础上,将功能附加到对象上,提供了比继承更有弹性的替代方案(扩展原有对象的功能)
装饰(Decorator)模式中的角色:
- 抽象构件(Component):具体构件和抽象装饰类的共同父类。
- 具体构件(ConcreteComponent):抽象构件类的子类。用于定义具体的构件对象,装饰类可以给他增加额外的职责(方法)。
- 抽象装饰(Decorator):继承或实现抽象构件,并包含具体构件的实例,可以通过其子类扩展具体构件的功能。
- 具体装饰(ConcreteDecorator):实现抽象装饰的相关方法,并给具体构件对象添加附加的责任。
实现
//抽象构件
public abstract class Component {
public abstract void operation();
}
//具体构件
public class ConcreteComponent extends Component {
public void operation() {
// 基本功能实现
}
}
//抽象装饰
public class Decorator extends Component {
private Component component; // 维持一个对抽象构件对象的引用
// 注入一个抽象构件类型的对象
public Decorator(Component component) {
this.component = component;
}
public void operation() {
component.operation(); // 调用原有业务方法
}
}
//具体装饰
public class ConcreteDecorator extends Decorator {
public ConcreteDecorator(Component component) {
super.operation(); // 调用原有业务方法
addedBehavior(); // 调用新增业务方法
}
public void addedBehavior() {
// 新增功能实现
}
}
使用场景
- 当不能采用继承的方式对系统进行扩充或者采用继承不利于系统扩展和维护时。
- 不能采用继承的情况主要有两类:
- 第一类是系统中存在大量独立的扩展,为支持每一种组合将产生大量的子类,使得子类数目呈爆炸性增长;
- 第二类是因为类定义不能继承(如final类)
在不影响其他对象的情况下,以动态、透明的方式给单个对象添加职责。
- 当对象的功能要求可以动态地添加,也可以再动态地撤销时。
特点:
- 装饰器模式主要解决继承关系过于复杂的问题,通过组合来替代继承。它主要的作用是给原始类添加增强功能。这也是判断是否该用装饰器模式的一个重要的依据。
- 除此之外,装饰器模式还有一个特点,那就是可以对原始类嵌套使用多个装饰器。为了满足这个应用场景,在设计的时候,装饰器类需要跟原始类继承相同的抽象类或者接口。
4 桥接模式(Bridge Pattern)
桥接模式也称为桥梁模式、接口模式或者柄体(Handle and Body)模式,把抽象(abstraction)与行为实现(implementation)分离开来,通过组合的方式建立两个类之间的联系,而不是继承。
桥接(Bridge)模式包含以下主要角色:
- 抽象化(Abstraction)角色 :定义抽象类,并包含一个对实现化对象的引用。
- 扩展抽象化(Refined Abstraction)角色 :是抽象化角色的子类,实现父类中的业务方法,并通过组合关系调用实现化角色中的业务方法。
- 实现化(Implementor)角色 :定义实现化角色的接口,供扩展抽象化角色调用。
- 具体实现化(Concrete Implementor)角色 :给出实现化角色接口的具体实现。
抽象
public abstract class Phone {
protected Software software;
public void setSoftware(Software software) {
this.software = software;
}
public abstract void run();
}
public class Oppo extends Phone {
@Override
public void run() {
Coming Soon();
}
}
public class Vivo extends Phone {
@Override
public void run() {
Coming Soon();
}
}
实现
public interface Software {
public void run();
}
public class LoL implements Software {
@Override
public void run() {
System.out.println("run lol");
}
}
public class Wangzhe implements Software {
@Override
public void run() {
System.out.println("run wangzhe");
}
}
4.2 总结
适用场景:
- 在抽象和具体实现之间需要增加更多的灵活性的场景。
- 一个类存在两个(或多个)独立变化的维度,而这两个(或多个)维度都需要独立进行扩展。
- 不希望使用继承,或因为多层继承导致系统类的个数剧增。
优点:
- 分离抽象部分及其具体实现部分。
- 提高了系统的扩展性。
- 符合开闭原型。
- 符合合成复用原则。
缺点:
- 增加了系统的理解与设计难度。
- 需要正确地识别系统中两个独立变化的维度。
5 外观模式
外观模式又称门面模式,提供了一个统一的接口,用来访问子系统中的一群接口。
特征:门面模式定义了一个高层接口,让子系统更容易使用。
外观(Facade)模式包含以下主要角色:
- 外观(Facade)角色:为多个子系统对外提供一个共同的接口。
- 子系统(Sub System)角色:实现系统的部分功能,客户可以通过外观角色访问它。
实现
子系统类
public class CPU {
public void start() {
System.out.println("cpu is start...");
}
public void shutDown() {
System.out.println("CPU is shutDown...");
}
}
public class Disk {
public void start() {
System.out.println("Disk is start...");
}
public void shutDown() {
System.out.println("Disk is shutDown...");
}
}
public class Memory {
public void start() {
System.out.println("Memory is start...");
}
public void shutDown() {
System.out.println("Memory is shutDown...");
}
}
门面类
public class ComputerFacade {
private CPU cpu;
private Memory memory;
private Disk disk;
public Computer() {
cpu = new CPU();
memory = new Memory();
disk = new Disk();
}
public void start() {
System.out.println("Computer start begin");
cpu.start();
disk.start();
memory.start();
System.out.println("Computer start end");
}
public void shutDown() {
System.out.println("Computer shutDown begin");
cpu.shutDown();
disk.shutDown();
memory.shutDown();
System.out.println("Computer shutDown end...");
}
}
好处:
- 降低了子系统与客户端之间的耦合度,使得子系统的变化不会影响调用它的客户类。
- 对客户屏蔽了子系统组件,减少了客户处理的对象数目,并使得子系统使用起来更加容易。
缺点:
- 不符合开闭原则,修改很麻烦
6.组合模式(Composite Pattern)
组合模式也称为整体-部分(Part-Whole)模式,它的宗旨是通过将单个对象(叶子结点)和组合对象(树枝节点)用相同的接口进行表示。
作用:使客户端对单个对象和组合对象保持一致的方式处理。
组合模式主要包含三种角色:
抽象根节点(Component):定义系统各层次对象的共有方法和属性,可以预先定义一些默认行为和属性。
树枝节点(Composite):定义树枝节点的行为,存储子节点,组合树枝节点和叶子节点形成一个树形结构。
叶子节点(Leaf):叶子节点对象,其下再无分支,是系统层次遍历的最小单位。
实现
// 菜单组件
public abstract class MenuComponent {
String name;
Integer level;
public void add(MenuComponent menuComponent) {
throw new UnsupportedOperationException("不支持添加操作!");
}
public void remove(MenuComponent menuComponent) {
throw new UnsupportedOperationException("不支持删除操作!");
}
public MenuComponent getChild(Integer i) {
throw new UnsupportedOperationException("不支持获取子菜单操作!");
}
public String getName() {
throw new UnsupportedOperationException("不支持获取名字操作!");
}
public void print() {
throw new UnsupportedOperationException("不支持打印操作!");
}
}
// 菜单类
public class Menu extends MenuComponent {
private List<MenuComponent> menuComponentList = new ArrayList<>();
public Menu(String name,int level){
this.level = level;
this.name = name;
}
@Override
public void add(MenuComponent menuComponent) {
menuComponentList.add(menuComponent);
}
@Override
public void remove(MenuComponent menuComponent) {
menuComponentList.remove(menuComponent);
}
@Override
public MenuComponent getChild(Integer i) {
return menuComponentList.get(i);
}
@Override
public void print() {
for (int i = 1; i < level; i++) {
System.out.print("--");
}
System.out.println(name);
for (MenuComponent menuComponent : menuComponentList) {
menuComponent.print();
}
}
}
// 子菜单类
public class MenuItem extends MenuComponent {
public MenuItem(String name,int level) {
this.name = name;
this.level = level;
}
@Override
public void print() {
for (int i = 1; i < level; i++) {
System.out.print("--");
}
System.out.println(name);
}
}
// 测试方法
public static void main(String[] args) {
//创建一级菜单
MenuComponent component = new Menu("系统管理",1);
MenuComponent menu1 = new Menu("用户管理",2);
menu1.add(new MenuItem("新增用户",3));
menu1.add(new MenuItem("修改用户",3));
menu1.add(new MenuItem("删除用户",3));
MenuComponent menu2 = new Menu("角色管理",2);
menu2.add(new MenuItem("新增角色",3));
menu2.add(new MenuItem("修改角色",3));
menu2.add(new MenuItem("删除角色",3));
menu2.add(new MenuItem("绑定用户",3));
//将二级菜单添加到一级菜单中
component.add(menu1);
component.add(menu2);
//打印菜单名称(如果有子菜单一块打印)
component.print();
}
// 测试结果
系统管理
--用户管理
----新增用户
----修改用户
----删除用户
--角色管理
----新增角色
----修改角色
----删除角色
----绑定用户
6.2 总结
适用场景:
- 希望客户端可以忽略组合对象与单个对象的差异时。
- 对象层次具备整体和部分,呈树形结构(如树形菜单,操作系统目录结构,公司组织架构等)。
优点:
- 清楚地定义分层次的复杂对象,表示对象的全部或部分层次。
- 让客户端忽略了层次的差异,方便对整个层次结构进行控制。
- 简化客户端代码。
- 符合开闭原则。
缺点:
- 限制类型时会较为复杂。
- 使设计变得更加抽象。
分类:
- 透明组合模式
透明组合模式中,抽象根节点角色中声明了所有用于管理成员对象的方法,比如在示例中MenuComponent声明了add() 、 remove() 、getChild()方法,这样做的好处是确保所有的构件类都有相同的接口。透明组合模式也是组合模式的标准形式。
透明组合模式的缺点是不够安全,因为叶子对象和容器对象在本质上是有区别的,叶子对象不可能有下一个层次的对象,即不可能包含成员对象,因此为其提供 add()、remove() 等方法是没有意义的,这在编译阶段不会出错,但在运行阶段如果调用这些方法可能会出错(如果没有提供相应的错误处理代码) - 安全组合模式
在安全组合模式中,在抽象构件角色中没有声明任何用于管理成员对象的方法,而是在树枝节点Menu类中声明并实现这些方法。安全组合模式的缺点是不够透明,因为叶子构件和容器构件具有不同的方法,且容器构件中那些用于管理成员对象的方法没有在抽象构件类中定义,因此客户端不能完全针对抽象编程,必须有区别地对待叶子构件和容器构件。
7.享元模式(Flyweight Pattern)
享元模式又称为轻量级模式,是对象池的一种实现,类似于线程池,线程池可以避免不停的创建和销毁多个对象,消耗性能。提供了减少对象数量从而改善应用所需的对象结构的方式。宗旨:共享细粒度对象,将多个对同一对象的访问集中起来。
享元(Flyweight )模式中存在以下两种状态:
- 内部状态,即不会随着环境的改变而改变的可共享部分。
- 外部状态,指随环境改变而改变的不可以共享的部分。享元模式的实现要领就是区分应用中的这两种状态,并将外部状态外部化。
享元模式的主要有以下角色:
- 抽象享元角色(Flyweight):通常是一个接口或抽象类,在抽象享元类中声明了具体享元类公共的方法,这些方法可以向外界提供享元对象的内部数据(内部状态),同时也可以通过这些方法来设置外部数据(外部状态)。
- 具体享元(Concrete Flyweight)角色 :它实现了抽象享元类,称为享元对象;在具体享元类中为内部状态提供了存储空间。通常我们可以结合单例模式来设计具体享元类,为每一个具体享元类提供唯一的享元对象。
- 非享元(Unsharable Flyweight)角色 :并不是所有的抽象享元类的子类都需要被共享,不能被共享的子类可设计为非共享具体享元类;当需要一个非共享具体享元类的对象时可以直接通过实例化创建。
- 享元工厂(Flyweight Factory)角色 :负责创建和管理享元角色。当客户对象请求一个享元对象时,享元工厂检査系统中是否存在符合要求的享元对象,如果存在则提供给客户;如果不存在的话,则创建一个新的享元对象。
实现
// 享元对象接口
public interface ITicket {
void show(String seat);
}
//具体享元对象
public class TrainTicket implements ITicket {
private String from;
private String to;
private Integer price;
public TrainTicket(String from, String to) {
this.from = from;
this.to = to;
}
@Override
public void show(String seat) {
this.price = new Random().nextInt(500);
System.out.println(from + "->" + to + ":" + seat + "价格:" + this.price);
}
}
// 工厂类
public class TicketFactory {
private static Map<String, ITicket> pool = new ConcurrentHashMap<>();
public static ITicket getTicket(String from, String to) {
String key = from + "->" + to;
if (pool.containsKey(key)) {
System.out.println("使用缓存获取火车票:" + key);
return pool.get(key);
}
System.out.println("使用数据库获取火车票:" + key);
ITicket ticket = new TrainTicket(from, to);
pool.put(key, ticket);
return ticket;
}
}
// 测试
public static void main(String[] args) {
ITicket ticket = getTicket("北京", "上海");
//使用数据库获取火车票:北京->上海
//北京->上海:二等座价格:20
ticket.show("二等座");
ITicket ticket1 = getTicket("北京", "上海");
//使用缓存获取火车票:北京->上海
//北京->上海:商务座价格:69
ticket1.show("商务座");
ITicket ticket2 = getTicket("上海", "北京");
//使用数据库获取火车票:上海->北京
//上海->北京:一等座价格:406
ticket2.show("一等座");
System.out.println(ticket == ticket1);//true
System.out.println(ticket == ticket2);//false
}
可以看到ticket和ticket2是使用数据库查询的,而ticket1是使用缓存查询的,同时ticket == ticket1返回的是true,ticket == ticket2返回的是false,证明ticket和ticket1是共享的对象。
7.2 总结
适用场景:
- 一个系统有大量相同或者相似的对象,造成内存的大量耗费。
- 对象的大部分状态都可以外部化,可以将这些外部状态传入对象中。
- 在使用享元模式时需要维护一个存储享元对象的享元池,而这需要耗费一定的系统资源,因此,应当在需要多次重复使用享元对象时才值得使用享元模式。
优点:
- 减少对象的创建,降低内存中对象的数量,降低系统的内存,提高效率。
- 减少内存之外的其他资源占用。
缺点:
- 关注内、外部状态。
- 关注线程安全问题。
- 使系统、程序的逻辑复杂化。
行为型
1 模板方法模式(Template method pattern)
模板方法模式通常又叫模板模式,是指定义一个算法的骨架,并允许子类为其中的一个或者多个步骤提供实现。模板方法模式使得子类可以在不改变算法结构的情况下,重新定义算法的某些步骤。
模板方法(Template Method)模式包含以下主要角色:
- 抽象类(Abstract Class):负责给出一个算法的轮廓和骨架。它由一个模板方法和若干个基本方法构成。
- 模板方法:定义了算法的骨架,按某种顺序调用其包含的基本方法。
- 基本方法:是实现算法各个步骤的方法,是模板方法的组成部分。基本方法又可以分为三种:
- 抽象方法(Abstract Method) :一个抽象方法由抽象类声明、由其具体子类实现。
具体方法(Concrete Method) :一个具体方法由一个抽象类或具体类声明并实现,其子类可以进行覆盖也可以直接继承。 - 钩子方法(Hook Method) :在抽象类中已经实现,包括用于判断的逻辑方法和需要子类重写的空方法两种。一般钩子方法是用于判断的逻辑方法,这类方法名一般为isXxx,返回值类型为boolean类型。
- 具体子类(Concrete Class):实现抽象类中所定义的抽象方法和钩子方法,它们是一个顶级逻辑的组成步骤。
1.1 代码实现
请假流程
public abstract class DayOffProcess {
// 请假模板
public final void dayOffProcess() {
// 领取申请表
this.pickUpForm();
// 填写申请信息
this.writeInfo();
// 签名
this.signUp();
// 提交到不同部门审批
this.summit();
// 行政部备案
this.filing();
}
private void filing() {
System.out.println("行政部备案");
}
protected abstract void summit();
protected abstract void signUp();
private void writeInfo() {
System.out.println("填写申请信息");
}
private void pickUpForm() {
System.out.println("领取申请表");
}
}
public class ZhangSan extends DayOffProcess {
@Override
protected void summit() {
System.out.println("张三签名");
}
@Override
protected void signUp() {
System.out.println("提交到技术部审批");
}
}
public class Lisi extends DayOffProcess {
@Override
protected void summit() {
System.out.println("李四签名");
}
@Override
protected void signUp() {
System.out.println("提交到市场部审批");
}
}
// 测试方法
public static void main(String[] args) {
DayOffProcess zhangsan = new ZhangSan();
//领取申请表
//填写申请信息
//提交到技术部审批
//张三签名
//行政部备案
zhangsan.dayOffProcess();
DayOffProcess lisi = new Lisi();
//领取申请表
//填写申请信息
//提交到市场部审批
//李四签名
//行政部备案
lisi.dayOffProcess();
}
1.2 总结
适用场景:
- 一次性实现一个算法不变的部分,并将可变的行为留给子类来实现。
- 各子类中公共的行为被提取出来并集中到一个公共的父类中,从而避免代码重复。
优点:
- 利用模板方法将相同处理逻辑的代码放到抽象父类中,可以提高代码的复用性。
- 将不同的代码不同的子类中,通过对子类的扩展增加新的行为,提高代码的扩展性。
- 把不变的行为写在父类上,去除子类的重复代码,提供了一个很好的代码复用平台,符合开闭原则。
缺点:
- 类数目的增加,每一个抽象类都需要一个子类来实现,这样导致类的个数增加。
- 类数量的增加,间接地增加了系统实现的复杂度。
- 继承关系自身缺点,如果父类添加新的抽象方法,所有子类都要改一遍。
2策略模式(Strategy Pattern)
策略模式又叫政策模式(Policy Pattern),它是将定义的算法家族分别封装起来,让它们之间可以互相替换,从而让算法的变化不会影响到使用算法的用户。可以避免多重分支的if…else和switch语句。
策略模式的主要角色如下:
- 抽象策略(Strategy)类:这是一个抽象角色,通常由一个接口或抽象类实现。此角色给出所有的具体策略类所需的接口。
- 具体策略(Concrete Strategy)类:实现了抽象策略定义的接口,提供具体的算法实现或行为。
- 环境(Context)类:持有一个策略类的引用,最终给客户端调用。
2.1 代码实现
会员卡打折案例
// 会员卡接口 (抽象策略)
public interface VipCard {
public void discount();
}
//具体策略
public class GoldCard implements VipCard {
@Override
public void discount() {
System.out.println("金卡打7折");
}
}
//具体策略
public class SilverCard implements VipCard {
@Override
public void discount() {
System.out.println("银卡打8折");
}
}
//具体策略
public class CopperCard implements VipCard {
@Override
public void discount() {
System.out.println("铜卡打9折");
}
}
//具体策略
public class Normal implements VipCard {
@Override
public void discount() {
System.out.println("普通会员没有折扣");
}
}
// 会员卡容器类 (策略工厂上下文)
public class VipCardFactory {
private static Map<String, VipCard> map = new ConcurrentHashMap<>();
static {
map.put("gold", new GoldCard());
map.put("silver", new SilverCard());
map.put("copper", new CopperCard());
}
public static VipCard getVIPCard(String level) {
return map.get(level) != null ? map.get(level) : new Normal();
}
}
// 测试方法
public static void main(String[] args) {
//金卡打7折
VipCardFactory.getVIPCard("gold").discount();
//银卡打8折
VipCardFactory.getVIPCard("silver").discount();
//普通会员没有折扣
VipCardFactory.getVIPCard("other").discount();
}
2.2 总结
适用场景:
- 系统中有很多类,而它们的区别仅仅在于它们的行为不同。
- 系统需要动态地在几种算法中选择一种。
- 需要屏蔽算法规则。
优点:
- 符合开闭原则。
- 避免使用多重条件语句。
- 可以提高算法的保密性和安全性。
- 易于扩展。
缺点:
- 客户端必须知道所有的策略,并且自行决定使用哪一个策略类。
- 代码中会产生非常多的策略类,增加维护难度。
3 命令模式(Command Pattern)
命令模式是对命令的封装,每一个命令都是一个操作:请求的一方发出请求要求执行一个操作;接收的一方收到请求,并执行操作。命令模式解耦了请求方和接收方,请求方只需请求执行命令,不用关心命令是怎样被接收,怎样被操作以及是否被执行等。本质:解耦命令的请求与处理。
命令模式包含以下主要角色:
- 命令接口(Command Interface):定义了执行命令的方法。
- 具体命令类(Concrete Command):实现命令接口,封装了请求的接收者和执行操作。
- 接收者类(Receiver):实际执行请求的操作。
- 请求者类(Invoker):负责创建命令对象并将其发送给请求接收者执行。
- 客户端(Client):创建并配置请求者和具体命令对象。
普通思路:
命令发送者——>命令接收者
在普通思路中,命令发送者直接作用命令接收者
命令模式思路:
命令发送者——>命令请求者——>命令接收者
在命令模式思路中,在两者之间增加了一个请求者类,命令发送者先于命令请求者交互,请求者再和命令接收者交互。在此过程中,请求者起到了一个桥梁的作用。
3.1 代码实现
//命令接口
interface ICommand {
public void sweep();
}
//具体命令
class TeacherCommand implements ICommand {
private Student student;
public TeacherCommand(Student student) {
this.student = student;
}
@Override
public void sweep() {
student.sweep();
}
}
//命令接收者
class Student {
public void sweep() {
System.out.println("开始打扫卫生");
}
}
//命令请求者
class Invoke {
ICommand command;
public Invoke(ICommand command) {
this.command = command;
}
public void execute() {
command.sweep();
}
}
//测试
public class A {
public static void main(String[] args) throws Exception {
//命令接收者
Student student = new Student();
//具体命令
TeacherCommand command = new TeacherCommand(student);
//命令请求者
Invoke invoke = new Invoke(command);
invoke.execute();
}
}
3.2 总结
适用场景:
- 现实语义中具备“命令”的操作(如命令菜单,shell命令…)。
- 请求调用者和请求接收者需要解耦,使得调用者和接收者不直接交互。
- 需要抽象出等待执行的行为,比如撤销操作和恢复操作等。
- 需要支持命令宏(即命令组合操作)。
优点:
- 通过引入中间件(抽象接口),解耦了命令的请求与实现。
- 扩展性良好,可以很容易地增加新命令。
- 支持组合命令,支持命令队列。
- 可以在现有的命令的基础上,增加额外功能。
缺点:
- 具体命令类可能过多。
- 增加了程序的复杂度,理解更加困难。
4.职责链模式(chain of responsibility pattern)
职责链模式用于将请求的发送者和接收者解耦,使多个对象都有机会处理请求。请求沿着一个链路传递,直到有一个对象处理它为止
职责链模式主要包含以下角色:
- 抽象处理者(Handler):定义处理请求的接口,包含一个处理请求的方法,并可以设置下一个处理者。
- 具体处理者(ConcreteHandler):实现抽象处理者接口,负责处理具体的请求,如果自己无法处理,则传递给下一个处理者处理。
- 客户端(Client):创建职责链,将请求发送给链的第一个处理者。
4.1 代码实现
简单的登录校验流程
//用户实体类
@Data
class User {
private String username;
private String password;
private String role;
}
//抽象处理类
abstract class Handler {
protected Handler nextHandler;
public void setNextHandler(Handler nextHandler) {
this.nextHandler = nextHandler;
}
public abstract void doHandler(User user);
}
//校验用户名或者密码是否为空
class ValidateHandler extends Handler {
@Override
public void doHandler(User user) {
if (StringUtils.isBlank(user.getUsername()) || StringUtils.isBlank(user.getPassword())) {
System.out.println("用户名或者密码为空");
return;
}
System.out.println("校验通过");
nextHandler.doHandler(user);
}
}
// 权限校验
class AuthHandler extends Handler {
@Override
public void doHandler(User user) {
if (!"admin".equals(user.getRole())) {
System.out.println("无权限操作!");
return;
}
System.out.println("角色为管理员,可以进行下一步操作!");
}
}
//登录流程,创建处理链
class LoginService {
public void login(User user) {
Handler validateHandler = new ValidateHandler();
Handler authHandler = new AuthHandler();
validateHandler.setNextHandler(authHandler);
validateHandler.doHandler(user);
}
}
//测试
public class Test {
public static void main(String[] args) throws Exception {
User user = new User();
user.setUsername("张三");
user.setPassword("123");
user.setRole("admin");
LoginService loginService = new LoginService();
loginService.login(user);
}
}
4.2 结合建造者模式
与基础版本区别主要是Handler类中新增一个Builder的内部类,以及流程类里改用链式写法,具体如下:
//用户实体类
@Data
class User {
private String username;
private String password;
private String role;
}
//抽象处理类
abstract class Handler<T> {
protected Handler nextHandler;
public Handler next(Handler nextHandler) {
this.nextHandler = nextHandler;
return nextHandler;
}
public abstract void doHandler(User user);
static class Builder<T> {
private Handler<T> head;
private Handler<T> tail;
public Builder<T> addHandler(Handler<T> handler) {
if (this.head == null) {
this.head = this.tail = handler;
return this;
}
this.tail.next(handler);
this.tail = handler;
return this;
}
public Handler<T> build() {
return this.head;
}
}
}
//校验用户名或者密码是否为空
class ValidateHandler extends Handler {
@Override
public void doHandler(User user) {
if (StringUtils.isBlank(user.getUsername()) || StringUtils.isBlank(user.getPassword())) {
System.out.println("用户名或者密码为空");
return;
}
System.out.println("校验通过");
nextHandler.doHandler(user);
}
}
// 权限校验
class AuthHandler extends Handler {
@Override
public void doHandler(User user) {
if (!"admin".equals(user.getRole())) {
System.out.println("无权限操作!");
return;
}
System.out.println("角色为管理员,可以进行下一步操作!");
}
}
//登录流程,创建处理链
class LoginService {
public void login(User user) {
Builder builder = new Handler.Builder();
Handler validateHandler = new ValidateHandler();
Handler authHandler = new AuthHandler();
builder.addHandler(validateHandler).addHandler(authHandler);
builder.build().doHandler(user);
}
}
//测试
public class Test {
public static void main(String[] args) throws Exception {
User user = new User();
user.setUsername("张三");
user.setPassword("123");
user.setRole("admin");
LoginService loginService = new LoginService();
loginService.login(user);
}
}
4.3 总结
适用场景:
- 多个对象可以处理同一请求,但具体由哪个对象处理则在运行时动态决定。
- 在不明确指定接收者的情况下,向多个对象中的一个提交一个请求。
- 可动态指定一组对象处理请求。
优点:
- 将请求与处理解耦。
- 请求处理者(节点对象)只需关注自己感兴趣的请求进行处理即可,对于不感兴趣的请求,直接转发给下一级节点对象。
- 具备链式传递处理请求功能,请求发送者无需知晓链路结构,只需等待请求处理结果。
- 链路结构灵活,可以通过改变链路结构动态地新增或删减责任。
- 易于扩展新的请求处理类(节点),符合开闭原则。
缺点:
- 责任链太长或者处理时间过长,会影响整体性能。
- 如果节点对象存在循环引用时,会造成死循环,导致系统崩溃。
5.状态模式(State Pattern)
状态模式也称为状态机模式(State Machine Pattern),是允许对象在内部状态发生改变时改变它的行为,对象看起来好像修改了它的类。
状态模式包含以下主要角色:
- 环境(Context)角色:也称为上下文,它定义了客户程序需要的接口,维护一个当前状态,并将与状态相关的操作委托给当前状态对象来处理。
- 抽象状态(State)角色:定义一个接口,用以封装环境对象中的特定状态所对应的行为。
- 具体状态(Concrete State)角色:实现抽象状态所对应的行为。
5.1 代码实现
// 电梯状态
public abstract class LiftState {
protected Context context;
public abstract void open();
public abstract void close();
public abstract void run();
public abstract void stop();
}
// 开门状态
public class OpenState extends LiftState {
@Override
public void open() {
System.out.println("电梯门打开了");
}
@Override
public void close() {
super.context.setLiftState(Context.CLOSE_STATE);
super.context.close();
}
@Override
public void run() {
}
@Override
public void stop() {
}
}
// 关门状态
public class CloseState extends LiftState {
@Override
public void open() {
super.context.setLiftState(Context.OPEN_STATE);
super.context.open();
}
@Override
public void close() {
System.out.println("电梯门关闭了!");
}
@Override
public void run() {
super.context.setLiftState(Context.RUN_STATE);
super.context.run();
}
@Override
public void stop() {
super.context.setLiftState(Context.STOP_STATE);
super.context.stop();
}
}
// 运行状态
public class RunState extends LiftState {
@Override
public void open() {
}
@Override
public void close() {
}
@Override
public void run() {
System.out.println("电梯正在运行...");
}
@Override
public void stop() {
super.context.setLiftState(Context.STOP_STATE);
super.context.stop();
}
}
// 停止状态
public class StopState extends LiftState {
@Override
public void open() {
super.context.setLiftState(Context.OPEN_STATE);
super.context.open();
}
@Override
public void close() {
super.context.setLiftState(Context.CLOSE_STATE);
super.context.close();
}
@Override
public void run() {
super.context.setLiftState(Context.RUN_STATE);
super.context.run();
}
@Override
public void stop() {
System.out.println("电梯停止了!");
}
}
// 上下文
public class Context {
private LiftState liftState;
public static final LiftState OPEN_STATE = new OpenState();
public static final LiftState CLOSE_STATE = new CloseState();
public static final LiftState RUN_STATE = new RunState();
public static final LiftState STOP_STATE = new StopState();
public void setLiftState(LiftState liftState) {
this.liftState = liftState;
this.liftState.setContext(this);
}
public void open() {
this.liftState.open();
}
public void close() {
this.liftState.close();
}
public void run() {
this.liftState.run();
}
public void stop() {
this.liftState.stop();
}
}
// 测试
public static void main(String[] args){
Context context = new Context();
context.setLiftState(new CloseState());
//电梯门打开了
//电梯门关闭了!
//电梯正在运行...
//电梯停止了!
context.open();
context.close();
context.run();
context.stop();
}
5.3 总结
适用场景:
- 行为随状态改变而改变的场景。
- 一个操作中含有庞大的多分支结构,并且这些分支取决于对象的状态。
优点:
- 结构清晰:将状态独立为类,消除了冗余的if…else或switch…case语句,使代码更加简洁,提高系统可维护性。
- 将状态转换显示化:通常的对象内部都是使用数值类型来定义状态,状态的切换是通过赋值进行表现,不够直观;而使用状态类,在切换状态时,是以不同的类进行表示,转换目的更加明确。
- 状态类职责明确且具备扩展性。
缺点:
- 类膨胀:如果一个事物具备很多状态,则会造成状态类太多。
- 状态模式的结构与实现都较为复杂,如果使用不当将导致程序结构和代码的混乱。
- 状态模式对开闭原则的支持并不太好,对于可以切换状态的状态模式,增加新的状态类需要修改那些负责状态转换的源代码,否则无法切换到新增状态,而且修改某个状态类的行为也需修改对应类的源代码。
6.观察者模式(Observer Mode)
观察者模式,又叫发布-订阅(Publish/Subscribe)模式。定义一种一对多的依赖关系,一个主题对象可被多个观察者同时监听,使得每当主题对象状态变化时,所有依赖于它的对象都会得到通知并被自动更新。
在观察者模式中有如下角色:
- Subject:抽象主题(抽象被观察者),抽象主题角色把所有观察者对象保存在一个集合里,每个主题都可以有任意数量的观察者,抽象主题提供一个接口,可以增加和删除观察者对象。
- ConcreteSubject:具体主题(具体被观察者),该角色将有关状态存入具体观察者对象,在具体主题的内部状态发生改变时,给所有注册过的观察者发送通知。
- Observer:抽象观察者,是观察者的抽象类,它定义了一个更新接口,使得在得到主题更改通知时更新自己。
- ConcrereObserver:具体观察者,实现抽象观察者定义的更新接口,以便在得到主题更改通知时更新自身的状态。
6.1 代码实现
//抽象观察者接口
interface Observer {
void update(String message);
}
//具体观察者 微信用户是具体观察者
@AllArgsConstructor
class WeixinUser implements Observer {
private String name;
@Override
public void update(String message) {
System.out.println(name + "接收到了消息(观察到了):" + message);
}
}
//被观察者接口
interface Observable {
//新增用户(新增观察者)
void add(Observer observer);
//移除用户,或者说用户取消订阅(移除观察者)
void del(Observer observer);
//发布 推送消息
void notify(String message);
}
//具体被观察者(公众号)
class Subject implements Observable {
//观察者列表
private List<Observer> list = new ArrayList<>();
@Override
public void add(Observer observer) {
list.add(observer);
}
@Override
public void del(Observer observer) {
list.remove(observer);
}
@Override
public void notify(String message) {
list.forEach(observer -> observer.update(message));
}
}
//测试
public class Test {
public static void main(String[] args) {
Observable o = new Subject();
WeixinUser user1 = new WeixinUser("张三");
WeixinUser user2 = new WeixinUser("李四");
WeixinUser user3 = new WeixinUser("王五");
o.add(user1);
o.add(user2);
o.add(user3);
o.notify("薛之谦演唱会要来到广州啦!");
}
}
6.2 JDK实现
在 Java 中,通过java.util.Observable类和 java.util.Observer接口定义了观察者模式,只要实现它们的子类就可以编写观察者模式实例。
6.2.1 Observable类
Observable类是抽象目标类(被观察者),它有一个Vector集合成员变量,用于保存所有要通知的观察者对象,下面是它最重要的 3 个方法:
void addObserver(Observer o) 方法:用于将新的观察者对象添加到集合中。
void notifyObservers(Object arg) 方法:调用集合中的所有观察者对象的update方法,通知它们数据发生改变。通常越晚加入集合的观察者越先得到通知。
void setChange() 方法:用来设置一个boolean类型的内部标志,注明目标对象发生了变化。当它为true时,notifyObservers() 才会通知观察者。
6.2.2 Observer 接口
Observer 接口是抽象观察者,它监视目标对象的变化,当目标对象发生变化时,观察者得到通知,并调用 update 方法,进行相应的工作。
6.2.3 代码实现
//具体观察者 微信用户是具体观察者
@AllArgsConstructor
class WeixinUser implements Observer {
//微信用户的名字
private String name;
/**
* @param o 被观察者
* @param arg 被观察者带过来的参数,此例子中是公众号发布的消息
*/
@Override
public void update(Observable o, Object arg) {
System.out.println(name + "关注了公众号(被观察者):" + ((Subject) o).getName() + ",接收到消息:" + arg);
}
}
//具体被观察者(公众号)
@Data
@AllArgsConstructor
class Subject extends Observable {
//公众号的名字
private String name;
public void notifyMessage(String message) {
System.out.println(this.name + "公众号发布消息:" + message + "请关注用户留意接收!");
super.setChanged();
super.notifyObservers(message);
}
}
//测试
public class Test {
public static void main(String[] args) {
WeixinUser user1 = new WeixinUser("张三");
WeixinUser user2 = new WeixinUser("李四");
WeixinUser user3 = new WeixinUser("王五");
Subject subject = new Subject("演唱会消息发布");
subject.addObserver(user1);
subject.addObserver(user2);
subject.addObserver(user3);
subject.notifyMessage("薛之谦演唱会要来到广州啦!");
}
}
6.4 总结
适用场景:
- 当一个抽象模型包含两个方面内容,其中一个方面依赖于另一个方面。
- 其他一个或多个对象的变化依赖于另一个对象的变化。
- 实现类似广播机制的功能,无需知道具体收听者,只需分发广播,系统中感兴趣的对象会自动接收该广播。
- 多层级嵌套使用,形成一种链式触发机制,使得事件具备跨域(跨越两种观察者类型)通知。
优点:
- 观察者和被观察者是松耦合(抽象耦合)的,符合依赖倒置原则。
- 分离了表示层(观察者)和数据逻辑层(被观察者),并且建立了一套触发机制,使得数据的变化可以相应到多个表示层上。
- 实现了一对多的通讯机制,支持事件注册机制,支持兴趣分发机制,当被观察者触发事件时,只有感兴趣的观察者可以接收到通知。
缺点:
- 如果观察者数量过多,则事件通知会耗时较长。
- 事件通知呈线性关系,如果其中一个观察者处理事件卡壳,会影响后续的观察者接收该事件。
- 如果观察者和被观察者之间存在循环依赖,则可能造成两者之间的循环调用,导致系统崩溃。
7.中介者模式(mediator pattern)
中介者模式又称为调解者模式或调停者模式。用一个中介对象封装一系列的对象交互,中介者使各对象不需要显示地相互作用,从而使其耦合松散,而且可以独立地改变它们之间的交互。
核心:通过中介者解耦系统各层次对象的直接耦合,层次对象的对外依赖通信统统交由中介者转发。
中介者模式包含以下主要角色:
- 抽象中介者(Mediator)角色:它是中介者的接口,提供了同事对象注册与转发同事对象信息的抽象方法。
- 具体中介者(ConcreteMediator)角色:实现中介者接口,定义一个 List 来管理同事对象,协调各个同事角色之间的交互关系,因此它依赖于同事角色。
- 抽象同事类(Colleague)角色:定义同事类的接口,保存中介者对象,提供同事对象交互的抽象方法,实现所有相互影响的同事类的公共功能。
- 具体同事类(Concrete Colleague)角色:是抽象同事类的实现者,当需要与其他同事对象交互时,由中介者对象负责后续的交互。
7.1 代码实现
通过一个租房例子简单实现下逻辑,房主通过中介公司发布自己的房子的信息,而租客则需要通过中介公司获取到房子的信息:
// 抽象同事类
@AllArgsConstructor
public class Person {
protected String name;
protected MediatorCompany mediatorCompany;
}
// 房主
public class HouseOwner extends Person {
public HouseOwner(String name, MediatorCompany mediatorCompany) {
super(name, mediatorCompany);
}
// 联络方法
public void connection(String message) {
mediatorCompany.connection(this, message);
}
// 获取消息
public void getMessage(String message) {
System.out.println("房主" + name + "获取到的信息:" + message);
}
}
// 租客
public class Tenant extends Person {
public Tenant(String name, MediatorCompany mediatorCompany) {
super(name, mediatorCompany);
}
public void connection(String message) {
mediatorCompany.connection(this, message);
}
public void getMessage(String message) {
System.out.println("租客" + name + "获取到的信息:" + message);
}
}
// 中介公司(中介者)
@Data
public class MediatorCompany {
private HouseOwner houseOwner;
private Tenant tenant;
public void connection(Person person, String message) {
// 房主需要通过中介获取租客信息
if (person.equals(houseOwner)) {
this.tenant.getMessage(message);
} else { // 反之租客通过中介获取房主信息
this.houseOwner.getMessage(message);
}
}
}
// 测试
public static void main(String[] args){
// 先创建三个角色,中介公司,房主,租客
MediatorCompany mediatorCompany = new MediatorCompany();
// 房主和租客都在同一家中介公司
HouseOwner houseOwner = new HouseOwner("张三", mediatorCompany);
Tenant tenant = new Tenant("李四", mediatorCompany);
// 中介公司获取房主和租客的信息
mediatorCompany.setHouseOwner(houseOwner);
mediatorCompany.setTenant(tenant);
// 房主和租客都在这家中介公司发布消息,获取到对应的消息
tenant.connection(tenant.name + "想租一房一厅!");
houseOwner.connection(houseOwner.name + "这里有!来看看呗!");
// 测试结果
// 房主张三获取到的信息:李四想租一房一厅!
// 租客李四获取到的信息:张三这里有!来看看呗!
}
7.2 总结
适用场景:
- 系统中对象之间存在复杂的引用关系,产生的我相互依赖关系结构混乱且难以理解。
- 交互的公共行为,如果需要改变行为则可以增加新的中介者类。
优点:
- 减少类间的依赖,将多对多依赖转化成了一对多,降低了类间耦合。
- 类间各司其职,符合迪米特法则。
缺点:
中介者模式中将原本多个对象直接的相互依赖变成了中介者和多个同事类的依赖关系。当同事类越多时,中介者就会越臃肿,变得复杂且难以维护。
8.迭代器模式(Iterator Pattern)
迭代器模式又称为游标模式(Cursor Pattern),它提供一种顺序访问集合/容器对象元素的方法,而又无须暴露结合内部表示。
本质:抽离集合对象迭代行为到迭代器中,提供一致访问接口。
迭代器模式主要包含以下角色:
- 抽象聚合(Aggregate)角色:定义存储、添加、删除聚合元素以及创建迭代器对象的接口。
- 具体聚合(ConcreteAggregate)角色:实现抽象聚合类,返回一个具体迭代器的实例。
- 抽象迭代器(Iterator)角色:定义访问和遍历聚合元素的接口,通常包含 hasNext()、next() 等方法。
- 具体迭代器(Concretelterator)角色:实现抽象迭代器接口中所定义的方法,完成对聚合对象的遍历,记录遍历的当前位置。
8.1 代码实现
//迭代器接口
interface Iterator<T> {
Boolean hashNext();
T next();
}
//迭代器接口实现类
class IteratorImpl<T> implements Iterator<T> {
private List<T> list;
private Integer cursor;
private T element;
public IteratorImpl(List<T> list) {
this.list = list;
}
@Override
public Boolean hashNext() {
return cursor < list.size();
}
@Override
public T next() {
element = list.get(cursor);
cursor++;
return element;
}
}
//容器接口
interface Aggregate<T> {
void add(T t);
void remove(T t);
Iterator<T> iterator();
}
//容器接口实现类
class AggregateImpl<T> implements Aggregate<T> {
private List<T> list = new ArrayList<>();
@Override
public void add(T t) {
list.add(t);
}
@Override
public void remove(T t) {
list.remove(t);
}
@Override
public Iterator iterator() {
return new IteratorImpl<>(list);
}
}
//测试
public class Test {
public static void main(String[] args) {
Aggregate aggregate = new AggregateImpl();
aggregate.add(1);
aggregate.add(2);
Iterator iterator = aggregate.iterator();
while (iterator.hashNext()) {
System.out.println(iterator.next());
}
}
}
8.2 总结
适用场景:
- 访问一个集合对象的内容而无需暴露它的内部表示。
- 为遍历不同的集合结构提供一个统一的访问接口。
优点:
- 多态迭代:为不同的聚合结构提供一致的遍历接口,即一个迭代接口可以访问不同的聚集对象。
- 简化集合对象接口:迭代器模式将集合对象本身应该提供的元素迭代接口抽取到了迭代器中,使集合对象无须关心具体迭代行为。
- 元素迭代功能多样化:每个集合对象都可以提供一个或多个不同的迭代器,使的同种元素聚合结构可以有不同的迭代行为。
- 解耦迭代与集合:迭代器模式封装了具体的迭代算法,迭代算法的变化,不会影响到集合对象的架构。
缺点:
- 对于比较简单的遍历(像数组或者有序列表),使用迭代器方式遍历较为繁琐。
- 增加了类的个数,在一定程度上增加了系统的复杂性。
9.访问者模式(Visitor Pattern)
访问者模式是一种将数据结构与数据操作分离的设计模式。是指封装一些作用于某种数据结构中的各元素的操作。
特征:可以在不改变数据结构的前提下定义作用于这些元素的新的操作。
访问者模式包含以下主要角色:
- 抽象访问者(Visitor)角色:定义了对每一个元素 (Element) 访问的行为,它的参数就是可以访问的元素,它的方法个数理论上来讲与元素类个数(Element的实现类个数)是一样的,从这点不难看出,访问者模式要求元素类的个数不能改变。
- 具体访问者(ConcreteVisitor)角色:给出对每一个元素类访问时所产生的具体行为。
- 抽象元素(Element)角色:定义了一个接受访问者的方法( accept ),其意义是指,每一个元素都要可以被访问者访问。
- 具体元素(ConcreteElement)角色: 提供接受访问方法的具体实现,而这个具体的实现,通常情况下是使用访问者提供的访问该元素类的方法。
- 对象结构(Object Structure)角色:定义当中所提到的对象结构,对象结构是一个抽象表述,具体点可以理解为一个具有容器性质或者复合对象特性的类,它会含有一组元素( Element ),并且可以迭代这些元素,供访问者访问。
9.1 代码实现
//抽象访问者接口
interface Person {
void feed(Cat cat);
void feed(Dog dog);
}
//具体访问者
class Owner implements Person {
@Override
public void feed(Cat cat) {
System.out.println("主人喂食猫");
}
@Override
public void feed(Dog dog) {
System.out.println("主人喂食狗");
}
}
//具体访问者
class Someone implements Person {
@Override
public void feed(Cat cat) {
System.out.println("客人喂食猫");
}
@Override
public void feed(Dog dog) {
System.out.println("其他人喂食狗");
}
}
//抽象元素
interface Animal {
void accept(Person person);
}
//具体元素 1
class Dog implements Animal {
@Override
public void accept(Person person) {
person.feed(this);
System.out.println("好好吃,汪汪汪!!!!");
}
}
//具体元素 2
class Cat implements Animal {
@Override
public void accept(Person person) {
person.feed(this);
System.out.println("好好吃,喵喵喵!!!");
}
}
//对象结构
class Home {
private List<Animal> nodeList = new ArrayList<>();
public void action(Person person) {
for (Animal node : nodeList) {
node.accept(person);
}
}
//添加操作
public void add(Animal animal) {
nodeList.add(animal);
}
}
//测试
public class Test {
public static void main(String[] args) {
Home home = new Home();
home.add(new Dog());
home.add(new Cat());
Owner owner = new Owner();
home.action(owner);
Someone someone = new Someone();
home.action(someone);
}
}
9.3 总结
适用场景:
- 数据结构稳定,作用于数据结构的操作经常变化的场景。
- 需要数据结构与数据操作分离的场景。
- 需要对不同数据类型(元素)进行操作,而不使用分支判断具体类型的场景。
优点:
- 解耦了数据结构与数据操作,使得操作集合可以独立变化。
- 扩展性好:可以通过扩展访问者角色,实现对数据集的不同操作。
- 元素具体类型并非单一,访问者均可操作。
- 各角色职责分离,符合单一职责原则。
缺点:
- 无法增加元素类型:若系统数据结构对象易于变化,经常有新的数据对象增加进来,则访问者类必须增加对应元素类型的操作,违背了开闭原则。
- 具体元素变更困难:具体元素增加属性,删除属性等操作会导致对应的访问者类需要进行相应的修改,尤其当有大量访问者类时,修改访问太大。
- 违背依赖倒置原则:为了达到“区别对待”,访问者依赖的是具体元素类型,而不是抽象。
10 备忘录模式(Memento Pattern)
备忘录模式又称为快照模式(Snapshot Pattern)或令牌模式(Token Pattern),是指在不破坏封装的前提下,捕获一个对象的内部状态,并在对象之外保存这个状态,这样以后就可将该对象恢复到原先保存的状态。
特征:“后悔药”
备忘录模式的主要角色如下:
- 发起人(Originator)角色:记录当前时刻的内部状态信息,提供创建备忘录和恢复备忘录数据的功能,实现其他业务功能,它可以访问备忘录里的所有信息。
- 备忘录(Memento)角色:负责存储发起人的内部状态,在需要的时候提供这些内部状态给发起人。
- 管理者(Caretaker)角色:对备忘录进行管理,提供保存与获取备忘录的功能,但其不能对备忘录的内容进行访问与修改。
备忘录有两个等效的接口:
- 窄接口:管理者(Caretaker)对象(和其他发起人对象之外的任何对象)看到的是备忘录的窄接口(narror Interface),这个窄接口只允许他把备忘录对象传给其他的对象。
- 宽接口:与管理者看到的窄接口相反,发起人对象可以看到一个宽接口(wide Interface),这个宽接口允许它读取所有的数据,以便根据这些数据恢复这个发起人对象的内部状态。
10.1 “白箱”备忘录模式
下面就以游戏打怪为简单的例子进行代码实现(下面“黑箱”同这个例子):
备忘录角色对任何对象都提供一个宽接口,备忘录角色的内部所存储的状态就对所有对象公开。
// 游戏角色类
@Data
public class GameRole {
private Integer vit; // 生命力
private Integer atk; // 攻击力
private Integer def; // 防御力
// 初始化状态
public void init() {
this.vit = 100;
this.atk = 100;
this.def = 100;
}
// 战斗到0
public void fight() {
this.vit = 0;
this.atk = 0;
this.def = 0;
}
// 保存角色状态
public RoleStateMemento saveState() {
return new RoleStateMemento(this.vit, this.atk, this.def);
}
// 回复角色状态
public void recoverState(RoleStateMemento roleStateMemento) {
this.vit = roleStateMemento.getVit();
this.atk = roleStateMemento.getAtk();
this.def = roleStateMemento.getDef();
}
// 展示状态
public void showState() {
System.out.println("角色生命力:" + this.vit);
System.out.println("角色攻击力:" + this.atk);
System.out.println("角色防御力:" + this.def);
}
}
// 游戏状态存储类(备忘录类)
@Data
@AllArgsConstructor
public class RoleStateMemento {
private Integer vit; // 生命力
private Integer atk; // 攻击力
private Integer def; // 防御力
}
// 角色状态管理者类
@Data
public class RoleStateCaretaker {
private RoleStateMemento roleStateMemento;
}
// 测试结果
public static void main(String[] args){
System.out.println("===========打boss前状态===========");
GameRole gameRole = new GameRole();
gameRole.init();
gameRole.showState();
// 保存进度
RoleStateCaretaker roleStateCaretaker = new RoleStateCaretaker();
roleStateCaretaker.setRoleStateMemento(gameRole.saveState());
System.out.println("===========打boss后状态===========");
gameRole.fight();
gameRole.showState();
System.out.println("===========恢复状态===========");
gameRole.recoverState(roleStateCaretaker.getRoleStateMemento());
gameRole.showState();
// ===========打boss前状态===========
// 角色生命力:100
// 角色攻击力:100
// 角色防御力:100
// ===========打boss后状态===========
// 角色生命力:0
// 角色攻击力:0
// 角色防御力:0
// ===========恢复状态===========
// 角色生命力:100
// 角色攻击力:100
// 角色防御力:100
}
“白箱”备忘录模式是破坏封装性的,但是通过程序员自律,同样可以在一定程度上实现大部分的用意。
10.2 “黑箱”备忘录模式
备忘录角色对发起人对象提供了一个宽接口,而为其他对象提供一个窄接口,在Java语言中,实现双重接口的办法就是将备忘录类设计成发起人类的内部成员类。
将RoleStateMemento设为GameRole的内部类,从而将RoleStateMemento对象封装在GameRole 里面;在外面提供一个标识接口Memento给RoleStateCaretaker及其他对象使用。这样GameRole类看到的是RoleStateMemento所有的接口,而RoleStateCaretaker及其他对象看到的仅仅是标识接口Memento所暴露出来的接口,从而维护了封装型。
// 窄接口,标识接口
public interface Memento {
}
// 角色状态管理者类
@Data
public class RoleStateCaretaker {
private Memento memento;
}
// 游戏角色类
@Data
public class GameRole {
private Integer vit; // 生命力
private Integer atk; // 攻击力
private Integer def; // 防御力
// 初始化状态
public void init() {
this.vit = 100;
this.atk = 100;
this.def = 100;
}
// 战斗到0
public void fight() {
this.vit = 0;
this.atk = 0;
this.def = 0;
}
// 保存角色状态
public RoleStateMemento saveState() {
return new RoleStateMemento(this.vit, this.atk, this.def);
}
// 回复角色状态
public void recoverState(Memento memento) {
RoleStateMemento roleStateMemento = (RoleStateMemento) memento;
this.vit = roleStateMemento.getVit();
this.atk = roleStateMemento.getAtk();
this.def = roleStateMemento.getDef();
}
// 展示状态
public void showState() {
System.out.println("角色生命力:" + this.vit);
System.out.println("角色攻击力:" + this.atk);
System.out.println("角色防御力:" + this.def);
}
// 备忘录内部类
@Data
@AllArgsConstructor
private class RoleStateMemento implements Memento {
private Integer vit; // 生命力
private Integer atk; // 攻击力
private Integer def; // 防御力
}
}
// 测试结果
public static void main(String[] args){
System.out.println("===========打boss前状态===========");
GameRole gameRole = new GameRole();
gameRole.init();
gameRole.showState();
// 保存进度
RoleStateCaretaker roleStateCaretaker = new RoleStateCaretaker();
roleStateCaretaker.setMemento(gameRole.saveState());
System.out.println("===========打boss后状态===========");
gameRole.fight();
gameRole.showState();
System.out.println("===========恢复状态===========");
gameRole.recoverState(roleStateCaretaker.getMemento());
gameRole.showState();
// ===========打boss前状态===========
// 角色生命力:100
// 角色攻击力:100
// 角色防御力:100
// ===========打boss后状态===========
// 角色生命力:0
// 角色攻击力:0
// 角色防御力:0
// ===========恢复状态===========
// 角色生命力:100
// 角色攻击力:100
// 角色防御力:100
}
10.3 总结
适用场景:
- 需要保存历史快照的场景。
- 希望在对象之外保存状态,且除了自己其他类对象无法访问状态保存具体内容。
优点:
- 简化发起人实体类职责,隔离状态存储与获取,实现了信息的封装,客户端无需关心状态的保存细节。
- 提供状态回滚功能。
缺点:
- 消耗资源:如果需要保存的状态过多时,每一次保存都会消耗很多内存。
11 解释器模式(interpreter pattern)
解释器模式给定一个语言,定义它的文法的一种表示,并定义一个解释器,这个解释器使用该表示来解释语言中的句子。
特征:为了解释一种语言,而为语言创建的解释器。
解释器模式包含以下主要角色:
- 抽象表达式(Abstract Expression)角色:定义解释器的接口,约定解释器的解释操作,主要包含解释方法 interpret()。
- 终结符表达式(Terminal Expression)角色:是抽象表达式的子类,用来实现文法中与终结符相关的操作,文法中的每一个终结符都有一个具体终结表达式与之相对应。
- 非终结符表达式(Nonterminal Expression)角色:也是抽象表达式的子类,用来实现文法中与非终结符相关的操作,文法中的每条规则都对应于一个非终结符表达式。
- 环境(Context)角色:通常包含各个解释器需要的数据或是公共的功能,一般用来传递被所有解释器共享的数据,后面的解释器可以从这里获取这些值。
- 客户端(Client):主要任务是将需要分析的句子或表达式转换成使用解释器对象描述的抽象语法树,然后调用解释器的解释方法,当然也可以通过环境角色间接访问解释器的解释方法。
11.1 代码实现
//抽象表达式接口
interface Expression {
int interpret(Context context);
}
//终结符表达式
class NumberExpression implements Expression {
private int number;
public NumberExpression(int number) {
this.number = number;
}
@Override
public int interpret(Context context) {
return number;
}
}
//非终结符表达式
class AddExpression implements Expression {
Expression leftExpression;
Expression rightExpression;
public AddExpression(Expression leftExpression, Expression rightExpression) {
this.leftExpression = leftExpression;
this.rightExpression = rightExpression;
}
@Override
public int interpret(Context context) {
return leftExpression.interpret(context) + rightExpression.interpret(context);
}
}
//上下文Context类
class Context {
private Map<String, Expression> variables;
public Context() {
variables = new HashMap<>();
}
public void setVariables(String variableName, Expression expression) {
variables.put(variableName, expression);
}
public Expression getVariables(String variableName) {
return variables.get(variableName);
}
}
//客户端
public class Client {
public static void main(String[] args) {
Context context = new Context();
context.setVariables("x", new NumberExpression((5)));
context.setVariables("y", new NumberExpression((6)));
Expression expression = new AddExpression(context.getVariables("x"),
new AddExpression(new NumberExpression(2), context.getVariables("y")));
int interpret = expression.interpret(context);
System.out.println(interpret);
}
}
11.2 总结
适用场景:
- 一些重复出现的问题可以用一种简单的语言来进行表述。
- 一个简单语法需要解释的场景。
优点:
- 扩展性强:在解释器模式中由于语法是由很多类表示的,当语法规则更改时,只需修改相应的非终结符表达式即可;若扩展语法时,只需添加相应非终结符类即可。
- 增加了新的解释表达式的方式。
- 易于实现文法:解释器模式对应的文法应当是比较简单且易于实现的,过于复杂的语法并不适合使用解释器模式。
缺点:
- 语法规则较复杂时,会引起类膨胀。
- 执行效率比较低