使用书籍:《大话设计模式》——程杰著

相关代码:Gitee

使用设计模式的目的:可维护、可扩展、可复用、灵活性好 装逼,写一些别人看不懂的代码

一、简单工厂模式

个人理解:通过创建同一类型下的不同子类来实现多态,难点在于定义抽象类,需要找出共同特征,确保需要实现的方法返回值相同,参数一致

二、策略模式

  • 面向对象的编程,并不是类越多越好,类的划分是为了封装,但分类的基础是抽象,具有相同属性和功能的对象的抽象集合才是类
  • 用工厂来生成算法对象,这没有错,但算法本身只是一种策略,最重要的是这些算法是随时都可能互相替换的,这就是变化点,而封装变化点是我们面向对象的一种很重要的思维方式
  • 策略模式是一种定义一系列算法的方法,从概念上来看,所有这些算法完成的都是相同的工作,只是实现不同,它可以以相同的方式调用所有的算法,减少了各种算法类与使用算法类之间的耦合
  • 当不同的行为堆砌在一个类中时,就很难避免使用条件语句来选择合适的行为。将这些行为封装在一个个独立的 Strategy类中,可以在使用这些行为的类中消除条件语句

个人理解:在简单工厂模式的基础上增加了一层 context,使得具体的业务策略与客户端分离,减少了各种算法类与使用算法类之间的耦合

三、单一职责原则

就一个类而言,应该仅有一个引起它变化的原因

四、开放封闭原则

  • 对于扩展是开放的,对于修改是封闭的
  • 我们在做任何系统的时候,都不要指望系统一开始时需求确定,就再也不会变化,这是不现实也不科学的想法
  • 开发人员应该仅对程序中呈现出频繁变化的那些部分 做出抽象,然而, 对于应用程序中的每个部分都刻意地进行抽象同样不是一个好主意。拒绝不成熟的抽象和抽象本身一样重要

五、依赖倒转原则

  • 抽象不应该依赖细节,细节应该依赖于抽象
    • 针对接口编程,不要对实现编程
  • 里氏代换原则:子类型必须能够替换掉它们的父类型
    • 一个软件实体如果使用的是一个父类的话,那么一定适用于其子类,而且它察觉不出父类对象和子类对象的区别。也就是说,在软件里面,把父类都替换成它的子类,程序的行为没有变化
    • 也正因为有了这个原则,使得继承复用成为可能。只有当子类可以替换掉父类,软件单位的功能不受到影响时,父类才能真正被复用,而子类也能够在父类的基础上增加新的行为
    • 正是由于子类型的可替换性才使得使用父类类型的模块在无须修改的情况下就可以扩展

六、装饰模式

  • 动态地给一个对象添加一些额外的职责
  • 把类的装饰功能从类中搬移去除,这样可以简化原有的类
  • 有效地把类的核心功能和装饰功能区分开了,而且可以去除相关类中重复的装饰逻辑

个人理解:首先区分出装饰者与被装饰者,比如人被球鞋,裤子,T恤装饰;将所有的装饰者提取出一个父类,该父类与被装饰者具备一个共同特征(需要抽象为接口),其实就是需要装饰的具体功能,比如人和衣物能够对外展示;该父类还需提供一个被装饰者的属性,这个例子中,衣物的展示本质上是调用被装饰者的展示;具体的子类在装饰功能中不能忘记以 super 的方式调用上级装饰

七、代理模式

为其他对象提供一种代理以控制对这个对象的访问

应用场景

  • 远程代理,也就是为一个对象在不同的地址空间提供局部代表。这样可以隐藏一个对象存在于不同地址空间的事实
  • 虚拟代理,是根据需要创建开销很大的对象。通过它来存放实例化需要很长时间的真实对象
  • 安全代理,用来控制真实对象访问时的权限
  • 智能指引,是指当调用真实的对象时,代理处理另外一些事

个人理解:找到需要被代理的类,根据它的行为创建出代理类(实现同样的行为接口);在代理类中具有被代理类的一个属性,可以在代理类初始化的时候为被代理类赋值;代理类的行为实际上是调用被代理类的行为方法

八、工厂方法模式

定义一个用于创建对象的接口,让子类决定实例化哪一个类。工厂方法使一个类的实例化延迟到其子类,是对简单工厂模式的进一步抽象和推广,本质是对获取对象过程的抽象

