软件设计模式

学习方法:

1.设计模式的学习提倡多问为什么并且要对比没有该设计模式和有该设计模式的差异,才能体会到每一种设计模式的奇妙之处

2.每一种设计模式的代码都需要手动去实现

软件设计原则

核心内容:所有设计模式都是在这些原则的基础上进行设计的

开闭原则

1
2
3
4
/**
* 实现动态切换搜狗输入法的皮肤(多态实现),满足开闭原则,对拓展开放,对修改关闭
* 我们想要拓展输入法的皮肤,只需要自己加抽象类的子类即可,无需变动原有任何的皮肤代码如默认皮肤,黑马皮肤,抽象皮肤
* 核心是无需无需变动原有代码便可实现代码的拓展

里氏替换原则

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
/**
* 正方形是长宽相等的特殊长方形(这里演示子类重写父类非抽象方法带来的后果,也称违反里氏代换原则)
* 这里的特性可以指成员变量,成员方法
* 核心的点在于:子类的特性可能会与父类原有的特性产生矛盾,如果你用子类的特性去适配原有的特性,就会产生错误
* 比如:你用正方形的边长都相等的特性去适配长方形原有的拓展宽,直到宽大于长的方法,就会产生错误,因为正方形边长永远相等,不可能有宽大于长的情况
* 再比如:燕子和几维鸟都属于鸟类,但你用几维鸟无法飞行的特性去适配鸟类原有的计算飞行速度方法的特性,就会产生错误,因为几维鸟本身根本不会飞行的特性,怎么可能计算飞行速度
*
* 总结:
* 出现bug原因:
* 核心点就在于不要用子类特有的特性去适配父类原有的特性,转化成代码来说就是不要重写父类固有的方法
* 如果是抽象方法则不会有此问题,因为父类的抽象方法的特性本身就是有子类去指定的,父类的抽象方法本身只一些规范,并没有指定其具体的特性,所以重写不存在子类需要去适配父类原有特性的问题
*
* 里氏替换原则的目的:子类能在任何场景替代父类去执行方法的调用而不出现bug(多态)
* 为后续面向抽象编程,抽象类可以根据实际的场景不断切换其具体的实现类而打好基础(你只有子类能替代父类后才有更进一步的父类可可以根据场景切换子类)

再次深入理解里氏替换原则:

经过郑建华老师的通俗讲解:深入理解到里氏替换原则是为了合理的面向抽象编程,因为依赖倒转原则教会我们要面向抽象编程,要抽象依赖于抽象,那么抽象有两种实现方案:
A.向上抽取一个抽象父类(继承)
B.向上抽取一个接口(实现)

如果你是利用继承实现抽象的方案的,那么你必须满足继承的特性,即同属一类事物,才能实现动态切换具体的实现进而实现多态调用。比如鸭子是抽象父类,里面有一个游泳的功能,那么你让一个烧鸭去继承该抽象父类,等到调用时传入具体的烧鸭给抽象父类(鸭),由鸭调用游泳的功能。这就必然出现问题了,因为烧鸭不会游泳这个特性,所以如果你根据游泳这个特性把烧鸭和鸭归为同一类事物,实际这个归类就是错误的,即烧鸭继承鸭的这个关系是错误的

那么回归问题的原始起点,什么才叫合理的的抽象呢?就是在满足该关系的前提下进行抽象,比如使用继承实现抽象,那么就必须满足继承的特性(父子类同属同一类事物),如果使用接口实现抽象,那么就必须满足接口实现的特性(实现类必须接口的行为特性,比如你不能让老虎实现飞行的接口,因为老虎根本没有飞行的行为特性)

里氏替换的原则的表达是:父类出现的地方,子类都能将其替换,归根到底就是因为父类和子类是属于同一类事物或者拥有同样的行为特性(接口和实现类)

那么子类能否重写父类的方法呢?这个问题在理解了里氏替换原则的核心之后非常好回答
答案是可以的,只要子类重写父类方法的时候没有违反该关系(继承or实现)的特性,那么就没有任何问题
比如刚才的鸭子和小黄鸭,假设鸭子和小黄鸭二者都会游泳,利用游泳这个特性向上抽象,但是小黄鸭重写了鸭子的游泳方法,说自己不会游泳,那此时就违反了继承关系了,因为你是根据游泳的特性向上抽象的,结果你重写后说小黄鸭不会游泳了,那就直接破快了继承关系了

*
*/

依赖倒转原则

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* 这样设计有一个问题:计算机的配置只支持希捷硬盘,因特尔cpu,金士顿内存条了,要想切换这些组件必须修改Computer的代码了,违反了开闭原则
* 实质这里是违反了依赖倒转原则,而依赖倒转原则是可以保证遵循开闭原则的


* 依赖倒转原则:高层模块不应该依赖低层模块,二者都应该依赖其抽象,抽象不应该依赖细节,细节应该依赖抽象
依赖抽象的目的就是让其(其代指高层和底层)可以根据实际情况任意的切换具体的实现

* 这个案例是电脑不再依赖底层因特尔cpu,希捷硬盘,金士顿内存条的具体实现,而转换为依赖底层的抽象cpu,硬盘,内存
*
* 实际上就是一句话:面向抽象编程(面向接口编程),模块之间的依赖全部通过抽象发生,让模块之间可以实现任意切换
*
* 再深入点理解:我拓展或者修改底层的模块,不影响高层代码的调用,即尽最大可让高层和底层独立起来,互不影响(降低耦合度)
* 依赖倒转原则是保证开闭原则的手段,这里的互不影响是指拓展底层 or 修改底层代码不需要去修改高层的代码
*
*/

接口隔离原则

1
2
3
4
5
/**
* 接口隔离原则:每个接口都是单一职责的,如果一个接口存在多个方法,那么就意味着实现类需要重写其所有方法(可能实现类只需要其中的一个方法,其他方法都不需要)
* 比如安全门:黑马安全门是可以防火,防盗,防水的,但传智的安全门是没有防水这个功能的,如果三个方法写一个接口,意味着传智也有了防水的这个功能..
* 解决方案:将接口按职责拆分所需方法,做到让其实现类实现接口时确保接口中的所有方法都是必须的(当然,非必须方法可以用接口中的默认方法来解决)
*/

迪米特原则

​ 迪米特法则要求我们在设计系统时,应该尽量减少对象之间的交互,如果两个对象之间不必彼此直接通信,那么这两个对象就不应当发生任何直接的相互作用,如果其中的一个对象需要调用另一个对象的某一个方法的话,可以通过第三者转发这个调用。简言之,就是通过引入一个合理的第三者来降低现有对象之间的耦合度

合成复用原则

合成复用原则是指:尽量先使用组合或者聚合等关联关系来实现,其次才考虑使用继承关系来实现。

通常类的复用分为继承复用和合成复用两种。

继承复用虽然有简单和易实现的优点,但它也存在以下缺点:

  1. 继承复用破坏了类的封装性。因为继承会将父类的实现细节暴露给子类,父类对子类是透明的,所以这种复用又称为“白箱”复用。
  2. 子类与父类的耦合度高。父类的实现的任何改变都会导致子类的实现发生变化,这不利于类的扩展与维护。
  3. 它限制了复用的灵活性。从父类继承而来的实现是静态的,在编译时已经定义,所以在运行时不可能发生变化。

采用组合或聚合复用时,可以将已有对象纳入新对象中,使之成为新对象的一部分,新对象可以调用已有对象的功能,它有以下优点:

  1. 它维持了类的封装性。因为成分对象的内部细节是新对象看不见的,所以这种复用又称为“黑箱”复用。
  2. 对象间的耦合度低。可以在类的成员位置声明抽象。
  3. 复用的灵活性高。这种复用可以在运行时动态进行,新对象可以动态地引用与成分对象类型相同的对象。

合成复用原则拥有灵活性的核心:面向抽象编程,因为面向编程可以任意接口的实现类 or 子类的切换,大大降低了类与类之间的耦合

细节:具体的代码在idea中有写,如果idea中没写(大部分都有写),可以去参考黑马资料中的代码案例

设计原则总结

核心是开闭原则,其次是合成复用原则

1.开闭原则是目的,里氏替换原则是保证开闭原则能够正常实现的基础,依赖倒转原则是实现开闭原则的手段

2.合成复用原则可以降低因为继承带来的强耦合,并且提高了非常高的灵活性,因为抽象+组合的模式可以实现动态的切换实现类,灵活性非常高

3.迪米特原则更像是引入一个中间人,降低客户和服务提供者二者之间的耦合

软件设计模式

创建者模式

单例模式

单例模式的实现有两种,饿汉式和懒汉式实现

饿汉式实现的方式有两种:静态变量实现,静态代码块实现

懒汉式实现的方式有四种:

1.静态方法实现(需要加synchronized防止线程安全问题)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* 懒汉式单例模式
* 存在并发线程安全问题(使用synchronized同步关键字解决)
*/
public class Singleton {

private static Singleton singleton;

private Singleton(){

}

public static synchronized Singleton getInstance(){
if (singleton == null){
singleton = new Singleton();
}

return singleton;

}
}

2.静态方法+双重检查锁的方式实现(同样是为了避免 线程安全问题)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
/**
* 懒汉式单例模式
* 双重检查锁模式实现
* 改进synchronized关键字放在方法上导致锁的范围过大
* 因为除了第一次是写操作,后续全是读操作,读操作是不存在并发安全问题的
*
* 待学习:
* 双重检查锁模式其实是存在问题,在多线程的情况下,可能会出现空指针问题,出现问题的原因是JVM在实例化对象的时候会进行优化和指令重排序操作。
* 要解决双重检查锁模式带来空指针异常的问题,只需要使用 `volatile` 关键字, `volatile` 关键字可以保证可见性和有序性。
*/
public class Singleton {

private static volatile Singleton singleton;

private Singleton(){

}

public static Singleton getInstance(){
//第一次检查,过滤掉读操作,如果是读操作直接返回即可,不需加锁(第一次检查提高了读操作的性能)
if (singleton == null){
synchronized (Singleton.class){
//第二次检查,进来的都是写操作,但同一时刻可能有多个写操作进来(线程并发安全问题),所以需要加锁
//而并发进来的线程由于锁会等待前一个线程构建好单例,其他线程判断单例不为空后直接返回
//第二次检查提高了多个并发写操作的性能(因为实质只有一个写操作构建单例,其他并发写操作判断后直接返回了)
if (singleton == null){
singleton = new Singleton();
}
}
}

return singleton;
}
}

3.静态内部类实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
* 懒汉式实现单例模式
* 用静态内部类的方式实现
* 改进上一种静态方法实现单例模式的线程安全问题和由于jvm优化产生的指令顺序不一致的空指针问题
* 因为静态内部类只会加载一次,而静态方法虽然只会被加载一次,但有可能会被调用多次,所以静态内部类不存在线程安全问题,而静态方法存在
* 而且jvm在初始化的时候只会加载外部类,内部类是要等待调用时才加载,并且加载内部类时会保证jvm指令的执行顺序,不会产生空指针问题
*/
public class Singleton {

private Singleton(){

}

private static class SingletonHolder{
private static final Singleton singleton = new Singleton();
}

public static Singleton getInstance(){
return SingletonHolder.singleton;
}

}

4.枚举实现

1
2
3
4
5
6
7
/**
* 饿汉式时实现单例模式
* 利用枚举类本身的特性实现
*/
public enum Singleton {
INSTANCE;
}

破坏单例模式的两种情况

1.对象的反序列化进行破坏及解决方案

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
/**
* 对象的反序列化方法会破坏单例模式,因为对象底层反序列化的readObject()方法会利用反射方式构建出一个新对象
* 解决方案:指定对象的反序列化解析方法,避免使用默认的反射构造对象的反序列化方式
* 其实问题的核心原因还是因为反射可以暴力访问私有的构造方法
*/
public class Singleton implements Serializable {

//私有构造方法
private Singleton() {}

private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}

//对外提供静态方法获取该对象
public static Singleton getInstance() {
return SingletonHolder.INSTANCE;
}

/**
* 下面是为了解决序列化反序列化破解单例模式
* 指定本对象反序列化方式(让其获取本类的单例对象而不是去反射构造对象)
*/
private Object readResolve() {
return SingletonHolder.INSTANCE;
}


}

2.反射进行破坏

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
/**
* 双重检查锁方式实现懒汉式单例模式
* 演示如何解决反射导致的破坏单例模式问题
* 问题的核心在于:反射能够无视访问权限访问私有构造方法进行对象的构造
* 解决方案:在私有构造方法中检测单例是否已经构造出来,如果是重复构造则抛出异常,阻止其构造
*/
public class Singleton {

private static volatile Singleton instance;
//私有构造方法
private Singleton() {

if (instance != null){
throw new RuntimeException("单例对象已构造成功,不允许重复构造");
}

}


//对外提供静态方法获取该对象
public static Singleton getInstance() {

if(instance != null) {
return instance;
}

synchronized (Singleton.class) {
if(instance != null) {
return instance;
}
instance = new Singleton();
return instance;
}
}
}

工厂方法模式

案例需求:设计一个咖啡店点餐系统。

设计一个咖啡类(Coffee),并定义其两个子类(美式咖啡【AmericanCoffee】和拿铁咖啡【LatteCoffee】);再设计一个咖啡店类(CoffeeStore),咖啡店具有点咖啡的功能

1.简单工厂方法模式

案例的类图

简单工厂模式

2.工厂方法模式

案例的类图

工厂方法模式

3.工厂方法模式的另一种实现->配置文件方式实现

4.抽象工厂方法模式

案例的类图

抽象工厂模式

精华总结

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
/**
* 1.创建者模式的基本理解:
* 创建者模式的核心是让创建和使用分离,这里就算让咖啡店专门销售咖啡,而咖啡工厂来生产咖啡,让生产效率更高,管理起来更好
* 如果咖啡店即生产咖啡又销售咖啡,那么全部活都是一个人做,这样如果这个人生产咖啡没做好,那么就销售不了咖啡了(一荣俱荣,一损俱损)
* 但如果使用工厂模式管理,把生产咖啡和销售咖啡分给两个人干,首先生产咖啡专心生产咖啡即可,不用担心销售咖啡,销售咖啡同样也只是做销售咖啡即可
* 这样二者互不影响,降低二者的耦合度,大大生产的效率
* 你可能会问销售咖啡不是得等生产咖啡生产好了才能销售吗?确实是这样子,但是我把工序分开了之后,如果你这个人没有生产好咖啡,那我就换个人来对接呗,
* 你是否能顺利生产咖啡与我是否能顺利销售咖啡关联不大(因为你不行那就换一个人呗)
*
* 2.工厂模式的初步理解:
* 深入理解工厂模式的理念:生产产品的工作模式倡导分工合作,每个人工作职责单一,避免一个人出现职责过多,一个崩溃,整条生产线就崩溃了
* 那么工厂模式倡导分开的两个职责分别是产品的创建和产品的使用,让二者独立开来,可以任意切换而不受对方影响
* 要想实现任意切换,核心是面向抽象编程
* 产品使用者得现获取产品现,所以是产品使用者去调用创建产品的人,也就是产品使用者要能任意切换想要的产品
* 产品的创建者是工厂,所以工厂必须抽象起来
*
*
* 3.简单工厂模式,抽象工厂模式的深入理解:
* 简单工厂模式只做到了分开职责,但做不到可以任意切换(这就得抽象工厂上场了)
* 因为简单工厂模式中工厂是具体的工厂,根本切换不了
* 具体怎么做呢?
* 把工厂抽象起来即可,让使用者根据实际所需的产品动态选择相应的产品工厂去创建(动态切换创建产品的工厂)
*
* 抽象工厂分为两种:
* A.工厂只生产一种类型的产品如只生产咖啡,咖啡的类型可以任意切换,但不能生产其他类型的产品如奶茶,饮料等(对应设计模式的工厂方法模式)
* 工厂方法模式只有一层抽象:同一种工厂不同产品的抽象(咖啡工厂有拿铁咖啡,美式咖啡)
*
* 优点:一个工厂对应一种类型的产品,如果需要添加产品,只需增加相应的产品工厂即可即可,原有工厂的产品不受任何影响,不需要修改
* 缺点:一个工厂只能生产一种类型的产品,如咖啡工厂只能生产咖啡。范围太局限了,如果产品类型很多,就会导致产生很多工厂接口/类,就会导致结构非常复杂
* 核心问题在于工厂能生产的产品范围太小,局限于一种类型的产品
* 使用场景:适合频繁切换单种类型产品的,比如我只切换帽子,衣服和鞋子都不变(适合小范围的产品切换,即同种类型产品的切换)
*
* B.工厂支持生产多种不同类型的产品,如生产咖啡,生产奶茶,饮料等都可以(对应设计模式的抽象工厂模式)
* 抽象工厂支持生产的产品范围会大很多,不再局限于一种类型的产品(华为品牌有手机产品,电脑产品,电视产品...小米同样也有很多类型的产品)
*
* 抽象工厂解决了工厂方法模式的缺点:把一个工厂能生产的产品范围扩大了很多,如以前是只能生产一种类型产品,现在一个工厂可以生产一种品牌商品
* 优点:大大扩大了工厂生产产品的范围,减少了不必要的工厂接口
* 缺点:如果我想增加一个产品类型,那么所有依赖该工厂接口都要修改(工厂供应的范围太大了,整个范围都要相应的发生修改)
* 适合场景:要换就整套换,如要换衣服就把帽子,衣服,鞋子都换了...(大范围的切换,即同时切换多种不同类型的产品)
*
*
*
* 4.工厂模式->开闭原则的深入理解:
* 从工厂模式得到的感悟,工厂模式这样做就是为了满足开闭原则,使对象的创建和使用满足开闭原则(使对象的创建和使用如果拓展的话,不用修改原有业务逻辑代码)
*
* 5.以下是我对开闭原则的深入理解
* 这就是实现开闭原则的核心:怎么把业务逻辑的耦合关联->类与类的关联->抽象类(接口)与抽象类(接口)的关联,就是抽象与抽象的关联
* 一旦转换成功,我们拓展功能就只需要增加相应类的or接口即可,不需要再动原来的业务逻辑代码便可实现业务的拓展
* 下面是经典咖啡店案例,将根据咖啡类型字段动态选择咖啡的逻辑->抽象与抽象的关联
*
*
* 6.工厂方法模式还有另一种写法(配置方式实现)
* 传统写法是使用程序动态传入所需要生产的产品来进行生产产品的切换
* 如咖啡店生产什么类型的咖啡,要根据调用者传入具体类型的咖啡实现,工厂再开始构造
*
* 即结合配置文件的写法,不用程序动态传入实际产品类型,而使用动态加载配置文件的方式传入实际产品(实质就是使 用配置文件的方式传入实际的产品)
* 这种方式也是SpringIOC底层beanFactory的实现方式,好处是更灵活了,耦合度更低了,可以通过配置文件的方式进行动态配置而不是使用程序来进行动态配置
*
*
*/

每一种代码在idea种都有手写,如果不清晰可以结合代码再次分析

原型模式

概念

用一个已经创建的实例作为原型,通过复制该原型对象来创建一个和原型对象相同的新对象

原理

原型模式的克隆分为浅克隆和深克隆。

浅克隆:创建一个新对象,新对象的属性和原来对象完全相同,对于非基本类型属性,仍指向原有属性所指向的对象的内存地址。

深克隆:创建一个新对象,属性中引用的其他对象也会被克隆,不再指向原有对象地址

使用场景

  • 对象的创建非常复杂,可以使用原型模式快捷的创建对象
  • 性能和安全要求比较高

建造者模式

概述

将一个复杂对象的构建与表示分离,使得同样的构建过程可以创建不同的表示。

![建造者模式 1](建造者模式 1.png)

  • 分离了部件的构造(由Builder来负责)和装配(由Director负责)。 从而可以构造出复杂的对象。这个模式适用于:某个对象的构建过程复杂的情况。
  • 由于实现了构建和装配的解耦。不同的构建器,相同的装配,也可以做出不同的对象;相同的构建器,不同的装配顺序也可以做出不同的对象。也就是实现了构建算法、装配算法的解耦,实现了更好的复用。
  • 建造者模式可以将部件和其组装过程分开,一步一步创建一个复杂的对象。用户只需要指定复杂对象的类型就可以得到该对象,而无须知道其内部的具体构造细节。

案例

创建共享单车

生产自行车是一个复杂的过程,它包含了车架,车座等组件的生产。而车架又有碳纤维,铝合金等材质的,车座有橡胶,真皮等材质。对于自行车的生产就可以使用建造者模式。

这里Bike是产品,包含车架,车座等组件;Builder是抽象建造者,MobikeBuilder和OfoBuilder是具体的建造者;Director是指挥者。类图如下:

建造者模式2

具体的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
//自行车类
public class Bike {
private String frame;
private String seat;

public String getFrame() {
return frame;
}

public void setFrame(String frame) {
this.frame = frame;
}

public String getSeat() {
return seat;
}

public void setSeat(String seat) {
this.seat = seat;
}
}

/**
* 建造者设计模式的目的:将复杂对象的创建和复杂对象的装配分离开来(只有复杂对象才有很多部件对象需要创建,创建完需要装配)
*
* 部件的对象创建和部件对象的装配解耦开来,无论是创建什么部件,无论你创建多少个部件,和我的部件装配都无关
* 你只要把相应的部件传输给我,然后我进行相应的构造即可
* 比如电脑主机所需要的配件是硬盘,内存,cpu,我只负责装配这三个部件,而这三个部件具体是什么产品与我无关(比如cpu有华为的,小米的,内存有金士顿的,希捷的..)
* 再比如下面的单车配件有车架,车座,我只负责装配车架和车座,至于你的车架是什么牌子,mobike哈爱上ofo与我的装配无关
*
* 实现复杂对象的创建和复杂对象的装配的解耦关键还是抽象
* 只有抽象才可以实现任意的部件产品的切换,而装配顺序任意切换同样得益于抽象
* 因为抽象可以实现不限定具体的产品和具体的产品装配顺序的关联
*
* 建造者设计模式有四个角色:复杂对象的构建者(builder),具体对象的构建者,抽象指挥者(装配组件对象的角色)和具体指挥者
* 与工厂模式的比较:工厂模式只负责对象的创建,不负责对象的装配,也就是工厂模式缺少了指挥者这个角色(非复杂对象不需要装配,也就不需要指挥者了)
* 如果指挥者 or 构建者的功能实现不是很复杂,可以将二者的角色合并,减少不必要的类结构
*
* 有一个小的感悟:好像如果是面向抽象编程,无论你怎样任意拓展和切换,原有的业务逻辑的调用代码都不需要修改,只需要增加相应的接口 or 实现类即可
* 不用修改的核心原因有两个:1.面向抽象编程 2.是多态进行的调用,多态可以动态绑定运行类,进而执行相应的代码
* 以前只发现了面向抽象编程这一点,没有发现多态在这里面也起到了巨大的作用
*/
public abstract class Builder {

public Bike mBike = new Bike();

public abstract void buildFrame();
public abstract void buildSeat();
public abstract Bike createBike();

}


//摩拜单车Builder类
public class MobikeBuilder extends Builder {

@Override
public void buildFrame() {
mBike.setFrame("铝合金车架");
}

@Override
public void buildSeat() {
mBike.setSeat("真皮车座");
}

@Override
public Bike createBike() {
return mBike;
}
}

