文章目录
oop七大原则
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MY3kBxtP-1663574218481)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20220826104829865.png)]
1、开闭原则(Open Close Principle)
开闭原则就是说对扩展开放,对修改关闭。在程序需要进行拓展的时候,不能去修改原有的代码,实现一个热插拔的效果。所以一句话概括就是:为了使程序的扩展性好,易于维护和升级。想要达到这样的效果,我们需要使用接口和抽象类,后面的具体设计中我们会提到这点。
2、里氏代换原则(Liskov Substitution Principle)
里氏代换原则(Liskov Substitution Principle LSP)面向对象设计的基本原则之一。 里氏代换原则中说,任何基类可以出现的地方,子类一定可以出现。 LSP是继承复用的基石,只有当衍生类可以替换掉基类,软件单位的功能不受到影响时,基类才能真正被复用,而衍生类也能够在基类的基础上增加新的行为。里氏代换原则是对“开-闭”原则的补充。实现“开-闭”原则的关键步骤就是抽象化。而基类与子类的继承关系就是抽象化的具体实现,所以里氏代换原则是对实现抽象化的具体步骤的规范。—— From Baidu 百科
3、依赖倒转原则(Dependence Inversion Principle)
这个是开闭原则的基础,具体内容:真对接口编程,依赖于抽象而不依赖于具体。
4、接口隔离原则(Interface Segregation Principle)
这个原则的意思是:使用多个隔离的接口,比使用单个接口要好。还是一个降低类之间的耦合度的意思,从这儿我们看出,其实设计模式就是一个软件的设计思想,从大型软件架构出发,为了升级和维护方便。所以上文中多次出现:降低依赖,降低耦合。
5、迪米特法则(最少知道原则)(Demeter Principle)
为什么叫最少知道原则,就是说:一个实体应当尽量少的与其他实体之间发生相互作用,使得系统功能模块相对独立。
6、合成复用原则(Composite Reuse Principle)
原则是尽量使用合成/聚合的方式,而不是使用继承。
GoF23
一、设计模式的分类
创建型模式
工厂方法模式、抽象工厂模式、单例模式、建造者模式、原型模式。
结构型模式
适配器模式、装饰器模式、代理模式、外观模式、桥接模式、组合模式、享元模式。
行为型模式
策略模式、模板方法模式、观察者模式、迭代子模式、责任链模式、命令模式、备忘录模式、状态模式、访问者模式、中介者模式、解释器模式。
单列模式
课程来自:https://www.bilibili.com/video/BV1K54y197iS?spm_id_from=333.999.0.0
1.饿汉式单例模式
/**
* 饿汉式单例模式,一加载就创建单例对象
*/
public class Hungry {
private Hungry(){
}
private static final Hungry hungry = new Hungry();
public static Hungry getInstance(){
return hungry;
}
}
2.懒汉式单例模式
/**
* 懒汉式单例模式
*/
public class LazyMan {
private LazyMan() {
System.out.println(Thread.currentThread().getName());
}
private static LazyMan lazyMan;
public static LazyMan getInstance(){
if (lazyMan==null){
lazyMan = new LazyMan();
}
return lazyMan;
}
// 多线程并发创建对象存在问题。
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread(()->{
LazyMan.getInstance();
}).start();
}
}
}
创建多个对象
3.双重检测锁单例模式DCL
相对比较安全创建单例的模式,但是反射可以破坏
反射里面的方法可以无视无参构造方法
/**
* 懒汉式单例模式
*/
public class LazyMan01 {
private LazyMan01(){
System.out.println(Thread.currentThread().getName());
}
// 加上volatile关键字禁止指令重排
private volatile static LazyMan01 lazyMan01;
/**
* 双重检测锁模式,DCL懒汉式
* @return
*/
public static LazyMan01 getInstance(){
if (lazyMan01==null){
synchronized(LazyMan01.class){
if (lazyMan01==null){
lazyMan01 = new LazyMan01();// 不是原子性操作,可能会有指令重排
/*
* 1.分配内存空间
* 2.执行构造器,初始化对象
* 3.把对象指向内存空间
* 正常情况下:按照123执行;发生指令重排,可能会先执行132
* 多线程情况下:如果第一个线程执行了13,此时第二个线程过来可能就会判断lazyMan01不为空,直接就返回了lazyMan01
* 此时,lazyMan01对象内存空是空的。
* */
}
}
}
return lazyMan01;
}
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread(()->{
LazyMan01.getInstance();
}).start();
}
}
}
4.静态内部类创建单例模式
/**
* 静态内部类创建单例模式
*/
public class LazyMan02 {
private LazyMan02(){
}
public static LazyMan02 getInstance(){
return InnerClass.lazyMan02;
}
static class InnerClass{
private static final LazyMan02 lazyMan02 = new LazyMan02();
}
}
5.枚举创建单例
反射不可破坏枚举的单例
public enum EnumSingle {
INSTANCE;
public static EnumSingle getInstance(){
return INSTANCE;
}
}
工厂方法模式
工厂模式
作用:实现创建者和调用者分离
分类:
- 简单工厂模式:用来生产同一等级结构中的任意产品(对于增加新的产品,需覆盖现有代码)
- 工厂方法模式:用来生产同一等级结构中的固定产品(支持增加任意产品)
- 抽象工厂模式:围绕一个超级工厂创建其他工厂
工厂模式满足三个原则:
- 开闭原则:一个软件的实体应当扩展开放,对修改关闭
- 依赖倒转原则:只针对接口编程,不针对实体编程
- 迪米特法则:只与你直接的朋友通信,二避免和陌生人通信
核心:实例化对象,不使用new,用工厂方法代替,将选择实现类,创建对象同一管理和控制,从而将调用者和我们的实现类解耦
简单工厂模式(静态工厂模式)
先上代码:
//接口public interface Car { void name();}//实现类class Tesla implements Car { @Override public void name() { System.out.println("特斯拉"); }}//实现类class Wuling implements Car{ @Override public void name() { System.out.println("五菱宏光"); }}
工厂类:
public class CarFactory { public static Car getCar(String brand) { if (brand.equals("五菱宏光")) { return new Wuling(); } else if (brand.equals("特斯拉")) { return new Tesla(); } else { return null; } }}
消费者:
public class Consumer { public static void main(String[] args) { Car car = CarFactory.getCar("五菱宏光"); Car car1 = CarFactory.getCar("特斯拉"); car.name(); car1.name(); }}
测试结果:
在之前我们想实现这个功能,我们得了解接口、所有的实现类、然后再new出每个想要的对象,很显然,这不符合面向对象的思想,因此,我们就需要用到工厂模式,通过上面的工厂模式我们不要再面对接口和实现类,这些都交给工厂去做了,而消费者只需要通过工厂获取即可。
但是上面的这种模式有没有什么弊端呢?当我们想增加一个新的产品时,必须要修改系统代码,不满足开闭原则,那应当怎样优化呢,这就需要我们下面介绍的工厂方法模式
工厂方法模式
相比于简单工厂模式,工厂方法模式实现了工厂的横向扩展,即当我们想增加一个新的产品时,不用再修改原有代码,直接实现工厂接口即可实现扩展。但弊端也很明显,多出了很多代码,而且
public interface Car { void name();}
public interface CarFactory { Car getCar();}
//实现类class Tesla implements Car { @Override public void name() { System.out.println("特斯拉"); }}class Wuling implements Car { @Override public void name() { System.out.println("五菱宏光"); }}
//工厂类class TeslaFactory implements CarFactory{ @Override public Car getCar() { return new Tesla(); }}class WulingFactory implements CarFactory { @Override public Car getCar() { return new Wuling(); }}
消费者:
public class Consumer { public static void main(String[] args) { Car car = new TeslaFactory().getCar(); Car car1 = new WulingFactory().getCar(); car.name(); car1.name(); }}
执行结果:
总结
在实际的开发中,常用的工厂模式仍是简单工厂模式,虽然其不符合设计原则。
抽象工厂模式
抽象工厂模式
- 围绕一个超级工厂创建其他工厂。该超级工厂
- 可以增加产品族,不可以增加新产品,可以增加新品牌,不可以增加新产品(会更改代码)
- 工厂的工厂
- [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xsUte5qx-1663574218484)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20220905104023140.png)]
建造者模式
参考
https://www.bilibili.com/video/BV1mc411h719?p=5&vd_source=299f4bc123b19e7d6f66fefd8f124a03
建造者模式(Builder Pattern)属于创建型模式
概述
建造者模式使用多个简单的对象一步一步构建成一个复杂的对象。
一个 Builder 类会一步一步构造最终的对象。该 Builder 类是独立于其他对象的。
经常使用的 StringBuiler
就是建造者模式的典型实现。
代码实现
普通实现
先来看下建造者模式的普通实现。这里模拟快餐店点餐场景:
1、定义快餐食品
/**
* 快餐食品(产品)
*/
public class Product {
/**
* 快餐 A 默认为汉堡
*/
private String BuildA = "汉堡";
/**
* 快餐 B 默认为可乐
*/
private String BuildB = "可乐";
/**
* 快餐 C 默认为薯条
*/
private String BuildC = "薯条";
/**
* 快餐 D 默认为甜点
*/
private String BuildD = "甜点";
public String getBuildA() {
return BuildA;
}
public void setBuildA(String buildA) {
BuildA = buildA;
}
public String getBuildB() {
return BuildB;
}
public void setBuildB(String buildB) {
BuildB = buildB;
}
public String getBuildC() {
return BuildC;
}
public void setBuildC(String buildC) {
BuildC = buildC;
}
public String getBuildD() {
return BuildD;
}
public void setBuildD(String buildD) {
BuildD = buildD;
}
@Override
public String toString() {
return "Product{" +
"BuildA='" + BuildA + '\'' +
", BuildB='" + BuildB + '\'' +
", BuildC='" + BuildC + '\'' +
", BuildD='" + BuildD + '\'' +
'}';
}
}
2、定义厨房
/**
* 厨房(建造者)
*/
public abstract class Kitchen {
/**
* 制作快餐 A
* @param msg 快餐名称
* @return 快餐
*/
abstract Kitchen builderA(String msg);
/**
* 制作快餐 B
* @param msg 快餐名称
* @return 快餐
*/
abstract Kitchen builderB(String msg);
/**
* 制作快餐 C
* @param msg 快餐名称
* @return 快餐
*/
abstract Kitchen builderC(String msg);
/**
* 制作快餐 D
* @param msg 快餐名称
* @return 快餐
*/
abstract Kitchen builderD(String msg);
/**
* 获取产品
* @return 产品
*/
abstract Product getProduct();
}
3、定义服务员
/**
* 服务员(传递者)
*/
public class Waiter extends Kitchen {
private Product product;
public Waiter(){
product = new Product();
}
@Override
Kitchen builderA(String msg) {
product.setBuildA(msg);
return this;
}
@Override
Kitchen builderB(String msg) {
product.setBuildB(msg);
return this;
}
@Override
Kitchen builderC(String msg) {
product.setBuildC(msg);
return this;
}
@Override
Kitchen builderD(String msg) {
product.setBuildD(msg);
return this;
}
@Override
Product getProduct() {
return product;
}
}
4、客人点餐
// 叫服务员
Waiter waiter = new Waiter();
// 可以选择套餐,省事,直接告诉服务员要套餐即可
Product product1 = waiter.getProduct();
System.out.println(product1);
// 也可以自己点餐,点了哪些上哪些
Product product2 = waiter
.builderA("炸鸡")
.builderB("雪碧")
.builderC(null)
.builderD(null)
.getProduct();
System.out.println(product2);
输出结果为:
Product{BuildA='汉堡', BuildB='可乐', BuildC='薯条', BuildD='甜点'}
Product{BuildA='炸鸡', BuildB='雪碧', BuildC='null', BuildD='null'}
如果选择套餐,就按照套餐默认的快餐食品送餐。
如果自己点餐,就按照点的送餐,点了哪些上哪些。
指挥者实现
在工地建筑时,除了建造本身,建造的顺序也非常重要,因此工地上一般都会有一个指挥者来决定建造的顺序。
1、定义一栋楼(产品)
/**
* 一栋楼(产品)
*/
public class Product {
/**
* 地基
*/
private String productA;
/**
* 主体
*/
private String productB;
/**
* 粉刷
*/
private String productC;
/**
* 绿化
*/
private String productD;
public String getProductA() {
return productA;
}
public void setProductA(String productA) {
this.productA = productA;
}
public String getProductB() {
return productB;
}
public void setProductB(String productB) {
this.productB = productB;
}
public String getProductC() {
return productC;
}
public void setProductC(String productC) {
this.productC = productC;
}
public String getProductD() {
return productD;
}
public void setProductD(String productD) {
this.productD = productD;
}
@Override
public String toString() {
return "Product{" +
"productA='" + productA + '\'' +
", productB='" + productB + '\'' +
", productC='" + productC + '\'' +
", productD='" + productD + '\'' +
'}';
}
}
2、定义包工头(抽象建造者)
/**
* 包工头(抽象建造者)
* @author yifan
*/
public abstract class Builder {
/**
* 打地基
*/
abstract void buildA();
/**
* 建主体
*/
abstract void buildB();
/**
* 去粉刷
*/
abstract void buildC();
/**
* 搞绿化
*/
abstract void buildD();
/**
* 建一栋楼
* @return 一栋楼
*/
abstract Product getProduct();
}
3、定义工人(实际建造者)
/**
* 工人(实际建造者)
*/
public class Worker extends Builder{
private Product product;
public Worker() {
// 指定要建设的楼
product = new Product();
}
@Override
void buildA() {
product.setProductA("地基");
System.out.println("地基");
}
@Override
void buildB() {
product.setProductB("主体");
System.out.println("主体");
}
@Override
void buildC() {
product.setProductC("粉刷");
System.out.println("粉刷");
}
@Override
void buildD() {
product.setProductD("绿化");
System.out.println("绿化");
}
@Override
Product getProduct() {
return product;
}
}
4、定义施工调度员(指挥者)
/**
* 施工调度员(指挥者)
*/
public class Director {
/**
* 指挥包工头按照顺序建楼
* @param builder 包工头
* @return 楼
*/
public Product build(Builder builder){
builder.buildA();
builder.buildB();
builder.buildC();
builder.buildD();
return builder.getProduct();
}
}
5、测试
// 施工调度员指挥包工头,包工头找到具体的工人按照施工调度员指定的顺序建造
new Director().build(new Worker());
这样就用代码实现了工地上各岗位的协作,如果工程需要调整建造顺序,只需要更改指挥者的 build 方法即可,非常方便。
设计模式的思想起源于建筑行业,从建造者模式这里就能体现得淋漓尽致。
优缺点
优点
1、建造者独立,易扩展。
2、便于控制细节风险。
缺点
1、产品必须有共同点,范围有限制。
2、如内部变化复杂,会有很多的建造类。
使用场景
1、需要生成的对象具有复杂的内部结构。
2、需要生成的对象内部属性本身相互依赖。
注意事项
与工厂模式的区别是:建造者模式更加关注与零件装配的顺序。
原型模式
参考
https://www.bilibili.com/video/BV1mc411h719?p=6&vd_source=299f4bc123b19e7d6f66fefd8f124a03
原型模式(Prototype Pattern)属于创建型模式
概述
原型模式实际上就是对象的拷贝。
原型模式使用原型实例指定待创建对象的类型,并且通过复制这个原型来创建新的对象。也就是说,原型对象作为模板,通过克隆操作,来产生更多的对象,就像细胞的复制一样。
原型模式的拷贝分为浅拷贝和深拷贝。
代码实现
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lTu3SGNg-1663574218485)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20220913175525727.png)]
浅拷贝
对于类中基本数据类型,会直接复制值给拷贝对象。
对于类中引用类型,只会复制对象的地址,而实际上指向的还是原来的那个对象。
// 基本类型浅拷贝
int a = 10;
int b = a;
// 输出:true
System.out.println(a == b);
// 引用类型浅拷贝,拷贝的仅仅是对上面对象的引用
Object o = new Object();
Object k = o;
// 输出:true
System.out.println(o == k);
在 Java 中,就可以实现 Cloneable 接口提供的拷贝机制,来实现原型模式:
深拷贝
无论是基本类型还是引用类型,深拷贝会将引用类型的所有内容,全部拷贝为一个新的对象,包括对象内部的所有成员变量,也会进行拷贝。
1、对成员变量也进行拷贝
@Override
public Object clone() throws CloneNotSupportedException {
Student student = (Student) super.clone();
// 这里我们改进一下,针对成员变量也进行拷贝
student.name = new String(name);
// 成员拷贝完成后,再返回
return student;
}
2、再执行上述的代码
可以看到, Student 对象和其中的属性 name 都进行了拷贝,是完全不一样的对象了。这样就是拷贝生成了一个全新的对象,也就是深拷贝了。
优缺点
优点
1、性能提高。
2、逃避构造函数的约束。
缺点
1、配备克隆方法需要对类的功能进行通盘考虑,这对于全新的类不是很难,但对于已有的类不一定很容易,特别当一个类引用不支持串行化的间接对象,或者引用含有循环结构的时候。
2、必须实现 Cloneable
接口。
使用场景
1、资源优化场景。
2、类初始化需要消化非常多的资源,这个资源包括数据、硬件资源等。
3、性能和安全要求的场景。
4、通过 new
产生一个对象需要非常繁琐的数据准备或访问权限,则可以使用原型模式。
5、一个对象多个修改者的场景。
6、一个对象需要提供给其他对象访问,而且各个调用者可能都需要修改其值时,可以考虑使用原型模式拷贝多个对象供调用者使用。
7、在实际项目中,原型模式很少单独出现,一般是和工厂方法模式一起出现,通过 clone
的方法创建一个对象,然后由工厂方法提供给调用者。原型模式已经与 Java 融为浑然一体,可以随手拿来使用。
注意事项
与通过对一个类进行实例化来构造新对象不同的是,原型模式是通过拷贝一个现有对象生成新对象的。浅拷贝实现 Cloneable
,重写 clone
方法;深拷贝是通过实现 Serializable
读取二进制流。
转化为深拷贝的方法:重写clone方法,序列化,反序列化
适配器模式
定义
将一个接口转换成客户希望的另一个接口,使接口不兼容的那些类可以一起工作,其别名为包装器(Wrapper)。适配器模式既可以作为类结构型模式,也可以作为对象结构型模式。
在适配器模式中,我们通过增加一个新的适配器类来解决接口不兼容的问题,使得原本没有任何关系的类可以协同工作。
根据适配器类与适配者类的关系不同,适配器模式可分为对象适配器和类适配器两种,在对象适配器模式中,适配器与适配者之间是关联关系;在类适配器模式中,适配器与适配者之间是继承(或实现)关系。
例子:电脑连接转接器上网
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yASl5GId-1663574218486)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20220913174745232.png)]
类适配器
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4jqW6b35-1663574218487)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20220913174814815.png)]
对象适配器
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-iSOKnQeu-1663574218487)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20220913174933030.png)]
装饰器模式
参考
https://www.bilibili.com/video/BV1u3411P7Na?p=17&vd_source=299f4bc123b19e7d6f66fefd8f124a03
装饰器模式(Decorator Pattern)属于结构型模式
概述
装饰,顾名思义,就是在原有基础上增添东西以显示更好的效果。
生活中非常多这样的例子,衣服饰品、珠宝首饰、房子装修都是在进行装饰。
开发中这样的情况也非常多,比如用户的性别在数据库表中一般都是存的编码,然后通过字典表进行翻译,进而在页面展示为男、女。
代码实现
这里以用户服务的查询为例介绍装饰器模式:
1、定义用户服务
/**
* 用户服务
*/
public interface UserService {
/**
* 查询
*/
public void query();
}
2、实现用户服务
/**
* 用户服务实现类
*/
public class UserServiceImpl implements UserService {
@Override
public void query() {
System.out.println("查询了一个用户");
}
}
3、定义装饰器
/**
* 装饰器
*/
public class Decorator implements UserService{
/**
* 用户服务<br>
* 定义为 protected 是为了方便子类的后置处理器使用
*/
protected UserService userService;
/**
* 装饰用户服务
* @param userService 用户服务
*/
public Decorator(UserService userService) {
this.userService = userService;
}
@Override
public void query() {
userService.query();
}
}
4、定义装饰器后置处理器
/**
* 装饰器后置处理器
*/
public class DecoratorPostProcessor extends Decorator{
/**
* 使用父类的装饰器装饰用户服务
* @param userService 用户服务
*/
public DecoratorPostProcessor(UserService userService) {
super(userService);
}
@Override
public void query() {
userService.query();
// 后置处理
System.out.println("查找字典表给性别赋值");
}
}
5、调用
// 装饰用户服务实现类(注意,这里需要用装饰器后置处理器装饰,因为后置处理的业务逻辑写在里面)
Decorator decorator = new DecoratorPostProcessor(new UserServiceImpl());
// 调用装饰后的查询方法
decorator.query();
输出结果:
查询了一个用户
查找字典表给性别赋值
6、嵌套使用
在开发场景中,有时候一次后置处理并不能得到预期的数据(比如数字转化为编码,编码再转化为名称),需要再次处理,此时再去定义一个装饰器是没有必要的,可以嵌套使用。
// 嵌套使用
DecoratorPostProcessor decorator1 = new DecoratorPostProcessor(decorator);
decorator1.query();
输出结果:
查询了一个用户
查找字典表给性别赋值
查找字典表给性别赋值
这样只需要在后置处理做判断需要处理的为数字还是编码即可实现嵌套装饰的效果。
与代理模式区别
可以发现,装饰器模式与前面介绍的代理模式在结构上是一样的。对装饰器模式来说,装饰者和被装饰者都实现同一个接口或抽象类;对代理模式来说,代理类和被代理的类都实现同一个接口或抽象类,在结构上确实没有区别。
但是它们的作用不同。装饰器模式强调的是增强自身,在被装饰之后你能够在被增强的类上使用增强后的功能,增强后你还是你,只是被强化了;代理模式强调要让别人帮你去做事情,以及添加一些与你本身的业务没有太多关系的事情(记录日志、设置缓存等),重点在于让别人帮你做。
因此,装饰模式和代理模式的不同之处不在于实现方式,在于用途和思想。
优缺点
优点
装饰类和被装饰类可以独立发展,不会相互耦合,装饰模式是继承的一个替代模式,装饰模式可以动态扩展一个实现类的功能。
缺点
多层装饰比较复杂。
使用场景
1、扩展一个类的功能。
2、动态增加功能,动态撤销。
注意事项
可代替继承。
代理模式
参考
https://www.bilibili.com/video/BV1mc411h719?p=9&vd_source=299f4bc123b19e7d6f66fefd8f124a03
代理模式(Proxy Pattern)属于结构型模式
概述
代理模式就是一个代理对象来间接访问对象,常用于无法直接访问某个对象或访问某个对象不方便的情况。
实际上代理在生活中处处都存在,比如房屋中介就是代理,Apple 的授权经销商就是代理,访问国外网站所用的代理服务器也是代理,Spring 框架的 AOP 也是通过代理模式实现的。
这些代理都有一个共同特点,就是使用的一致性和中间环节的透明性,也就是说找代理做的事情需要与找对象本身做的事情是一样的,只是中间环节隐藏了而已。
代理模式分为静态代理和动态代理。
静态代理
静态代理一般包含以下角色:
- 动作 : 一般使用接口或者抽象类来实现。
- 真实角色 : 被代理的角色。
- 代理角色 : 代理真实角色 ; 代理真实角色后 , 一般会做一些附属的操作。
- 客户 : 使用代理角色来进行一些操作。
代码实现
1、定义租赁操作
/**
* 租赁操作
*/
public interface Rent {
/**
* 租房
*/
void rentHouse();
}
2、定义房东
/**
* 房东
*/
public class Landlord implements Rent{
@Override
public void rentHouse() {
System.out.println("房东出租房子");
}
}
3、定义中介
/**
* 中介
*/
public class Intermediary implements Rent{
/**
* 房东
*/
private Landlord landlord;
public Intermediary() {
}
public Intermediary(Landlord landlord) {
this.landlord = landlord;
}
@Override
public void rentHouse() {
// 看房
seeHouse();
// 签合同
contract();
// 租房
landlord.rentHouse();
// 收取费用
toll();
}
/**
* 看房
*/
public void seeHouse() {
System.out.println("中介带你看房");
}
/**
* 签合同
*/
public void contract() {
System.out.println("签租赁合同");
}
/**
* 收取费用
*/
public void toll() {
System.out.println("收中介费");
}
}
4、租客租房
// 房东
Landlord landlord = new Landlord();
// 中介给房东代理
Proxy proxy = new Proxy(landlord);
// 租房。不用面对房东,直接找中介租房即可
proxy.rentHouse();
在这个过程中,租客直接接触的是中介,见不到房东,但是租客依旧通过代理租到了房东的房子。
优缺点
优点
1、可以使得真实角色更加轻松,不用再去关注一些琐碎的事情。
2、公共的业务由代理来完成,实现了业务的分工。
3、公共业务发生变化时扩展更加方便。
缺点
类变多了,多了代理类,工作量变大了,开发效率降低。
我们想要静态代理的优点,又不想要静态代理的缺点,所以 , 就有了动态代理 。
动态代理
- 动态代理的角色和静态代理的一样。
- 动态代理的代理类是动态生成的,静态代理的代理类是提前写好的。
- 动态代理分为两类 : 一类是基于接口 , 一类是基于类
- 基于接口的动态代理:JDK 动态代理。
- 基于类的动态代理:CGLIB 动态代理。
- 现在用的比较多的是 Javassist 来生成动态代理。
- 这里使用 JDK 的原生代码来实现,其余的道理都是一样的。
JDK 的动态代理需要了解两个类:Proxy
和 InvocationHandler
。查看 JDK 帮助文档:
Proxy:代理类
Proxy
提供了创建动态代理类和实例的静态方法,它也是由这些方法创建的所有动态代理类的超类。
代理接口是由代理类实现的接口。 代理实例是代理类的一个实例。 每个代理实例都有一个关联的调用处理程序对象,它实现了接口 InvocationHandler
。
newProxyInstance
方法:
public static Object newProxyInstance(ClassLoader loader,
Class<?>[] interfaces,
InvocationHandler h) throws IllegalArgumentException
返回指定接口的代理类的实例,该接口将方法调用分派给指定的调用处理程序。
参数:
- loader - 类加载器来定义代理类。
- interfaces - 代理类实现的接口列表。
- h - 调度方法调用的调用处理函数。
返回值:
具有由指定的类加载器定义并实现指定接口的代理类的指定调用处理程序的代理实例。
异常:
IllegalArgumentException
:非法参数异常。
InvocationHandler:调用处理程序
InvocationHandler
是由代理实例的调用处理程序实现的接口 。 每个代理实例都有一个关联的调用处理程序。
invoke
方法:
Object invoke(Object proxy,
Method method,
Object[] args) throws Throwable
处理代理实例上的方法调用并返回结果。 当在与之关联的代理实例上调用方法时,将在调用处理程序中调用此方法。
参数:
- proxy - 调用该方法的代理实例。
- method - 所述方法对应于调用代理实例上的接口方法的实例。方法对象的声明类将是该方法声明的接口,它可以是代理类继承该方法的代理接口的超级接口。
- args - 包含的方法调用传递代理实例的参数值的对象的阵列,或 null 如果接口方法没有参数。原始类型的参数包含在适当的原始包装器类的实例中,例如java.lang.Integer 或 java.lang.Boolean。
代码实现
抽象角色和真实角色和之前的一样。
1、定义租赁操作
/**
* 租赁操作
*/
public interface Rent {
/**
* 租房
*/
void rentHouse();
}
2、定义房东
/**
* 房东
*/
public class Landlord implements Rent {
@Override
public void rentHouse() {
System.out.println("房东出租房子");
}
}
3、定义中介
/**
* 中介
*/
public class Intermediary implements InvocationHandler {
/**
* 租赁操作
*/
private Rent rent;
/**
* 代理租赁
*
* @param rent 需要租赁的对象
*/
public void setRent(Rent rent) {
this.rent = rent;
}
/**
* 生成代理对象
*
* @return 代理对象
*/
public Object getProxy() {
// 重点是第二个参数,获取要代理的抽象角色,之前都是一个角色,现在可以代理一类角色
return Proxy.newProxyInstance(this.getClass().getClassLoader(), rent.getClass().getInterfaces(), this);
}
/**
* 处理代理实例上的方法调用并返回结果
*
* @param proxy 代理类
* @param method 代理类的调用处理程序的方法对象
* @param args 包含的方法调用传递代理实例的参数值的对象的阵列
* @return 代理对象
* @throws Throwable 错误
*/
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 看房
seeHouse();
// 签合同
contract();
// 动态代理租房业务:本质利用反射实现
Object result = method.invoke(rent, args);
// 收取费用
toll();
return result;
}
/**
* 看房
*/
public void seeHouse() {
System.out.println("中介带你看房");
}
/**
* 签合同
*/
public void contract() {
System.out.println("签租赁合同");
}
/**
* 收取费用
*/
public void toll() {
System.out.println("收中介费");
}
}
4、租客租房
// 房东
Landlord landlord = new Landlord();
// 中介
Intermediary intermediary = new Intermediary();
// 中介给房东提供代理服务
intermediary.setRent(landlord);
// 动态生成对应的代理类
Rent proxy = (Rent) intermediary.getProxy();
// 代理类执行租房操作
proxy.rentHouse();
一个动态代理 , 一般代理某一类业务 , 一个动态代理可以代理多个类,代理的是接口。
优缺点
优点
1、静态代理有的它都有,静态代理没有的,它也有。
2、可以使得真实角色更加轻松,不用再去关注一些琐碎的事情。
3、公共的业务由代理来完成,实现了业务的分工。
4、公共业务发生变化时扩展更加方便。
5、动态代理可以代理一类业务。
6、动态代理可以代理多个类,代理的是接口。
缺点
1、需要对实现动态代理的类和方法有一定了解,学习成本较静态代理更高。
2、动态代理的使用逻辑更为复杂,不如静态代理好理解。
使用场景
按职责来划分,通常有以下使用场景:
1、远程代理。
2、虚拟代理。
3、Copy-on-Write 代理。
4、保护(Protect or Access)代理。
5、Cache代理。
6、防火墙(Firewall)代理。
7、同步化(Synchronization)代理。
8、智能引用(Smart Reference)代理。
注意事项
1、和适配器模式的区别:适配器模式主要改变所考虑对象的接口,而代理模式不能改变所代理类的接口。
2、和装饰器模式的区别:装饰器模式为了增强功能,而代理模式是为了加以控制。
外观模式
参考
https://www.bilibili.com/video/BV1u3411P7Na?p=19&vd_source=299f4bc123b19e7d6f66fefd8f124a03
外观模式(Facade Pattern)属于结构型模式
概述
在生活中,经常遇到这样的情况:办理一个业务,需要找很多部门签字盖章,这些部门往往距离较远,无奈只得四处奔波。这时候相信所有人都有一个同样的诉求:要是找一个部门就能办理完所有业务就好了!
编程来源于生活,生活中是这样,代码中也是这样。如果一个业务逻辑需要调几个独立的系统,不仅编写复杂,还不方便管理和维护,任意系统的业务变更都将影响很多地方。这时也是同样的诉求,要是调一个系统就能解决业务就好了!
这时外观模式就应运而生了,它隐藏了各系统间调用的复杂性,并向客户端提供了可以一次访问就实现各系统间逻辑的接口。
外观模式充分体现了迪米特法则。一般大型项目都有很多个子系统,可以在这些子系统的上面加一个门面(Facade),当外部需要与各个子系统交互时,无需再去单独调用各个子系统,而是直接与门面进行交互,再由门面与后面的各个子系统操作。这样方便了调用方代码的编写,统一找门面就行,不需要去详细了解各个子系统,并且,当子系统需要修改时,只需修改门面中的逻辑,不需要变动调用方的代码,遵循迪米特法则尽可能少的交互,极大的提高了便利性和健壮性。
代码实现
这里以找各个部门办理业务为例介绍外观模式:
1、定义A部门
/**
* A部门
*/
public class DepartmentA {
/**
* 办理业务
*/
public void business() {
System.out.println("A部门办理了业务");
}
}
2、定义B部门
/**
* B部门
*/
public class DepartmentB {
/**
* 办理业务
*/
public void business() {
System.out.println("B部门办理了业务");
}
}
3、定义C部门
/**
* C部门
*/
public class DepartmentC {
/**
* 办理业务
*/
public void business() {
System.out.println("C部门办理了业务");
}
}
4、定义外观部门
/**
* 外观部门
*/
public class Facade {
/**
* A部门
*/
private DepartmentA departmentA;
/**
* B部门
*/
private DepartmentB departmentB;
/**
* C部门
*/
private DepartmentC departmentC;
/**
* 无参构造器中给包装的部门赋值
*/
public Facade() {
departmentA = new DepartmentA();
departmentB = new DepartmentB();
departmentC = new DepartmentC();
}
/**
* 办理业务
*/
public void business() {
departmentA.business();
departmentB.business();
departmentC.business();
}
}
5、调用
// 找到外观部门
Facade facade = new Facade();
// 办理业务
facade.business();
输出结果:
A部门办理了业务
B部门办理了业务
C部门办理了业务
如此即实现了找一个部门就办理了多个部门的业务。不管多复杂的业务,麻烦的都是外观部门,而不是办理业务的人。哪个部门的业务发生变化了,外观部门去跟它们沟通协调做相应调整就行,办理业务的人不用再熟悉调整后的业务,还像往常一样找外观部门办理即可。
这就是外观模式带来的巨大便利,希望现实生活中如此场景的外观模式也早日到来!
优缺点
优点
1、减少系统相互依赖。
2、提高灵活性。
3、提高了安全性。
缺点
不符合开闭原则,如果要改东西很复杂,继承重写都不合适。
使用场景
1、为复杂的模块或子系统提供外界访问的模块。
2、子系统相对独立。
3、预防低水平人员带来的风险。
注意事项
在层次化结构中,可以使用外观模式定义系统中每一层的入口。
桥接模式
概述
桥接模式是将抽象部分与它的实现部分分离,使它们都可以独立地变化。它是一种对象结构型模式,又称为柄体(Handle and Body)模式或接口(Interface)模式。
比如需要创建电脑对象,市面上的电脑非常多,品牌有华为、苹果、联想等等,类型有台式、笔记本、平板等等。如果给每个品牌的每个类型的电脑都创建一个对象,那是十分繁琐的,而且非常不方便扩展。试想如果增加了一个品牌或者类型,那就需要增加大量的对象,这无疑是笨重和低效的。
这时我们会想,如果给品牌和类型分别设置一个维度,再将这两个维度组合起来就得到了一个对象,这样只需要定义所有的品牌和所有的类型即可,如果需要增加,只需要增加一个品牌或者类型即可。
代码实现
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8BSgVKan-1663574218488)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20220913194606011.png)]
优缺点
优点
1、桥接模式偶尔类似于多继承方案,但是多继承方案违背了类的单一职责原则,复用性比较差,类的个数也比较多,桥接模式是比多继承方案更好的解决方法。极大的减少了子类的个数,从而降低管理和维护的成本。
2、桥接模式提高了系统的可扩充性,在两个变化维度中任意扩展一个维度,都不需要修改原有系统。符合开闭原则,就像一座桥,可以把两个变化的维度连接起来。
3、 抽象和实现的分离。
4、优秀的扩展能力。
5、实现细节对客户透明。
缺点
1、桥接模式的引入会增加系统的理解与设计难度,由于聚合关联关系建立在抽象层,要求开发者针对抽象进行设计与编程。
2、桥接模式要求正确识别出系统中两个独立变化的维度,因此其使用范围具有一定的局限性。
使用场景
1、如果一个系统需要在构建的抽象角色和具体角色之间增加更多的灵活性,避免在两个层次之间建立静态的继承联系,通过桥接模式可以使它们在抽象层建立一个关联关系。抽象角色和具体角色可以以继承的方式独立扩展而互不影响,在程序运行时可以动态将一个抽象子类的对象和一个具体类的对象进行组合,即系统需要对抽象角色和具体角色进行动态耦合。
2、一个类存在两个独立变化的维度,且这两个维度都需要进行扩展。
3、虽然在系统中使用继承是没有问题的,但是由于抽象角色和具体角色需要独立变化,设计要求需要独立管理这两者。对于那些不希望使用继承或因为多层次继承导致系统类的个数急剧增加的系统,桥接模式尤为适用。
4、Java 语言通过 Java 虚拟机与操作系统的桥接实现了平台的无关性。
5、AWT 中的 Peer 架构。
6、JDBC 驱动程序。
注意事项
对于两个独立变化的维度,使用桥接模式再适合不过了。
组合模式
参考
https://www.bilibili.com/video/BV1u3411P7Na?p=16&vd_source=299f4bc123b19e7d6f66fefd8f124a03
组合模式(Composite Pattern)属于结构型模式
概述
组合模式实际上就是将多个组件进行组合,让用户可以对它们进行一致性处理。
比如文件夹,一个文件夹中可能包含有很多个子文件夹或是文件。它就像是一个树形结构一样,有分支有叶子,而组合模式则是可以对整个树形结构上的所有节点进行递归处理,比如我们现在希望将所有文件夹中的文件的名称前面都添加一个前缀,那么就可以使用组合模式。
代码实现
1、定义组件
/**
* 组件
*/
public abstract class Component {
/**
* 添加子组件
* @param component 组件
*/
public abstract void add(Component component);
/**
* 删除子组件
* @param component 组件
*/
public abstract void remove(Component component);
/**
* 获取子组件
* @param index 组件序号
* @return 子组件
*/
public abstract Component getChild(int index);
/**
* 修改名称
*/
public abstract void editName();
}
2、定义文件夹
/**
* 目录
* <p>目录可以包含多个文件或目录
*/
public class Directory extends Component{
/**
* 使用 List 来存放目录中的子组件
*/
List<Component> child = new ArrayList<>();
@Override
public void add(Component component) {
child.add(component);
}
@Override
public void remove(Component component) {
child.remove(component);
}
@Override
public Component getChild(int index) {
return child.get(index);
}
@Override
public void editName() {
// 继续调用所有子组件的 editName 方法执行业务
child.forEach(Component::editName);
}
}
3、定义文件
/**
* 文件
*/
public class File extends Component{
@Override
public void add(Component component) {
throw new UnsupportedOperationException();
}
@Override
public void remove(Component component) {
throw new UnsupportedOperationException();
}
@Override
public Component getChild(int index) {
throw new UnsupportedOperationException();
}
@Override
public void editName() {
// 具体的名称修改操作
System.out.println("修改文件:" + this);
}
}
4、客户端调用
// 新建一个外层目录
Directory out = new Directory();
// 新建一个内层目录
Directory in = new Directory();
// 外层目录添加文件
out.add(new File());
// 里层目录添加文件
in.add(new File());
in.add(new File());
// 将内层目录放置在外层目录下
out.add(in);
// 修改文件名
out.editName();
执行结果为:
修改文件:cn.codesail.composite.File@311d617d
修改文件:cn.codesail.composite.File@7c53a9eb
修改文件:cn.codesail.composite.File@ed17bee
可以看到对最外层目录进行操作后,会递归向下处理当前目录和子目录中所有的文件。
优缺点
优点
1、高层模块调用简单。
2、节点自由增加。
缺点
在使用组合模式时,其叶子和树枝的声明都是实现类,而不是接口,违反了依赖倒置原则。
使用场景
部分、整体场景,如树形菜单,文件、文件夹的管理。
注意事项
定义时为具体类。
享元模式
参考
https://www.bilibili.com/video/BV1u3411P7Na?p=20&vd_source=299f4bc123b19e7d6f66fefd8f124a03
享元模式(Flyweight Pattern)属于结构型模式
概述
享元,英文名称为 Flyweigh,轻量级的意思。它通过与其他类似对象共享数据来减小内存占用,也就是它名字的来由:享-分享。
大家都知道围棋有黑白子,用程序定义一局围棋时,如果给每颗黑子和每颗白子都定义一个对象,那一局围棋会产生大量的对象,这样有必要吗?每颗黑子都是类似的,每颗白子也是类似的,完全可以只定义一颗黑子对象和一颗白子对象,其余的棋子都复用这两个对象,这样不仅节省空间,编写也会简单很多,这就是享元模式的思想。
代码实现
这里以下围棋为例介绍享元模式:
1、定义棋子(抽象享元角色)
/**
* 棋子
*/
public interface Piece {
/**
* 落子
*/
public void fall();
}
2、定义具体棋子(具体享元角色)
/**
* 具体棋子
*/
public class PieceImpl implements Piece{
/**
* 棋子
*/
private String piece;
/**
* 构造棋子
* @param color 棋子颜色
*/
public PieceImpl(String color) {
this.piece = color;
}
@Override
public void fall() {
System.out.println(this.piece);
}
}
3、定义棋子工厂(享元工厂)
/**
* 棋子工厂
*/
public enum PieceFactory {
/**
* 这里将前面介绍的单例模式应用起来<br>
* 单例模式的最佳实现是使用枚举类型。<br>
* 只需要编写一个包含单个元素的枚举类型即可<br>
* 简洁,且无偿提供序列化,并由 JVM 从根本上提供线程安全的保障,绝对防止多次实例化,且能够抵御反射和序列化的攻击。
*/
INSTANCE;
/**
* 棋盒
*/
private Map<String,Piece> pieceBox = new HashMap<>();
/**
* 获取棋子
* @param color 棋子颜色
* @return 棋子
*/
public Piece getPiece(String color) {
// 先从棋盒获取棋子
Piece piece = this.pieceBox.get(color);
// 如果棋盒里没有棋子
if (piece == null) {
// 创建一颗棋子
piece = new PieceImpl(color);
// 放入棋盒
this.pieceBox.put(color, piece);
}
// 得到棋子
return piece;
}
}
4、调用
// 获取棋子1
Piece piece1 = PieceFactory.INSTANCE.getPiece("黑子");
// 获取棋子2
Piece piece2 = PieceFactory.INSTANCE.getPiece("黑子");
// 获取棋子3
Piece piece3 = PieceFactory.INSTANCE.getPiece("白子");
// 落子
piece1.fall();
piece2.fall();
piece3.fall();
// 比较两颗黑子是否为同一对象
System.out.println(piece1 == piece2);
// 比较两颗白子是否为同一对象
System.out.println(piece1 == piece3);
输出结果为:
黑子
黑子
白子
true
false
可以发现,两颗黑子为同一对象,黑子与白子为不同对象,这样不管后面定义多少黑子与白子,都可以公用现存的两个对象,极大程度的节省了内存,这就是享元模式的妙处。
与单例模式区别
可以发现,享元工厂的实现方式与单例模式很相似,都在强调复用对象。但它们还是有本质区别的:
- 享元是对象级别的:在多个使用到这个对象的地方都只需要使用这一个对象即可满足要求。
- 单例是类级别的:这个类必须只能实例化出一个对象。
可以这么说:单例是享元的一种特例。单例可以看做是享元的实现方式中的一种,只不过比享元更加严格的控制了对象的唯一性。
线程安全问题
前面的例子都是使用 HashMap 作为对象池,在多线程场景下是不安全的:
for (int i = 0; i < 10; i++) {
new Thread(() -> {
Piece piece1 = PieceFactory.INSTANCE.getPiece("黑子");
Piece piece2 = PieceFactory.INSTANCE.getPiece("黑子");
System.out.println(piece1 == piece2);
}).start();
}
输出结果为:
false
true
true
true
false
false
true
true
true
true
可以看到,里面部分实例对象是不相等的,没有实现享元。
因此必须给工厂里的 getxxx 方法加锁,直接使用 synchronized 关键字即可:
public synchronized Piece getPiece(String color) {
}
这样输出的都是同一个对象了:
true
true
true
true
true
true
true
true
true
true
优缺点
优点
大幅度地降低内存中对象的数量,对象数量越多,越能体现得明显。
缺点
1、享元模式使得系统更加复杂。为了使对象可以共享,需要将一些状态外部化,这使得程序的逻辑复杂化。
2、享元模式将享元对象的状态外部化,而读取外部状态使得运行时间稍微变长。
3、享元模式需要维护一个记录了系统已有的所有享元的哈希表,也称之为对象池,这也需要耗费一定的资源。
使用场景
结合上述的优缺点,不难推出享元模式的使用场景:足够多的对象可共享时才值得使用享元模式。
String
比如 Java 中的字符串 String,组成字符串的字符 char 常常有大量重复的,如果给每个字符都创建对象的话对内存是灾难级的!String 就是享元模式的典型实现。
代码示例:
String a = "a";
String a1 = "a";
String b = "b";
String ab = "ab";
String ab1 = "a" + "b";
System.out.println(a == a1);
System.out.println(a == b);
System.out.println(ab == ab1);
输出结果为:
true
false
true
可以发现,同样字符的 String 指向同一个对象,不用字符的 String 指向不同的对象,字符拼接后相等字符的 String 也指向相同的对象。
String 在 Java 中太常用了,如果它的分配和其他的对象分配一样,将会耗费高昂的时间与空间代价,JVM 为了提高性能和减少内存开销,在实例化字符串常量的时候进行了一些优化。
具体做法是 String 类维护了一个字符串池,每当代码创建字符串常量时,JVM 会首先检查字符串常量池,如果字符串已经在池中,就返回池中的实例的引用;如果字符串不在池中,就会实例化一个字符串并放到池中,Java 能够进行这样的优化是因为字符串是不可变的,可以不用担心数据冲突。
Integer.valueOf
示例1:
Integer a = 1;
Integer b = 1;
System.out.print(a == b); // true
示例2:
Integer a = 200;
Integer b = 200;
System.out.println(a == b); // false
很奇怪,为什么赋值为 1 的 Integer 对象相等,而赋值为 200 的 Integer 对象不相等。
反编译上述程序,得到如下结果:
public static main([Ljava/lang/String;)V
L0
LINENUMBER 19 L0
SIPUSH 200
INVOKESTATIC java/lang/Integer.valueOf (I)Ljava/lang/Integer; // 使用了自动装箱
ASTORE 1
L1
LINENUMBER 20 L1
SIPUSH 200
INVOKESTATIC java/lang/Integer.valueOf (I)Ljava/lang/Integer; // 使用了自动装箱
ASTORE 2
可以发现每次赋值都使用了 Integer.valueOf
自动装箱,再看该方法源码:
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
可以发现,当使用 Integer 的自动装箱时,i 值在 cache 的 low 和 high 之间时,也就是 -128 到 +127 之间,会用缓存保存起来,供客户端多次使用,以节约内存;如果不在这个范围内,则创建一个新的 Integer 对象,这也是对享元模式的应用。
注意事项
1、注意划分外部状态和内部状态,否则可能会引起线程安全问题。
2、这些类必须有一个工厂对象加以控制。
策略模式
参考
https://www.bilibili.com/video/BV1u3411P7Na?p=30&vd_source=299f4bc123b19e7d6f66fefd8f124a03
策略模式(Strategy Pattern)属于行为型模式
概述
生活中会面临很多抉择,每一个抉择会导向不同的结果,这时就会出现策略的选择。
程序中也是一样,而且更加复杂,生活中的抉择一般只有几个,但程序中可能有十多个甚至数十个,也就是我们常用的 if-else
或者 switch
。当只有少量选择时,用这些没有问题,但一旦量比较大,代码的可读性和维护难度会迅速上升,这时候就建议使用策略模式了。
代码实现
这里以相加相减策略来介绍策略模式:
1、定义策略
/**
* 策略
*/
public interface Strategy {
/**
* 运行策略
* @param num1 数字1
* @param num2 数字2
* @return 结果
*/
int doStrategy(int num1, int num2);
}
2、定义相加策略
/**
* 相加策略
*/
public class Add implements Strategy {
@Override
public int doStrategy(int num1, int num2) {
return num1 + num2;
}
}
3、定义相减策略
/**
* 相减策略
*/
public class Subtract implements Strategy {
@Override
public int doStrategy(int num1, int num2) {
return num1 - num2;
}
}
4、定义上下文
/**
* 上下文
*/
public class Context {
private Strategy strategy;
/**
* 指定策略
* @param strategy 策略
*/
public void setStrategy(Strategy strategy) {
this.strategy = strategy;
}
/**
* 执行策略
* @param num1 数字1
* @param num2 数字2
* @return 结果
*/
public int executeStrategy(int num1, int num2) {
return this.strategy.doStrategy(num1, num2);
}
}
5、调用
// 定义上下文
Context context = new Context();
// 设置相加策略
context.setStrategy(new Add());
System.out.println(context.executeStrategy(2, 1));
// 设置相减策略
context.setStrategy(new Subtract());
System.out.println(context.executeStrategy(2, 1));
输出结果为:
3
1
可以发现,按照对应的策略执行了运算,这就是策略模式的基本实现。
与状态模式区别
可以看出,策略模式的实现方式与前面讲的状态模式非常相似,但它们是有本质区别的。
主要体现在转换是主动还是被动的。状态模式是由状态自动触发的,是被动的;策略模式是指定策略再执行的,是主动的。
优缺点
优点
1、算法可以自由切换。
2、避免使用多重条件判断。
3、扩展性良好。
缺点
1、策略类会增多。
2、所有策略类都需要对外暴露。
使用场景
1、如果在一个系统里面有许多类,它们之间的区别仅在于它们的行为,那么使用策略模式可以动态地让一个对象在许多行为中选择一种行为。
2、一个系统需要动态地在几种算法中选择一种。
3、如果一个对象有很多的行为,如果不用恰当的模式,这些行为就只好使用多重的条件选择语句来实现。
注意事项
如果一个系统的策略多于 4 个,就需要考虑使用混合模式,解决策略类膨胀的问题。
模板方法模式
模板方法模式
Template method
模板方法模式: 在抽象类中规定多个步骤方法的模板方法架构,子类去实现时不能改变架构但是可以根据子类实际情况去重写步骤方法
比如规定制作汉堡过程:
下层面包垫底->加入蔬菜->加入酱料->加入肉馅->上层面板封顶
这5个步骤可以分为5个方法,在抽象类中规定一定顺序的调用这些步骤的方法(这个方法要final修饰不可重写,防止子类改变算法架构),而子类鸡肉(或牛肉)汉堡根据实际情况去重写加入肉馅的步骤方法
抽象类是模板,抽象类的方法是模板方法,规定了一系列的步骤
角色
AbstractClass: 抽象类,模板类,在这个类中规定模板方法
ConcreteClass: 模板类的实现类,可以根据情况重写模板方法中的步骤方法
制作汉堡模板方法模式代码
结构图
在抽象层定义了一个钩子方法:hookMethod ,它被当作模板方法的步骤方法,但是它默认什么也不做,子类有其他需求可以重写它 (比如还需要往汉堡中加其他东西)
AbstractClass
/**
* @author Tc.l
* @Date 2021/2/5
* @Description:模板类-制作汉堡
*/
public abstract class AbstractClass {
//模板方法 final修饰不可重写 不能改变算法架构
public final void makeHamburg() {
lowerBread();
addVegetables();
addSauce();
addMeat();
hookMethod();
topBread();
}
/**
* 钩子方法:留给子类实现加入其他东西
*/
protected void hookMethod() {
}
protected void lowerBread() {
System.out.println("下层面包垫底");
}
protected void addVegetables() {
System.out.println("添加蔬菜");
}
protected void addSauce() {
System.out.println("添加酱料");
}
abstract void addMeat();
protected void topBread() {
System.out.println("上层面包封顶");
}
}
ConcreteClassA
/**
* @author Tc.l
* @Date 2021/2/5
* @Description:模板类的实现类-牛排汉堡
*/
public class ConcreteClassA extends AbstractClass{
@Override
void addMeat() {
System.out.println("加入牛排");
}
}
ConcreteClassB
/**
* @author Tc.l
* @Date 2021/2/5
* @Description:模板类的实现类-鲜虾芝士汉堡
*/
public class ConcreteClassB extends AbstractClass{
@Override
void addMeat() {
System.out.println("加入虾仁");
}
@Override
protected void hookMethod() {
System.out.println("加入芝士");
}
}
Client
public class Client {
public static void main(String[] args) {
AbstractClass beefHamburg = new ConcreteClassA();
AbstractClass shrimpHamburg = new ConcreteClassB();
System.out.println("****制作牛肉汉堡****");
beefHamburg.makeHamburg();
System.out.println();
System.out.println("****=制作虾仁汉堡******=");
shrimpHamburg.makeHamburg();
}
}
/*
****制作牛肉汉堡****
下层面包垫底
添加蔬菜
添加酱料
加入牛排
上层面包封顶
****=制作虾仁汉堡******=
下层面包垫底
添加蔬菜
添加酱料
加入虾仁
加入芝士
上层面包封顶
*/
总结
使用场景
完成一系列步骤,这一系列步骤基本相同,但个别步骤实现不同时
(都是制作汉堡但是放的肉馅不同)
特点
- 将一系列细节相同的代码抽象为父类,去除子类重复代码,实现复用
- 如果不用模板方法模式,制作不同的汉堡过程会有相同的步骤(重复代码) 这种相同步骤可以称为:不变行为
- 将不变行为抽取到父类中,防止重复,实现代码复用
- 模板方法要用final修饰,防止子类修改模板方法的架构
观察者模式
参考
https://www.bilibili.com/video/BV1u3411P7Na?p=28&vd_source=299f4bc123b19e7d6f66fefd8f124a03
观察者模式(Observer Pattern)属于行为型模式
概述
在 Java 中,一个对象的状态发生改变,就可能会影响到其他的对象,与之相关的对象可能也会联动的进行改变。
比如监听器机制,当具体的事件触发时,可以在创建的监听器中执行相关的逻辑。
观察者模式可以实现这样的功能,当对象发生改变时,观察者能够立即观察到并进行一些联动操作。
代码实现
自定义实现
这里以监听主体改变为例来介绍观察者模式:
1、定义观察者
/**
* 观察者
*/
public interface Observer {
/**
* 更新操作
*/
void update();
}
2、定义观察者实现类
/**
* 观察者实现类
*/
public class ObserverImpl implements Observer {
@Override
public void update() {
System.out.println("进行了修改");
}
}
3、定义主体
/**
* 主体
*/
public class Subject {
/**
* 观察者集合
*/
private final Set<Observer> observerSet = new HashSet<>();
/**
* 添加观察者
* @param observer 观察者
*/
public void observe(Observer observer) {
observerSet.add(observer);
}
/**
* 修改
*/
public void modify() {
// 当对象发生修改时,会通知所有的观察者,并进行方法回调
observerSet.forEach(Observer::update);
}
}
4、调用
// 主体
Subject subject = new Subject();
// 添加观察者
subject.observe(new ObserverImpl());
// 修改
subject.modify();
输出结果为:
进行了修改
可以发现,当主体调用修改方法时,添加的观察者观察到了变化,执行了操作。这就是观察者模式的自定义实现。
JDK自带实现
JDK 也提供了实现观察者模式的方式:
1、定义 JDK 实现的观察主体
/**
* JDK实现的观察主体<br>
* 继承 Observable 表示支持观察者
*/
public class Subject4Jdk extends Observable {
/**
* 修改
*/
public void modify() {
System.out.println("进行了修改");
// 当对象修改后,需要 setChanged 来设定为已修改状态
this.setChanged();
// 使用 notifyObservers 方法来通知所有的观察者
// 注意只有已修改状态下通知观察者才会有效,并且可以给观察者传递参数,这里传递了一个时间对象
this.notifyObservers(new Date());
}
}
2、调用
// 主体
Subject4Jdk subject4Jdk = new Subject4Jdk();
// 添加观察者(Observable提供的方法)
subject4Jdk.addObserver((o, arg) -> System.out.println("监听到变化,并得到参数:" + arg));
// 进行修改操作
subject4Jdk.modify();
输出结果为:
进行了修改
监听到变化,并得到参数:Tue Aug 16 21:32:43 CST 2022
可以发现,JDK 自带的实现方式更为简洁,且功能更为强大,还能传递参数。
优缺点
优点
1、观察者和被观察者是抽象耦合的。
2、建立一套触发机制。
缺点
1、如果一个被观察者对象有很多的直接和间接的观察者的话,将所有的观察者都通知到会花费很多时间。
2、如果在观察者和观察目标之间有循环依赖的话,观察目标会触发它们之间进行循环调用,可能导致系统崩溃。
3、观察者模式没有相应的机制让观察者知道所观察的目标对象是怎么发生变化的,而仅仅只是知道观察目标发生了变化。
使用场景
1、一个抽象模型有两个方面,其中一个方面依赖于另一个方面。将这些方面封装在独立的对象中使它们可以各自独立地改变和复用。
2、一个对象的改变将导致其他一个或多个对象也发生改变,而不知道具体有多少对象将发生改变,可以降低对象之间的耦合度。
3、一个对象必须通知其他对象,而并不知道这些对象是谁。
4、需要在系统中创建一个触发链,A 对象的行为将影响 B 对象,B 对象的行为将影响 C 对象……,可以使用观察者模式创建一种链式触发机制。
注意事项
1、Java 中已经有了对观察者模式的支持类。
2、避免循环引用。
3、如果顺序执行,某一观察者错误会导致系统卡壳,一般采用异步方式。
迭代器模式
参考
https://www.bilibili.com/video/BV1u3411P7Na?p=25&vd_source=299f4bc123b19e7d6f66fefd8f124a03
迭代器模式(Iterator Pattern)属于行为型模式
概述
迭代器模式是 Java 中非常常用的设计模式。这种模式用于顺序访问集合对象的元素,而不需要知道集合对象的底层表示。
迭代器是学习 Java 语言的基础,没有迭代器,集合类的遍历就成了问题,正是因为有迭代器的存在,才能更加优雅的使用 foreach 语法。
Java 中的增强 for 循环是使用迭代器实现的:
List<String> list = Arrays.asList("AAA", "BBB", "CCC");
// 使用 foreach 语法糖进行迭代,依次获取每一个元素
for (String s : list) {
// 打印元素
System.out.println(s);
}
编译之后的代码如下:
List<String> list = Arrays.asList("AAA", "BBB", "CCC");
// 这里本质是通过 List 生成的迭代器来遍历每个元素的
Iterator var2 = list.iterator();
// 判断是否还有元素可以迭代,没有就返回false
while(var2.hasNext()) {
// 通过 next 方法得到下一个元素,每调用一次,迭代器会向后移动一位
String s = (String)var2.next();
// 打印元素
System.out.println(s);
}
可以看到,当使用迭代器对 List 进行遍历时,实际上就像是在操作一个指向列表头部的指针,通过不断向后移动指针来依次获取所指向的元素。
代码实现
这里依照 JDK 提供的迭代器接口(JDK 已经定义好了一个迭代器的具体相关操作接口),也来设计一个迭代器:
1、定义数组集合
/**
* 数组集合<br>
* 实现 Iterable 接口表示此类是支持迭代的
*/
public class ArrayCollection<T> implements Iterable<T> {
/**
* 使用数组来存放数据
*/
private final T[] array;
/**
* 构造器私有,自己用
* @param array 数组
*/
private ArrayCollection(T[] array) {
this.array = array;
}
/**
* 使用静态方法获取对象
* @param array 数组
* @return 数组集合对象
* @param <T> 实体类
*/
public static <T> ArrayCollection<T> of(T[] array) {
return new ArrayCollection<>(array);
}
/**
* 实现 iterator 方法,此方法会返回一个迭代器,用于迭代我们集合中的元素
* @return 迭代器
*/
@Override
public Iterator<T> iterator() {
return new ArrayIterator();
}
/**
* 这里自定义 ArrayIterator,注意别用静态,需要使用对象中存放的数组
*/
public class ArrayIterator implements Iterator<T> {
// 这里通过指针表示当前的迭代位置
private int index = 0;
/**
* 判断是否还有下一个元素
* @return 结果
*/
@Override
public boolean hasNext() {
// 如果指针大于或等于数组最大长度,就不能再继续了
return index < array.length;
}
/**
* 返回当前指针位置的元素并向后移动一位
* @return
*/
@Override
public T next() {
// 正常返回对应位置的元素,并将指针自增
return array[index++];
}
}
}
2、调用
// 定义数组
String[] arr = new String[]{"AAA", "BBB", "CCC", "DDD"};
// 使用数组集合处理数组
ArrayCollection<String> collection = ArrayCollection.of(arr);
// 使用 foreach 语法糖遍历,最后还是会变成迭代器调用
for (String s : collection) {
System.out.println(s);
}
编译后的代码为:
String[] arr = new String[]{"AAA", "BBB", "CCC", "DDD"};
ArrayCollection<String> collection = ArrayCollection.of(arr);
// 首先获取迭代器,实际上就是调用我们实现的 iterator 方法
Iterator var3 = collection.iterator();
while(var3.hasNext()) {
// 使用 next() 方法不断向下获取
String s = (String)var3.next();
System.out.println(s);
}
输出结果为:
AAA
BBB
CCC
DDD
这样就实现了自定义迭代器来遍历数组。
优缺点
优点
1、它支持以不同的方式遍历一个聚合对象。
2、迭代器简化了聚合类。
3、在同一个聚合上可以有多个遍历。
4、在迭代器模式中,增加新的聚合类和迭代器类都很方便,无须修改原有代码。
缺点
由于迭代器模式将存储数据和遍历数据的职责分离,增加新的聚合类需要对应增加新的迭代器类,类的个数成对增加,这在一定程度上增加了系统的复杂性。
使用场景
1、访问一个聚合对象的内容而无须暴露它的内部表示。
2、需要为聚合对象提供多种遍历方式。
3、为遍历不同的聚合结构提供一个统一的接口。
注意事项
迭代器模式就是分离了集合对象的遍历行为,抽象出一个迭代器类来负责,这样既可以做到不暴露集合的内部结构,又可让外部代码透明地访问集合内部的数据。
责任链模式
参考
https://www.bilibili.com/video/BV1u3411P7Na?p=23&vd_source=299f4bc123b19e7d6f66fefd8f124a03
责任链模式(Chain of Responsibility Pattern)属于行为型模式
概述
责任链,即责任的链条,在生活中是很常见的。
比如在工作中提交审批,会经历责任人的层层审批,最后才会通过,这一审批流程就是责任链。
再比如,JavaWeb 中学习的 Filter 过滤器,正是采用的责任链模式,通过将请求一级一级不断向下传递,来对所需要的请求进行过滤和处理。
运用责任链模式,可以让整个流程变得更加清晰,且每个环节的调用顺序可以灵活的调整,利于维护和扩展。
代码实现
这里以面试过程为例介绍责任链模式:
1、定义面试处理程序
/**
* 面试处理程序
*/
public abstract class Handler {
/**
* 下一轮面试
*/
protected Handler successor;
/**
* 开始下一轮面试
* @param successor 下一轮面试
* @return 面试处理程序
*/
public Handler connect(Handler successor) {
this.successor = successor;
// 这里返回后继节点,方便后面链式调用
return successor;
}
/**
* 开始面试
*/
public void handle() {
// 面试过程。由不同的子类实现具体处理过程
this.doHandle();
//责任链上如果还有后继节点,就继续向下传递
Optional.ofNullable(this.successor).ifPresent(Handler::handle);
}
/**
* 面试过程<br>
* 采用模板模式,变化的逻辑定义为抽象的,交由子类继承实现
*/
public abstract void doHandle();
}
2、定义第一轮面试
/**
* 第一轮面试
*/
public class FirstHandler extends Handler {
@Override
public void doHandle() {
System.out.println("第一轮面试");
}
}
3、定义第二轮面试
/**
* 第二轮面试
*/
public class SecondHandler extends Handler {
@Override
public void doHandle() {
System.out.println("第二轮面试");
}
}
4、定义第三轮面试
/**
* 第三轮面试
*
* @author CodeSail
* @date 2022-08-14 15:39:52
*/
public class ThirdHandler extends Handler {
@Override
public void doHandle() {
System.out.println("第三轮面试");
}
}
5、调用
// 首先定义第一轮面试
Handler handler = new FirstHandler();
// 连接后续两轮面试
handler.connect(new SecondHandler()).connect(new ThirdHandler());
// 开始面试
handler.handle();
输出结果为:
第一轮面试
第二轮面试
第三轮面试
这样就实现按照预定的顺序的三轮面试。如果需要将第三轮面试调整到第二轮之前,只需要将连接顺序改变即可:
handler.connect(new ThirdHandler()).connect(new SecondHandler());
这样的输出就变成了:
第一轮面试
第三轮面试
第二轮面试
这就是责任链模式的好处,可以非常方便的对流程进行调整和控制。
优缺点
优点
1、降低耦合度。将请求的发送者和接收者解耦。
2、简化了对象。使得对象不需要知道链的结构,只负责自己的逻辑定义即可。
3、增强给对象指派职责的灵活性。通过改变链内的成员或者调动它们的顺序,可以动态地新增或者删除责任。
4、增加新的请求处理类很方便。
缺点
1、不能保证请求一定被接收。
2、系统性能将受到一定影响,而且在进行代码调试时不太方便,可能会造成循环调用。
3、可能不容易观察运行时的特征,有碍于除错。
使用场景
1、多个对象处理同一个请求,具体哪个对象处理该请求由运行时刻自动确定。
2、在不明确指定接收者的情况下,向多个对象中的一个提交一个请求。
3、可动态指定一组对象处理请求。
注意事项
在需要使用链式调用的场景都可以考虑使用责任链模式(Java Web中尤其多)。
命令模式
参考
https://www.bilibili.com/video/BV1u3411P7Na?p=24&vd_source=299f4bc123b19e7d6f66fefd8f124a03
命令模式(Command Pattern)属于行为型模式
概述
现在各大电子厂商都在推智能家居,即可以通过手机这一个终端控制多个家用电器,比之前的单个设备智能由对应遥控器控制的方案要方便很多。这其实就是命令模式的应用。
命令模式将请求以命令的形式包裹在对象中,并传给调用对象。调用对象寻找可以处理该命令的合适的对象,并把该命令传给相应的对象,该对象执行命令。
代码实现
这里以手机控制电视机开启为例介绍命令模式:
1、定义命令接收者
/**
* 命令接收者
*/
public interface Receiver {
/**
* 开启
*/
void open();
}
2、定义电视机
/**
* 电视机
*/
public class Television implements Receiver{
@Override
public void open() {
System.out.println("电视机已开启");
}
}
3、定义命令
/**
* 命令
*/
public abstract class Command {
/**
* 命令接收者
*/
private final Receiver receiver;
/**
* 绑定命令与命令接收者
* @param receiver 命令接收者
*/
public Command(Receiver receiver) {
this.receiver = receiver;
}
/**
* 执行命令
*/
public void execute() {
// 开启
this.receiver.open();
}
}
4、定义开启命令
/**
* 开启命令
*/
public class OpenCommand extends Command {
/**
* 开启电视机
* @param television 电视机
*/
public OpenCommand(Television television) {
super(television);
}
}
5、定义控制器
/**
* 控制器
*/
public class Controller {
/**
* 发出命令
* @param command 命令
*/
public static void call(Command command) {
command.execute();
}
}
6、调用
// 定义电视机
Television television = new Television();
// 使用终端设备发送命令到控制器
// 控制器发出开启命令到电视机
// 电视机接收到开启命令后开始启动
Controller.call(new OpenCommand(television));
输出结果为:
电视机已开启
这样就实现了通过命令开启电视机。如果需要添加其他命令,比如说关闭,只需要定义一个关闭命令继承 Command,再用构造方法指定要控制的设备即可。
优缺点
优点
1、降低了系统耦合度。
2、新的命令可以很容易添加到系统中去。
缺点
使用命令模式可能会导致某些系统有过多的具体命令类。
使用场景
需要用命令的地方都可以使用命令模式,比如:
1、GUI 中每一个按钮都是一条命令。
2、模拟 CMD。
注意事项
系统需要支持命令的撤销(Undo)操作和恢复(Redo)操作,也可以考虑使用命令模式。
命令模式结构示意图:
备忘录模式
参考
https://www.bilibili.com/video/BV1u3411P7Na?p=27&vd_source=299f4bc123b19e7d6f66fefd8f124a03
备忘录模式(Memento Pattern)属于行为型模式
概述
备忘录模式就是为程序提供了一个可回溯的时间节点,如果程序在运行过程中某一步出现了错误,就可以回到之前的某个被保存的节点上重新来过(就像艾克的大招)。平时编辑文本的时候,当编辑出现错误时,就需要撤回,此时只需要按下 Ctrl + Z 就可以回到上一步,这样大大方便了文本编辑。
其实备忘录模式在程序中的应用十分广泛,比如安卓程序在很多情况下都会重新加载 Activity
,实际上安卓中 Activity
的 onSaveInstanceState
和 onRestoreInstanceState
就是用到了备忘录模式,分别用于保存和恢复,这样就算重新加载也可以恢复到之前的状态。
代码实现
下面以学生学习为例介绍备忘录模式:
1、定义学生
/**
* 学生
*/
public class Student {
/**
* 当前正在做的事
*/
private String currentThing;
/**
* 当前做的事完成百分比
*/
private int percentage;
/**
* 做事
* @param currentThing 当前正在做的事
*/
public void todo(String currentThing) {
this.currentThing = currentThing;
this.percentage = new Random().nextInt(100);
}
/**
* 保存当前状态
* @return 当前状态
*/
public State save() {
return new State(this.currentThing, this.percentage);
}
/**
* 重置状态
* @param state 状态
*/
public void restore(State state){
this.currentThing = state.getCurrentThing();
this.percentage = state.getPercentage();
}
@Override
public String toString() {
return "现在正在做:" + this.currentThing + ",进度:" + this.percentage + "%";
}
}
2、定义状态
/**
* 状态
*/
public class State {
/**
* 当前正在做的事
*/
private final String currentThing;
/**
* 当前做的事完成百分比
*/
private final int percentage;
public State(String currentThing, int percentage) { //仅开放给同一个包下的Student类使用
this.currentThing = currentThing;
this.percentage = percentage;
}
public String getCurrentThing() {
return currentThing;
}
public int getPercentage() {
return percentage;
}
}
3、调用
// 定义学生
Student student = new Student();
// 开始学习
student.todo("学习");
// 查看当前的状态
System.out.println(student);
// 保存当前的状态
State savedState = student.save();
// 学到中途打游戏去了
student.todo("打游戏");
// 查看当前的状态
System.out.println(student);
// 打着游戏又想着该学习,又回去学习,回到上次的进度继续学习
student.restore(savedState);
// 查看当前的状态
System.out.println(student);
输出结果为:
现在正在做:学习,进度:4%
现在正在做:打游戏,进度:14%
现在正在做:学习,进度:4%
这样就实现了状态的回溯并继续进行,是备忘录模式的典型应用。
优缺点
优点
1、给用户提供了一种可以恢复状态的机制,可以使用户能够比较方便地回到某个历史的状态。
2、实现了信息的封装,使得用户不需要关心状态的保存细节。
缺点
消耗资源。需要冗余属性相同的类,如果类的成员变量过多,势必会占用比较大的资源,而且每一次保存都会消耗一定的内存。
使用场景
1、需要保存或者恢复数据的相关状态场景。
2、需要提供一个可回滚的操作。
注意事项
1、为了符合迪米特原则,还要增加一个管理备忘录的类。
2、为了节约内存,可使用原型模式 + 备忘录模式。
状态模式
参考
https://www.bilibili.com/video/BV1u3411P7Na?p=29&vd_source=299f4bc123b19e7d6f66fefd8f124a03
状态模式(State Pattern)属于行为型模式
概述
在标准大气压下,水在 0 ~ 100 度之间时,会呈现液态;在 0 度以下会变成固态;100 度以上会变成气态。
物质在不同的条件下呈现不同的状态,对象可能也会像这样存在很多种状态,在不同的状态下可能会有不同的行为,可以通过状态模式来实现这样的效果。
代码实现
下面以水的三种状态对应的不同行为来介绍状态模式:
1、定义状态
/**
* 状态
*/
public enum State {
/**
* 液态
*/
LIQUID,
/**
* 固态
*/
SOLID,
/**
* 气态
*/
GASEOUS
}
2、定义水
/**
* 水
*
* @author CodeSail
* @date 2022-08-17 21:20:18
*/
public class Water {
/**
* 状态
*/
public State state;
/**
* 设置温度
* @param temperature 温度
*/
public void setTemperature(int temperature) {
// 低于 0 度,状态变为固态
if (temperature < 0) {
this.state = State.SOLID;
}
// 0 度到 100 度,状态变为液态
else if (temperature < 100) {
this.state = State.LIQUID;
}
// 高于 100 度,状态变为气态
else {
this.state = State.GASEOUS;
}
}
/**
* 行为
*/
public void action() {
// 不同的状态对应不同的行为
switch (this.state) {
case LIQUID:
System.out.println("水,内有千丘外无锋。风怒吼,狂澜卷巨峰。");
break;
case SOLID:
System.out.println("水,寒彻三尺非日艰。钢铁骨,千钧无纹动。");
break;
case GASEOUS:
System.out.println("水,飘洒随风影无踪。心高远,直上九重天。");
break;
default:
System.out.println("未知");
}
}
}
3、调用
// 定义水
Water water = new Water();
// 定义温度数组
int[] temperatures = {50, -1, 101};
// 遍历温度,查看各温度下水的行为
for (int temperature : temperatures) {
// 设置温度
water.setTemperature(temperature);
// 水的行为
water.action();
}
输出结果为:
水,内有千丘外无锋。风怒吼,狂澜卷巨峰。
水,寒彻三尺非日艰。钢铁骨,千钧无纹动。
水,飘洒随风影无踪。心高远,直上九重天。
可以看到,将水的温度设置为不同值后,到了指定温度,水的状态就会发生改变,体现出其状态对应的行为,这就是状态模式的简单实现。
优缺点
优点
1、封装了转换规则。
2、枚举可能的状态,在枚举状态之前需要确定状态种类。
3、将所有与某个状态有关的行为放到一个类中,并且可以方便地增加新的状态,只需要改变对象状态即可改变对象的行为。
4、允许状态转换逻辑与状态对象合成一体,而不是某一个巨大的条件语句块。
5、可以让多个环境对象共享一个状态对象,从而减少系统中对象的个数。
缺点
1、状态模式的使用必然会增加系统类和对象的个数。
2、状态模式的结构与实现都较为复杂,如果使用不当将导致程序结构和代码的混乱。
3、状态模式对开闭原则的支持并不太好,对于可以切换状态的状态模式,增加新的状态类需要修改那些负责状态转换的源代码,否则无法切换到新增状态,而且修改某个状态类的行为也需修改对应类的源代码。
使用场景
1、行为随状态改变而改变的场景。
2、条件、分支语句的代替者。
注意事项
一般在行为受状态约束且状态不超过 5 个时,使用状态模式。
访问者模式
参考
https://www.bilibili.com/video/BV1u3411P7Na?p=31&vd_source=299f4bc123b19e7d6f66fefd8f124a03
访问者模式(Visitor Pattern)属于行为型模式
概述
生活中经常会有这样的情况,同样的事物不同人有完全不同的感受,正所谓一千个读者一千个哈姆雷特。
程序中也是一样,往往不同的访问者会有不同的行为以及结果,这就是访问者模式。
代码实现
这里以李白和苏轼对庐山不同的感受为例介绍访问者模式:
1、定义庐山
/**
* 庐山
*/
public class Lushan {
}
2、定义访问者
/**
* 访问者
*/
public interface Visitor {
/**
* 感受
* @param lushan 庐山
*/
void feel(Lushan lushan);
}
3、定义李白
/**
* 李白
*/
public class LiBai implements Visitor {
@Override
public void feel(Lushan lushan) {
System.out.println("《望庐山瀑布》李白");
System.out.println("日照香炉生紫烟,");
System.out.println("遥看瀑布挂前川。");
System.out.println("飞流直下三千尺,");
System.out.println("疑是银河落九天。");
}
}
4、定义苏轼
/**
* 苏轼
*/
public class SuShi implements Visitor{
@Override
public void feel(Lushan lushan) {
System.out.println("《题西林壁》苏轼");
System.out.println("横看成岭侧成峰,");
System.out.println("远近高低各不同。");
System.out.println("不识庐山真面目,");
System.out.println("只缘身在此山中。");
}
}
5、调用
// 李白
Visitor liBai = new LiBai();
// 苏轼
Visitor suShi = new SuShi();
// 李白对庐山的感受
liBai.feel(new Lushan());
// 苏轼对庐山的感受
suShi.feel(new Lushan());
输出结果为:
《望庐山瀑布》李白
日照香炉生紫烟,
遥看瀑布挂前川。
飞流直下三千尺,
疑是银河落九天。
《题西林壁》苏轼
横看成岭侧成峰,
远近高低各不同。
不识庐山真面目,
只缘身在此山中。
可以看出,李白和苏轼同样作为访问者访问了庐山,但得到的结果是完全不一样的,这就是访问者模式的简单实现。
优缺点
优点
1、符合单一职责原则。
2、优秀的扩展性。
3、灵活性。
缺点
1、具体元素对访问者公布细节,违反了迪米特原则。
2、具体元素变更比较困难。
3、违反了依赖倒置原则,依赖了具体类,没有依赖抽象。
使用场景
1、对象结构中对象对应的类很少改变,但经常需要在此对象结构上定义新的操作。
2、需要对一个对象结构中的对象进行很多不同的并且不相关的操作,而需要避免让这些操作”污染”这些对象的类,也不希望在增加新操作时修改这些类。
注意事项
访问者可以对功能进行统一,可以做报表、UI、拦截器与过滤器。
中介者模式
参考
https://www.bilibili.com/video/BV1u3411P7Na?p=26&vd_source=299f4bc123b19e7d6f66fefd8f124a03
中介者模式(Mediator Pattern)属于行为型模式
概述
一提到中介,大家都非常熟悉,生活中最常见的就是房屋中介。
虽然中介要收取一定费用,但却能给房东和租客都提供大量遍历,房东只需要把房屋信息提供给中介,租客也只需要将租金交给中介,由中介处理中介环节,给两方都提供了便利。这就是中介模式的应用。
代码实现
这里以房屋中介来介绍中介模式:
1、定义用户
/**
* 用户
*/
public class User {
/**
* 姓名
*/
private final String name;
/**
* 电话
*/
private final String phone;
/**
* 构造姓名和电话
* @param name 姓名
* @param phone 电话
*/
public User(String name, String phone) {
this.name = name;
this.phone = phone;
}
/**
* 需要具体想找的地址和中介
* @param address 地址
* @param mediator 中介
* @return 用户
*/
public User find(String address, Mediator mediator) {
// 中介根据地址找房
return mediator.find(address);
}
@Override
public String toString() {
return "(姓名:" + name + ",电话:" + phone + ")";
}
}
2、定义中介
/**
* 中介
*/
public class Mediator {
/**
* 存储在售房屋
*/
private final Map<String, User> userMap = new HashMap<>();
/**
* 登记房屋
* @param address 地址
* @param user 用户
*/
public void register(String address, User user) {
userMap.put(address, user);
}
/**
* 查找房源
* @param address 地址
* @return 房东
*/
public User find(String address) {
return userMap.get(address);
}
}
3、调用
// 房东
User landlord = new User("张三", "10086");
// 租客
User tenant = new User("李四", "10010");
// 中介
Mediator mediator = new Mediator();
// 中介登记房产
mediator.register("重庆市北碚区金科小区", landlord);
// 租客委托中介查找符合要求房子的房东
User result = tenant.find("重庆市北碚区龙湖小区", mediator);
if (result == null) {
System.out.println("没有找到对应的房源");
} else {
System.out.print("成功找到对应房源:");
System.out.println(result);
}
// 再次查找
result = tenant.find("重庆市北碚区金科小区", mediator);
if(result == null) {
System.out.println("没有找到对应的房源");
} else {
System.out.print("成功找到对应房源:");
System.out.println(result);
}
输出结果为:
没有找到对应的房源
成功找到对应房源:(姓名:张三,电话:10086)
这样就实现了日常生活中的通过中介租房。中介者模式优化了原有的复杂多对多关系,而是将其简化为一对多的关系,更容易理解一些。
优缺点
优点
1、降低了类的复杂度,将一对多转化成了一对一。
2、各个类之间的解耦。
3、符合迪米特原则。
缺点
中介者会庞大,变得复杂难以维护。
使用场景
1、系统中对象之间存在比较复杂的引用关系,导致它们之间的依赖关系结构混乱而且难以复用该对象。
2、想通过一个中间类来封装多个类中的行为,而又不想生成太多的子类。
注意事项
不应当在职责混乱的时候使用。
解释器模式(用的很少)
参考
https://www.bilibili.com/video/BV1u3411P7Na?p=21&vd_source=299f4bc123b19e7d6f66fefd8f124a03
解释器模式(Interpreter Pattern)属于行为型模式
概述
解释器模式是指给定一门语言, 基于它的语法, 定义解释器来解释语言中的句子。是一种按照规定的语法进行解析的模式。
就比如编译器可以将源码编译解释为机器码, 让 CPU 能进行识别并运行。解释器模式的作用其实与编译器一样,都是将一些固定的语法进行解释,构建出一个解释句子的解释器。
解释器是一个语法分析工具,它可以识别句子语义,分离终结符号和非终结符号,提取出需要的信息,针对不同的信息做出相应的处理。其核心思想是识别语法,构建解释。
解释器模式主要包含 4 种角色:
-
抽象表达式(Expression) :负责定义解释方法
interpret
, 交由子类进行具体解释。 -
终结符表达式(Terminal Expression) :实现文法中与终结符有关的解释操作。
文法中的每一个终结符都有一个具体终结表达式与之相对应,比如公式 R = R1 + R2,R1 和 R2 就是终结符,对应的解析 R1 和 R2 的解释器就是终结符表达式。通常一个解释器模式中只有一个终结符表达式,但有多个实例,对应不同的终结符。
-
非终结符表达式(Nonterminal Expression):实现文法中与非终结符有关的解释操作。
文法中的每条规则都对应于一个非终结符表达式。非终结符表达式一般是文法中的运算符或者其他关键字,比如公式 R = R1 + R2 中, + 就是非终结符,解析它的解释器就是一个非终结符表达式。非终结符表达式根据逻辑的复杂程度而增加,原则上每个文法规则都对应一个非终结符表达式。
-
上下文环境(Context) :包含解释器之外的全局信息。
它的任务一般是用来存放文法中各个终结符所对应的具体值,比如 R = R1 + R2,给 R1 赋值 100,给 R2 赋值 200,这些信息需要存放到环境中。
代码实现
这里以根据乘客年龄和身高来判断乘坐公交车是否免费为例介绍解释器模式:
1、定义乘客
/**
* 乘客
*/
public class Passenger {
/**
* 姓名
*/
private String name;
/**
* 年龄
*/
private Integer age;
/**
* 身高
*/
private Double height;
public Passenger(String name, int age, double height) {
this.name = name;
this.age = age;
this.height = height;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
public Double getHeight() {
return height;
}
public void setHeight(Double height) {
this.height = height;
}
}
2、定义表达式
/**
* 表达式
*/
public interface Expression {
/**
* 解释年龄
* @param age 年龄
* @return 解释结果
*/
boolean interpret(int age);
/**
* 解释身高
* @param height 身高
* @return 解释结果
*/
boolean interpret(double height);
}
3、定义比较器
/**
* 比较器
*/
public enum Compare {
/**
* 较大
*/
GT,
/**
* 相等
*/
EQ,
/**
* 较小
*/
LT
}
4、定义终结符表达式
/**
* 终结符表达式
*/
public class TerminalExpression implements Expression {
/**
* 年龄
*/
private Integer age;
/**
* 身高
*/
private Double height;
/**
* 比较器
*/
private final Compare compare;
/**
* 构造年龄比较
* @param age 年龄
* @param compare 比较器
*/
public TerminalExpression(int age, Compare compare) {
this.age = age;
this.compare = compare;
}
/**
* 构造身高比较
* @param height 身高
* @param compare 比较器
*/
public TerminalExpression(double height, Compare compare) {
this.height = height;
this.compare = compare;
}
@Override
public boolean interpret(int age) {
// 比较年龄大小
switch (compare) {
// 较大
case GT:
return age > this.age;
// 相等
case EQ:
return age == this.age;
// 较小
case LT:
return age < this.age;
default:
return false;
}
}
@Override
public boolean interpret(double height) {
// 比较身高大小
switch (compare) {
// 较大
case GT:
return height > this.height;
// 相等
case EQ:
return height == this.height;
// 较小
case LT:
return height < this.height;
default:
return false;
}
}
}
5、定义非终结符表达式
与表达式:
/**
* 与表达式
*/
public class AndExpression implements Expression {
/**
* 表达式1
*/
private Expression expression1;
/**
* 表达式2
*/
private Expression expression2;
/**
* 构造表达式
* @param expression1 表达式1
* @param expression2 表达式2
*/
public AndExpression(Expression expression1, Expression expression2) {
this.expression1 = expression1;
this.expression2 = expression2;
}
@Override
public boolean interpret(int age) {
return this.expression1.interpret(age) && this.expression2.interpret(age);
}
@Override
public boolean interpret(double height) {
return this.expression1.interpret(height) && this.expression2.interpret(height);
}
}
或表达式:
/**
* 或表达式
*/
public class OrExpression implements Expression {
/**
* 表达式1
*/
private Expression expression1;
/**
* 表达式2
*/
private Expression expression2;
/**
* 构造表达式
* @param expression1 表达式1
* @param expression2 表达式2
*/
public OrExpression(Expression expression1, Expression expression2) {
this.expression1 = expression1;
this.expression2 = expression2;
}
@Override
public boolean interpret(int age) {
return this.expression1.interpret(age) || this.expression2.interpret(age);
}
@Override
public boolean interpret(double height) {
return this.expression1.interpret(height) || this.expression2.interpret(height);
}
}
6、定义免费标准
/**
* 免费标准
*/
public class Free {
/**
* 年龄表达式
*/
private Expression ageExpression;
/**
* 身高表达式
*/
private Expression heightExpression;
/**
* 构造免费情况
* @param age 年龄
* @param height 身高
*/
public Free(int age, double height) {
// 大于等于设定年龄
Expression expression1 = new TerminalExpression(age, Compare.GT);
Expression expression2 = new TerminalExpression(age, Compare.EQ);
ageExpression = new OrExpression(expression1, expression2);
// 小于等于设定身高
expression1 = new TerminalExpression(height, Compare.LT);
expression2 = new TerminalExpression(height, Compare.EQ);
heightExpression = new OrExpression(expression1, expression2);
}
/**
* 结果
* @param age 年龄
* @param height 身高
* @return 判定结果
*/
public boolean result(int age, double height) {
return ageExpression.interpret(age) || heightExpression.interpret(height);
}
}
7、客户端调用
// 定义乘客集合
List<Passenger> list = new ArrayList<>();
Passenger p1 = new Passenger("张三", 65, 170.0);
Passenger p2 = new Passenger("李四", 10, 130.0);
Passenger p3 = new Passenger("王五", 50, 170.0);
list.add(p1);
list.add(p2);
list.add(p3);
// 所有年龄大于等于65或者身高小于等于130的乘客免费乘车
for (Passenger p : list) {
// 定义免费标准
Free free = new Free(65, 130);
// 满足条件则免费
if (free.result(p.getAge(), p.getHeight())) {
System.out.println(p.getName() + ":免费");
}
// 不满足条件则正常收费
else {
System.out.println(p.getName() + ":刷卡或投币");
}
}
输出结果:
张三:免费
李四:免费
王五:刷卡或投币
可以看到,按照预期输出了结果,实现了根据年龄和身高自动判断是否免费的功能。
优缺点
优点
1、扩展性好。解释器模式中使用类来表示语言的文法规则,可以比较方便的通过继承等机制来改变或扩展文法。
2、容易实现。在语法树中的每个表达式节点类都是相似的,实现其文法较为容易。
缺点
1、执行效率较低。解释器模式中通常使用大量的循环和递归调用,当要解释的句子较复杂时,其运行速度很慢,且代码的调试过程也比较麻烦。
2、会引起类膨胀。解释器模式中的每条规则至少需要定义一个类,当包含的文法规则很多时,类的个数将急剧增加,导致系统难以管理与维护,因此可应用的场景比较少。
使用场景
JDK 源码中的 Pattern 对正则表达式的编译和解析
1、Pattern 有参构造器
private Pattern(String p, int f) {
// 保存数据
pattern = p;
flags = f;
// 如果存在 UNICODE_CHARACTER_CLASS,则使用 UNICODE_CASE
if ((flags & UNICODE_CHARACTER_CLASS) != 0) {
flags |= UNICODE_CASE;
}
// 重置组索引计数
capturingGroupCount = 1;
localCount = 0;
if (!pattern.isEmpty()) {
try {
// 调用编译方法
compile();
} catch (StackOverflowError soe) {
throw error("Stack overflow during pattern compilation");
}
} else {
root = new Start(lastAccept);
matchRoot = lastAccept;
}
}
2、编译方法 compile()
private void compile() {
// 处理规范等价
if (has(CANON_EQ) && !has(LITERAL)) {
normalize();
} else {
normalizedPattern = pattern;
}
patternLength = normalizedPattern.length();
// 为方便起见,将模式复制到 int 数组,使用双零终止模式
temp = new int[patternLength + 2];
hasSupplementary = false;
int c, count = 0;
// 将所有字符转换为代码点
for (int x = 0; x < patternLength; x += Character.charCount(c)) {
c = normalizedPattern.codePointAt(x);
if (isSupplementary(c)) {
hasSupplementary = true;
}
temp[count++] = c;
}
// patternLength 现在在代码点中
patternLength = count;
if (! has(LITERAL)) {
RemoveQEQuoting();
}
// 在这里分配所有临时对象
buffer = new int[32];
groupNodes = new GroupHead[10];
namedGroups = null;
if (has(LITERAL)) {
// 文字模式处理
matchRoot = newSlice(temp, patternLength, hasSupplementary);
matchRoot.next = lastAccept;
} else {
// 开始递归下降解析
matchRoot = expr(lastAccept);
// 检查额外的模式字符
if (patternLength != cursor) {
if (peek() == ')') {
throw error("Unmatched closing ')'");
} else {
throw error("Unexpected internal error");
}
}
}
// 窥孔优化
if (matchRoot instanceof Slice) {
root = BnM.optimize(matchRoot);
if (root == matchRoot) {
root = hasSupplementary ? new StartS(matchRoot) : new Start(matchRoot);
}
} else if (matchRoot instanceof Begin || matchRoot instanceof First) {
root = matchRoot;
} else {
root = hasSupplementary ? new StartS(matchRoot) : new Start(matchRoot);
}
// 释放临时存储
temp = null;
buffer = null;
groupNodes = null;
patternLength = 0;
compiled = true;
}
注意事项
sion(height, Compare.LT);
expression2 = new TerminalExpression(height, Compare.EQ);
heightExpression = new OrExpression(expression1, expression2);
}
/**
* 结果
* @param age 年龄
* @param height 身高
* @return 判定结果
*/
public boolean result(int age, double height) {
return ageExpression.interpret(age) || heightExpression.interpret(height);
}
}
7、客户端调用
```java
// 定义乘客集合
List<Passenger> list = new ArrayList<>();
Passenger p1 = new Passenger("张三", 65, 170.0);
Passenger p2 = new Passenger("李四", 10, 130.0);
Passenger p3 = new Passenger("王五", 50, 170.0);
list.add(p1);
list.add(p2);
list.add(p3);
// 所有年龄大于等于65或者身高小于等于130的乘客免费乘车
for (Passenger p : list) {
// 定义免费标准
Free free = new Free(65, 130);
// 满足条件则免费
if (free.result(p.getAge(), p.getHeight())) {
System.out.println(p.getName() + ":免费");
}
// 不满足条件则正常收费
else {
System.out.println(p.getName() + ":刷卡或投币");
}
}
输出结果:
张三:免费
李四:免费
王五:刷卡或投币
可以看到,按照预期输出了结果,实现了根据年龄和身高自动判断是否免费的功能。
优缺点
优点
1、扩展性好。解释器模式中使用类来表示语言的文法规则,可以比较方便的通过继承等机制来改变或扩展文法。
2、容易实现。在语法树中的每个表达式节点类都是相似的,实现其文法较为容易。
缺点
1、执行效率较低。解释器模式中通常使用大量的循环和递归调用,当要解释的句子较复杂时,其运行速度很慢,且代码的调试过程也比较麻烦。
2、会引起类膨胀。解释器模式中的每条规则至少需要定义一个类,当包含的文法规则很多时,类的个数将急剧增加,导致系统难以管理与维护,因此可应用的场景比较少。
使用场景
JDK 源码中的 Pattern 对正则表达式的编译和解析
1、Pattern 有参构造器
private Pattern(String p, int f) {
// 保存数据
pattern = p;
flags = f;
// 如果存在 UNICODE_CHARACTER_CLASS,则使用 UNICODE_CASE
if ((flags & UNICODE_CHARACTER_CLASS) != 0) {
flags |= UNICODE_CASE;
}
// 重置组索引计数
capturingGroupCount = 1;
localCount = 0;
if (!pattern.isEmpty()) {
try {
// 调用编译方法
compile();
} catch (StackOverflowError soe) {
throw error("Stack overflow during pattern compilation");
}
} else {
root = new Start(lastAccept);
matchRoot = lastAccept;
}
}
2、编译方法 compile()
private void compile() {
// 处理规范等价
if (has(CANON_EQ) && !has(LITERAL)) {
normalize();
} else {
normalizedPattern = pattern;
}
patternLength = normalizedPattern.length();
// 为方便起见,将模式复制到 int 数组,使用双零终止模式
temp = new int[patternLength + 2];
hasSupplementary = false;
int c, count = 0;
// 将所有字符转换为代码点
for (int x = 0; x < patternLength; x += Character.charCount(c)) {
c = normalizedPattern.codePointAt(x);
if (isSupplementary(c)) {
hasSupplementary = true;
}
temp[count++] = c;
}
// patternLength 现在在代码点中
patternLength = count;
if (! has(LITERAL)) {
RemoveQEQuoting();
}
// 在这里分配所有临时对象
buffer = new int[32];
groupNodes = new GroupHead[10];
namedGroups = null;
if (has(LITERAL)) {
// 文字模式处理
matchRoot = newSlice(temp, patternLength, hasSupplementary);
matchRoot.next = lastAccept;
} else {
// 开始递归下降解析
matchRoot = expr(lastAccept);
// 检查额外的模式字符
if (patternLength != cursor) {
if (peek() == ')') {
throw error("Unmatched closing ')'");
} else {
throw error("Unexpected internal error");
}
}
}
// 窥孔优化
if (matchRoot instanceof Slice) {
root = BnM.optimize(matchRoot);
if (root == matchRoot) {
root = hasSupplementary ? new StartS(matchRoot) : new Start(matchRoot);
}
} else if (matchRoot instanceof Begin || matchRoot instanceof First) {
root = matchRoot;
} else {
root = hasSupplementary ? new StartS(matchRoot) : new Start(matchRoot);
}
// 释放临时存储
temp = null;
buffer = null;
groupNodes = null;
patternLength = 0;
compiled = true;
}
注意事项
解释器模式在实际的软件开发中使用比较少,因为它会引起效率、性能以及维护等问题。如果碰到对表达式的解释,在 Java 中可以用 Expression4J 或 Jep 等来设计。