个人理解:在简单工厂的基础上,将具有共同特征的子类又划分了一层工厂,使用者根据参数决定使用哪个工厂,工厂来负责实例化具体对象

九、原型模式

从一个对象再创建另外一个可定制的对象,而且不需要知道任何创建的细节

一般在初始化的信息不发生变化的情况下,克隆是最好的办法。这既隐藏了对象创建的细节,又对性能是大大的提高。不用重新初始化对象,而是动态地获得对象运行时的状态

super.clone() 方法

如果字段是值类型的,则对该字段进行逐位复制,如果字段是引用类型,则复制引用但不复制引用的对象;因此,原始对象及其副本引用同一对象

个人理解:一种能创建对象且复制对象属性值的方式,需要实现 Cloneable 接口,重写 clone 方法。需要注意的是,如果是引用类型的属性,则该引用类型也要重写 clone 方法,这样引用的才不会是原始对象。

重写示例
1
2
3
4
5
6
7
8
9
10
@Override
public WorkDemo clone() {
WorkDemo object;
try {
object = ((WorkDemo) super.clone());
} catch (CloneNotSupportedException e) {
throw new RuntimeException("clone失败");
}
return object;
}

十、模板方法模式

定义一个操作中的算法的骨架,而将一些步骤延迟到子类中。模板方法使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤

模板方法模式是通过把不变行为搬移到超类,去除子类中的重复代码来体现它的优势

个人理解:定义一个抽象类,提供一些抽象方法交由子类实现,这些抽象方法只有抽象类中的普通方法 能调用,在普通方法中提供通用实现

十一、迪米特法则

迪米特法则,也叫最少知识原则:如果两个类不必彼此直接通信,那么这两个类就不应当发生直接的相互作用。如果其中一个类需要调用另一个类的某一个方法,可以通过第三者转发这个调用

在类的结构设计上,每一个类都应当尽量降低成员的访问权限
类之间的耦合越弱,越有利于复用,一个处在弱耦合的类被修改,不会对有关系的类造成波及

十二、外观模式

为子系统中的一组接口提供一个一致的界面,此模式定义了一个高层接口,这个接口使得这一子系统更加容易使用

使用场景

  • 首先,在设计初期阶段,应该要有意识地将不同的两个层分离
  • 其次,在开发阶段,子系统往往因为不断地重构演化而变得越来越复杂。增加外观 Facade 可以提供一个简单的接口,减少它们之间的依赖
  • 第三,在维护一个遗留的大型系统时,可能这个系统已经非常难以维护和扩展了

个人理解:其实就是分层的概念,由上层来封装下一层具体的调用过程

十三、建造者模式

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

指挥者:用它来控制建造过程,也用它来隔离用户与建造过程的关联

建造者模式是在当创建复杂对象的算法应该独立于该对象的组成部分以及它们的装配方式时适用的模式

个人理解:核心部分为一个指挥者类和一个建造者的抽象类,指挥者需要对外提供一个方法,负责去调用建造者中的抽象方法,这个方法就是建造过程,参数为一名建造者

十四、观察者模式

观察者模式定义了一种一对多的依赖关系,让多个观察者对象同时监听某一个主题对象。这个主题对象在状态发生变化时,会通知所有观察者对象,使它们能够自动更新自己。

使用场景

  • 当一个对象的改变需要同时改变其他对象的时候
  • 不知道具体有多少对象需要改变
  • 当一个抽象模型有两个方面,其中一方面依赖于另一方面,这时使用观察者模式可以将这两者封装在独立的对象中使它们各自独立地改变和复用

个人理解:两个关键角色,抽象观察者和抽象通知者。抽象观察者中通常具备一个事件类型的属性,再提供一个改变自身行为的抽象方法,由通知者来调用;抽象通知者中会有一个抽象观者者类型的数组,这里就是所有需要通知的观察者,并提供方法入口用来添加和删除它们;当通知者发起通知时,就是遍历该数组,提供事件类型,并调用观察者中的改变方法

十五、抽象工厂模式

提供一个创建一系列相关或相互依赖对象的接口,而无须指定它们具体的类

个人理解:提供一系列接口,由访问者类根据情况决定具体实例化某个类

十六、状态模式

状态模式主要解决的是当控制一个对象状态转换的条件表达式过于复杂时的情况。把状态的判断逻辑转移到表示不同状态的一系列类当中,可以把复杂的判断逻辑简化。