//ofo单车Builder类
public class OfoBuilder extends Builder {

@Override
public void buildFrame() {
mBike.setFrame("碳纤维车架");
}

@Override
public void buildSeat() {
mBike.setSeat("橡胶车座");
}

@Override
public Bike createBike() {
return mBike;
}
}

//指挥者类
public class Director {
private Builder mBuilder;

public Director(Builder builder) {
mBuilder = builder;
}

public Bike construct() {
mBuilder.buildFrame();
mBuilder.buildSeat();
return mBuilder.createBike();
}
}

//测试类
public class Client {
public static void main(String[] args) {
showBike(new OfoBuilder());
showBike(new MobikeBuilder());
}
private static void showBike(Builder builder) {
Director director = new Director(builder);
Bike bike = director.construct();
System.out.println(bike.getFrame());
System.out.println(bike.getSeat());
}
}

注意:

上面示例是 Builder模式的常规用法,指挥者类 Director 在建造者模式中具有很重要的作用,它用于指导具体构建者如何构建产品,控制调用先后次序,并向调用者返回完整的产品类,但是有些情况下需要简化系统结构,可以把指挥者类和抽象建造者进行结合

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 抽象 builder 类
public abstract class Builder {

protected Bike mBike = new Bike();

public abstract void buildFrame();
public abstract void buildSeat();
public abstract Bike createBike();

public Bike construct() {
this.buildFrame();
this.BuildSeat();
return this.createBike();
}
}

说明:

这样做确实简化了系统结构,但同时也加重了抽象建造者类的职责,也不是太符合单一职责原则,如果construct() 过于复杂,建议还是封装到 Director 中。

优缺点

优点:

  • 建造者模式的封装性很好。使用建造者模式可以有效的封装变化,在使用建造者模式的场景中,一般产品类和建造者类是比较稳定的,因此,将主要的业务逻辑封装在指挥者类中对整体而言可以取得比较好的稳定性。
  • 在建造者模式中,客户端不必知道产品内部组成的细节,将产品本身与产品的创建过程解耦,使得相同的创建过程可以创建不同的产品对象。
  • 可以更加精细地控制产品的创建过程 。将复杂产品的创建步骤分解在不同的方法中,使得创建过程更加清晰,也更方便使用程序来控制创建过程。
  • 建造者模式很容易进行扩展。如果有新的需求,通过实现一个新的建造者类就可以完成,基本上不用修改之前已经测试通过的代码,因此也就不会对原有功能引入风险。符合开闭原则。

缺点:

造者模式所创建的产品一般具有较多的共同点,其组成部分相似,如果产品之间的差异性很大,则不适合使用建造者模式,因此其使用范围受到一定的限制。

使用场景

建造者(Builder)模式创建的是复杂对象,其产品的各个部分经常面临着剧烈的变化,但将它们组合在一起的算法却相对稳定,所以它通常在以下场合使用。

  • 创建的对象较复杂,由多个部件构成,各部件面临着复杂的变化,但构件间的建造顺序是稳定的。
  • 创建复杂对象的算法独立于该对象的组成部分以及它们的装配方式,即产品的构建过程和最终的表示是独立的。

拓展的建造者模式

demo3

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
public class Phone {
private String cpu;
private String screen;
private String memory;
private String mainboard;

public Phone(String cpu, String screen, String memory, String mainboard) {
this.cpu = cpu;
this.screen = screen;
this.memory = memory;
this.mainboard = mainboard;
}

public String getCpu() {
return cpu;
}

public void setCpu(String cpu) {
this.cpu = cpu;
}

public String getScreen() {
return screen;
}

public void setScreen(String screen) {
this.screen = screen;
}

public String getMemory() {
return memory;
}

public void setMemory(String memory) {
this.memory = memory;
}

public String getMainboard() {
return mainboard;
}

public void setMainboard(String mainboard) {
this.mainboard = mainboard;
}

@Override
public String toString() {
return "Phone{" +
"cpu='" + cpu + '\'' +
", screen='" + screen + '\'' +
", memory='" + memory + '\'' +
", mainboard='" + mainboard + '\'' +
'}';
}
}

demo4

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
/**
* 为了解决demo3通过构造函数构建复杂对象造成的可读性非常差的问题
* 实现链式调用构建复杂对象(链式调用充当指挥者,控制着构建复杂对象的装配顺序)
*
* Phone即是构建者又是指挥者
* 但这里的合并存在一个较大的问题
* 构建者和指挥者两者的耦合度会高很多,构建者的组件类型改变,指挥者相关的装配api要跟着改变
*
* 同时这里的构造逻辑也不太合理?
* 因为已经通过new的方式把对象构造出来再装配
* 通俗点的例子就是单车都造出来了,你再跟我说装配单车零件?这不是笑话
* 正确的逻辑应该是现创建组件对象再装配,装配完再进行构造
*/
public class Phone {
private String cpu;
private String screen;
private String memory;
private String mainboard;


public Phone(){

}

public Phone cpu(String cpu){
this.cpu = cpu;
return this;
}

public Phone screen(String screen){
this.screen = screen;
return this;
}
public Phone memory(String memory){
this.memory = memory;
return this;
}
public Phone mainboard(String mainboard){
this.mainboard = mainboard;
return this;
}

@Override
public String toString() {
return "Phone{" +
"cpu='" + cpu + '\'' +
", screen='" + screen + '\'' +
", memory='" + memory + '\'' +
", mainboard='" + mainboard + '\'' +
'}';
}
}

demo5

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
/**
* 为了解决demo4构造逻辑不合理的方案
* 使用静态内部类现把组件构造好再进行装配,最后再构造对象进行返回
* 同样是使用链式调用构建复杂对象(链式调用充当指挥者,控制着构建复杂对象的装配顺序)
* 但多了一个静态内部类作为调正构造逻辑,使构造逻辑合理化
*
* Phone即是构建者又是指挥者
* 但这里的同样合并存在一个较大的问题
* 构建者和指挥者两者的耦合度会高很多,构建者的组件类型改变,指挥者相关的装配api要跟着改变
* 因为这里存在一个二者关联的赋值过程,所以二者任意一个类型发生改变,对方都需要跟着改变
*
* 这种场景适用于组件的类型不需要频繁的改变(or 固定)
*/

public class Phone {
private String cpu;
private String screen;
private String memory;
private String mainboard;

private Phone(Builder builder){
this.cpu = builder.cpu;
this.screen = builder.screen;
this.memory = builder.memory;
this.mainboard = builder.mainboard;
}



@Override
public String toString() {
return "Phone{" +
"cpu='" + cpu + '\'' +
", screen='" + screen + '\'' +
", memory='" + memory + '\'' +
", mainboard='" + mainboard + '\'' +
'}';
}

public static final class Builder{
private String cpu;
private String screen;
private String memory;
private String mainboard;

public Builder cpu(String cpu){
this.cpu = cpu;
return this;
}

public Builder screen(String screen){
this.screen = screen;
return this;
}
public Builder memory(String memory){
this.memory = memory;
return this;
}
public Builder mainboard(String mainboard){
this.mainboard = mainboard;
return this;
}

public Phone build(){
return new Phone(this);
}
}
}

创建者模式对比

工厂方法模式和建造者模式对比

工厂方法模式注重的是整体对象的创建方式;而建造者模式注重的是部件构建的过程,意在通过一步一步地精确构造创建出一个复杂的对象

通俗点来说:

工厂方法模式只注重生产产品的结果,不注重产品构建过程,比如生产一台汽车,他直接就是给你返回一台汽车对象,并不关心汽车的内部的汽车组件(轮胎,底盘,天窗,方向盘…)是如何装配的

建造者模式则相反,非常注重产品的构建过程,比如生产汽车,他非常注重汽车组件的装配顺序,是现装配轮胎现还是先装配天窗先?先装配方向盘还是先装配天窗?…

抽象工厂和建造者模式对比

抽象工厂和工厂方法模式思想上是一致的,只是抽象工厂生产的产品范围更大,不局限于一种产品类型,比如工厂方法模式只生产电脑,而我抽象工厂就可以生产电器设备,如电脑,手机,电视…

跟建造者模式对比的结果也是一致,抽象工厂同样只注重生产产品的结果,不注重产品的构建过程,建造者模式则相反

他们之间的联系建造者需要先把产品的相关组件对象构造出来先,再进行特定顺序的装配。而构建产品的相关组件这一工作就是由工厂方法模式完成的

创建型模式总结

创建型模式的目的是让对象的创建和获取(使用)相分离

实现的核心思想是面向抽象编程+多态

对象的创建和使用分离是什么意思?比如你点饮料,无论你点的是可,雪碧,还是酸梅汁,他们的使用方法不会因为他们点的饮料类型不一样而发生改变

那么在代码层面怎么实现呢?

只有多态的思想能实现,因为多态支持动态绑定,他会根据你实际传入的类型进行动态绑定(动态调用)

那么如何实现多态呢?

多态实现的核心是面向抽象编程,因为面向抽象编程可以实现动态切换具体的接口实现类 or 抽象类的子类,只有先实现动态切换才能有多态的动态绑定和调用

下面我以工厂模式,建造者模式来举例说明

工厂模式

工厂模式的目的是需要产生各种类型的产品并且获取产品的方式不变

实现的核心是把工厂抽象起来,利用抽象实现工厂的动态切换,等待切换成功后利用多态实现动态绑定到具体工厂的创建产品方式

建造者模式

建造者模式的目的是创建复杂的对象,创建复杂的对象分为两步创建零件属性(对象),对这些零件实现装配

建造者模式的核心是实现零件对象的创建和零件对象的装配相分离,通俗点来说无论创建的是什么类型的零件,零件装配的顺序都不变

实现的核心是将零件的装配顺序抽象起来,利用抽象实现零件装配顺序的动态切换,等待切换成功后利用多态实现动态绑定到具体的零件装配顺序

以上的不变并不是真正的不变,而是表面上的调用不变,实际是利用了多态实现了动态绑定,进而实现相应的变化

至于另外两种创建者模式,单例模式和原型模式为什么不用抽象+多态呢?因为另外两种只创建一种类型的对象实例,不需要动态切换,所以不需要抽象+多态的思想实现

虽然创建者模式实现的核心思想是一致的,但不同的创建型模式功能不一样,如单例设计模式是为了产生单例对象,原型模式为了克隆复杂的对象,工厂模式是为了创建不同类型对象,构建者模式是为了创建复杂对象(需要定制复杂对象的零件装配顺序)

遗留的疑问:工厂和构建者设计模式创建的对象是否要求单例?

答案:可以实现单例也可以不实现单例,但为了对象资源的复用,最好还是实现单例为好

结构型模式

结构型模式描述如何将类或者对象结合在一起形成更复杂,功能更强大的结构

它分为类结构型模式和对象结构型模式,前者采用继承机制来组织接口和类,后者釆用组合或聚合来组合对象。

由于组合关系或聚合关系比继承关系耦合度低,满足“合成复用原则”,所以对象结构型模式比类结构型模式具有更大的灵活性

结构型模式的核心理解:实际就是通过类与类的关系(继承),类与接口的方式(实现),类与对象的方式(组合),三者进行动态的结合,进而形成更复杂,功能更强大的结构

当然结合的过程一直贯穿着面向抽象编程的思想,毕竟要降低模块与模块之间的耦合,增加模块的独立性

不同的结构型模式,他们组合类 or 对象的方式不尽相同,对应着不同的业务场景

代理模式

核心概念:代理究竟代理的是什么?如何理解代理这个词语?

以前我的理解帮助被代理人干一些杂活,边缘性的业务,让被代理人可以专注的干自己重要的事情而不被这些次要的,或者不那么重要的事情分心,但现在我发现这个理解仍不够正确。

以前以为代理人和被代理人干的不是同一件事情,这是错误的想法,实际上代理人和被代理人干的同样的一件事

虽然干的是同一件事情,但他们在其中分工不同。因为一件事情可以进一步拆分若干件小事,那么重要的就由被代理人自己亲自去干,不那么重要或者边缘性的事情又代理人帮忙去干

代理模式的实现方式

继承实现和接口实现,详细的链接如下(4条消息) 【Java】代理模式(Proxy模式)详解_java proxy_果壳~的博客-CSDN博客

主要搞清楚继承实现和接口实现两种方式优缺点即可,根据不同的场合选择不同的实现

案例说明

比如下面的火车站买票案例,火车站和代售点都干的是卖票这一件事情,但卖票这件事情可以划分为生产火车票,销售火车票两件事情,生产火车票是非常重要的事情,不允许出错的,所以由火车站亲自去干,而销售火车票不那么重要的事情就交给代理人去干,最终他们共同完成卖票这一任务

【例】火车站卖票

如果要买火车票的话,需要去火车站买票,坐车到火车站,排队等一系列的操作,显然比较麻烦。而火车站在多个地方都有代售点,我们去代售点买票就方便很多了。这个例子其实就是典型的代理模式,火车站是目标对象,代售点是代理对象。类图如下:

案例类图
静态代理

静态代理