当一个对象的行为取决于它的状态,并且它必须在运行时刻根据状态改变它的行为时,就可以考虑使用状态模式了。

状态模式的好处是将与特定状态相关的行为局部化,并且将不同状态的行为分割开来。

个人理解:提供一个实体类和一个抽象状态类,实体类中可以设置不同的状态(初始化实体的时候可以提供一个状态),抽象状态类则有一个抽象的行为方法,参数为实体类;实体类中需要提供一个工作方法,用来调用自身当前状态的行为方法;在抽象状态类的行为方法中,子类在这里实现不同的行为,且可以改变实体的当前状态,改变状态之后再调用实体的工作方法。这样就完成了一个事物经过不同状态时的变化

十七、适配器模式

适配器模式主要应用于希望复用一些现存的类,但是接口又与复用环境要求不一致的情况

个人理解:核心思想也是加了一层,当通用的调用逻辑不满足时,新建一个具体的实现类,由具体实现类来处理新的逻辑,通用的调用逻辑改为调用该新类的逻辑。

十八、备忘录模式

在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态。这样以后就可将该对象恢复到原先保存的状态

  • Originator(发起人):负责创建一个备忘录Memento,用以记录当前时刻它的内部状态,并可使用备忘录恢复内部状态。Originator可根据需要决定Memento存储Originator的哪些内部状态
  • Memento(备忘录):负责存储Originator对象的内部状态,并可防止Originator以外的其他对象访问。备忘录有两个接口,Caretaker只能看到备忘录的窄接口,它只能将备忘录传递给其他对象。Originator能够看到一个宽接口,允许它访问返回到先前状态所需的所有数据
  • Caretaker(管理者):负责保存好备忘录Memento,不能对备忘录的内容进行操作或检查

个人理解:提供一个备忘录类,能够保存目标实体中的属性值;而目标实体中需要自己决定如何创建备忘录,备忘录中保存哪些数据,同时还要提供一个能根据备忘录恢复的方法;还要有一个管理者类,专门负责保管备忘录(内存中)

十九、组合模式

整体与部分可以一致对待

何时使用组合模式

希望可以忽略组合对象与单个对象的不同,统一地使用组合结构中的所有对象时,就可以考虑使用组合模式

个人理解:适用于功能一致的一组对象,实现同一个抽象类,每一层只实现自己需要的功能,常用于表示父子组合;“父”中会有一个 children 属性,当“父”实现自身功能时,可以遍历 children 并调用其中的”子”功能(各司其职,“父”的职责就是调用”子“的功能)

二十、迭代器模式

迭代器模式就是分离了集合对象的遍历行为,抽象出一个迭代器类来负责,这样既可以做到不暴露集合的内部结构,又可以让外部代码透明地访问集合内部的数据。

个人理解:其实就是各大语言中都有的遍历的概念,讲究一个遍历的规则

二十一、单例模式

保证一个类仅有一个单例,并提供一个访问它的全局访问点

单例模式因为单例类封装它的唯一实例,这样它可以严格地控制客户怎样访问它以及何时访问它,简单来说就是对唯一实例的受控访问

  • 饿汉式单例类:静态初始化,在自己被加载的时候就将自己实例化。会提前占用系统资源
  • 懒汉式单例类:在第一次被引用时,才会进行实例化。面临多线程访问的安全性问题,需要做双重否定的处理

个人理解:隐藏构造方法,提供一个静态的 getInstance 方法。

代码示例
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 Singleton {

private volatile static Singleton instance;
// 静态初始化
// private static Singleton instance = new Singleton();

private Singleton() {

}

/**
* 双重否定
*/
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}

二十二、桥接模式

桥接模式:将抽象部分与它的实现部分分离,使它们都可以独立地变化

尽量使用合成/聚合,而不是使用类继承

聚合表示一种弱的“拥有”关系,体现的是A对象可以包含B对象,但B对象不是A对象的一部分;合成则是一种强的“拥有”关系,体现了严格的部分和整体关系

个人理解:将A类对象和B类对象抽象出两个类,A抽象类中有B抽象类的属性并提供赋值方法,使它们能够自由组合

二十三、命令模式

命令模式把请求一个操作的对象与知道怎么执行一个操作的对象分隔开

个人理解:需要有一个抽象命令类与命令执行类,命令执行类为抽象命令类中的一个属性,抽象方法为执行命令,我们使用的时候是使用具体的命令类