1
2
3
4
5
6
7
/**
* 买票接口(定义抽象的卖票逻辑,可以根据业务情况切换卖票逻辑,比如火车站,地铁站,高铁站等卖票的逻辑可能都有所不同)
*/
public interface SellTickets {

void sell();
}
1
2
3
4
5
6
7
8
9
10
/**
* 火车站,这是被代理类,被代理类和代理都有买票这个功能,都需要实现买票的接口
* 只是代理类作为一个中介代理买票,提高卖票的效率
*/
public class TrainStation implements SellTickets{

public void sell() {
System.out.println("火车站卖票");
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* 代售点(代理类)
* 需要传入被代理对象,否职责不知道代理的是那个火车站or 地铁站,高铁站等的卖票功能
*
* 静态代理模式
* 之所以是静态代理模式是因为代理对象并不是抽象的,代理类并不能任意切换他所代理的对象,在编译期就已经决定了他代理是谁
* 如果将被代理对象进行抽象,改为运行时传入具体的代理对象,那么代理类就可以任意切换他所代理的对象
*
* 这里是没学动态代理前的猜想,现在确定这个猜想是错误的,动态代理的核心思想是动态而非面向抽象编程
*
*
*/
public class ProxyPoint implements SellTickets {

private final TrainStation trainStation = new TrainStation();

public void sell() {
trainStation.sell();
}

}
1
2
3
4
5
6
7
8
9
10
/**
* 测试类
*/
public class Client {

public static void main(String[] args) {
SellTickets proxyPoint =new ProxyPoint();
proxyPoint.sell();
}
}

动态代理

jdk动态代理(接口实现,支持代理多个类)

demo1是用了jdk封装好的动态工具实现的动态

1
2
3
4
5
6
7
/**
* 买票接口(定义抽象的卖票逻辑,可以根据业务情况切换卖票逻辑,比如火车站,地铁站,高铁站等卖票的逻辑可能都有所不同)
*/
public interface SellTickets {

void sell();
}
1
2
3
4
5
6
7
8
9
10
/**
* 火车站,这是被代理类,被代理类和代理都有买票这个功能,都需要实现买票的接口
* 只是代理类作为一个中介代理买票,提高卖票的效率
*/
public class TrainStation implements SellTickets {

public void sell() {
System.out.println("火车站卖票");
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
/**
* 疑问:
* 1.动态代理种的动态是什么意思?为什么要动态,他对比静态有什么好处?
* 动态代理的意思是根据程序的运行需要动态产生对应的代理类,对比静态代理类来说非常灵活
* 因为静态代理需要在程序编译阶段就提前写好需要代理的对象,不能根据程序的运行动态产生相应的代理类
*
* 因为JDK动态代理是接口实现,所以代理类和被代理类是支持一对多的关系,需要使用抽象来实现动态切换被代理类。所以这里面也有面向抽象编程的思想。但动态代理中的动态的意思仅仅是在程序运行阶段动态产生相应的代理类,并没有包含面向抽象编程动态的意思在里面
*
*
* 产生的新问题
* 面向抽象编程的动态切换中的动态和动态代理中的动态是一个意思吗?怎么理解这两个动态?
* 两个动态体现的思想是一致的,但实现方式是不一样的。
* 思想一致是因为都是运行时才能确定具体的类 or 行为,编译时是不能确定的
* 实现方式不一致是因为前者是通过运行时动态绑定具体的实现类实现的,而后者是通过反射实现的



* 2.为什么需要动态代理,动态代理实际上有什么好处和作用?为什么静态代理不能实现呢?为什么需要在程序运行阶段产生代理类对象,在编译阶段产生代理类对象不行吗?
* 静态代理和动态代理的具体对比后面总结可见,自然就明白为什么了
*
*
* 3.使用代理模式的场景有那些,好处有那些?如果不使用代理模式会有什么样的弊端?
* spring的aop切面编程,好处是统一增强方法的调用逻辑而不不用修改原有的代码,弊端是增加了代码的复杂度
*/
public class ProxyFactory {


private TrainStation trainStation = new TrainStation();

//动态获取代理类对象
public SellTickets getProxyObject(){

SellTickets sellTickets = (SellTickets)Proxy.newProxyInstance(trainStation.getClass().getClassLoader(),
trainStation.getClass().getInterfaces(),
new InvocationHandler() {
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("增强被代理类方法");
//代理类调用被代理类的卖票方法
// 卖票还得是火车站卖,代理类只是负责一个中介作用,最后要把真正卖票的数据交给火车站的
// 也就是实际上核心功能还是被代理类自己做,只是一些边角料的或者比较麻烦的功能交给代理类去做
method.invoke(trainStation, args);

return proxy;
}
});

return sellTickets;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
/**
* 测试
*/
public class Client {

public static void main(String[] args) {
ProxyFactory proxyFactory = new ProxyFactory();
SellTickets proxyObject = proxyFactory.getProxyObject();
proxyObject.sell();

}
}

demo2是利用通过阿里巴巴开源的 Java 诊断工具(Arthas【阿尔萨斯】)查看代理类的结构,获取jdk封装好的动态代理所生成的动态代理类进行分析和学习

1
2
3
4
5
6
7
/**
* 买票接口(定义抽象的卖票逻辑,可以根据业务情况切换卖票逻辑,比如火车站,地铁站,高铁站等卖票的逻辑可能都有所不同)
*/
public interface SellTickets {

void sell();
}
1
2
3
4
5
6
7
8
9
10
/**
* 火车站,这是被代理类,被代理类和代理都有买票这个功能,都需要实现买票的接口
* 只是代理类作为一个中介代理买票,提高卖票的效率
*/
public class TrainStation implements SellTickets {

public void sell() {
System.out.println("火车站卖票");
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
/**
* 本类是jdk动态生成的代理类,这里我们利用反编译工具将其提取出来进行学习
*
* 疑问:
* 1.为什么代理类获取的被代理类的方法是要静态的?
* 因为程序在运行时是利用反射动态构建出代理类,生成的是代理类而非代理类对象,而此时需要在并没有创建代理对象前提下获取抽象接口的方法逻辑并给赋值给代理类
* 在没有对象的前提对类的属性进行操作,那么该类的属性只能是静态属性了(静态加载)
* 至于为什么代理类和被代理都需要有抽象方法的逻辑
* 类比卖票的那个案例,代售点需要卖票,火车站也需要卖票,并且代售点的卖票工作实际还是交由火车站这个被代理实现,代售点只是实现一些麻烦且边角料的工作罢了,减少火车站的负担
*
* 2.为什么单独抽象一个调用方的接口InvocationHandler?我直接调用反射获取到的被代理方法method.invoke()不行吗?
* 这是每个动态生产的代理类对应的被代理类都不一样,因为是一一对应的关系,所以代理类的抽象方法逻辑也是会动态变化的
* 如果不把方法的逻辑进行抽象,就意味着每一种具体的代理的方法逻辑都需要单独写一个调用的方法,这样会非常的冗余
* 因为代理类的方法都是对被代理的方法进行增强,所以调用逻辑是一致的,只是具体的实现不一样,使用面向抽象编程就可以实现动态的切换具体的实现逻辑,这样就可以减少方法调用的冗余
*
*/
public class ProxyPoint extends Proxy implements SellTickets{



private static Method method;

static {
try {
method = Class.forName("com.czq.design.pattern.proxy.jdk_proxy_demo2.SellTickets").getMethod("sell");
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}


public ProxyPoint(InvocationHandler invocationHandler){
super(invocationHandler);

}


public void sell() {
try {
this.h.invoke(this,method,null);
} catch (Throwable throwable) {
throwable.printStackTrace();
}
}



}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
* 测试
*/
public class Client {

private static TrainStation trainStation = new TrainStation();

public static void main(String[] args) {

ProxyPoint proxyPoint = new ProxyPoint(new InvocationHandler() {
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("增强被代理类方法");
//method的就是被代理类接口的抽象方法,我们用被代理类去调用该接口的抽象方法,即抽象的卖票方法,因为被代理类是该接口的实现类
//实际上这里是一个多态的调用,左边是方法的逻辑接口,右边是被代理类的具体方法的实现逻辑
//目的就是调用原生的被代理类本来的方法逻辑,然后代理类在其前后进行方法的增强
method.invoke(trainStation,args);
return proxy;
}
});

proxyPoint.sell();
}
}

cglib实现动态代理

1
2
3
4
5
6
7
8
9
10
/**
* 火车站,这是被代理类,被代理类和代理都有买票这个功能,使用继承实现(jdk动态代理只能代理接口不能代理类)
* 代理类只是作为一个中介代理买票,提高卖票的效率
*/
public class TrainStation {

public void sell() {
System.out.println("火车站卖票");
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
/**
* jdk动态代理代理类代理的是接口(接口的实现类),而cglib动态代理类代理的是父类
* 思想是一致的,但实现的原理不一致,jdk动态代理是用反射实现的,cglib动态代理是ASM字节码技术生成代理类的
* 只不过一个是用接口实现,一个是用继承实现
*
* 具体的过程和jdk动态代理的实现是一致的
* 1.enhance对象就类似jdk动态代理的Proxy对象,setSuperClass设置被代理类和Proxy.newProxyInstance(Clazz...)方法设置代理类也是一致的,都是设置被代理的Class信息
* 2.MethodInterceptor接口和InvocationHandler接口是一致的,都是对方法逻辑的抽象抽取
*
*/
public class ProxyFactory{

//窜入被代理类对象
TrainStation trainStation = new TrainStation();

public TrainStation getProxyObject(){

Enhancer enhancer = new Enhancer();
//设置被代理类,即代理类的父类
enhancer.setSuperclass(TrainStation.class);
enhancer.setCallback(new MethodInterceptor() {
public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
System.out.println("cglib动态代理增强了方法");
method.invoke(trainStation,objects);
return o;
}
});

return (TrainStation) enhancer.create();
}


}
1
2
3
4
5
6
7
8
9
10
11
/**
* 测试
*/
public class Client {

public static void main(String[] args) {
ProxyFactory proxyFactory = new ProxyFactory();
TrainStation trainStation = proxyFactory.getProxyObject();
trainStation.sell();
}
}

代理模式间的对比

  • 动态代理和静态代理对比

    动态代理和静态代理是两种不同的代理模式,它们在代理的实现方式和使用场景上有所不同。

    1. 实现方式:

      • 静态代理:在编译阶段已经确定了要代理的目标对象,代理类需要手动编写,与目标对象实现相同的接口或继承相同的父类,并在代理类中调用目标对象的方法,并可以在方法前后添加额外的逻辑
      • 动态代理:在运行时动态生成代理类,不需要手动编写代理类。通过 Java 的反射机制,在程序运行时动态地创建代理对象,并关联到目标对象上。通常会用到 Java 提供的两个代理实现方式:JDK 动态代理和 CGLIB 动态代理
    2. 使用场景:

      • 静态代理:适用于只有少量目标对象需要代理,并且代理对象的行为相对固定的情况。可以在代理类中额外做一些前置或后置处理操作,如日志记录、性能统计等
      • 动态代理:适用于需要代理的目标对象很多,且代理对象的行为需要根据具体情况动态调整的情况。动态代理能够在运行期间根据需要生成相应的代理对象,并且能够在方法调用前后执行一些通用的操作,比如事务管理、权限控制等
    3. 性能:

      • 静态代理:由于代理类在编译阶段就确定了,所以在运行时性能较高
      • 动态代理:由于需要在运行时动态生成代理类,所以相比静态代理会稍微降低一些性能,但这种性能损耗在大多数情况下是可以接受的

    总的来说,静态代理是通过编写代理类来实现代理逻辑(需要手动编写非常麻烦,且编译时就确定,灵活性非常差),适用于目标对象较少、代理逻辑相对固定的情况;而动态代理是在运行时动态生成代理对象,适用于目标对象较多、代理逻辑需要根据实际情况动态调整的场景。**动态代理相对灵活(运行时自动生成,非常方便,也非常灵活)**,但性能稍低。根据具体的需求和场景选择合适的代理模式

    JDK动态代理和CGLIB代理对比

    JDK 动态代理和 CGLIB 动态代理是两种常用的动态代理实现方式。它们在实现原理、适用场景和性能方面有所不同。

    1. 实现原理:
    • JDK 动态代理:JDK 动态代理是通过 Java 标准库中的 java.lang.reflect.Proxy 类和 java.lang.reflect.InvocationHandler 接口实现的。在运行时动态生成的代理类实现了目标接口,并且代理类的方法调用被转发到InvocationHandler接口的实现类中处理。JDK 动态代理只能代理实现了接口的类
    • CGLIB 动态代理:CGLIB(Code Generation Library)是基于 ASM(Java 字节码操控框架)的代码生成库,通过生成目标类的子类来实现代理。在运行时动态生成的代理类继承了目标类,并且重写了目标类中的方法,在代理类中增加了额外的逻辑。CGLIB 动态代理可以代理没有实现接口的类
    1. 适用场景:
    • JDK 动态代理:适用于需要代理的目标对象实现了接口的情况。JDK 动态代理相对于 CGLIB 动态代理更加轻量级,生成的代理对象性能较好
    • CGLIB 动态代理:适用于需要代理的目标对象没有实现接口的情况。CGLIB 动态代理通过继承目标类来实现代理,因此不能代理 final 类和 final 方法
    1. 性能:
    • JDK 动态代理:由于 JDK 动态代理是基于接口实现的,只能代理接口方法,不会代理目标对象中的非接口方法。生成的代理类相对较小,所以在程序运行时的性能上相对较高
    • CGLIB 动态代理:由于 CGLIB 动态代理是通过生成目标类的子类来实现代理,涉及到继承和重写方法,性能相对于 JDK 动态代理略低。适用于对性能要求不是非常苛刻的场景

    综上所述,JDK 动态代理和 CGLIB 动态代理都是常见的动态代理实现方式。JDK 动态代理适用于接口代理的场景,性能相对较高;而 CGLIB 动态代理适用于类代理的场景,性能稍低。根据具体需求和场景选择合适的动态代理方式

总结

1.代理模式实现的两种方式

2.静态代理和动态代理对比

3.JDK动态代理和CGLIB动态代理对比以及他们的实现原理

4.代理模式的作用

A.代理模式在客户端与目标对象之间起到一个中介作用和保护目标对象的作用;

B.代理对象可以扩展目标对象的功能**(可以实现在不修改源代码的前提下对源码的功能进行拓展**);

C.代理模式能将客户端与目标对象分离,在一定程度上降低了系统的耦合度;

缺点:

增加了系统的复杂度;

适配器模式

概述

如果去欧洲国家去旅游的话,他们的插座如下图最左边,是欧洲标准。而我们使用的插头如下图最右边的。因此我们的笔记本电脑,手机在当地不能直接充电。所以就需要一个插座转换器,转换器第1面插入当地的插座,第2面供我们充电,这样使得我们的插头在当地能使用。生活中这样的例子很多,手机充电器(将220v转换为5v的电压),读卡器等,其实就是使用到了适配器模式

实质就是生活中的转接器,我自己的笔记本电脑只有usb接口,就使用网络接口的适配器,将网线接口适配成usb接口

转接头

定义:

将一个类的接口转换成客户希望的另外一个接口,使得原本由于接口不兼容而不能一起工作的那些类能一起工作

​ 适配器模式分为类适配器模式和对象适配器模式,前者类之间的耦合度比后者高,且要求程序员了解现有组件库中的相关组件的内部结构,所以应用相对较少些。

类适配器模式实现

实现方式:定义一个适配器类来实现当前系统的业务接口,同时又继承现有组件库中已经存在的组件。

【例】读卡器

现有一台电脑只能读取SD卡,而要读取TF卡中的内容的话就需要使用到适配器模式。创建一个读卡器,将TF卡中的内容读取出来。

类图如下:

对象适配器实现

对象适配器

1
2
3
4
5
6
7
//SD卡的接口
public interface SDCard {
//读取SD卡方法
String readSD();
//写入SD卡功能
void writeSD(String msg);
}
1
2
3
4
5
6
7
8
9
10
11
//SD卡实现类
public class SDCardImpl implements SDCard {
public String readSD() {
String msg = "sd card read a msg :hello word SD";
return msg;
}

public void writeSD(String msg) {
System.out.println("sd card write msg : " + msg);
}
}
1
2
3
4
5
6
7
//TF卡接口
public interface TFCard {
//读取TF卡方法
String readTF();
//写入TF卡功能
void writeTF(String msg);
}
1
2
3
4
5
6
7
8
9
10
11
12
//TF卡实现类
public class TFCardImpl implements TFCard {

public String readTF() {
String msg ="tf card read msg : hello word tf card";
return msg;
}

public void writeTF(String msg) {
System.out.println("tf card write a msg : " + msg);
}
}
1
2
3
4
5
6
7
8
9
10
11
//电脑类,负责读取sd卡中的数据
public class Computer {

public String readSD(SDCard sdCard) {
if(sdCard == null) {
throw new NullPointerException("sd card null");
}
return sdCard.readSD();
}

}

适配器类(继承方式实现)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
/**
* 适配器类(通过继承的方式实现适配者和被适配者类型的兼容)
*
* 适配器的作用:让本来不兼容的类型通过一个适配器(转接器)实现兼容的目的
* 举个经典的生活案例:我的笔记本只有usb接口,没有网线接口,即usb接口和网线接口不兼容
* 那么我想通过通过usb接口连接网线怎么办呢?引入usb接口适配器,让网线通过适配器可以连接到usb中
*
* 实现的核心思想:适配器要同时兼容适配和被适配类型并且可以实现转换(适配)
* 比如usb接口适配器要同时能够连接usb和网线,并且能实现他们之间的数据传输
*
* 转化成代码的核心实现思想:适配器类要同时兼容适配者和被适配者的类型,并实现他们之间的数据传输
* 这里的案例是SD适配器要同时兼容SD卡类型(通过实现接口的方式实现)和TF卡类型(通过继承的方式实现)
*
* 通过继承的方式实现适配器模式有一个缺点:
* 因为适配要兼容多种不同的类型,而通过继承只能标识一个类型,如果另一个类型不是用接口来标识而是用类来标识话
* 那么这种方式就实现不了适配,因为继承只能单继承
*
* 通过组合的方式实现适配器模式,就可以兼容多种不同的类型,因为一个类型是通过接口来标识,另一个类型是通过传入的接口实现类(对象)来标识
* 所以不存在单继承的限制
*/
public class SDAdapterTF extends TFCardImpl implements SDCard{

@Override
public String readSD() {
return this.readTF();
}

@Override
public void writeSD(String msg) {
this.writeTF(msg);
}
}

组合方式实现适配器模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* 组合的方式实现适配器模式
*/
public class SDAdapterTFTwo implements SDCard{

private TFCard tfCard;

public SDAdapterTFTwo(TFCard tfCard) {
this.tfCard = tfCard;
}

@Override
public String readSD() {
return tfCard.readTF();
}

@Override
public void writeSD(String msg) {
tfCard.writeTF(msg);
}
}

适配器模式的应用场景(作用)

A.以前开发的系统存在满足新系统功能需求的类,但其接口同新系统的接口不一致

B.使用第三方提供的组件,但组件接口定义和自己要求的接口定义不同

装饰者设计模式

定义:指在不改变现有对象结构的情况下,动态地给该对象增加一些职责(即增加其额外功能)的模式

快餐案例

快餐店有炒面、炒饭这些快餐,可以额外附加鸡蛋、火腿、培根这些配菜,当然加配菜需要额外加钱,每个配菜的价钱通常不太一样,那么计算总价就会显得比较麻烦

全部用继承实现装饰者设计模式

继承实现装饰者设计模式

核心问题:产生过多的冗余子类

组合实现装饰者设计模式

组合实现装饰者设计模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
/**
* 抽象快餐类
*
* 全部用继承的思想实现装饰器设计模式的弊端解析
*
* 全部用继承对功能进行拓展最大的问题就是继承是强耦合关系,是不能进行动态切换
* 比如这个快餐案例,如果全用继承进行拓展
* 那么炒面要加这两种配料培根和鸡蛋,炒饭也要加培根和鸡蛋,在这种情况下
* 因为培根和鸡蛋已经跟炒面绑定在一起了,要想炒饭也加培根和鸡蛋就得再次重新制作培根和鸡蛋这两种配料
*
* 举一个生活种的案例:比如平时吃面有很多配料如辣椒酱,番茄酱,芝麻等等
* 用继承来给面加配料就是,这碗面已经跟整个配料瓶绑定了,其他的面要想再加这些配料就得重复弄新的配料瓶,非常的冗余
* 核心就是继承会把父类和子类绑定起来作为一个整体工作,不能各自的独立工作,耦合度非常高,灵活性非常差,不能实现动态的切换
*
* 组合+抽象的思想实现装饰器设计模式
* 组合+抽象的思想就可以把二者分离开来,大大降低彼此的耦合度,可以实现动态的切换,非常的灵活
*/
//快餐接口
public abstract class FastFood {
private float price;
private String desc;

public FastFood() {
}

public FastFood(float price, String desc) {
this.price = price;
this.desc = desc;
}

public void setPrice(float price) {
this.price = price;
}

public float getPrice() {
return price;
}

public String getDesc() {
return desc;
}

public void setDesc(String desc) {
this.desc = desc;
}

//获取价格
public abstract float cost();

}
1
2
3
4
5
6
7
8
9
10
11
//炒饭快餐
public class FriedRice extends FastFood {

public FriedRice() {
super(10, "炒饭");
}

public float cost() {
return getPrice();
}
}
1
2
3
4
5
6
7
8
9
10
11
//炒面快餐
public class FriedNoodles extends FastFood {

public FriedNoodles() {
super(12, "炒面");
}

public float cost() {
return getPrice();
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
/**
*
* 抽象配料类
* 疑问:为什么抽象配料类已经传入了具体要装饰的构件快餐但是其本身还要继承快餐构件呢?
* 现阶段的理解:配料装饰完快餐,那其整体本质上也是一个快餐,所以必须继承快餐类
* 举一个生活的案例:炒饭加完鸡蛋,他还是一碗炒饭,作为一碗炒饭来出售的,不可能单独售卖鸡蛋配料的
*
* 装饰器的核心理解:装饰器装饰完构件,那么他还是一个构件,只是一个增强版的构件
* 装饰部分用组合来实现,保证装饰完其本身还是构件类型(用继承来实现)
*
* 在代码上实际的作用是:可以在不修改构建类的基础上利用装饰类拓展构建类的功能(在满足开闭原则的前提下实现功能的增强)
*/
public abstract class Garnish extends FastFood{


private FastFood fastFood;

public Garnish(FastFood fastFood){
this.fastFood = fastFood;
}

public void setFastFood(FastFood fastFood) {
this.fastFood = fastFood;
}

public FastFood getFastFood() {
return fastFood;
}

public Garnish(FastFood fastFood, float price, String desc) {
super(price,desc);
this.fastFood = fastFood;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* 鸡蛋配料类
*/
public class Egg extends Garnish{

public Egg(FastFood fastFood) {
super(fastFood,1,"鸡蛋配料");
}


@Override
public float cost() {
//配料的费用+快餐的费用
return getPrice()+getFastFood().getPrice();
}

@Override
public String getDesc() {
return getFastFood().getDesc()+super.getDesc();
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//培根配料类
public class Bacon extends Garnish{


public Bacon(FastFood fastFood) {
super(fastFood,2,"培根配料");
}

@Override
public float cost() {
return getPrice()+getFastFood().getPrice();
}

@Override
public String getDesc() {
return getFastFood().getDesc()+super.getDesc();
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* 测试类
*/
public class Client1 {

public static void main(String[] args) {
FastFood fastFood = new FriedRice();
System.out.println(fastFood.getDesc()+fastFood.getPrice());
//快餐加鸡蛋
fastFood = new Egg(fastFood);
System.out.println(fastFood.getDesc()+fastFood.cost());


}
}

装饰器的作用:指在不改变现有对象结构的情况下,动态地给该对象增加一些职责(即增加其额外功能)的模式

疑问:同一个层级的装饰器只能装饰一种配料嘛?不能装饰多种配料吗?

解答:同一层级肯定只能加一种配料,但可以层层累加配料

代理和装饰者设计模式的区别

相同点:二者都可以增强方法逻辑

不同点:代理模式是在拓展类内部直接new 的被拓展类对象,对被拓展类对象起到了很好的隐藏作用,而装饰者设计模式是通过参数传入被拓展类对象的,并没有很好的隐藏和保护被拓展类对象

待解决问题

这个教学案例很不好,需要自己真正理解装饰模式再重构这个案例,理解装饰者模式的核心是理解他的四个角色,抽象组件,具体组件,抽象装饰器,具体装饰器

抽象组件:就是被装饰者,为什么被装饰的类要设计成抽象的呢?因为要将装饰者和被装饰着解耦,如果不设计成抽象的,一旦被装饰想要变化,那么所有对应的被装饰者都要跟着变化,这就是强耦合带来的牵一发动全身效应。

核心就一句话:在不影响装饰者的前提下实现被装饰者的动态切换(抽象带来的松耦合)

具体组件:具体被装饰者

抽象装饰器:就是装饰者了,为什么装饰者也要被设计成抽象的?原因还是将装饰者和被装饰者解耦,如果不设计成抽象的,被装饰者就无法动态变化

核心就一句话:在不影响被装饰者的前提下是实现装饰者的动态切换(抽象带来的松耦合)

具体装饰器:具体装饰者

核心思想:面向抽象编程,将强耦合变成松耦合

相关链接

设计模式-装饰器模式 - 掘金 (juejin.cn)

桥接模式

定义将抽象与实现分离,使它们可以独立变化(支持多维度变化)。它是用组合关系代替继承关系来实现,从而降低了抽象和实现这两个可变维度的耦合度

因为继承关系是强耦合,不同维度之间是可以任意组合的,但如果使用继承来实现,因为继承是强耦合,会导致产生过多的冗余类

案例

桥接模式

【例】视频播放器

需要开发一个跨平台视频播放器,可以在不同操作系统平台(如Windows、Mac、Linux等)上播放多种格式的视频文件,常见的视频格式包括RMVB、AVI、WMV等。该播放器包含了两个维度,适合使用桥接模式

1
2
3
4
//视频文件
public interface VideoFile {
void decode(String fileName);
}
1
2
3
4
5
6
7
//rmvb文件
public class REVBBFile implements VideoFile {

public void decode(String fileName) {
System.out.println("rmvb文件:" + fileName);
}
}
1
2
3
4
5
6
//avi文件
public class AVIFile implements VideoFile {
public void decode(String fileName) {
System.out.println("avi视频文件:"+ fileName);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
//操作系统版本
//这里仅仅只是传入VideoFile这个维度和操作本身的维度进行组合,并没有让这二者在同一纬度上,所以并不属于同一纬度的增强,是属于多维度的组合进而实现增强

public abstract class OperatingSystemVersion {

protected VideoFile videoFile;

public OperatingSystemVersion(VideoFile videoFile) {
this.videoFile = videoFile;
}

public abstract void play(String fileName);
}
1
2
3
4
5
6
7
8
9
10
11
//mac版本
public class Mac extends OperatingSystemVersion {

public Mac(VideoFile videoFile) {
super(videoFile);
}

public void play(String fileName) {
videoFile.decode(fileName);
}
}
1
2
3
4
5
6
7
8
9
10
11
//Windows版本
public class Windows extends OperatingSystemVersion {

public Windows(VideoFile videoFile) {
super(videoFile);
}

public void play(String fileName) {
videoFile.decode(fileName);
}
}

精华:桥接模式和前面的三种结构型模式(代理模式,适配器模式,装饰者模式)都有所不同,前面的三种模式都是同一纬度的(同一类型的),强调同一纬度下的增强,所以前面的三种模式都有使用继承 or 实现接口保持同一维度(类型),而桥接模式是支持多维度的组合增强,并不是仅仅在原有维度的基础上进行增强

外观模式

感觉外观模式就是对外提供一个统一访问的接口,屏蔽接口内部实现细节,降低客户端和服务器端之间的耦合,客户端通过访问统一接口进而访问服务器端,服务器端的具体实现他是不需要知道的,同时客户端和服务器端二者也是独立的,互不影响的

比如炒股,你一个普通人不懂得炒股,但是你可以通过基金这个接口找到专业的人士帮你炒股,而专业人士如何炒股你是不需要知道的

外观模式的核心作用:有非常好的封装性,隐藏了内部的实现细节,保护内部代码不受侵害

源码分析

使用tomcat作为web容器时,接收浏览器发送过来的请求,tomcat会将请求信息封装成ServletRequest对象,如下图①处对象。但是大家想想ServletRequest是一个接口,它还有一个子接口HttpServletRequest,而我们知道该request对象肯定是一个HttpServletRequest对象的子实现类对象,到底是哪个类的对象呢?可以通过输出request对象,我们就会发现是一个名为RequestFacade的类的对象。

RequestFacade类就使用了外观模式。先看结构图:

为什么在此处使用外观模式呢?

​ 定义 RequestFacade 类,分别实现 ServletRequest ,同时定义私有成员变量 Request ,并且方法的实现调用 Request 的实现。然后,将 RequestFacade上转为 ServletRequest 传给 servlet 的 service 方法,这样即使在 servlet 中被下转为 RequestFacade ,也不能访问私有成员变量对象中的方法。既用了 Request ,又能防止其中方法被不合理的访问。

组合模式

概述

组合模式1

对于这个图片肯定会非常熟悉,上图我们可以看做是一个文件系统,对于这样的结构我们称之为树形结构。在树形结构中可以通过调用某个方法来遍历整个树,当我们找到某个叶子节点后,就可以对叶子节点进行相关的操作。可以将这颗树理解成一个大的容器,容器里面包含很多的成员对象,这些成员对象即可是容器对象也可以是叶子对象。但是由于容器对象和叶子对象在功能上面的区别,使得我们在使用的过程中必须要区分容器对象和叶子对象,但是这样就会给客户带来不必要的麻烦,作为客户而已,它始终希望能够一致的对待容器对象和叶子对象

定义:

​ 又名部分整体模式,是用于把一组相似的对象当作一个单一的对象。组合模式依据树形结构来组合对象,用来表示部分以及整体层次。这种类型的设计模式属于结构型模式,它创建了对象组的树形结构。

理解核心:将一组相似的对象当作一个对象进行一致性对待

案例实现

如下图,我们在访问别的一些管理系统时,经常可以看到类似的菜单。一个菜单可以包含菜单项(菜单项是指不再包含其他内容的菜单条目),也可以包含带有其他菜单项的菜单,因此使用组合模式描述菜单就很恰当,我们的需求是针对一个菜单,打印出其包含的所有菜单以及菜单项的名称

组合模式2

要实现该案例,我们先画出类图:

组合模式3

代码实现:

不管是菜单还是菜单项,都应该继承自统一的接口,这里姑且将这个统一的接口称为菜单组件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
//菜单组件  不管是菜单还是菜单项,都应该继承该类
public abstract class MenuComponent {

protected String name;
protected int level;

//添加菜单
public void add(MenuComponent menuComponent){
throw new UnsupportedOperationException();
}

//移除菜单
public void remove(MenuComponent menuComponent){
throw new UnsupportedOperationException();
}

//获取指定的子菜单
public MenuComponent getChild(int i){
throw new UnsupportedOperationException();
}

//获取菜单名称
public String getName(){
return name;
}

public void print(){
throw new UnsupportedOperationException();
}
}

这里的MenuComponent定义为抽象类,因为有一些共有的属性和行为要在该类中实现,Menu和MenuItem类就可以只覆盖自己感兴趣的方法,而不用搭理不需要或者不感兴趣的方法,举例来说,Menu类可以包含子菜单,因此需要覆盖add()、remove()、getChild()方法,但是MenuItem就不应该有这些方法。这里给出的默认实现是抛出异常,你也可以根据自己的需要改写默认实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
public class Menu extends MenuComponent {

private List<MenuComponent> menuComponentList;

public Menu(String name,int level){
this.level = level;
this.name = name;
menuComponentList = new ArrayList<MenuComponent>();
}

@Override
public void add(MenuComponent menuComponent) {
menuComponentList.add(menuComponent);
}

@Override
public void remove(MenuComponent menuComponent) {
menuComponentList.remove(menuComponent);
}

@Override
public MenuComponent getChild(int 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();
}
}
}

Menu类已经实现了除了getName方法的其他所有方法,因为Menu类具有添加菜单,移除菜单和获取子菜单的功能

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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);
}
}

MenuItem是菜单项,不能再有子菜单,所以添加菜单,移除菜单和获取子菜单的功能并不能实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public class Client {

public static void main(String[] args) {
//创建菜单树
MenuComponent menu1 = new Menu("菜单管理",2);
menu1.add(new MenuItem("页面访问",3));
menu1.add(new MenuItem("展开菜单",3));
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));

MenuComponent menu3 = new Menu("角色管理",2);
menu3.add(new MenuItem("页面访问",3));
menu3.add(new MenuItem("新增角色",3));
menu3.add(new MenuItem("修改角色",3));

//创建一级菜单
MenuComponent component = new Menu("系统管理",1);
//将二级菜单添加到一级菜单中
component.add(menu1);
component.add(menu2);
component.add(menu3);


//打印菜单名称(如果有子菜单一块打印)
component.print();
}
}

组合模式的核心:将相似的组件对象当作一致性对象来对待,巧妙的利用这一点将相似对象的操作(递归)转化成了类与类之间的关系,进一步避免了客户端对相似对象的复杂操作

利用案例解析组合模式的核心思想

如上面的菜单树案例,如果没有使用组合模式,我们需要使用方法的递归来实现树的构建和遍历,实质上递归也是一种一致性思想的体现,他将树枝节点和叶子节点统一看成是单一的对象来对待,只是递归的一致性思想是使用方法来实现的而组合模式将其一致性对待的方式是通过继承让其保持一致的类型,并且每种类型都有其自己相应的一致性方法,此时就不需要进行方法的递归了,因为每种类型的节点都有相应的一致性方法,只需要动态切换对应的节点对象即可调用相应的方法即可

组合模式的分类

透明组合模式,安全组合模式

  • 透明组合模式

    透明组合模式中,抽象根节点角色中声明了所有用于管理成员对象的方法,比如在示例中 MenuComponent 声明了 addremovegetChild 方法,这样做的好处是确保所有的构件类都有相同的接口。透明组合模式也是组合模式的标准形式

    透明组合模式的缺点是不够安全,因为叶子对象和容器对象在本质上是有区别的,叶子对象不可能有下一个层次的对象,即不可能包含成员对象,因此为其提供 add()、remove() 等方法是没有意义的,这在编译阶段不会出错,但在运行阶段如果调用这些方法可能会出错(如果没有提供相应的错误处理代码)

  • 安全组合模式

    在安全组合模式中,在抽象构件角色中没有声明任何用于管理成员对象的方法,而是在树枝节点 Menu 类中声明并实现这些方法。安全组合模式的缺点是不够透明,因为叶子构件和容器构件具有不同的方法,且容器构件中那些用于管理成员对象的方法没有在抽象构件类中定义,因此客户端不能完全针对抽象编程,必须有区别地对待叶子构件和容器构件

    组合模式-安全性

优点

  • **组合模式可以清楚地定义分层次的复杂对象,表示对象的全部或部分层次(使用类的关系来实现)**,它让客户端忽略了层次的差异,方便对整个层次结构进行控制。
  • 客户端可以一致地使用一个组合结构或其中单个对象,不必关心处理的是单个对象还是整个组合结构,简化了客户端代码。
  • 在组合模式中增加新的树枝节点和叶子节点都很方便,无须对现有类库进行任何修改,符合“开闭原则”。
  • 组合模式为树形结构的面向对象实现提供了一种灵活的解决方案,通过叶子节点和树枝节点的递归组合,可以形成复杂的树形结构,但对树形结构的控制却非常简单。

使用场景

组合模式正是应树形结构而生,所以组合模式的使用场景就是出现树形结构的地方。比如:文件目录显示,多级目录呈现等树形结构数据的操作

享元模式

定义:

​ 运用共享技术来有效地支持大量细粒度对象的复用,它通过共享已经存在的对象来大幅度减少需要创建的对象数量、避免大量相似对象的开销,从而提高系统资源的利用率。

享元模式的核心思想:复用共享资源,避免共享资源重复创建而导致的浪费

案例实现

【例】俄罗斯方块

下面的图片是众所周知的俄罗斯方块中的一个个方块,如果在俄罗斯方块这个游戏中,每个不同的方块都是一个实例对象,这些对象就要占用很多的内存空间,下面利用享元模式进行实现。

俄罗斯方块

先来看类图:

享元模式

代码如下:

俄罗斯方块有不同的形状,我们可以对这些形状向上抽取出AbstractBox,用来定义共性的属性和行为。

1
2
3
4
5
6
7
public abstract class AbstractBox {
public abstract String getShape();

public void display(String color) {
System.out.println("方块形状:" + this.getShape() + " 颜色:" + color);
}
}

接下来就是定义不同的形状了,IBox类、LBox类、OBox类等。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class IBox extends AbstractBox {

@Override
public String getShape() {
return "I";
}
}

public class LBox extends AbstractBox {

@Override
public String getShape() {
return "L";
}
}

public class OBox extends AbstractBox {

@Override
public String getShape() {
return "O";
}
}

提供了一个工厂类(BoxFactory),用来管理享元对象(也就是AbstractBox子类对象),该工厂类对象只需要一个,所以可以使用单例模式。并给工厂类提供一个获取形状的方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class BoxFactory {

private static HashMap<String, AbstractBox> map;

private BoxFactory() {
map = new HashMap<String, AbstractBox>();
AbstractBox iBox = new IBox();
AbstractBox lBox = new LBox();
AbstractBox oBox = new OBox();
map.put("I", iBox);
map.put("L", lBox);
map.put("O", oBox);
}

public static final BoxFactory getInstance() {
return SingletonHolder.INSTANCE;
}

private static class SingletonHolder {
private static final BoxFactory INSTANCE = new BoxFactory();
}

public AbstractBox getBox(String key) {
return map.get(key);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class Client {
public static void main(String[] args) {
//获取I图形对象
AbstractBox box1 = BoxFactory.getInstance().getBox("I");
box1.display("灰色");

//获取L图形对象
AbstractBox box2 = BoxFactory.getInstance().getBox("L");
box2.display("绿色");

//获取O图形对象
AbstractBox box3 = BoxFactory.getInstance().getBox("O");
box3.display("灰色");

//获取O图形对象
AbstractBox box4 = BoxFactory.getInstance().getBox("O");
box4.display("红色");

System.out.println("两次获取到的O图形对象是否是同一个对象:" + (box3 == box4));
}
}

享元模式和工厂模式的区别

享元模式共享的是大量相似 or 相同的对象资源,比如大量的共享单车(仅仅只是厂家不一样,颜色不一样…),支持修改外部状态且不影响内部状态

单例模式和工厂模式(创建型模式)的核心工作是将对象创建和使用相分离(解耦),并不是要复用对象资源,二者的应用场景完全不一样,当然单例模式和工厂模式可以结合享元模式使用,即实现了对象的创建和使用相分离,又实现了大量相似 or 相同的对象资源复用

享元模式是要先创建对象资源再复用对象资源的,在创建对象资源这部分情景,可以结合创建型模式的单例模式或者工厂模式

优缺点和使用场景

1,优点

  • 极大减少内存中相似或相同对象数量,节约系统资源,提供系统性能
  • 享元模式中的外部状态相对独立,且不影响内部状态

2,缺点:

为了使对象可以共享,需要将享元对象的部分状态外部化,分离内部状态和外部状态,使程序逻辑复杂

3,使用场景:

  • 一个系统有大量相同或者相似的对象,造成内存的大量耗费。
  • 对象的大部分状态都可以外部化,可以将这些外部状态传入对象中。
  • 在使用享元模式时需要维护一个存储享元对象的享元池,而这需要耗费一定的系统资源,因此,应当在需要多次重复使用享元对象时才值得使用享元模式。

维护大量的相似 or 相同的共享对象资源,这一现象又可共享资源池(享元池)

典型享元模式的案例有:jdk源码种的Integer常量池(-128-127),字符串常量池,数据库连接池..

行为模式

行为型模式用于描述程序在运行时复杂的流程控制(着重于业务逻辑),即描述多个类或对象之间怎样相互协作共同完成单个对象都无法单独完成的任务,它涉及算法与对象间职责的分配(如何将业务逻辑同类之间的分工合作 or 对象之间的分工合作完成复杂的业务逻辑)

行为型模式分为类行为模式和对象行为模式,前者采用继承机制来在类间分派行为,后者采用组合或聚合在对象间分配行为。由于组合关系或聚合关系比继承关系耦合度低,满足“合成复用原则”,所以对象行为模式比类行为模式具有更大的灵活性

行为型模式分为:

  • 模板方法模式
  • 策略模式
  • 命令模式
  • 职责链模式
  • 状态模式
  • 观察者模式
  • 中介者模式
  • 迭代器模式
  • 访问者模式
  • 备忘录模式
  • 解释器模式

以上 11 种行为型模式,除了模板方法模式和解释器模式是类行为型模式,其他的全部属于对象行为型模式

模板方法模式

概述

在面向对象程序设计过程中,程序员常常会遇到这种情况:设计一个系统时知道了算法所需的关键步骤,而且确定了这些步骤的执行顺序,但某些步骤的具体实现还未知,或者说某些步骤的实现与具体的环境相关

例如,去银行办理业务一般要经过以下4个流程:取号、排队、办理具体业务、对银行工作人员进行评分等,其中取号、排队和对银行工作人员进行评分的业务对每个客户是一样的,可以在父类中实现,但是办理具体业务却因人而异,它可能是存款、取款或者转账等,可以延迟到子类中实现。

定义(模板方法的核心理解)

定义一个操作中的算法骨架(算法骨架已经固定,不会随着环境的变化而变化),而将算法的一些步骤延迟到子类中(将会随着环境变化的具体执行步骤延迟到子类中),使得子类可以不改变该算法结构的情况下重定义该算法的某些特定步骤

案例实现

【例】炒菜

炒菜的步骤(算法骨架)是固定的,分为倒油、热油、倒蔬菜、倒调料品、翻炒等步骤。现通过模板方法模式来用代码模拟。类图如下:

模板方法模式

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
public abstract class AbstractClass {

public final void cookProcess() {
//第一步:倒油
this.pourOil();
//第二步:热油
this.heatOil();
//第三步:倒蔬菜
this.pourVegetable();
//第四步:倒调味料
this.pourSauce();
//第五步:翻炒
this.fry();
}

public void pourOil() {
System.out.println("倒油");
}

//第二步:热油是一样的,所以直接实现
public void heatOil() {
System.out.println("热油");
}

//第三步:倒蔬菜是不一样的(一个下包菜,一个是下菜心)
public abstract void pourVegetable();

//第四步:倒调味料是不一样
public abstract void pourSauce();


//第五步:翻炒是一样的,所以直接实现
public void fry(){
System.out.println("炒啊炒啊炒到熟啊");
}
}

public class ConcreteClass_BaoCai extends AbstractClass {

@Override
public void pourVegetable() {
System.out.println("下锅的蔬菜是包菜");
}

@Override
public void pourSauce() {
System.out.println("下锅的酱料是辣椒");
}
}

public class ConcreteClass_CaiXin extends AbstractClass {
@Override
public void pourVegetable() {
System.out.println("下锅的蔬菜是菜心");
}

@Override
public void pourSauce() {
System.out.println("下锅的酱料是蒜蓉");
}
}

public class Client {
public static void main(String[] args) {
//炒手撕包菜
ConcreteClass_BaoCai baoCai = new ConcreteClass_BaoCai();
baoCai.cookProcess();

//炒蒜蓉菜心
ConcreteClass_CaiXin caiXin = new ConcreteClass_CaiXin();
caiXin.cookProcess();
}
}

注意:为防止恶意操作,一般模板方法都加上 final 关键词。

优缺点

优点:

  • 提高代码复用性

    将相同部分的代码放在抽象的父类中,而将不同的代码放入不同的子类中。

  • 实现了反向控制

    **通过一个父类调用其子类的操作,通过对子类的具体实现扩展不同的行为,实现了反向控制 ,并符合“开闭原则(可以让算法的执行步骤根据具体的环境变化而变化)**”。

缺点:

  • 对每个不同的实现都需要定义一个子类,这会导致类的个数增加,系统更加庞大,设计也更加抽象。
  • 父类中的抽象方法由子类实现,子类执行的结果会影响父类的结果,这导致一种反向的控制结构,它提高了代码阅读的难度

适用场景

  • 算法的整体步骤很固定,但其中个别部分易变时,这时候可以使用模板方法模式,将容易变的部分抽象出来,供子类实现。
  • 需要通过子类来决定父类算法中某个步骤是否执行,实现子类对父类的反向控制。

策略模式

概述

先看下面的图片,我们去旅游选择出行模式有很多种,可以骑自行车、可以坐汽车、可以坐火车、可以坐飞机。

策略模式1

作为一个程序猿,开发需要选择一款开发工具,当然可以进行代码开发的工具有很多,可以选择Idea进行开发,也可以使用eclipse进行开发,也可以使用其他的一些开发工具。

策略模式2

定义:

该模式定义了一系列算法,并将每个算法封装起来,使它们可以相互替换,且算法的变化不会影响使用算法的客户。策略模式属于对象行为模式,它通过对算法进行封装,把使用算法的责任和算法的实现分割开来,并委派给不同的对象对这些算法进行管理

核心:同一种功能的使用有很多不同的算法策略去实现,让功能的使用和功能对应的算法实现相分离,让客户可以根据具体的业务场景选择对应的算法实现该功能

比如:

1.旅游出行方式,要使用的功能是旅游出现,可供选择算法策略是走路,骑自行车,汽车,飞机等

2.程序员要使用开发工具实现开发的功能,可供选择的开发工具有很多,有idea,eclipse,visio…

3.真实的业务场景,用户要使用支付的功能,可供选择支付算法有很多,微信支付,支付宝支付,银行卡支付,现金支付…

具体的代码实现是:面向抽象+组合动态选择该功能的算法实现

角色

抽象策略类,具体策略类,策略环境类

案例

【例】促销活动

一家百货公司在定年度的促销活动。针对不同的节日(春节、中秋节、圣诞节)推出不同的促销活动,由促销员将促销活动展示给客户。类图如下:

策略模式3

代码如下:

定义百货公司所有促销活动的共同接口

1
2
3
public interface Strategy {
void show();
}

定义具体策略角色(Concrete Strategy):每个节日具体的促销活动

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//为春节准备的促销活动A
public class StrategyA implements Strategy {

public void show() {
System.out.println("买一送一");
}
}

//为中秋准备的促销活动B
public class StrategyB implements Strategy {

public void show() {
System.out.println("满200元减50元");
}
}

//为圣诞准备的促销活动C
public class StrategyC implements Strategy {

public void show() {
System.out.println("满1000元加一元换购任意200元以下商品");
}
}

定义环境角色(Context):用于连接上下文,即把促销活动推销给客户,这里可以理解为销售员

1
2
3
4
5
6
7
8
9
10
11
12
13
public class SalesMan {                        
//持有抽象策略角色的引用
private Strategy strategy;

public SalesMan(Strategy strategy) {
this.strategy = strategy;
}

//向客户展示促销活动
public void salesManShow(){
strategy.show();
}
}

关于策略设计模式理解犯的两个错误(以上的策略模式案例尚存在两个问题)

1.将具体的策略和环境对象耦合起来了,实质上环境只是存储和获取策略的容器,环境对象不应该和策略产生强耦合关系,比如具体的策略对象直接跟环境对象耦合起来,弱耦合关系才对,环境对象提供好注册和获取具体的方法即可

2.并没有完全做到对修改关闭的原则,因为如果添加新策略,即使策略接口本身不需要改变,但你需要手动在环境里中添加,还是修改了环境类的代码,本质修改还是没有关闭,只是从策略类中转移到了环境类中

​ 核心的点在于添加策略时,为什么是由环境方来添加呢?为什么不是由策略本身来自己实现策略的添加呢?如果是前者的话就需要改动原来的代码,违反修改关闭的原则,如果是后者的话就不会修改原有的代码了

​ 所以我们现在的目标是让新的策略自己实现自动注册,而无需环境方再手动帮他注册,那具体怎么实现呢?
任何一个类都是以对象的形式发挥作用的,所以我们只需要在起创建的对象的时候调用注册方法即可,
具体的做法是在其构造函数中注册即可,这样就把创新对象和注册两个功能捆绑在了一起,只要其创建对象就自然而然会将策略对象注册到容器中

关于策略模式的两个深刻理解

1.因为多态的原理是根据具体的实现类去动态绑定具体的实现方法,进而根据具体的环境选择具体的实现类执行具体的实现方法,但往往很多业务有业务的区别标识,而不是以类的类型为区分标识。所以要在环境类中额外建立这二者的映射关系,一般是使用map集合来建立

2.接口中的默认方法也能被实现类继承过来,那为什么我们需要额外定义一个抽象类来实现自动注册的功能呢?为什么不能直接用接口实现呢?把自动注册的功能放在接口上也是可以实现自动注册的功能的呀?

​ 因为策略接口是暴露给外界调用的,如果你把注册功能也写在接口上,等于也暴露出去了。虽然功能上都可以实现自动注册,
但破坏了注册功能的封装性,没有实现屏蔽内部实现细节的作用,可以让调用者清晰的看见你的注册接口,但是注册功能是自动注册,
没有暴露给外界的,封装性更好也可以给接口提供更好的保护作用,也可以降低使用者的使用成本

​ 最顶层(基类中的顶层)的类型才是暴露给外界使用的,第二点让我真正体会到了封装性

改进后的案例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* 策略接口
*/
public interface Strategy {
void show();
}

/**
* 抽象策略模式,实现策略的自动注册
*/
public abstract class AbstractArticleStrategy implements ArticleStrategy{

public void register(Byte type){
ArticleStrategyContext.register(type,this);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
//为春节准备的促销活动A
public class StrategyA extends AbstractStrategy implements Strategy {

public StrategyA(){
register("A");
}

public void show() {
System.out.println("买一送一");
}
}

//为中秋准备的促销活动B
public class StrategyB extends AbstractStrategy implements Strategy {


public StrategyB(){
register("B");
}

public void show() {
System.out.println("满200元减50元");
}
}

//为圣诞准备的促销活动C
public class StrategyC extends AbstractStrategy implements Strategy {

public StrategyC(){
register("C");
}

public void show() {
System.out.println("满1000元加一元换购任意200元以下商品");
}
}

定义环境角色(Context):用于连接上下文,即把促销活动推销给客户,这里可以理解为销售员

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* 策略环境
*/
public class SalesMan {

private static Map<String,Strategy> map = new HashMap<>();

public static void register(String type,Strategy strategy){
map.putIfAbsent(type,strategy);
}

public static Strategy getStrategy(String type){
return map.get(type);
}
}

优缺点

1,优点:

  • 策略类之间可以自由切换

    由于策略类都实现同一个接口,所以使它们之间可以自由切换。

  • 易于扩展

    增加一个新的策略只需要添加一个具体的策略类即可,基本不需要改变原有的代码,符合“开闭原则“

  • 避免使用多重条件选择语句(if else),充分体现面向对象设计思想

    意思是可以替换同一种功能的不同实现的多种if-else语法,因为策略模式本身就是面向同一种功能的使用选择不同的策略(算法)实现

2,缺点:

  • 客户端必须知道所有的策略类,并自行决定使用哪一个策略类。
  • 策略模式将造成产生很多策略类,可以通过使用享元模式在一定程度上减少对象的数量。

使用场景

  • 一个系统需要动态地在几种算法中选择一种时,可将每个算法封装到策略类中
  • 一个类定义了多种行为,并且这些行为在这个类的操作中以多个条件语句的形式出现,可将每个条件分支移入它们各自的策略类中以代替这些条件语句
  • 系统中各算法彼此完全独立,且要求对客户隐藏具体算法的实现细节时
  • 系统要求使用算法的客户不应该知道其操作的数据时,可使用策略模式来隐藏与算法相关的数据结构
  • 多个类只区别在表现行为不同,可以使用策略模式,在运行时动态选择具体要执行的行为

策略模式在实际的业务场景用的非常多,因为很多业务场景都是同一种功能的使用有种很多不同的策略实现,要支持动态的切换,比如支付场景,评论场景(区分是否是会员)….太多了

命令模式

定义:

将一个请求封装为一个对象,**使发出请求的责任和执行请求的责任分割开(将命令的发出和命令的执行进行解耦)**。这样两者之间通过命令对象进行沟通,这样方便将命令对象进行存储、传递、调用、增加与管理。

案例实现

案例场景:实现服务员下订单的命令和厨师执行订单命令的解耦合,让服务员可以只关心命令的下达而无需关心的命令的接收者是谁,命令具体怎么执行

命令模式

这个类图画的不好,Client类只是测试类,没有任何意义,不应该画上来,简单解析一下该类图

Waitor:服务员类,负责下达命令,list集合代表服务员可以下达多个命令

Command:抽象命令类,负责根据具体的业务场景切换具体的命令

OrderCommand:具体的订单下达命令,命令下达时需要根据实际情况指定具体的命令接收者和命令具体的执行内容,所以需要传入接收者,和命令的内容(订单)

SeniorChef:厨师类,命令的接收者

Order:订单类,命令执行的具体内容

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
public interface Command {
void execute();//只需要定义一个统一的执行方法
}

public class OrderCommand implements Command {

//持有接受者对象
private SeniorChef receiver;
private Order order;

public OrderCommand(SeniorChef receiver, Order order){
this.receiver = receiver;
this.order = order;
}

public void execute() {
System.out.println(order.getDiningTable() + "桌的订单:");
Set<String> keys = order.getFoodDic().keySet();
for (String key : keys) {
receiver.makeFood(order.getFoodDic().get(key),key);
}

try {
Thread.sleep(100);//停顿一下 模拟做饭的过程
} catch (InterruptedException e) {
e.printStackTrace();
}


System.out.println(order.getDiningTable() + "桌的饭弄好了");
}
}

public class Order {
// 餐桌号码
private int diningTable;

// 用来存储餐名并记录份数
private Map<String, Integer> foodDic = new HashMap<String, Integer>();

public int getDiningTable() {
return diningTable;
}

public void setDiningTable(int diningTable) {
this.diningTable = diningTable;
}

public Map<String, Integer> getFoodDic() {
return foodDic;
}

public void setFoodDic(String name, int num) {
foodDic.put(name,num);
}
}

// 资深大厨类 是命令的Receiver
public class SeniorChef {

public void makeFood(int num,String foodName) {
System.out.println(num + "份" + foodName);
}
}

public class Waitor {

private ArrayList<Command> commands;//可以持有很多的命令对象

public Waitor() {
commands = new ArrayList();
}

public void setCommand(Command cmd){
commands.add(cmd);
}

// 发出命令 喊 订单来了,厨师开始执行
public void orderUp() {
System.out.println("美女服务员:叮咚,大厨,新订单来了.......");
for (int i = 0; i < commands.size(); i++) {
Command cmd = commands.get(i);
if (cmd != null) {
cmd.execute();
}
}
}
}

public class Client {
public static void main(String[] args) {
//创建2个order
Order order1 = new Order();
order1.setDiningTable(1);
order1.getFoodDic().put("西红柿鸡蛋面",1);
order1.getFoodDic().put("小杯可乐",2);

Order order2 = new Order();
order2.setDiningTable(3);
order2.getFoodDic().put("尖椒肉丝盖饭",1);
order2.getFoodDic().put("小杯雪碧",1);

//创建接收者
SeniorChef receiver=new SeniorChef();
//将订单和接收者封装成命令对象
OrderCommand cmd1 = new OrderCommand(receiver, order1);
OrderCommand cmd2 = new OrderCommand(receiver, order2);
//创建调用者 waitor
Waitor invoker = new Waitor();
invoker.setCommand(cmd1);
invoker.setCommand(cmd2);

//将订单带到柜台 并向厨师喊 订单来了
invoker.orderUp();
}
}

优缺点

1,优点:

  • 降低系统的耦合度。**命令模式能将调用操作的对象与实现该操作的对象解耦(命令的发出者和命令的执行者解耦)**。
  • **增加或删除命令非常方便(因为对命令进行了抽象,所以可以非常灵活的拓展)**。采用命令模式增加与删除命令不会影响其他类,它满足“开闭原则”,对扩展比较灵活。
  • 可以实现宏命令。命令模式可以与组合模式结合,将多个命令装配成一个组合命令,即宏命令。
  • 方便实现 Undo 和 Redo 操作。命令模式可以与后面介绍的备忘录模式结合,实现命令的撤销与恢复。

2,缺点:

  • 使用命令模式可能会导致某些系统有过多的具体命令类
  • 系统结构更加复杂。

使用场景

  • 系统需要将请求调用者和请求接收者解耦(命令的发出者和命令的执行者解耦),使得调用者和接收者不直接交互
  • 系统需要在不同的时间指定请求、将请求排队和执行请求。
  • 系统需要支持命令的撤销(Undo)操作和恢复(Redo)操作。

命令模式的核心就是将命令抽象起来,让命令的发送者只关心命令发送即可,而命令的接收者和命令具体怎么执行可以根据实际情况进行动态的切换,命令的发送者无需关心这部分内容,实际上就是一句话,将命令的发送和命令的执行解耦合,提高二者的独立性

典型的消息发布订阅模式就是采用这种设计模式的,消息的发送者只需要发送消息即可不用关心消息具体是由谁来接受,是由谁来执行,消息接收者可以根据需求自己选择需要接受的内容(错误的理解)

消息的发布订阅模式是用观察者模式实现,首先消息的发布订阅机制是一个一对多的依赖关系,而命令模式是一个一对一的依赖关系,每个命令都有唯一的接收者,只能接收一次,不能群体接收该命令并执行,其次消息的发布订阅模式是需要知道谁订阅了我们的消息进而实现消息传递的,而命令模式将命令的发送和命令的执行解耦了,是不知道谁执行了命令的,所以消息的发布订阅机制不是用命令模式实现的

造成错误理解的根本原因是对消息的发布订阅机制不熟悉

jDK源码解析

Runable是一个典型命令模式,Runnable担当命令的角色,Thread充当的是调用者,start方法就是其执行方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
//命令接口(抽象命令角色)
public interface Runnable {
public abstract void run();
}

//调用者
public class Thread implements Runnable {
private Runnable target;

public synchronized void start() {
if (threadStatus != 0)
throw new IllegalThreadStateException();

group.add(this);

boolean started = false;
try {
start0();
started = true;
} finally {
try {
if (!started) {
group.threadStartFailed(this);
}
} catch (Throwable ignore) {
}
}
}

private native void start0();
}

会调用一个native方法start0(),调用系统方法,开启一个线程。而接收者是对程序员开放的,可以自己定义接收者。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* jdk Runnable 命令模式
* TurnOffThread : 属于具体
*/
public class TurnOffThread implements Runnable{
private Receiver receiver;

public TurnOffThread(Receiver receiver) {
this.receiver = receiver;
}
public void run() {
receiver.turnOFF();
}
}
1
2
3
4
5
6
7
8
9
10
11
/**
* 测试类
*/
public class Demo {
public static void main(String[] args) {
Receiver receiver = new Receiver();
TurnOffThread turnOffThread = new TurnOffThread(receiver);
Thread thread = new Thread(turnOffThread);
thread.start();
}
}

职责链模式

概述

在现实生活中,常常会出现这样的事例:一个请求有多个对象可以处理,但每个对象的处理条件或权限不同。例如,公司员工请假,可批假的领导有部门负责人、副总经理、总经理等,但每个领导能批准的天数不同,员工必须根据自己要请假的天数去找不同的领导签名,也就是说员工必须记住每个领导的姓名、电话和地址等信息,这增加了难度。这样的例子还有很多,如找领导出差报销、生活中的“击鼓传花”游戏等

定义:

又名职责链模式,为了避免请求发送者与多个请求处理者耦合在一起,将所有请求的处理者通过前一对象记住其下一个对象的引用而连成一条链;当有请求发生时,可将请求沿着这条链传递,直到有对象处理它为止

职责链的核心理解:

1.将请求发送与多个请求处理者进行解耦,降低了二者的耦合,提高了二者的独立性,最明显的好处是请求发送者的业务逻辑会简单很多,因为他只需要把请求给链头对象即可

2.其次因为职责链上的对象符合单一职责原则,通俗点来说每个人只干一种活,然后彼此分工合作,降低了职责之间的耦合度,提高了每个职责的可扩展性,可维护性,进一步来说就是提高了系统的可扩展性,可维护性

案例

现需要开发一个请假流程控制系统。请假一天以下的假只需要小组长同意即可;请假1天到3天的假还需要部门经理同意;请求3天到7天还需要总经理同意才行。

类图如下:

责任链模式

类图解析:

Client是测试类,不需要理会

LeaveRequest:请假请求

Handler:抽象处理者类,负责抽取处理者的共性如处理属性,处理方法(处理方法必须是抽象的,因为每个处理者的职责不一样,对应的处理方案也不一样),然后还需要有一个传递请求给一个职责对象的方法

GroupLeader,Manager,GeneraManager:具体处理器类

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
//请假条
public class LeaveRequest {
private String name;//姓名
private int num;//请假天数
private String content;//请假内容

public LeaveRequest(String name, int num, String content) {
this.name = name;
this.num = num;
this.content = content;
}

public String getName() {
return name;
}

public int getNum() {
return num;
}

public String getContent() {
return content;
}
}

//处理者抽象类
public abstract class Handler {
//以下五个属性都是因为本案例要解决的问题只有请假,所以向上抽取请假的相关进行复用
//真正的职责链模式应该职责链上的每一个类都负责一个功能职责,不会产生这么大的共性的,所以本案例也可以使用策略模式实现
protected final static int NUM_ONE = 1;
protected final static int NUM_THREE = 3;
protected final static int NUM_SEVEN = 7;

//该领导处理的请假天数区间
private int numStart;
private int numEnd;

//领导上面还有领导(每个处理器处理请假条的职责功能,因为每个处理器处理的方式不一样,将其抽象起来)
private Handler nextHandler;

//设置请假天数范围 上不封顶
public Handler(int numStart) {
this.numStart = numStart;
}

//设置请假天数范围
public Handler(int numStart, int numEnd) {
this.numStart = numStart;
this.numEnd = numEnd;
}

//设置上级领导
public void setNextHandler(Handler nextHandler){
this.nextHandler = nextHandler;
}

//提交请假条(判断当下的处理器是否处理成功,如果没有处理成功则将需要处理的任务传递下去)
public final void submit(LeaveRequest leave){
if(0 == this.numStart){
return;
}

//如果请假天数达到该领导者的处理要求
if(leave.getNum() >= this.numStart){
this.handleLeave(leave);

//如果还有上级 并且请假天数超过了当前领导的处理范围
if(null != this.nextHandler && leave.getNum() > numEnd){
this.nextHandler.submit(leave);//继续提交
} else {
System.out.println("流程结束");
}
}
}

//各级领导处理请假条方法
protected abstract void handleLeave(LeaveRequest leave);
}

//小组长
public class GroupLeader extends Handler {
public GroupLeader() {
//小组长处理1-3天的请假
super(Handler.NUM_ONE, Handler.NUM_THREE);
}

@Override
protected void handleLeave(LeaveRequest leave) {
System.out.println(leave.getName() + "请假" + leave.getNum() + "天," + leave.getContent() + "。");
System.out.println("小组长审批:同意。");
}
}

//部门经理
public class Manager extends Handler {
public Manager() {
//部门经理处理3-7天的请假
super(Handler.NUM_THREE, Handler.NUM_SEVEN);
}

@Override
protected void handleLeave(LeaveRequest leave) {
System.out.println(leave.getName() + "请假" + leave.getNum() + "天," + leave.getContent() + "。");
System.out.println("部门经理审批:同意。");
}
}

//总经理
public class GeneralManager extends Handler {
public GeneralManager() {
//部门经理处理7天以上的请假
super(Handler.NUM_SEVEN);
}

@Override
protected void handleLeave(LeaveRequest leave) {
System.out.println(leave.getName() + "请假" + leave.getNum() + "天," + leave.getContent() + "。");
System.out.println("总经理审批:同意。");
}
}

//测试类
public class Client {
public static void main(String[] args) {
//请假条来一张
LeaveRequest leave = new LeaveRequest("小花",5,"身体不适");

//各位领导
GroupLeader groupLeader = new GroupLeader();
Manager manager = new Manager();
GeneralManager generalManager = new GeneralManager();

//之所以在这里设置上级领导,是因为可以根据实际需求来更改设置,如果实战中上级领导人都是固定的,则可以移到领导实现类中
groupLeader.setNextHandler(manager);//小组长的领导是部门经理
manager.setNextHandler(generalManager);//部门经理的领导是总经理


//提交申请
groupLeader.submit(leave);
}
}

优缺点

1,优点:

  • 降低了对象之间的耦合度

    该模式降低了请求发送者和接收者的耦合度(请求发送者只需要把请求发送给链头对象即可)

  • 增强了系统的可扩展性

    可以根据需要增加新的请求处理类,满足开闭原则(有新的职责链只需要直接加链尾即可,无需变动原有代码)

  • 增强了给对象指派职责的灵活性

    当工作流程发生变化,可以动态地改变链内的成员或者修改它们的次序,也可动态地新增或者删除责任。

  • 责任链简化了对象之间的连接

    **一个对象只需保持一个指向其后继者的引用,不需保持其他所有处理者的引用,这避免了使用众多的 if 或者 if···else 语句(一个处理者只需要和其后一个处理者进行交互,而不用和其他的处理者进行交互)**。

  • 责任分担

    每个类只需要处理自己该处理的工作,不能处理的传递给下一个对象完成,明确各类的责任范围,符合类的单一职责原则

2,缺点:

  • 不能保证每个请求一定被处理。由于一个请求没有明确的接收者,所以不能保证它一定会被处理,该请求可能一直传到链的末端都得不到处理
  • 对比较长的职责链,请求的处理可能涉及多个处理对象,系统性能将受到一定影响
  • 职责链建立的合理性要靠客户端来保证,增加了客户端的复杂性,可能会由于职责链的错误设置而导致系统出错,如可能会造成循环调用

源码解析

在javaWeb应用开发中,FilterChain是职责链(过滤器)模式的典型应用,以下是Filter的模拟实现分析:

  • 模拟web请求Request以及web响应Response

    1
    2
    3
    4
    5
    6
    7
    public interface Request{

    }

    public interface Response{

    }
  • 模拟web过滤器Filter

    1
    2
    3
    public interface Filter {
    public void doFilter(Request req,Response res,FilterChain c);
    }
  • 模拟实现具体过滤器

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    public class FirstFilter implements Filter {
    @Override
    public void doFilter(Request request, Response response, FilterChain chain) {

    System.out.println("过滤器1 前置处理");

    // 先执行所有request再倒序执行所有response
    chain.doFilter(request, response);

    System.out.println("过滤器1 后置处理");
    }
    }

    public class SecondFilter implements Filter {
    @Override
    public void doFilter(Request request, Response response, FilterChain chain) {

    System.out.println("过滤器2 前置处理");

    // 先执行所有request再倒序执行所有response
    chain.doFilter(request, response);

    System.out.println("过滤器2 后置处理");
    }
    }
  • 模拟实现过滤器链FilterChain

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    public class FilterChain {

    private List<Filter> filters = new ArrayList<Filter>();

    private int index = 0;

    // 链式调用
    public FilterChain addFilter(Filter filter) {
    this.filters.add(filter);
    return this;
    }

    public void doFilter(Request request, Response response) {
    if (index == filters.size()) {
    return;
    }
    Filter filter = filters.get(index);
    index++;
    filter.doFilter(request, response, this);
    }
    }
  • 测试类

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    public class Client {
    public static void main(String[] args) {
    Request req = null;
    Response res = null ;

    FilterChain filterChain = new FilterChain();
    filterChain.addFilter(new FirstFilter()).addFilter(new SecondFilter());
    filterChain.doFilter(req,res);
    }
    }

    可以打断点感受真实的过滤器的执行过程,并从中感受职责链设计模式

    和上面标准的职责链模式不一样的是,过滤器链抽取了一个过滤器链对象来维护职责链,并没有把维护过滤器链的任务耦合到每一个指责类上

状态模式

概述

【例】通过按钮来控制一个电梯的状态,一个电梯有开门状态,关门状态,停止状态,运行状态。每一种状态改变,都有可能要根据其他状态来更新处理。例如,如果电梯门现在处于运行时状态,就不能进行开门操作,而如果电梯门是停止状态,就可以执行开门操作。

类图如下:

状态模式前

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
public interface ILift {
//电梯的4个状态
//开门状态
public final static int OPENING_STATE = 1;
//关门状态
public final static int CLOSING_STATE = 2;
//运行状态
public final static int RUNNING_STATE = 3;
//停止状态
public final static int STOPPING_STATE = 4;

//设置电梯的状态
public void setState(int state);

//电梯的动作
public void open();
public void close();
public void run();
public void stop();
}

public class Lift implements ILift {
private int state;

@Override
public void setState(int state) {
this.state = state;
}

//执行关门动作
@Override
public void close() {
switch (this.state) {
case OPENING_STATE:
System.out.println("电梯关门了。。。");//只有开门状态可以关闭电梯门,可以对应电梯状态表来看
this.setState(CLOSING_STATE);//关门之后电梯就是关闭状态了
break;
case CLOSING_STATE:
//do nothing //已经是关门状态,不能关门
break;
case RUNNING_STATE:
//do nothing //运行时电梯门是关着的,不能关门
break;
case STOPPING_STATE:
//do nothing //停止时电梯也是关着的,不能关门
break;
}
}

//执行开门动作
@Override
public void open() {
switch (this.state) {
case OPENING_STATE://门已经开了,不能再开门了
//do nothing
break;
case CLOSING_STATE://关门状态,门打开:
System.out.println("电梯门打开了。。。");
this.setState(OPENING_STATE);
break;
case RUNNING_STATE:
//do nothing 运行时电梯不能开门
break;
case STOPPING_STATE:
System.out.println("电梯门开了。。。");//电梯停了,可以开门了
this.setState(OPENING_STATE);
break;
}
}

//执行运行动作
@Override
public void run() {
switch (this.state) {
case OPENING_STATE://电梯不能开着门就走
//do nothing
break;
case CLOSING_STATE://门关了,可以运行了
System.out.println("电梯开始运行了。。。");
this.setState(RUNNING_STATE);//现在是运行状态
break;
case RUNNING_STATE:
//do nothing 已经是运行状态了
break;
case STOPPING_STATE:
System.out.println("电梯开始运行了。。。");
this.setState(RUNNING_STATE);
break;
}
}

//执行停止动作
@Override
public void stop() {
switch (this.state) {
case OPENING_STATE: //开门的电梯已经是是停止的了(正常情况下)
//do nothing
break;
case CLOSING_STATE://关门时才可以停止
System.out.println("电梯停止了。。。");
this.setState(STOPPING_STATE);
break;
case RUNNING_STATE://运行时当然可以停止了
System.out.println("电梯停止了。。。");
this.setState(STOPPING_STATE);
break;
case STOPPING_STATE:
//do nothing
break;
}
}
}

public class Client {
public static void main(String[] args) {
Lift lift = new Lift();
lift.setState(ILift.STOPPING_STATE);//电梯是停止的
lift.open();//开门
lift.close();//关门
lift.run();//运行
lift.stop();//停止
}
}

问题分析:

  • 使用了大量的switch…case这样的判断(if…else也是一样),使程序的可阅读性变差。
  • 扩展性很差。如果新加了断电的状态,我们需要修改上面判断逻辑

定义:

对有状态的对象,把复杂的“判断逻辑”提取到不同的状态对象中,允许状态对象在其内部状态发生改变时改变其行为

1
2
3
4
/**
* 没用设计模式时,电梯的状态是用业务逻辑来进行控制的,用了状态设计模式,电梯的状态是用类与类的关系来实现和控制的
* 一旦将业务逻辑转化成类与类的设计,那么该业务逻辑的可扩展性和可维护性就非常强了,因为类支持继续,实现,组合,抽象各种维度进扩展和维护
*/

角色

抽象状态类,具体状态类,状态环境类

案例实现

对上述电梯的案例使用状态模式进行改进。类图如下:

状态模式

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
//抽象状态类
public abstract class LiftState {
//定义一个环境角色,也就是封装状态的变化引起的功能变化
protected Context context;

public void setContext(Context context) {
this.context = context;
}

//电梯开门动作
public abstract void open();

//电梯关门动作
public abstract void close();

//电梯运行动作
public abstract void run();

//电梯停止动作
public abstract void stop();
}

//开启状态
public class OpenningState extends LiftState {

//开启当然可以关闭了,我就想测试一下电梯门开关功能
@Override
public void open() {
System.out.println("电梯门开启...");
}

@Override
public void close() {
//状态修改
super.context.setLiftState(Context.closeingState);
//动作委托为CloseState来执行,也就是委托给了ClosingState子类执行这个动作
super.context.getLiftState().close();
}

//电梯门不能开着就跑,这里什么也不做
@Override
public void run() {
//do nothing
}

//开门状态已经是停止的了
@Override
public void stop() {
//do nothing
}
}

//运行状态
public class RunningState extends LiftState {

//运行的时候开电梯门?你疯了!电梯不会给你开的
@Override
public void open() {
//do nothing
}

//电梯门关闭?这是肯定了
@Override
public void close() {//虽然可以关门,但这个动作不归我执行
//do nothing
}

//这是在运行状态下要实现的方法
@Override
public void run() {
System.out.println("电梯正在运行...");
}

//这个事绝对是合理的,光运行不停止还有谁敢做这个电梯?!估计只有上帝了
@Override
public void stop() {
super.context.setLiftState(Context.stoppingState);
super.context.stop();
}
}

//停止状态
public class StoppingState extends LiftState {

//停止状态,开门,那是要的!
@Override
public void open() {
//状态修改
super.context.setLiftState(Context.openningState);
//动作委托为CloseState来执行,也就是委托给了ClosingState子类执行这个动作
super.context.getLiftState().open();
}

@Override
public void close() {//虽然可以关门,但这个动作不归我执行
//状态修改
super.context.setLiftState(Context.closeingState);
//动作委托为CloseState来执行,也就是委托给了ClosingState子类执行这个动作
super.context.getLiftState().close();
}

//停止状态再跑起来,正常的很
@Override
public void run() {
//状态修改
super.context.setLiftState(Context.runningState);
//动作委托为CloseState来执行,也就是委托给了ClosingState子类执行这个动作
super.context.getLiftState().run();
}

//停止状态是怎么发生的呢?当然是停止方法执行了
@Override
public void stop() {
System.out.println("电梯停止了...");
}
}

//关闭状态
public class ClosingState extends LiftState {

@Override
//电梯门关闭,这是关闭状态要实现的动作
public void close() {
System.out.println("电梯门关闭...");
}

//电梯门关了再打开,逗你玩呢,那这个允许呀
@Override
public void open() {
super.context.setLiftState(Context.openningState);
super.context.open();
}


//电梯门关了就跑,这是再正常不过了
@Override
public void run() {
super.context.setLiftState(Context.runningState);
super.context.run();
}

//电梯门关着,我就不按楼层
@Override
public void stop() {
super.context.setLiftState(Context.stoppingState);
super.context.stop();
}
}

//环境角色
public class Context {
//定义出所有的电梯状态
public final static OpenningState openningState = new OpenningState();//开门状态,这时候电梯只能关闭
public final static ClosingState closeingState = new ClosingState();//关闭状态,这时候电梯可以运行、停止和开门
public final static RunningState runningState = new RunningState();//运行状态,这时候电梯只能停止
public final static StoppingState stoppingState = new StoppingState();//停止状态,这时候电梯可以开门、运行


//定义一个当前电梯状态
private LiftState liftState;

public LiftState getLiftState() {
return this.liftState;
}

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 class Client {
public static void main(String[] args) {
Context context = new Context();
context.setLiftState(new ClosingState());

context.open();
context.close();
context.run();
context.stop();
}
}

案例缺点:

1.其实一种状态只是对应一种行为,上面的案例一种状态却写了多种行为

改进后的案例

1
2
3
4
5
6
7
/**
* 抽象状态类
*/
public interface LiftState {

void handleState(LiftStateContext context);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
//状态类负责的行为有两个要点,第一是执行相应状态对应的行为,第二个是负责切换下一个状态

/**
* 开门电梯状态类
*/
public class OpenLiftState implements LiftState{
@Override
public void handleState(LiftStateContext context) {
//执行行为
System.out.println("open");
//切换状态
context.setLiftState(new CloseLiftState());
}
}


/**
* 关门电梯状态类
*/
public class CloseLiftState implements LiftState{
@Override
public void handleState(LiftStateContext context) {
System.out.println("close");
context.setLiftState(new RunningLiftState());
}
}

/**
* 运行电梯状态类
*/
public class RunningLiftState implements LiftState{

@Override
public void handleState(LiftStateContext context) {
System.out.println("running");
context.setLiftState(new StopLiftState());
}
}

/**
* 停止电梯状态类
*/
public class StopLiftState implements LiftState {
@Override
public void handleState(LiftStateContext context) {
System.out.println("stop");
context.setLiftState(this);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
/**
* 电梯上下文状态,维护当前状态以及负责状态的切换
*/
public class LiftStateContext {


private LiftState liftState;


public LiftStateContext(LiftState state){
this.liftState = state;
}


public void setLiftState(LiftState liftState) {
this.liftState = liftState;
}

public LiftState getLiftState() {
return liftState;
}

/**
* 执行当前状态对应的行为
*/
public void handle(){
liftState.handleState(this);
}

状态模式的深入理解

1.一种状态对应一种行为,并且要用类的关系描述出不同状态对应不同的行为,一般是将状态和其对应的行为抽象起来即可

2.要从整体的角度看到状态的变化,所有状态的变化是属于一个流程的,比如这里的电话状态的变化,开门,关门,运行,停止,状态前后的流转是有严格的控制,比如只有开了门才能关门等等。核心是要从一个流程的角度看待状态变化。再比如订单状态的变化

3.如何描述状态的变化呢?前面也说了状态的转换是有严格流程控制的,所以在状态相应的行为中,根据流程进行状态的变化即可

优缺点

1,优点:

  • 将所有与某个状态有关的行为放到一个类中,并且可以方便地增加新的状态,只需要改变对象状态即可改变对象的行为。
  • 允许状态转换逻辑与状态对象合成一体,而不是某一个巨大的条件语句块

2,缺点:

  • 状态模式的使用必然会增加系统类和对象的个数。
  • 状态模式的结构与实现都较为复杂,如果使用不当将导致程序结构和代码的混乱。
  • 状态模式对”开闭原则”的支持并不太好

为什么状态模式对开闭原则的支持不太友好呢?

因为状态的变化是属于一个流程的,流程中的状态前后是耦合在一起的。比如电梯要想关门,前面的状态就必须是开门,只有开门了才能关门,所以如果你新添加一个电梯状态,那么可能电梯流程的状态转换都是需要发生相应的变化的。

​ 所以如果状态流转逻辑变化频繁,那么可能要慎重使用状态模式

使用场景

  • 当一个对象的行为取决于它的状态,并且它必须在运行时根据状态改变它的行为时,就可以考虑使用状态模式。
  • 一个操作中含有庞大的分支结构,并且这些分支决定于对象的状态时。

状态模式主要的问题不遵守开闭原则,如果想要拓展的话还是需要修改原代码,但最大的作用是避免了多种if-elses的状态判断,提高了代码的可阅读性

其实可以考虑优化成根据设置状态的动态选择需要执行的行为,这样的话虽然降低了代码的可阅读,但是可以遵守开闭原则,提高代码的可扩展性

观察者模式

概述

定义:

又被称为发布-订阅(Publish/Subscribe)模式,它定义了一种一对多的依赖关系,让多个观察者对象同时监听某一个主题对象。这个主题对象在状态变化时,会通知所有的观察者对象,使他们能够自动更新自己。

通俗理解:本质上就是监听模式,多个观察者监听同一个事物,如果事物发生变化,多个观察者监听到其变化后会做出相应的处理行为

角色

抽象主题,具体主题,抽象观察者,具体观察者

核心理解

1.核心是定义了一种一对多的依赖关系,该对象做出任何行为,其他依赖该对象的会同步监听到此行为并进行处理

2.观察者模式不仅仅是一对多的依赖的关系,他还对这个一对多的监听依赖关系进行了两层抽象,分别是主题层和观察者层

​ 主题层抽象是为了让观察的事物可以动态变化而不影响原来的代码,满足开闭原则;观察者层抽象同样是为了观察者的类型可以动态变化而不影响原来的代码,同样满足开闭原则

案例实现

【例】微信公众号

在使用微信公众号时,大家都会有这样的体验,当你关注的公众号中有新内容更新的话,它就会推送给关注公众号的微信用户端。我们使用观察者模式来模拟这样的场景,微信用户就是观察者,微信公众号是被观察者,有多个的微信用户关注了程序猿这个公众号。

代码如下:

定义抽象观察者类,里面定义一个更新的方法

1
2
3
public interface Observer {
void update(String message);
}

定义具体观察者类,微信用户是观察者,里面实现了更新的方法

1
2
3
4
5
6
7
8
9
10
11
12
public class WeixinUser implements Observer {
// 微信用户名
private String name;

public WeixinUser(String name) {
this.name = name;
}
@Override
public void update(String message) {
System.out.println(name + "-" + message);
}
}

定义抽象主题类,提供了attach、detach、notify三个方法

1
2
3
4
5
6
7
8
9
10
11
public interface Subject {
//增加订阅者
public void attach(Observer observer);

//删除订阅者
public void detach(Observer observer);

//通知订阅者更新消息
public void notify(String message);
}

微信公众号是具体主题(具体被观察者),里面存储了订阅该公众号的微信用户,并实现了抽象主题中的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
/**
* 核心是定义了一种一对多的依赖关系,该对象做出任何行为,其他依赖该对象的会同步监听到此行为并进行处理
* 因为对象做的行为是可变的,所以这个一要设计成抽象,并且因为不同的依赖对象监听到该行为做的处理也不一样,所以处理也得设计成抽象的
*/
public class SubscriptionSubject implements Subject {
//储存订阅公众号的微信用户
//subject是一,List<Observer>是多,形成一对多的依赖关系
private List<Observer> weixinUserlist = new ArrayList<Observer>();

@Override
public void attach(Observer observer) {
weixinUserlist.add(observer);
}

@Override
public void detach(Observer observer) {
weixinUserlist.remove(observer);
}

@Override
public void notify(String message) {
for (Observer observer : weixinUserlist) {
observer.update(message);
}
}
}

客户端程序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Client {
public static void main(String[] args) {
SubscriptionSubject mSubscriptionSubject=new SubscriptionSubject();
//创建微信用户
WeixinUser user1=new WeixinUser("孙悟空");
WeixinUser user2=new WeixinUser("猪悟能");
WeixinUser user3=new WeixinUser("沙悟净");
//订阅公众号
mSubscriptionSubject.attach(user1);
mSubscriptionSubject.attach(user2);
mSubscriptionSubject.attach(user3);
//公众号更新发出消息给订阅的微信用户
mSubscriptionSubject.notify("传智黑马的专栏更新了");
}
}

优缺点

1,优点:

  • 降低了目标与观察者之间的耦合关系,两者之间是抽象耦合关系(被观察者设置成抽象的,可以随意切换躯体的被观察者,并且观察者也设置成抽象的,因为不同的观察者对同一个消息会做出不同的处理)
  • 被观察者发送通知,所有注册的观察者都会收到信息【可以实现广播机制

2,缺点:

  • 如果观察者非常多的话,那么所有的观察者收到被观察者发送的通知会耗时
  • 如果被观察者有循环依赖的话,那么被观察者发送通知会使观察者循环调用,会导致系统崩溃

使用场景

  • 对象间存在一对多关系,一个对象的状态发生改变会影响其他对象
  • 当一个抽象模型有两个方面,其中一个方面依赖于另一方面时

JDK中提供的实现

在 Java 源码已经提供了设计好的观察者的相关api了,通过 java.util.Observable(被观察者)类和 java.util.Observer(观察者) 接口定义了观察者模式,只要实现它们的子类就可以编写观察者模式实例

1,Observable类

Observable 类是抽象目标类(被观察者),它有一个 Vector 集合成员变量,用于保存所有要通知的观察者对象,下面来介绍它最重要的 3 个方法。

  • void addObserver(Observer o) 方法:用于将新的观察者对象添加到集合中。

  • void notifyObservers(Object arg) 方法:调用集合中的所有观察者对象的 update方法,通知它们数据发生改变。通常越晚加入集合的观察者越先得到通知(底层是数组逆序遍历)

  • void setChange() 方法:用来设置一个 boolean 类型的内部标志,注明目标对象发生了变化。当它为true时,notifyObservers() 才会通知观察者。

2,Observer 接口

Observer 接口是抽象观察者,它监视目标对象的变化,当目标对象发生变化时,观察者得到通知,并调用 update 方法,进行相应的工作

中介者模式

概述

一般来说,同事类之间的关系是比较复杂的,**多个同事类之间互相关联时,他们之间的关系会呈现为复杂的网状结构,这是一种过度耦合的架构,即不利于类的复用,也不稳定(多对多个关联非常复杂,耦合度非常高)**。例如在下左图中,有六个同事类对象,假如对象1发生变化,那么将会有4个对象受到影响。如果对象2发生变化,那么将会有5个对象受到影响。也就是说,同事类之间直接关联的设计是不好的。

如果引入中介者模式,那么同事类之间的关系将变为星型结构,从下右图中可以看到,任何一个类的变动,只会影响的类本身,以及中介者,这样就减小了系统的耦合。一个好的设计,必定不会把所有的对象关系处理逻辑封装在本类中,而是使用一个专门的类来管理那些不属于自己的行为。

中介者模式1

定义:

又叫调停模式,定义一个中介角色来封装一系列对象之间的交互,使原有对象之间的耦合松散,且可以独立地改变它们之间的交互

核心的理解:

1.将原来多对图的复杂网状结构的关联转化成了两个一对多的关联(客户与中介者是n:1,中介者是服务提供者也是1:n),大大降低彼此之间关联的耦合度,提高了彼此之间的可拓展和可维护性

2.引入一个中介者将客户与服务提供者的耦合度转化成中介者与客户,中介者与服务提供者的耦合,大大降低了客户和服务提供者的耦合,增加二者的独立性,可拓展性和可维护性

案例实现

【例】租房

现在租房基本都是通过房屋中介,房主将房屋托管给房屋中介,而租房者从房屋中介获取房屋信息。房屋中介充当租房者与房屋所有者之间的中介者。

类图如下:

中介者模式2

简单解析一下类结构

Person:与中介者交互的抽象类,比如是租房人,包租公

Teant:与中介交互的具体租房人

HouseOwner:与中介交互的具体包租公

Mediator:抽象中介者,负责将客户的消息转发给服务提供者

MediatorStrcture:具体中介者,负责将具体的消息转发给服务提供者

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
//抽象中介者
public abstract class Mediator {
//申明一个联络方法
public abstract void constact(String message,Person person);
}

//抽象同事类
public abstract class Person {
protected String name;
protected Mediator mediator;

public Person(String name,Mediator mediator){
this.name = name;
this.mediator = mediator;
}
}

//具体同事类 房屋拥有者
public class HouseOwner extends Person {

public HouseOwner(String name, Mediator mediator) {
super(name, mediator);
}

//与中介者联系
public void constact(String message){
mediator.constact(message, this);
}

//获取信息
public void getMessage(String message){
System.out.println("房主" + name +"获取到的信息:" + message);
}
}

//具体同事类 承租人
public class Tenant extends Person {
public Tenant(String name, Mediator mediator) {
super(name, mediator);
}

//与中介者联系
public void constact(String message){
mediator.constact(message, this);
}

//获取信息
public void getMessage(String message){
System.out.println("租房者" + name +"获取到的信息:" + message);
}
}

//中介机构
public class MediatorStructure extends Mediator {
//首先中介结构必须知道所有房主和租房者的信息
private HouseOwner houseOwner;
private Tenant tenant;

public HouseOwner getHouseOwner() {
return houseOwner;
}

public void setHouseOwner(HouseOwner houseOwner) {
this.houseOwner = houseOwner;
}

public Tenant getTenant() {
return tenant;
}

public void setTenant(Tenant tenant) {
this.tenant = tenant;
}

public void constact(String message, Person person) {
if (person == houseOwner) { //如果是房主,则租房者获得信息
tenant.getMessage(message);
} else { //反正则是房主获得信息
houseOwner.getMessage(message);
}
}
}

//测试类
public class Client {
public static void main(String[] args) {
//一个房主、一个租房者、一个中介机构
MediatorStructure mediator = new MediatorStructure();

//房主和租房者只需要知道中介机构即可
HouseOwner houseOwner = new HouseOwner("张三", mediator);
Tenant tenant = new Tenant("李四", mediator);

//中介结构要知道房主和租房者
mediator.setHouseOwner(houseOwner);
mediator.setTenant(tenant);

tenant.constact("需要租三室的房子");
houseOwner.constact("我这有三室的房子,你需要租吗?");
}
}

优缺点

1,优点:

  • 松散耦合(通过引入中介者将客户与服务提供的耦合转化成了客户与中介者的耦合,中介者与服务提供者的耦合,并且该耦合是一对多,不再是之前的多对多,大大降低了彼此的耦合度)

    中介者模式通过把多个同事对象之间的交互封装到中介者对象里面,从而使得同事对象之间松散耦合,基本上可以做到互补依赖。这样一来,同事对象就可以独立地变化和复用,而不再像以前那样“牵一处而动全身”了

  • 集中控制交互

    多个同事对象的交互,被封装在中介者对象里面集中管理,使得这些交互行为发生变化的时候,只需要修改中介者对象就可以了,当然如果是已经做好的系统,那么就扩展中介者对象,而各个同事类不需要做修改

  • 对多关联转变为一对一的关联(将多对多的关联转化成了两个一对多的关联)

    没有使用中介者模式的时候,同事对象之间的关系通常是一对多的,引入中介者对象以后,中介者对象和同事对象的关系通常变成双向的一对一,这会让对象的关系更容易理解和实现

2,缺点:

当同事类太多时,中介者的职责将很大,它会变得复杂而庞大,以至于系统难以维护

使用场景

  • 系统中对象之间存在复杂的引用关系,系统结构混乱且难以理解
  • 当想创建一个运行于多个类之间的对象,又不想生成新的子类时

迭代器模式

概述

定义:

提供一个对象来顺序访问聚合对象中的一系列数据,而不暴露聚合对象的内部表示

核心理解:将容器的数据结构和容器的遍历方式进行解耦,让容器可以根据自己特定的结构 or 业务场景的需要随时定制和切换安自己的遍历方式

实现方式:抽象+多态

1.抽象迭代器类,因为每种容器的结构不一样,判断和获取元素的方式自然也就不一样,所以必须设计成抽象的,典型的案例就是数组结构实现的容器和链表结构实现的容器结构

2.抽象容器类,因为每种容器底层的数据结构不一样,对应的增删改查方法自然也就不一样,所以必须设计成抽象的

3.jdk源码中具体迭代器类是作为具体容器类的内部类的,外部类是没有查看权限的,进一步隐藏了容器内部的遍历细节

案例实现

【例】定义一个可以存储学生对象的容器对象,将遍历该容器的功能交由迭代器实现,涉及到的类如下:

迭代器模式

代码如下:

定义迭代器接口,声明hasNext、next方法

1
2
3
4
5
6
//抽象迭代器类,因为每种容器的结构不一样,判断和获取元素的方式自然也就不一样,所以必须设计成抽象的
//典型的案例就是数组结构实现的容器和链表结构实现的容器结构
public interface StudentIterator {
boolean hasNext();
Student next();
}

定义具体的迭代器类,重写所有的抽象方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class StudentIteratorImpl implements StudentIterator {
private List<Student> list;
private int position = 0;

public StudentIteratorImpl(List<Student> list) {
this.list = list;
}

@Override
public boolean hasNext() {
return position < list.size();
}

@Override
public Student next() {
Student currentStudent = list.get(position);
position ++;
return currentStudent;
}
}

定义抽象容器类,包含添加元素,删除元素,获取迭代器对象的方法

1
2
3
4
5
6
7
8
//抽象容器类,因为每种容器底层的数据结构不一样,对应的增删改查方法自然也就不一样,所以必须设计成抽象的
public interface StudentAggregate {
void addStudent(Student student);

void removeStudent(Student student);

StudentIterator getStudentIterator();
}

定义具体的容器类,重写所有的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class StudentAggregateImpl implements StudentAggregate {

private List<Student> list = new ArrayList<Student>(); // 学生列表

@Override
public void addStudent(Student student) {
this.list.add(student);
}

@Override
public void removeStudent(Student student) {
this.list.remove(student);
}

@Override
public StudentIterator getStudentIterator() {
return new StudentIteratorImpl(list);
}
}

优缺点

1,优点:

  • **它支持以不同的方式遍历一个聚合对象,在同一个聚合对象上可以定义多种遍历方式(因为将容器的结构和容器的遍历方式解耦了,所以同一种容器可以根据业务场景的需要动态的定制和切换自己的遍历方式)**,在迭代器模式中只需要用一个不同的迭代器来替换原有迭代器即可改变遍历算法,我们也可以自己定义迭代器的子类以支持新的遍历方式
  • 迭代器简化了聚合类。由于引入了迭代器,在原有的聚合对象中不需要再自行提供数据遍历等方法,这样可以简化聚合类的设计
  • 在迭代器模式中,由于引入了抽象层,增加新的聚合类和迭代器类都很方便,无须修改原有代码,满足 “开闭原则” 的要求(满足开闭原则是因为将容器的结构和容器的遍历方式进行了解耦,如果拓展只需要增加相应的接口实现类,自然满足开闭原则)

2,缺点:

增加了类的个数,这在一定程度上增加了系统的复杂性。

使用场景

  • 当需要为聚合对象提供多种遍历方式时
  • **当需要为遍历不同的聚合结构提供一个统一的接口(不同的容器结构遍历调用的是同一个接口的抽象方法,然后根据传入的具体容器结构动态绑定具体的遍历实现类)**时。
  • 当访问一个聚合对象的内容而无须暴露其内部细节的表示时。

JDK源码解析

迭代器模式在JAVA的很多集合类中被广泛应用,接下来看看JAVA源码中是如何使用迭代器模式的。

1
2
3
4
5
List<String> list = new ArrayList<>();
Iterator<String> iterator = list.iterator(); //list.iterator()方法返回的肯定是Iterator接口的子实现类对象
while (iterator.hasNext()) {
System.out.println(iterator.next());
}

看完这段代码是不是很熟悉,与我们上面代码基本类似。单列集合都使用到了迭代器,我们以ArrayList举例来说明

  • List:抽象聚合类
  • ArrayList:具体的聚合类
  • Iterator:抽象迭代器
  • list.iterator():返回的是实现了 Iterator 接口的具体迭代器对象

具体的来看看 ArrayList的代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable {

public Iterator<E> iterator() {
return new Itr();
}

private class Itr implements Iterator<E> {
int cursor; // 下一个要返回元素的索引
int lastRet = -1; // 上一个返回元素的索引
int expectedModCount = modCount;

Itr() {}

//判断是否还有元素
public boolean hasNext() {
return cursor != size;
}

//获取下一个元素
public E next() {
checkForComodification();
int i = cursor;
if (i >= size)
throw new NoSuchElementException();
Object[] elementData = ArrayList.this.elementData;
if (i >= elementData.length)
throw new ConcurrentModificationException();
cursor = i + 1;
return (E) elementData[lastRet = i];
}
...
}

这部分代码还是比较简单,大致就是在 iterator 方法中返回了一个实例化的 Iterator 对象。Itr是一个内部类,它实现了 Iterator 接口并重写了其中的抽象方法

Jdk源码已经帮我们提供好了抽象迭代器类java.util.Iterable,有需要的话直接使用该接口实现迭代器模式即可

注意:

​ 当我们在使用JAVA开发的时候,想使用迭代器模式的话,只要让我们自己定义的容器类实现java.util.Iterable并实现其中的iterator()方法使其返回一个 java.util.Iterator 的实现类就可以了

访问者模式

概述

定义:

封装一些作用于某种数据结构中的各元素的操作,它可以在不改变这个数据结构的前提下定义作用于这些元素的新的操作

核心理解:

1.将被访者和访问者进行解耦,即拓展和更新访问者的操作是不影响被访问者的代码实现的

2.其实将被放在和访问者进行解耦很简单,面向抽象+多态就可以实现(策略模式)动态切换访问者的行为,但这样是有限制的,每一个访问者只能有一种访问行为,虽然可以动态切换访问者进而达到切换访问行为的目的但是如果我想一个访问者有多个访问行为并且实现多个访问者多个行为的切换呢?那么这种方案就解决不了这个问题

访问者设计模式是用了双分派机制来实现一个访问者可以有多个行为,并且可以在多个访问者和多个行为之间切换

双分派机制的核心是:先用动态分派机制实现访问者的切换,再在动态执行的方法中使用静态分配机制实现同一个访问者不同的访问行为的切换

案例实现

【例】给宠物喂食

现在养宠物的人特别多,我们就以这个为例,当然宠物还分为狗,猫等,要给宠物喂食的话,主人可以喂,其他人也可以喂食。

  • 访问者角色:给宠物喂食的人
  • 具体访问者角色:主人、其他人
  • 抽象元素角色:动物抽象类
  • 具体元素角色:宠物狗、宠物猫
  • 结构对象角色:主人家

类图如下:

代码如下:

创建抽象访问者接口

1
2
3
4
5
public interface Person {
void feed(Cat cat);

void feed(Dog dog);
}

创建不同的具体访问者角色(主人和其他人),都需要实现 Person接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class Owner implements Person {

@Override
public void feed(Cat cat) {
System.out.println("主人喂食猫");
}

@Override
public void feed(Dog dog) {
System.out.println("主人喂食狗");
}
}

public class Someone implements Person {
@Override
public void feed(Cat cat) {
System.out.println("其他人喂食猫");
}

@Override
public void feed(Dog dog) {
System.out.println("其他人喂食狗");
}
}

定义抽象节点 – 宠物

1
2
3
public interface Animal {
void accept(Person person);
}

定义实现Animal接口的 具体节点(元素)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Dog implements Animal {

@Override
public void accept(Person person) {
person.feed(this);
System.out.println("好好吃,汪汪汪!!!");
}
}

public class Cat implements Animal {

@Override
public void accept(Person person) {
person.feed(this);
System.out.println("好好吃,喵喵喵!!!");
}
}

定义对象结构,此案例中就是主人的家

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Home {
private List<Animal> nodeList = new ArrayList<Animal>();

public void action(Person person) {
for (Animal node : nodeList) {
node.accept(person);
}
}

//添加操作
public void add(Animal animal) {
nodeList.add(animal);
}
}

测试类

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Client {
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);
}
}

优缺点

1,优点:

  • 扩展性好

    在不修改对象结构中的元素的情况下,为对象结构中的元素添加新的功能**(意思就拓展 or 修改访问者的行为不影响被访问者的对象结构)**

  • 复用性好

    通过访问者来定义整个对象结构通用的功能,从而提高复用程度。

  • 分离无关行为

    通过访问者来分离无关的行为,把相关的行为封装在一起,构成一个访问者,这样每一个访问者的功能都比较单一。

2,缺点:

  • 对象结构变化很困难

    在访问者模式中,每增加一个新的元素类,都要在每一个具体访问者类中增加相应的具体操作,这违背了“开闭原则”**(意思是如果修改被访问者的结构,那么他将影响访问者的结构,原因是因为静态分派机制)**。

  • 违反了依赖倒置原则

    访问者模式依赖了具体类(同样是因为静态分派机制),而没有依赖抽象类。

使用场景

  • 对象结构相对稳定,但其操作算法经常变化的程序

  • 对象结构中的对象需要提供多种不同且不相关的操作,而且要避免让这些操作的变化影响对象的结构(访问者需要动态添加各种访问行为,并且不能影响被访问者本身的结构)

扩展

访问者模式用到了一种双分派的技术

1,分派:

变量被声明时的类型叫做变量的静态类型,有些人又把静态类型叫做明显类型;而变量所引用的对象的真实类型又叫做变量的实际类型。比如 Map map = new HashMap() ,map变量的静态类型是 Map ,实际类型是 HashMap 。根据对象的类型而对方法进行的选择,就是分派(Dispatch),分派(Dispatch)又分为两种,即静态分派和动态分派

静态分派(Static Dispatch) 发生在编译时期,分派根据静态类型信息发生。静态分派对于我们来说并不陌生,方法重载就是静态分派**(方法重载就是静态分派,编译时就会根据声明的类型(不是实际的类型)确定调用的方法)**

动态分派(Dynamic Dispatch) 发生在运行时期,动态分派动态地置换掉某个方法。Java通过方法的重写支持动态分派**(方法覆盖重写就是动态分派机制,编译时是无法确定具体执行的方法,因为父子类可能都拥有该方法,只有在运行时才能确定传入的实际类型,进而调用对应的方法)**

2,动态分派:

通过方法的重写支持动态分派。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public class Animal {
public void execute() {
System.out.println("Animal");
}
}

public class Dog extends Animal {
@Override
public void execute() {
System.out.println("dog");
}
}

public class Cat extends Animal {
@Override
public void execute() {
System.out.println("cat");
}
}

public class Client {
public static void main(String[] args) {
Animal a = new Dog();
a.execute();

Animal a1 = new Cat();
a1.execute();
}
}

上面代码的结果大家应该直接可以说出来,这不就是多态吗!运行执行的是子类中的方法。

Java编译器在编译时期并不总是知道哪些代码会被执行,因为编译器仅仅知道对象的静态类型,而不知道对象的真实类型;

3,静态分派:

通过方法重载支持静态分派。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public class Animal {
}

public class Dog extends Animal {
}

public class Cat extends Animal {
}

public class Execute {
public void execute(Animal a) {
System.out.println("Animal");
}

public void execute(Dog d) {
System.out.println("dog");
}

public void execute(Cat c) {
System.out.println("cat");
}
}

public class Client {
public static void main(String[] args) {
Animal a = new Animal();
Animal a1 = new Dog();
Animal a2 = new Cat();

Execute exe = new Execute();
exe.execute(a);
exe.execute(a1);
exe.execute(a2);
}
}

运行结果:

静态分派结果

这个结果可能出乎一些人的意料了,为什么呢?

重载方法的分派是根据静态类型进行的,这个分派过程在编译时期就完成了。

4,双分派:

所谓双分派技术就是在选择一个方法的时候,不仅仅要根据消息接收者(receiver)的运行时区别,还要根据参数的运行时区别。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
public class Animal {
public void accept(Execute exe) {
//这里调用execute方法就是静态分派机制,传入声明的类型this
exe.execute(this);
}
}

public class Dog extends Animal {
//方法覆盖重写,动态分派机制
public void accept(Execute exe) {
//静态分派机制
exe.execute(this);
}
}

public class Cat extends Animal {
//方法覆盖重写,动态分派机制
public void accept(Execute exe) {
//静态分派机制
exe.execute(this);
}
}

public class Execute {
public void execute(Animal a) {
System.out.println("animal");
}

public void execute(Dog d) {
System.out.println("dog");
}

public void execute(Cat c) {
System.out.println("cat");
}
}

public class Client {
public static void main(String[] args) {
Animal a = new Animal();
Animal d = new Dog();
Animal c = new Cat();

Execute exe = new Execute();
a.accept(exe);
d.accept(exe);
c.accept(exe);
}
}

在上面代码中,客户端将Execute对象做为参数传递给Animal类型的变量调用的方法,这里完成第一次分派,这里是方法重写,所以是动态分派,也就是执行实际类型中的方法,同时也将自己this作为参数传递进去,这里就完成了第二次分派,这里的Execute类中有多个重载的方法,而传递进行的是this,就是具体的实际类型的对象。

说到这里,我们已经明白双分派是怎么回事了,但是它有什么效果呢?就是可以实现方法的动态绑定,我们可以对上面的程序进行修改。

运行结果如下:

动态分派结果

双分派实现动态绑定的本质,就是在重载方法委派的前面加上了继承体系中覆盖的环节,由于覆盖是动态的,所以重载就是动态的了,先动态分派后静态分派

备忘录模式

概述

备忘录模式提供了一种状态恢复的实现机制,使得用户可以方便地回到一个特定的历史步骤,当新的状态无效或者存在问题时,可以使用暂时存储起来的备忘录将状态复原,很多软件都提供了撤销(Undo)操作,如 Word、记事本、Photoshop、IDEA等软件在编辑时按 Ctrl+Z 组合键时能撤销当前操作,使文档恢复到之前的状态;还有在 浏览器 中的后退键、数据库事务管理中的回滚操作、玩游戏时的中间结果存档功能、数据库与操作系统的备份操作、棋类游戏中的悔棋功能等都属于这类。

定义:

又叫快照模式,在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态,以便以后当需要时能将该对象恢复到原先保存的状态

备忘录有两个等效的接口:

  • 窄接口:管理者(Caretaker)对象(和其他发起人对象之外的任何对象)看到的是备忘录的窄接口(narror Interface),这个窄接口只允许他把备忘录对象传给其他的对象
  • 宽接口:与管理者看到的窄接口相反,发起人对象可以看到一个宽接口(wide Interface),这个宽接口允许它读取所有的数据,以便根据这些数据恢复这个发起人对象的内部状态

案例实现

【例】游戏挑战BOSS

游戏中的某个场景,一游戏角色有生命力、攻击力、防御力等数据,在打Boss前和后一定会不一样的,我们允许玩家如果感觉与Boss决斗的效果不理想可以让游戏恢复到决斗之前的状态。

要实现上述案例,有两种方式:

  • “白箱”备忘录模式
  • “黑箱”备忘录模式

“白箱”备忘录模式

备忘录角色对任何对象都提供一个接口,即宽接口,备忘录角色的内部所存储的状态就对所有对象公开。类图如下:

白箱备忘录模式

简单解析一下类图:

GameRole:备忘录发起人,负责创建备忘录和恢复备忘录
RoleStateMemento:备忘录保存的状态的信息
RoleStateCaretaker:备忘录管理者,设置备忘录和负责获取备忘录

备忘录管理者的作用是负责存储和管理备忘录信息,简化发起人的职责,发起人只需要发起存储请求和恢复请求,具体的备忘录信息全部交由备忘录管理者来挂案例

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
//游戏角色类
public class GameRole {
private int vit; //生命力
private int atk; //攻击力
private int def; //防御力

//初始化状态
public void initState() {
this.vit = 100;
this.atk = 100;
this.def = 100;
}

//战斗
public void fight() {
this.vit = 0;
this.atk = 0;
this.def = 0;
}

//保存角色状态
public RoleStateMemento saveState() {
return new RoleStateMemento(vit, atk, def);
}

//回复角色状态
public void recoverState(RoleStateMemento roleStateMemento) {
this.vit = roleStateMemento.getVit();
this.atk = roleStateMemento.getAtk();
this.def = roleStateMemento.getDef();
}

public void stateDisplay() {
System.out.println("角色生命力:" + vit);
System.out.println("角色攻击力:" + atk);
System.out.println("角色防御力:" + def);
}

public int getVit() {
return vit;
}

public void setVit(int vit) {
this.vit = vit;
}

public int getAtk() {
return atk;
}

public void setAtk(int atk) {
this.atk = atk;
}

public int getDef() {
return def;
}

public void setDef(int def) {
this.def = def;
}
}

//游戏状态存储类(备忘录类)
public class RoleStateMemento {
private int vit;
private int atk;
private int def;

public RoleStateMemento(int vit, int atk, int def) {
this.vit = vit;
this.atk = atk;
this.def = def;
}

public int getVit() {
return vit;
}

public void setVit(int vit) {
this.vit = vit;
}

public int getAtk() {
return atk;
}

public void setAtk(int atk) {
this.atk = atk;
}

public int getDef() {
return def;
}

public void setDef(int def) {
this.def = def;
}
}

//角色状态管理者类
public class RoleStateCaretaker {
private RoleStateMemento roleStateMemento;

public RoleStateMemento getRoleStateMemento() {
return roleStateMemento;
}

public void setRoleStateMemento(RoleStateMemento roleStateMemento) {
this.roleStateMemento = roleStateMemento;
}
}

//测试类
public class Client {
public static void main(String[] args) {
System.out.println("------------大战Boss前------------");
//大战Boss前
GameRole gameRole = new GameRole();
gameRole.initState();
gameRole.stateDisplay();

//保存进度
RoleStateCaretaker roleStateCaretaker = new RoleStateCaretaker();
roleStateCaretaker.setRoleStateMemento(gameRole.saveState());

System.out.println("------------大战Boss后------------");
//大战Boss时,损耗严重
gameRole.fight();
gameRole.stateDisplay();
System.out.println("------------恢复之前状态------------");
//恢复之前状态
gameRole.recoverState(roleStateCaretaker.getRoleStateMemento());
gameRole.stateDisplay();

}
}

分析:白箱备忘录模式是破坏封装性的。但是通过程序员自律,同样可以在一定程度上实现模式的大部分用意。

“黑箱”备忘录模式

备忘录角色对发起人对象提供一个宽接口,而为其他对象提供一个窄接口。在Java语言中,实现双重接口的办法就是将备忘录类设计成发起人类的内部成员类

RoleStateMemento 设为 GameRole 的内部类,从而将 RoleStateMemento 对象(实际的接口实现类)封装在 GameRole 里面(作为GameRole的成员内部类)在外面提供一个标识接口 MementoRoleStateCaretaker 及其他对象使用。这样 GameRole 类看到的是 RoleStateMemento 所有的接口,而RoleStateCaretaker 及其他对象看到的仅仅是标识接口 Memento 所暴露出来的接口,从而维护了封装型。类图如下:

通俗点的解释:

1
2
3
4
5
//黑箱备忘录模式就是想实现除发起人外,其他人对备忘录的操作权限只有传递权限
//具体的实现思想是什么?
// 要让发起人拥有全部权限而其他人只有有备忘录的查看权限,怎么实现呢?
// 1.定义一个标识接口,里面没有任何定义行为,因为类型一致就能获取到对应的实现类对象(传递权限)
// 2.将具体的操作行为类设计在发起人的内部,这样就可实现发起人拥有全部权限,而其他人只拥有传递权限

黑箱备忘录模式

代码如下:

窄接口Memento,这是一个标识接口,因此没有定义出任何的方法

1
2
public interface Memento {
}

定义发起人类 GameRole,并在内部定义备忘录内部类 RoleStateMemento(该内部类设置为私有的)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
package com.czq.design.pattern.memento.black_box;

//黑箱模式就是想实现除发起人外,其他人对备忘录的操作权限只有传递权限
//具体的实现思想是什么?
// 要让发起人拥有全部权限而其他人只有有备忘录的查看权限,怎么实现呢?
// 1.定义一个标识接口,里面没有任何定义行为,因为类型一致就能获取到对应的实现类对象(即备忘录)
// 2.将具体的操作行为类设计在发起人的内部,这样就可实现发起人拥有全部权限,而其他人只拥有传递权限

//游戏角色类
public class GameRole {
private int vit; //生命力
private int atk; //攻击力
private int def; //防御力

//初始化状态
public void initState() {
this.vit = 100;
this.atk = 100;
this.def = 100;
}

//战斗
public void fight() {
this.vit = 0;
this.atk = 0;
this.def = 0;
}

//保存角色状态
public Memento saveState() {
return new RoleStateMemento(vit, atk, def);
}

//回复角色状态
public void recoverState(Memento memento) {
RoleStateMemento roleStateMemento = (RoleStateMemento) memento;
this.vit = roleStateMemento.getVit();
this.atk = roleStateMemento.getAtk();
this.def = roleStateMemento.getDef();
}

public void stateDisplay() {
System.out.println("角色生命力:" + vit);
System.out.println("角色攻击力:" + atk);
System.out.println("角色防御力:" + def);

}

public int getVit() {
return vit;
}

public void setVit(int vit) {
this.vit = vit;
}

public int getAtk() {
return atk;
}

public void setAtk(int atk) {
this.atk = atk;
}

public int getDef() {
return def;
}

public void setDef(int def) {
this.def = def;
}


private class RoleStateMemento implements Memento {
private int vit;
private int atk;
private int def;

public RoleStateMemento(int vit, int atk, int def) {
this.vit = vit;
this.atk = atk;
this.def = def;
}

public int getVit() {
return vit;
}

public void setVit(int vit) {
this.vit = vit;
}

public int getAtk() {
return atk;
}

public void setAtk(int atk) {
this.atk = atk;
}

public int getDef() {
return def;
}

public void setDef(int def) {
this.def = def;
}
}
}

负责人角色类 RoleStateCaretaker 能够得到的备忘录对象是以 Memento 为接口的,由于这个接口仅仅是一个标识接口,因此负责人角色不可能改变这个备忘录对象的内容

1
2
3
4
5
6
7
8
9
10
11
12
//角色状态管理者类
public class RoleStateCaretaker {
private Memento memento;

public Memento getMemento() {
return memento;
}

public void setMemento(Memento memento) {
this.memento = memento;
}
}

客户端测试类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class Client {
public static void main(String[] args) {
System.out.println("------------大战Boss前------------");
//大战Boss前
GameRole gameRole = new GameRole();
gameRole.initState();
gameRole.stateDisplay();

//保存进度
RoleStateCaretaker roleStateCaretaker = new RoleStateCaretaker();
roleStateCaretaker.setMemento(gameRole.saveState());

System.out.println("------------大战Boss后------------");
//大战Boss时,损耗严重
gameRole.fight();
gameRole.stateDisplay();
System.out.println("------------恢复之前状态------------");
//恢复之前状态
gameRole.recoverState(roleStateCaretaker.getMemento());
gameRole.stateDisplay();
}
}

优缺点

1,优点:

  • 提供了一种可以恢复状态的机制。当用户需要时能够比较方便地将数据恢复到某个历史的状态
  • 实现了内部状态的封装,除了创建它的发起人之外,其他对象都不能够访问这些状态信息
  • 简化了发起人类,发起人不需要管理和保存其内部状态的各个备份,所有状态信息都保存在备忘录中,并由管理者进行管理,这符合单一职责原则

2,缺点:

  • 资源消耗大。如果要保存的内部状态信息过多或者特别频繁,将会占用比较大的内存资源。

使用场景

  • 需要保存与恢复数据的场景,如玩游戏时的中间结果的存档功能

  • 需要提供一个可回滚操作的场景,如 Word、记事本、Photoshop,idea等软件在编辑时按 Ctrl+Z 组合键,还有数据库中事务操作

解释器模式

概述

解释器模式1

如上图,设计一个软件用来进行加减计算。我们第一想法就是使用工具类,提供对应的加法和减法的工具方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//用于两个整数相加
public static int add(int a,int b){
return a + b;
}

//用于两个整数相加
public static int add(int a,int b,int c){
return a + b + c;
}

//用于n个整数相加
public static int add(Integer ... arr) {
int sum = 0;
for (Integer i : arr) {
sum += i;
}
return sum;
}

上面的形式比较单一、有限,如果形式变化非常多,这就不符合要求,因为加法和减法运算,两个运算符与数值可以有无限种组合方式。比如 1+2+3+4+5、1+2+3-4等等。

显然,现在需要一种翻译识别机器,能够解析由数字以及 + - 符号构成的合法的运算序列。如果把运算符和数字都看作节点的话,能够逐个节点的进行读取解析运算,这就是解释器模式的思维。

定义:

给定一个语言,定义它的文法表示,并定义一个解释器,这个解释器使用该标识来解释语言中的句子。

在解释器模式中,我们需要将待解决的问题,提取出规则,抽象为一种“语言”。比如加减法运算,规则为:由数值和+-符号组成的合法序列,“1+3-2” 就是这种语言的句子。

核心理解:对表达式的解析规则进行抽象并根据具体的规则定制解析方案,比如一个基本算数运算,有加法规则,减法规则,乘法规则,除法规则

解释器就是要解析出来语句的含义。但是如何描述规则呢

文法(语法)规则:

文法是用于描述语言的语法结构的形式规则。

1
2
3
4
5
6
7
8
//抽象表达式
expression ::= value | plus | minus
//加法表达式
plus ::= expression ‘+’ expression
//减法表达式
minus ::= expression ‘-’ expression
//获取值的表达式
value ::= integer

注意: 这里的符号“::=”表示“定义为”的意思,竖线 | 表示或,左右的其中一个,引号内为字符本身,引号外为语法。

上面规则描述为 :

表达式可以是一个值,也可以是plus或者minus运算,而plus和minus又是由表达式结合运算符构成,值的类型为整型数

抽象语法树:

在计算机科学中,抽象语法树(AbstractSyntaxTree,AST),或简称语法树(Syntax tree),是源代码语法结构的一种抽象表示。它以树状的形式表现编程语言的语法结构,树上的每个节点都表示源代码中的一种结构。

用树形来表示符合文法规则的句子

解释器模式2

案例实现

【例】设计实现加减法的软件

对类图的简单解析:

AbstractExpression:抽象表达式类,定义抽象表达式的解析规则

Plus:加法表达式,定义加法表达式的解析规则

Minus:减法表达式,定义减法表达式的解析规则

Value:值表达式,定义获取值的解析规则

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
//抽象角色AbstractExpression
public abstract class AbstractExpression {
public abstract int interpret(Context context);
}

//终结符表达式角色
public class Value extends AbstractExpression {
private int value;

public Value(int value) {
this.value = value;
}

@Override
public int interpret(Context context) {
return value;
}

@Override
public String toString() {
return new Integer(value).toString();
}
}

//非终结符表达式角色 加法表达式
public class Plus extends AbstractExpression {
private AbstractExpression left;
private AbstractExpression right;

public Plus(AbstractExpression left, AbstractExpression right) {
this.left = left;
this.right = right;
}

@Override
public int interpret(Context context) {
return left.interpret(context) + right.interpret(context);
}

@Override
public String toString() {
return "(" + left.toString() + " + " + right.toString() + ")";
}
}

///非终结符表达式角色 减法表达式
public class Minus extends AbstractExpression {
private AbstractExpression left;
private AbstractExpression right;

public Minus(AbstractExpression left, AbstractExpression right) {
this.left = left;
this.right = right;
}

@Override
public int interpret(Context context) {
return left.interpret(context) - right.interpret(context);
}

@Override
public String toString() {
return "(" + left.toString() + " - " + right.toString() + ")";
}
}

//终结符表达式角色 变量表达式
public class Variable extends AbstractExpression {
private String name;

public Variable(String name) {
this.name = name;
}

@Override
public int interpret(Context ctx) {
return ctx.getValue(this);
}

@Override
public String toString() {
return name;
}
}

//环境类,保存非终结符表达式的值(也是作为解析时递归的出口)
public class Context {
private Map<Variable, Integer> map = new HashMap<Variable, Integer>();

public void assign(Variable var, Integer value) {
map.put(var, value);
}

public int getValue(Variable var) {
Integer value = map.get(var);
return value;
}
}

//测试类
public class Client {
public static void main(String[] args) {
Context context = new Context();

Variable a = new Variable("a");
Variable b = new Variable("b");
Variable c = new Variable("c");
Variable d = new Variable("d");
Variable e = new Variable("e");
//Value v = new Value(1);

context.assign(a, 1);
context.assign(b, 2);
context.assign(c, 3);
context.assign(d, 4);
context.assign(e, 5);

AbstractExpression expression = new Minus(new Plus(new Plus(new Plus(a, b), c), d), e);

System.out.println(expression + "= " + expression.interpret(context));
}
}

优缺点

1,优点:

  • 易于改变和扩展文法(因为语法表达式被定义为抽象的,直接继承就可以直接拓展不同规则的表达式了)

    由于在解释器模式中使用类来表示语言的文法规则,因此可以通过继承等机制来改变或扩展文法。每一条文法规则都可以表示为一个类,因此可以方便地实现一个简单的语言

  • 实现文法较为容易

    在抽象语法树中每一个表达式节点类的实现方式都是相似的,这些类的代码编写都不会特别复杂

  • 增加新的解释表达式较为方便**(还是因为表达式是设置为抽象的,拓展性强)**

    如果用户需要增加新的解释表达式只需要对应增加一个新的终结符表达式或非终结符表达式类,原有表达式类代码无须修改,符合 “开闭原则”

2,缺点:

  • 对于复杂文法难以维护

    在解释器模式中,每一条规则至少需要定义一个类,因此如果一个语言包含太多文法规则,类的个数将会急剧增加,导致系统难以管理和维护。

  • 执行效率较低

    由于在解释器模式中使用了大量的循环和递归调用,因此在解释较为复杂的句子时其速度很慢,而且代码的调试过程也比较麻烦

解释器设计模式本质上是将语法的解析逻辑转化成了类与类的关系来实现,如果是直接用逻辑来实现,必然是用树结构来实现的,但转化成了类与类的关系来实现,就不用树形结构实现,但底层实现的思想还是树型结构的思想(语法树),所以在表达式的解析过程中,还是用到了递归 or 循环来解析,当语法过于复杂时,解析的性能就会非常差

上面的案例是用递归来进行解析的,根据传入具体的表达式动态选择相应表达式解析规则类进行解析,是先自顶向下获取到终结表达式(变量的数值,终结表达式也是递归的出口),然后再自低向上进行不断的解析和运算,在这个过程中会不断的动态切换具体的规则解析类进行解析,和树的递归遍历思想是一致的

使用场景

  • 当语言的文法较为简单,且执行效率不是关键问题时

  • 当问题重复出现,且可以用一种简单的语言来进行表达时

  • 当一个语言需要解释执行,并且语言中的句子可以表示为一个抽象语法树的时候

设计模式总结

学了那么多设计模式最大的感觉除了面向抽象编程进行解耦合,多态动态选择执行的方法外,还有就是要将业务逻辑设计转化类与类的关系设计,因为一旦转化类与类的关系设计,那么可拓展性和可维护性就会强很多,因为类与类的关系支持拓展和维护的维度有很多,如继承,实现,组合,抽象等等

设计模式后续需要深入学习的地方

1.需要深入理解每种设计模式的实现过程,优缺点和应用场景

2.同一种类型的模式(如结构型模式)实现的思想非常类似,研究清楚他们的区别

3.结合开源框架的源码(如Spring,SpringMvc,SpringBoot)继续深入学习设计模式的实践应用

设计模式课程总结

此总结是学校实验和大作业总结,属于学校内部实践课总结

1.发现很多设计模式理解都比较浅,以下是某些设计模式经过这次实践后更为深入的理解

A.原型模式。原型模式分为浅克隆和深克隆,我们要的独立copy一份出来,不受原来的影响。如果只是复制原来的引用指针,那叫浅克隆,如果是完全新的一份数据副本,那叫深克隆

B.装饰器设计模式,核心是两层抽象实现被装饰者和装饰者的解耦。两层抽象分别是被装饰者层和装饰者,让其都可以实现任意切换。而且有个细节点就是装饰者本身也是属于装饰者的一种,因为被装饰者也可以被下一层装饰者进行装饰的(类型兼容)

C.外观模式就是暴露给外部使用的接口,但屏蔽所有使用细节,降低使用成本,比如启动电脑,你不需要里面具体的零件是怎么启动的,你只需要按开机键即可完成开机

D.命令模式暂时还没学明白,需要用到的时候再深入学习

E.迭代器模式是实现数据容器和数据遍历方式的解耦合,所以抽象层也有两层,分别是数据容器层和数据遍历方式层

F.观察者模式同样是两层抽象,被观察者的抽象,观察者的抽象

2.理解设计模式的核心思想是面向抽象编程的思想,设计模式的目的就是高内聚,低耦合,只有实现这个目的才能让程序有更好的扩展性和维护性,而高内聚,低耦合的核心思想就是面向抽象编程思想,只有抽象才能将强耦合转换成松耦合

3.根据设计模式面向抽象编程的核心思想,每种设计模式都划分出了对应的角色,比如抽象xxx角色,具体xxx角色,带着面向抽象编程的思想去理解相应的角色职责便能更好,更容易的理解设计模式

SpringIOC容器的简单解析

Spring核心功能结构

Spring大约有20个模块,由1300多个不同的文件构成。这些模块可以分为:

核心容器、AOP和设备支持、数据访问与集成、Web组件、通信报文和集成测试等,下面是 Spring 框架的总体架构图:

![Spring 框架的总体架构图](Spring 框架的总体架构图.png)

核心容器由 beans、core、context 和 expression(Spring Expression Language,SpEL)4个模块组成。

  • spring-beans和spring-core模块是Spring框架的核心模块,包含了控制反转(Inversion of Control,IOC)和依赖注入(Dependency Injection,DI)。BeanFactory使用控制反转对应用程序的配置和依赖性规范与实际的应用程序代码进行了分离(依赖注入将对象创建和使用先分离(实际上依赖注入就是设计模式中的依赖倒转原则))。BeanFactory属于延时加载,也就是说在实例化容器对象后并不会自动实例化Bean,只有当Bean被使用时,BeanFactory才会对该 Bean 进行实例化与依赖关系的装配。
  • spring-context模块构架于核心模块之上,扩展了BeanFactory,为它添加了Bean生命周期控制、框架事件体系及资源加载透明化等功能。此外,该模块还提供了许多企业级支持,如邮件访问、远程访问、任务调度等,ApplicationContext 是该模块的核心接口,它的超类是 BeanFactory。与BeanFactory不同,ApplicationContext实例化后会自动对所有的单实例Bean进行实例化与依赖关系的装配,使之处于待用状态。
  • spring-context-support模块是对Spring IoC容器及IoC子容器的扩展支持。
  • spring-context-indexer模块是Spring的类管理组件和Classpath扫描组件。
  • spring-expression 模块是统一表达式语言(EL)的扩展模块,可以查询、管理运行中的对象,同时也可以方便地调用对象方法,以及操作数组、集合等。它的语法类似于传统EL,但提供了额外的功能,最出色的要数函数调用和简单字符串的模板函数。EL的特性是基于Spring产品的需求而设计的,可以非常方便地同Spring IoC进行交互。

bean概述

Spring 就是面向 Bean 的编程(BOP,Bean Oriented Programming),Bean 在 Spring 中处于核心地位。Bean对于Spring的意义就像Object对于OOP的意义一样,Spring中没有Bean也就没有Spring存在的意义。Spring IoC容器通过配置文件或者注解的方式来管理bean对象之间的依赖关系。

spring中bean用于对一个类进行封装。如下面的配置:

下面的配置是对一个bean对象的相关数据的配置,后续将会加载这些bean的配置文件并解析

1
2
3
4
<bean id="userService" class="com.itheima.service.impl.UserServiceImpl">
<property name="userDao" ref="userDao"></property>
</bean>
<bean id="userDao" class="com.itheima.dao.impl.UserDaoImpl"></bean>

为什么Bean如此重要呢?

  • spring 将bean对象交由一个叫IOC容器进行管理。
  • bean对象之间的依赖关系在配置文件中体现,并由spring完成。

Spring IOC相关接口分析

BeanFactory解析

Spring中Bean的创建是典型的工厂模式,这一系列的Bean工厂,即IoC容器,为开发者管理对象之间的依赖关系提供了很多便利和基础服务,在Spring中有许多IoC容器的实现供用户选择,其相互关系如下图所示。

beanFactory的关系图

其中,BeanFactory作为最顶层的一个接口,定义了IoC容器的基本功能规范,BeanFactory有三个重要的子接口:ListableBeanFactory、HierarchicalBeanFactory和AutowireCapableBeanFactory。但是从类图中我们可以发现最终的默认实现类是DefaultListableBeanFactory,它实现了所有的接口。

那么为何要定义这么多层次的接口呢?

每个接口都有它的使用场合,主要是为了区分在Spring内部操作过程中对象的传递和转化,对对象的数据访问所做的限制,例如,

  • ListableBeanFactory接口表示这些Bean可列表化。
  • HierarchicalBeanFactory表示这些Bean 是有继承关系的,也就是每个 Bean 可能有父 Bean
  • AutowireCapableBeanFactory 接口定义Bean的自动装配规则。

这三个接口共同定义了Bean的集合、Bean之间的关系及Bean行为。最基本的IoC容器接口是BeanFactory,来看一下它的源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public interface BeanFactory {

String FACTORY_BEAN_PREFIX = "&";

//根据bean的名称获取IOC容器中的的bean对象
Object getBean(String name) throws BeansException;
//根据bean的名称获取IOC容器中的的bean对象,并指定获取到的bean对象的类型,这样我们使用时就不需要进行类型强转了
<T> T getBean(String name, Class<T> requiredType) throws BeansException;
Object getBean(String name, Object... args) throws BeansException;
<T> T getBean(Class<T> requiredType) throws BeansException;
<T> T getBean(Class<T> requiredType, Object... args) throws BeansException;

<T> ObjectProvider<T> getBeanProvider(Class<T> requiredType);
<T> ObjectProvider<T> getBeanProvider(ResolvableType requiredType);

//判断容器中是否包含指定名称的bean对象
boolean containsBean(String name);
//根据bean的名称判断是否是单例
boolean isSingleton(String name) throws NoSuchBeanDefinitionException;
boolean isPrototype(String name) throws NoSuchBeanDefinitionException;
boolean isTypeMatch(String name, ResolvableType typeToMatch) throws NoSuchBeanDefinitionException;
boolean isTypeMatch(String name, Class<?> typeToMatch) throws NoSuchBeanDefinitionException;
@Nullable
Class<?> getType(String name) throws NoSuchBeanDefinitionException;
String[] getAliases(String name);
}

**在BeanFactory里只对IoC容器的基本行为做了定义,根本不关心你的Bean是如何定义及怎样加载的(即只关心获取bean对象,至于具体bean对象是如何加载和初始化的交由具体的实现类来实现)**。正如我们只关心能从工厂里得到什么产品,不关心工厂是怎么生产这些产品的

BeanFactory有一个很重要的子接口,就是ApplicationContext接口,该接口主要来规范容器中的bean对象是非延时加载,即在创建容器对象的时候就对象bean进行初始化,并存储到一个容器中。

beanFactory的子接口applicationContext

要知道工厂是如何产生对象的,我们需要看具体的IoC容器实现,Spring提供了许多IoC容器实现,比如:

  • ClasspathXmlApplicationContext : 根据类路径加载xml配置文件,并创建IOC容器对象。
  • FileSystemXmlApplicationContext :根据系统路径加载xml配置文件,并创建IOC容器对象。
  • AnnotationConfigApplicationContext :加载注解类配置,并创建IOC容器。

BeanDefinition解析

Spring IoC容器管理我们定义的各种Bean对象及其相互关系,而Bean对象在Spring实现中是以BeanDefinition来描述的,如下面配置文件

BeanDefinition是用来描述和存储bean对象的配置文件信息,并且还可以描述bean与bean之间的依赖关系

BeanDefintion同样被设计成抽象的,因为描述的bean的方式有很多种,常见的有xml配置文件,propertity配置文件,注解等等,需要根据具体的业务场景进行实际的选择,同时设计成抽象的也方便后续拓展描述bean配置文件的方式

1
2
3
4
<bean id="userDao" class="com.itheima.dao.impl.UserDaoImpl"></bean>

bean标签还有很多属性:
scope、init-method、destory-method等。

其继承体系如下图所示。

BeanDefinition的关系图

BeanDefinitionReader解析

Bean的解析过程非常复杂,功能被分得很细,因为这里需要被扩展的地方很多,必须保证足够的灵活性,以应对可能的变化,Bean的解析主要就是对Spring配置文件的解析。这个解析过程主要通过BeanDefinitionReader来完成,看看Spring中BeanDefinitionReader的类结构图,如下图所示

既然bean的描述方式多种多样,那么bean的解析方式也多种多样,必须设计成抽象的,方便后续根据实际业务场景需要进行拓展和替换

BeanDefinitionReader的关系图

看看BeanDefinitionReader接口定义的功能来理解它具体的作用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public interface BeanDefinitionReader {

//获取BeanDefinitionRegistry注册器对象,注册表对象用于存储加载好的bean配置文件信息
BeanDefinitionRegistry getRegistry();

//抽象的配置文件加载器,用来了解析和加载bean的配置文件
@Nullable
ResourceLoader getResourceLoader();

//获取类加载器,用于加载指定路径下的配置文件
@Nullable
ClassLoader getBeanClassLoader();

BeanNameGenerator getBeanNameGenerator();

/*
下面的loadBeanDefinitions都是加载bean定义,从指定的资源中
*/
int loadBeanDefinitions(Resource resource) throws BeanDefinitionStoreException;
int loadBeanDefinitions(Resource... resources) throws BeanDefinitionStoreException;
int loadBeanDefinitions(String location) throws BeanDefinitionStoreException;
int loadBeanDefinitions(String... locations) throws BeanDefinitionStoreException;
}

BeanDefinitionRegistry解析

BeanDefinitionReader用来解析bean定义,并封装BeanDefinition对象,而我们定义的配置文件中定义了很多bean标签,所以就有一个问题,解析的BeanDefinition对象存储到哪儿?答案就是BeanDefinition的注册中心,而该注册中心顶层接口就是BeanDefinitionRegistry

将解析好的bean的配置文件信息对象全部存储到注册表BeanDefinitionRegistry中,因为bean的配置文件中定义多了bean对象的配置文件,所以必须有一个配置文件信息的容器来进行存储,同样存储bean配置文件的容器实现也有很多种,必须设计成抽象的,方便后续的拓展和替换

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public interface BeanDefinitionRegistry extends AliasRegistry {

//往注册表中注册bean
void registerBeanDefinition(String beanName, BeanDefinition beanDefinition)
throws BeanDefinitionStoreException;

//从注册表中删除指定名称的bean
void removeBeanDefinition(String beanName) throws NoSuchBeanDefinitionException;

//获取注册表中指定名称的bean
BeanDefinition getBeanDefinition(String beanName) throws NoSuchBeanDefinitionException;

//判断注册表中是否已经注册了指定名称的bean
boolean containsBeanDefinition(String beanName);

//获取注册表中所有的bean的名称
String[] getBeanDefinitionNames();

int getBeanDefinitionCount();
boolean isBeanNameInUse(String beanName);
}

继承结构图如下:

BeanDefinitionRegistry的关系图

从上面类图可以看到BeanDefinitionRegistry接口的子实现类主要有以下几个:

为什么使用 ConcurrentHashMaps而不使用HashMap,因为前者是线程安全的,后者是线程不安全的

  • DefaultListableBeanFactory

    在该类中定义了如下代码,就是用来注册bean

    1
    private final Map<String, BeanDefinition> beanDefinitionMap = new ConcurrentHashMap<>(256);
  • SimpleBeanDefinitionRegistry

    在该类中定义了如下代码,就是用来注册bean

    1
    private final Map<String, BeanDefinition> beanDefinitionMap = new ConcurrentHashMap<>(64);

创建容器

创建bean的IOC容器总共两个过程

1.加载和解析bean的配置文件,并将解析好的bean配置文件信息注册到注册表中

2.利用获取到的bean的配置文件信息初始化bean对象并将bean对象注册到IOC容器中

创建bean的IOC容器由beanFactory的具体实现类调用相应的方法,比如ClassPathXmlApplicationContext的方法就会调用refresh()方法来完成创建IOC容器的过程,因为IOC容器的创建算法是固定的,但里面很多步骤依赖于具体的环境变化而变化,所以使用模板方法的设计模式

创建IOC容器的相关类解析

BeanDefinition:负责描述和存储bean的配置文件信息

BeanDefinitionReader:负责加载和解析bean的配置文件信息

BeanDefinitionRegistry:负责将解析好的bean配置文件信息注册到注册表中

BeanFactory:负责创建IOC容器并提过获取IOC容器bean对象的方法

自定义SpirngIOC容器

pojo包:负责描述和存储Bean的配置文件信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
/**
* BeanDefinition类用来封装bean的配置文件信息,
* 主要包含id(即bean对象的名称)
* class(需要交由spring管理的类的全类名)
* 及子标签property数据。
*
*/
public class BeanDefinition {
private String id;
private String className;
private MutablePropertyValues propertyValues;

public BeanDefinition() {
propertyValues = new MutablePropertyValues();
}


public String getId() {
return id;
}

public void setId(String id) {
this.id = id;
}

public String getClassName() {
return className;
}

public void setClassName(String className) {
this.className = className;
}

public MutablePropertyValues getPropertyValues() {
return propertyValues;
}

public void setPropertyValues(MutablePropertyValues propertyValues) {
this.propertyValues = propertyValues;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
/**
* 一个bean标签可以有多个property子标签,所以再定义一个MutablePropertyValues类,用来存储并管理多个PropertyValue对象
* 所以本类是PropertyValue的容器对象,负责对PropertyValue对象进行管理(增删改查,判断是否为空等功能)
*/
public class MutablePropertyValues implements Iterable<PropertyValue>{

private final List<PropertyValue> propertyValueList;

public MutablePropertyValues() {
propertyValueList = new ArrayList<PropertyValue>();
}

public MutablePropertyValues(List<PropertyValue> propertyValueList) {
if (propertyValueList == null){
this.propertyValueList = new ArrayList<PropertyValue>();
}else {
this.propertyValueList = propertyValueList;
}
}

//获取容器的迭代器,可以使用迭代器进行遍历操作
public Iterator<PropertyValue> iterator() {
return propertyValueList.iterator();
}

//获取容器中的所有值,并以数组的形式返回
public PropertyValue[] getPropertyValues(){
return propertyValueList.toArray(new PropertyValue[0]);
}

//根据Property属性的name值获取指定的Property对象
public PropertyValue getPropertyValue(String name){
for (PropertyValue propertyValue : propertyValueList) {
if (propertyValue.getName().equals(name)){
return propertyValue;
}
}
return null;
}

//判断容器是否为空
public boolean isEmpty() {
return this.propertyValueList.isEmpty();
}

//添加元素到容器中,如果容器中有重复元素则进行覆盖,并且返回容器对象方便进行链式编程
public MutablePropertyValues addPropertyValue(PropertyValue pv) {
for (int i = 0; i < this.propertyValueList.size(); i++) {
PropertyValue currentPv = this.propertyValueList.get(i);
if (currentPv.getName().equals(pv.getName())) {
this.propertyValueList.set(i, new PropertyValue(pv.getName(),pv.getRef(), pv.getValue()));
return this;
}
}
this.propertyValueList.add(pv);
return this;
}

//根据元素的名字判断容器中是否有指定的元素
public boolean contains(String propertyName) {
return getPropertyValue(propertyName) != null;
}


}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
/**
* PropertyValue类是对应bean标签中的子标签property的相关属性数据
*/
public class PropertyValue {

private String name;
private String ref;
private String value;

public PropertyValue() {
}

public PropertyValue(String name, String ref, String value) {
this.name = name;
this.ref = ref;
this.value = value;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public String getRef() {
return ref;
}

public void setRef(String ref) {
this.ref = ref;
}

public String getValue() {
return value;
}

public void setValue(String value) {
this.value = value;
}
}

support包:负责加载和解析bean的配置文件信息,并将加载好的配置文件信息存储到注册表中

BeanDefinitionReader类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* BeanDefinitionReader是用来解析bean的配置文件并在注册表中注册bean的解析数据,定义了两个规范:
*
* 获取注册表的功能,让外界可以通过该对象获取注册表对象。
* 加载和解析配置文件,并注册bean的配置文件信息
*
* 之所以设计成抽象的是因为解析bean配置文件的方式有很多种,方便后续进行拓展
*/
public interface BeanDefinitionReader {

//获取注册表对象
BeanDefinitionRegistry getRegistry();
//加载配置文件并在注册表中进行注册
void loadBeanDefinitions(String configLocation) throws Exception;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
/**
* 使用xml配置文件加载和解析bean的配置文件信息,并将解析好的bean配置文件信息注册到注册表中
*
* 在bean的配置文件和bean的容器中间抽取了bean解析器类,降低了配置文件和bean配置文件信息容器类(就是注册表)的耦合,符合设计模式中的迪米特原则
*/
public class XmlBeanDefinitionReader implements BeanDefinitionReader {

private BeanDefinitionRegistry registry;

public XmlBeanDefinitionReader() {
this.registry = new SimpleBeanDefinitionRegistry();
}

@Override
public BeanDefinitionRegistry getRegistry() {
return registry;
}

@Override
public void loadBeanDefinitions(String configLocation) throws Exception {

//使用本类的类加载器来加载bean的配置文件
InputStream is = this.getClass().getClassLoader().getResourceAsStream(configLocation);
SAXReader reader = new SAXReader();
Document document = reader.read(is);
Element rootElement = document.getRootElement();
//解析bean标签
parseBean(rootElement);
}

//解析beans标签中的所有bean标签
private void parseBean(Element rootElement) {

List<Element> elements = rootElement.elements();
for (Element element : elements) {
//解析bean标签
String id = element.attributeValue("id");
String className = element.attributeValue("class");
BeanDefinition beanDefinition = new BeanDefinition();
beanDefinition.setId(id);
beanDefinition.setClassName(className);
//解析bean标签中的子标签property
List<Element> list = element.elements("property");
MutablePropertyValues mutablePropertyValues = new MutablePropertyValues();
for (Element element1 : list) {
String name = element1.attributeValue("name");
String ref = element1.attributeValue("ref");
String value = element1.attributeValue("value");
PropertyValue propertyValue = new PropertyValue(name,ref,value);
mutablePropertyValues.addPropertyValue(propertyValue);
}
beanDefinition.setPropertyValues(mutablePropertyValues);

//将解析好的bean对象注册到注册表中
registry.registerBeanDefinition(id,beanDefinition);
}
}
}

BeanDefinitionRegistry类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package com.czq.spring.framework.beas.factory.support;

import com.czq.spring.framework.beas.factory.pojo.BeanDefinition;

/**
*
* 定义bean的注册表类,注册表是用来存储解析好的bean配置文件信息的容器
* 负责管理bean的配置文件信息(增删改查,判空等功能)
*
* 设计成抽象是因为装载bean定义信息的容器实现有很多种,方便根据具体的业务场景替换具体的实现
*
*/
public interface BeanDefinitionRegistry {

//注册BeanDefinition对象到注册表中
void registerBeanDefinition(String beanName, BeanDefinition beanDefinition);

//从注册表中删除指定名称的BeanDefinition对象
void removeBeanDefinition(String beanName) throws Exception;

//根据名称从注册表中获取BeanDefinition对象
BeanDefinition getBeanDefinition(String beanName) throws Exception;

boolean containsBeanDefinition(String beanName);

int getBeanDefinitionCount();

String[] getBeanDefinitionNames();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
package com.czq.spring.framework.beas.factory.support;

import com.czq.spring.framework.beas.factory.pojo.BeanDefinition;

import java.util.HashMap;
import java.util.Map;

/**
* 注册表的具体实现类
*/
public class SimpleBeanDefinitionRegistry implements BeanDefinitionRegistry {

//map是用来存储bean的配置文件信息对象的容器
private Map<String, BeanDefinition> beanDefinitionMap = new HashMap<String, BeanDefinition>();

@Override
public void registerBeanDefinition(String beanName, BeanDefinition beanDefinition) {
beanDefinitionMap.put(beanName,beanDefinition);
}

@Override
public void removeBeanDefinition(String beanName) throws Exception {
beanDefinitionMap.remove(beanName);
}

@Override
public BeanDefinition getBeanDefinition(String beanName) throws Exception {
return beanDefinitionMap.get(beanName);
}

@Override
public boolean containsBeanDefinition(String beanName) {
return beanDefinitionMap.containsKey(beanName);
}

@Override
public int getBeanDefinitionCount() {
return beanDefinitionMap.size();
}

@Override
public String[] getBeanDefinitionNames() {
return beanDefinitionMap.keySet().toArray(new String[1]);
}
}

context包:负责创建IOC容器并获取bean对象的相关方法

1
2
3
4
5
6
7
8
9
/**
* 最顶层的工厂对象BeanFactory,负责定义获取bean对象的规范(根据名称获取,根据类型获取)
*/
public interface BeanFactory {
//根据bean对象的名称获取bean对象
Object getBean(String name) throws Exception;
//根据bean对象的名称获取bean对象,并进行类型转换
<T> T getBean(String name, Class<? extends T> clazz) throws Exception;
}
1
2
3
4
5
6
7
8
/**
* 具体的bean工厂类对象(IOC容器)
* 负责规范容器中的bean对象是非延时加载,即在创建容器对象的时候就对象bean进行初始化,并存储到一个容器中
*/
public interface ApplicationContext extends BeanFactory {
//进行配置文件加载并进行对象创建
void refresh() throws IllegalStateException, Exception;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
/**
* 负责创建IOC容器
* 创建IOC容器的过程是
* 1.加载并解析bean的配置文件信息,将bean的配置文件信息注册到注册表中
* 2.初始化(创建)bean对象,将bean对象注册到容器中(IOC容器)
*
* 之所以设计成抽象的类是因为getBean的方式有很多种不同的实现,方便后续拓展和维护
*/
public abstract class AbstractApplicationContext implements ApplicationContext {

//负责加载bean的配置文件并将bean对象注册到注册表中
//加载bean的配置方式也有很多种,所以也设计成抽象的
protected BeanDefinitionReader beanDefinitionReader;

//用来存储bean对象的容器 key存储的是bean的id值,value存储的是bean对象(IOC容器)
protected Map<String, Object> singletonObjects = new HashMap<String, Object>();

//存储配置文件的路径
protected String configLocation;

//refresh方法使用了模板方法设计模式,算法的骨架是加载bean的配置文件,初始化bean对象,将bean对象注册到容器中
//加载bean的配置是可以根据环境变化而变化的,加载和解析bean配置文件的方式也有很多种
//将bean对象注册到容器中也是可以根据环境变化而变化的,因为容器的实现有很多种
@Override
public void refresh() throws IllegalStateException, Exception {
//加载bean的配置文件,并获取解析好的beanDefinition的数据(bean定义的相关数据)
beanDefinitionReader.loadBeanDefinitions(configLocation);

//利用已经加载好的bean配置文件信息对bean进行初始化化并加载到bean的容器中
finishBeanInitialization();

}


private void finishBeanInitialization() throws Exception{
//获取bean的所有配置文件信息
BeanDefinitionRegistry registry = beanDefinitionReader.getRegistry();
String[] beanNames = registry.getBeanDefinitionNames();
for (String beanName : beanNames) {
getBean(beanName);
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
/**
* 1.具体创建IOC容器的实现
* 2.具体获取bean对象的实现
*
*/
public class ClassPathXmlApplicationContext extends AbstractApplicationContext{


public ClassPathXmlApplicationContext(String configLocation) {
this.configLocation = configLocation;
//具体的加载bean配置文件的实现方式
this.beanDefinitionReader = new XmlBeanDefinitionReader();

try {
//加载bean的配置文件信息并注册到注册表中
this.refresh();
} catch (Exception e) {
e.printStackTrace();
}

}

@Override
public Object getBean(String name) throws Exception {
//1.判断ioc容器中是否有该bean对象,如果有则直接返回
Object obj = singletonObjects.get(name);
if (obj !=null){
return obj;
}

//2.如果没有则根据bean的配置文件信息对象创建对应的bean对象并将其加载到ioc容器中
BeanDefinitionRegistry registry = beanDefinitionReader.getRegistry();
BeanDefinition beanDefinition = registry.getBeanDefinition(name);
String className = beanDefinition.getClassName();
Class<?> clazz = Class.forName(className);
Object beanObj = clazz.newInstance();
MutablePropertyValues propertyValues = beanDefinition.getPropertyValues();
for (PropertyValue propertyValue : propertyValues) {
String propertyValueName = propertyValue.getName();
//处理bean对象属性为对象类型的注入
String ref = propertyValue.getRef();
if (ref!=null && !"".equals(ref)){
//递归调用获取bean对象,处理不当会产生循环依赖问题
Object propertyBeanObj = getBean(propertyValueName);
String methodName = StringUtils.getSetterMethodNameByFieldName(propertyValueName);
// Method method = clazz.getDeclaredMethod(methodName);
// method.invoke(beanObj,propertyBeanObj);
Method[] methods = clazz.getMethods();
for (Method method : methods) {
if (method.getName().equals(methodName)){
method.invoke(beanObj,propertyBeanObj);
}
}
}

String value = propertyValue.getValue();
//处理bean对象属性为基本类型和String类型的诸如
if (value!=null && !"".equals(value)){
String methodName = StringUtils.getSetterMethodNameByFieldName(propertyValueName);
Method method = clazz.getMethod(methodName, String.class);
method.invoke(beanObj,value);
}
}

//3.将初始化好的bean对象装载到ioc容器中
singletonObjects.put(name,beanObj);

return beanObj;
}

@Override
public <T> T getBean(String name, Class<? extends T> clazz) throws Exception {

Object bean = getBean(name);
if (bean == null){
return null;
}

return clazz.cast(bean);
}
}

工具包

1
2
3
4
5
6
7
public class StringUtils {

//负责拼接set+属性名得到set属性方法的方法名再进行反射调用,注册相关的依赖属性
public static String getSetterMethodNameByFieldName(String fieldName){
return "set"+fieldName.substring(0,1).toUpperCase()+fieldName.substring(1);
}
}