二十四、职责链模式

使多个对象都有机会处理请求,从而避免请求的发送者和接收者之间的耦合关系。将这个对象连成一条链,并沿着这条链传递该请求,直到有一个对象处理它为止

职责链可简化对象的相互连接,它们仅需保持一个指向其后继者的引用,而无需保持它所有的候选接受者的引用

个人理解:首先封装一个条件类,该条件类通常具备一些条件类型,条件内容,数量等字段;然后抽象出一个管理者类,也就是来处理条件的,一定要有一个处理条件的抽象方法;管理者们通常会有层级之分,所以管理者的抽象类会有一个类型为自身的属性,这个属性代表上级管理者;在处理条件的方法中,如果当前管理者发现自身处理不了该条件,则需要将条件传递给上级去处理

二十五、中介者模式

用一个中介对象来封装一系列的对象交互。中介者使各对象不需要显示地相互引用,从而使其耦合松散,而且可以独立地改变它们之间的交互

中介者模式一般应用于一组对象已定义良好但是复杂的方式进行通信的场合。

个人理解:首先有一个抽象的实体类和一个抽象的中介者类,抽象实体类中有一个抽象中介者类的属性;它们都有一个声明的方法需要子类去实现,不过中介者的声明方法中会多一个实体类的参数,而实体类中的声明方法本质上是在调用中介者类的声明方法,并且是将自身作为参数,委托给中介者类去执行实体类中的其他业务方法

二十六、享元模式

运用共享技术有效地支持大量细粒度的对象

在程序设计中,有时需要生成大量细粒度的类实例来表示数据。当这些实例除了几个参数外基本相同时,如果能将参数移到类实例的外面,并在方法调用时将它们传递进来,就可以通过共享大幅度地减少单个实例的数量。

个人理解:就是利用内存减少创建对象的数量。一个工厂类中维护着一个 Map,这里存放的都是具有相同特征的实体

二十七、解释器模式

如果一种特定类型的问题发生的概率足够高,那么可能就值得将该问题的各个实例表述为一个简单语言中的句子。这样就可以构建一个解释器,该解释器通过解释这些句子来解决该问题

解释器模式的不足:解释器模式为文法中的每一条规定都至少定义了一个类,因此包含许多规则的文法可能难以管理和维护。建议当文法非常复杂时,使用其他的技术如语法分析程序或编译器生成器来处理

个人理解:有一个抽象表达式类,需要子类实现解释操作,解释的对象定义一个公共的实体…

二十八、访问者模式

访问者模式适用于数据结构相对稳定的系统

它把数据结构和作用于结构之上的操作之间的耦合解脱开,使得操作集合可以相对自由地演化

个人理解:重点在于定义一个抽象实体类和一个状态抽象类,抽象实体类中有一个应用的抽象方法,参数就是一个状态抽象类,这个方法的目的是实体针对不同状态的处理;可预计的抽象实体类有多少个子类,在状态抽象类中就会有个多少个抽象方法,这些方法的含义是当前状态遇到某种实体时的反应,参数为具体的某一种实体类型;在抽象实体类的实现中,应用方法其实就是去调用状态抽象类中对应自己为参数的那一个抽象方法,写法是以 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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
/**
* 抽象实体类
* 人类
*/
public abstract class Person {

public abstract void accept(Action visitor);
}

/**
* 男人
*/
public class Man extends Person {

/**
* 去调用状态抽象类中对应自己为参数的那个抽象方法
*/
@Override
public void accept(Action visitor) {
visitor.getManConclusion(this);
}
}

/**
* 状态抽象类
*/
public abstract class Action {

/**
* 男人的反应
*/
public abstract void getManConclusion(Man concreteElementA);

/**
* 女人的反应
*/
public abstract void getWomanConclusion(Woman concreteElementB);
}

/**
* 成功
*/
public class Success extends Action {
@Override
public void getManConclusion(Man concreteElementA) {
System.out.println(concreteElementA.getClass().getSimpleName() + " " + this.getClass().getSimpleName() + "时,背后多半有一个伟大的女人");
}

@Override
public void getWomanConclusion(Woman concreteElementB) {
System.out.println(concreteElementB.getClass().getSimpleName() + " " + this.getClass().getSimpleName() + "时,背后大多有一个不成功的男人");
}
}