设计模式学习

全文参考《设计模式之禅》Head First Design Pattern这本也不错。

准备

JavaScript学久了,在看设计原则时,顺带温习了下OOP中的一些传统概念。

类图

  • 一>: 关联,类定义中的相互引用,通常使用类的属性表达。 比如客户类和订单类
  • -->: 依赖,最弱的关系,对象间的临时关联,用函数参数、局部变量、返回值等表达
  • 一▷: 继承
  • --▷: 实现
  • 一◇: has-a关系,表示整体与局部,但不稳定。比如公司类和雇员类
  • 一◆: contains-a关系,表示整体与局部,部分不能脱离整体而存在。

override和overload

区别 覆写 重载
单词 OverLoading Override
概念 方法名称相同,参数的类型或个数不同 方法名称相同,参数的类型或个数相同,返回值类型相同
范围 发生在一个类之中 发生在类的继承关系中
权限 一个类中所重载多个方法可以不同的权限 被子类所覆写的方法不能拥有比父类更严格的访问控制权限

接口和抽象类的不同

两者都为“面向契约编程”而存在。总体来说,接口是对能力的抽象,抽象类是对事物的抽象。从而有着下面的不同:

  • 接口被类实现,抽象类被子类继承。
  • 接口只做方法声明,抽象类中可以做方法声明,也可以做方法实现。
  • 接口里定义的变量只能是公共静态常量,抽象类中的变量可以是普通变量。
  • 抽象类里可以没有抽象方法,接口是设计的结果,抽象类是重构的结果。
  • Java中接口可继承接口,并可多继承接口,但类只能单继承。

它们还有以下特点:

  • 在实现时必须全部实现,否则仍是接口/抽象类
  • 抽象类中可以没有抽象方法

设计6原则

SOLID原则:

  • 单一职责(接口细分到单一业务)
  • 里氏替换(实现都按接口来)
  • 依赖倒置(多使用抽象概念)
  • 接口隔离原则(接口尽量细分)
  • 迪米特法则(低耦合)
  • 开闭原则(高内聚,低耦合)

总结来说,就是好好设计、合理拆分接口,一旦确定,避免更改;减少接口耦合;面向接口编程;使用中间层应对需求变更

常见设计模式

单例模式

单例模式是最简单的设计模式。即一个类只有一个实例,或一个构造函数只能初始化一次(JS版本),且自行实例化,并像整个系统提供整个实例。常用在下面这些场景下,解决特定的问题:

  • 整个项目需要一个共享访问点或共享数据
  • 包含了大量静态常量(通常是配置数据)和静态方法的工具类
  • 创建一个对象需要消耗的资源过多

实现上,分懒汉型和饿汉型。它们的主要区别在初始化单一实例的时机在类创建时还是访问时。懒汉型需要注意线程安全

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 Singleton {
private static final Singleton singleton = new Singleton();

// 限制外部访问
private Singleton() {
}

// 暴露的public方法
public static Singleton getInstance() {
return singleton;
}

// 其他方法
// ...
}

// 懒汉型
public class Singleton {
private static Singleton singleton = null;

// 限制外部访问
private Singleton() {
}

// 暴露的public方法
public static sychronized Singleton getInstance() {
if (singleton == null) {
singleton = new Singleton();
}
return singleton;
}
}

JavaScript中就灵活多了。最常用的字面量变量就是最简单的单例,强行使用构造函数,也可以借助闭包的特点实现。另外,JavaScript是单线程,不需要考虑线程安全的问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 字面量变量
const singleton = {
// 一些属性
foo: 'bar',
// 一些方法
baz() {
console.log('Hello world!');
}
}

// 闭包
function Singleton() {
const singleton = this;
this.foo = 'bar';
this.baz = () => { console.log('Hello world!'); };
Singleton = () => singleton;
}

拓展

当放开单例限制,编程多例模式时,可以通过加入计数器来限制。但不常用,下面给出JavaScript版本例子。

1
2
3
4
5
6
7
8
9
10
11
12
13
function Singleton() {
const singleton = this = [];
// 比如限制为3
const limit = 3;
// 一些初始化工作
for (let i = 0; i < limit; i++) {
// ...
[].push({
// ...
});
}
Singleton = () => singleton;
}

工厂模式

工厂模式意思是,将创建对象的过程封装起来,在OOP语言中体现在定义一个创建一类对象的接口,让实例化过程在子类中完成。浅显的来讲,就是让工厂类将具有共性、创建过程复杂的一类对象的创建过程封装起来,便于业务类使用,同时也便于日后拓展。业务类只需要交给工厂类需要的对象类名(Java)或别的标志就可以得到所需对象。

使用场景上,它是new模式的替代品,在任何需要对象的场景下都可以使用,但是只有下面这些情况下是比较合适的:

  • 需要灵活解耦的框架
  • 产品类创建过程复杂、有差异性且有共性。比如连接邮箱客户端的三种协议POP3、IMAP、HTTP的构造过程抽象成一个IConnectMail接口,再设计各自的工厂类。在日后有新的连接邮箱客户端协议时,只需新增一个工厂类即可。

Java中工厂类可以使用反射等方法创建新对象。

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 abstractProduct {
// 共有方法
public void method() {}
// 抽象方法
public abstract void abstractMethod();
}

public class product1 extends abstractProduct() {
public abstractMethod() {
// 差异化方法
}
}

public class product2 extends abstractProduct() {
public abstractMethod() {
// 差异化方法
}
}

public abstract class Factory {
// 入参根据场景调整
public abstract <T extends abstractProduct> T createProduct(Class<T> c);
}

public class Factory1 extends Factory {
public <T extends Product> T createProduct(Class<T> c) {
Product p = null;
try {
p = (Product)Class.forName(c.getName()).newInstance();
} catch (Exception e) {
}
}
}

JavaScript中,工厂模式的实现更加轻量级,因为构造对象的方式更加简单,使用函数将多个产品类间的共性抽出来就行了。样例略。

抽象工厂模式

抽象模式在工厂模式的基础上又抽象了一层,产品类接口下是多个产品族抽象类,产品族下才是明确的产品类。相对应的,工厂接口下的多个工厂方法根据最细节的产品类生产对象。它的优势在:

  • 可以不公开地控制产品族间的约束
  • 更好地组织多维度(更多是2维)上多个产品间的生产

缺点也很明显,产品族的修改将会直接影响工厂接口和产品抽象类,这是开闭原则的大忌。因此,在产品维度固定,且有必要从多维度上划分产品时,才会使用抽象工厂模式。比如Windows、Linux、Mac OS上的文本编辑器与图像处理程序就是两个维度的多个产品。

样例略。

模板方法模式

模板方法模式比较好理解,就是将子类中共有的算法框架抽象到抽象类中实现,注意是框架,而不是具体的步骤。子类可以根据自己的需要,在不改变框架的基础上,重定义算法的某些步骤,得到不同的结果。下面举个例子就能更方便地看明白了。

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
const student = {
study() {
this.gotoPrimarySchool();
this.gotoMiddleSchool();
this.gotoUniversity();
}
};

const stu1 = Object.assign(Object.create(Student), {
gotoPrimarySchool() {
console.log("子弟学校 ");
}
gotoMiddleSchool() {
console.log("人大附中 ");
}
gotoUniversity() {
console.log("清华大学 ");
}
});

const stu2 = Object.assign(Object.create(Student), {
gotoPrimarySchool() {
console.log("子弟学校 ");
}
gotoMiddleSchool() {
console.log("华师一附中 ");
}
gotoUniversity() {
console.log("中科大 ");
}
});

stu1.study(); //子弟学校 人大附中 清华大学
stu2.study(); //子弟学校 华师一附中 中科大

可以看到,同样是调用学习方法studystu1stu2可以再不影响公用算法流程下,定义自己的算法步骤。使用Java实现是一样的思路:

  • 定义抽象类,声明可以差异化的基本方法,实现模板方法,在模板方法中调用可以差异化的基本方法
  • 子类根据需要,实现自己的基本方法

模板方法模式核心就在于封装不变部分,开放可变部分,共有的算法步骤也较容易维护。因此,使用在下面的场景里:

  • 子类共有相同算法流程
  • 将核心算法设计为模板方法,细节功能由子类补充

建造者模式

建造者模式和工厂模式类似,意思是,讲一个复杂对象的构建表示分离,使同样的构建过程可以有不同的表示。其中的构建强调的是不同基本方法的调用顺序安排,而不是基本方法的实现(这也是它和工厂方法的最大区别);表示是指产品子类对于基本方法的差异性实现。

对比上面模板方法模式来看,就是study的顺序对于不同人不一样,这个顺序有另外的建造类描述并实现。可以看到,建造者模式主要的使用场景是:

  • 相同的执行方法,不同的执行顺序,产生不同的结果
  • 产品类中,不同的构建顺序会有不同的结果
  • 用户希望执行次序可控

在实现时,建造类通过传入新状态或其他方式影响产品类的模板方式执行次序。在建造类和产品类之上,使用导演类可以起到封装的作用。

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 class Product {
public void method() {
// 差异化业务
}
}

// 抽象建造类
public abstract class Builder {
// 设置构建的次序,以获得不同的结果
public abstract void setSequence();
// 建造
public abstract Product build();
}

// 具体建造类
public class Builder1 extends Builder {
private Product product = new Product();
public void setSequence() {
// 差异化逻辑
}
public Product buildProduct() {
return product;
}
}

// 导演类
public class Director {
private Builder builder = new Builder1();
public Product getProductA() {
builder.setSequence();
return builder.build();
}
}

代理模式

代理模式即为其他对象提供一个代理来控制对这个对象的访问,浅显易懂。利用代理模式还可以拦截原始请求,做额外的事情。应用很广泛。

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 interface Object {
// 作为示例的一个方法
public void request();
}

public class RealObject implements Object {
public void request() {
// 业务逻辑
}
}

public class Proxy implements Object {
// 代理的对象
private Object object = null;

public Proxy() {
this.subject = new Proxy();
}

// 传递代理者
public Proxy(Object o) {
}

public void request() {
this.before();
this.object.request();
this.after();
}

// 预处理
public void before() {
}

// 善后处理
public void after() {
}
}

除了普通代理,还有强制代理即只能通过被代理对象对象访问到代理。另外,AOP(Aspect Oriented Progarmming)模式也是建立在代理模式的基础上的。ES6中的proxy和ES7中的装饰器就是AOP概念下的产物。

原型模式

原型模式即不通过new而是通过对象复制生产同类对象。非常好理解。在Java中,一个实现了cloneable接口的对象即使用了原型模式。而JavaScript更是天生使用原型模式实现对象的继承和拓展。

1
2
3
4
5
6
7
8
9
10
11
12
public class PrototypeClass implements Cloneable {
@Override
public PrototypeClass clone() {
PrototypeClass p = null;
try {
p = (PrototypeClass)super.clone();
// 其他操作
} catch(CloneNotSupportedException e) {
}
return p;
}
}

它的优势在于更加轻量级,避免了构造函数的约束。问题在于,第一会跳过构造函数(这个JavaScript没有),第二是深浅拷贝的问题,第三在Java中,clone带有final成员的类会抛出异常。

中介者模式

中介者模式主要用在多个对象那个间有比较复杂的交互场景下,用一个中介对象封装对象间的一系列交互,中介往往与各对象都有交互,从而使其耦合松散,符合迪米特法则。从类图上看,它把原先的蛛网状结构简化为了星型结构。

它的优点是减少了类间依赖,缺点是有些时候中介者会膨胀的很大。使用场景上,在有协调概念出现的场景都有它的发挥空间:

  • 机场调度中心
  • MVC框架中的Controller
  • 媒体网关,中介服务

因为应用场景广泛,这里不举样例。

命令模式

命令模式即将一个用对象组织每一个请求,从而允许使用请求完成一系列操作,同时还可以对请求排队或记录日志或撤销以及恢复。模式主要包括三个角色:

  • 接受者,完成请求内操作的角色
  • 命令,封装好的系列操作
  • 调用者,接受、执行命令的角色

这种设计模式在存在上面三种角色的场景很适用,易于封装常用的命令,且很容易拓展,当命令间共同点较多时,还可以结合模板方法模式进行改进。

例子如下:

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
public abstract class Receiver {
// 定义所有接受者必须完成的业务
public abstract void work() {
}
}

public class Receiver1 extends Receiver {
public void work() {
}

// 差异化业务
public void otherWork1() {
}
}

public class Receiver2 extends Receiver {
public void work() {
}

// 差异化业务
public void otherWork2() {
}
}

public abstract class Command {
public abstract void exec();
}

public class Command1 extends Command {
// 对特定接受者命令
private Receiver receiver;

public Command1(Receiver _receiver) {
this.receiver = _receiver;
}

public void exec() {
this.receiver.work();
}
}

public class Command2 extends Command {
// 对特定接受者命令
private Receiver receiver;

public Command2(Receiver _receiver) {
this.receiver = _receiver;
}

public void exec() {
this.receiver.work();
}
}

// 调用者
public class Invoker {
private Command command;

public void setCommand(Command _command) {
this.command = _command;
}

public void react() {
this.command.exec();
}
}

责任链模式

责任链模式重点在“链”,将有机会处理请求的所有对象组成链,沿着这条链传递请求直到有对象处理它为止。它和Express和Redux中的中间件的概念有相似之处,区别在于责任链上一般只会有一个或部分对象处理请求。它替换了场景代码中的大堆if elseswitch语句。

一个狭义的抽象处理者像下面这样。使用模板方法模式,将抽象的业务逻辑外的处理流程实现在抽象类,细节的业务逻辑放在子类里完成。

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
public abstract class Handler {
// 下一个处理者
private Handler next;
// 处理逻辑
public final Response handle(Request req) {
Response res = null;
if (this.getLevel().equals(req.getLevel())) {
// 只处理属于自己的level
res = this.exec(req);
} else {
// 如果有下一个处理者,交给它
if (this.next != null) {
res = this.next.handle(req);
} else {
// 自行处理
}
}
return res;
}

// 设置下一个处理者
public void setNext(Handler _handler) {
this.next = _handler;
}

// 处理者有自己的处理级别
protected abstract Level getLevel();

// 处理者有自己的处理逻辑
protected abstract Response exec(Request req);
}

// 子类示例
public classHandler1 extends Handler {
// 自己的处理逻辑
protected Response exec(Request req) {
return null;
}

// 自己的处理级别
protected Level getHandlerLevel() {
return null;
}
}

public class Level {
// 定义一个请求和处理等级
}

public class Request {
// 获取请求等级
public Level getRequestLevel() {
return null;
}
}

public class Response {
// 定义返回的数据
}

它的优点在解耦了请求与处理者,使系统更加灵活。问题在责任链比较长时,遍历整个责任链会带来性能问题。Express中类似的中间件则不会,因为处理后可以决定是否执行next(),跳到下一个中间件。

上面Java的实现通过next属性连接所有的处理者(类似链表),在JavaScript的工程实现上,一般在上层又有一层封装,用数组保存所有处理者,再建立之间的连接。这种动态的连接支持动态增删处理者,甚至改变他们的处理顺序。

装饰模式

装饰模式也是一种比较常见的模式,它可以动态地为一个对象增加一些额外的功能,使用装饰器类返回对象比定义子类继承要来得更加灵活。在设计上,有下面几个角色:

  • 抽象构件,即被修饰的对象抽象
  • 具体构件,被修饰的客体
  • 装饰器,一般是一个抽象类,将具体装饰器的共性抽出来,其必有一个private属性指向原始的抽象构件
  • 具体装饰器,装饰具体构件的原有方法,一般来说需要有所有与具体构件public方法同名的方法,且在方法内会使用到而非单纯替换原同名方法(类似滚雪球的过程)。

它的使用类似下面:

1
2
3
4
5
6
7
8
9
10
11
12
// 场景类
public class Scene {
public static void main(String[] args) {
Component c = new Component();
// 装饰
c = new Decorator1(c);
// 再次装饰
c = new Decorator2(c);
// 执行
c.exec();
}
}

JavaScript中的Object.create()Object.assign()和装饰模式有几分相似。

它的优势在于装饰类间不相互耦合,且装饰次序可以灵活修改,可以很好地重构、替换继承关系。劣势在于包装层次过多时,不利于调试时发现错误。装饰模式一般用于:

  • 动态增强一个类、对象的功能
  • 批量为一批对象或类改装或增加功能

总之就是使用继承总感觉小题大做的场合下(OOP语言中)。JS下扩展功能要容易很多。

策略模式

策略模式意为定义一组算法,将每个算法单独封装为可以相互替换的模块。在使用时,有三个主要角色:

  • 策略模块,被封装好的算法策略,屏蔽了高层逻辑对策略的直接访问(高内聚
  • 抽象策略,抽出策略共性的接口,如下面的
  • 具体策略,具体的算法策略,包含具体的算法

在算法间可以自由切换、最好屏蔽算法细节时常用,比如Web编程中常见的表格验证

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const strategy = {
notEmpty: val => val.length > 0,
name: val => val.length > 1 && val.length < 10,
age: val => +val > 0,
password: val => val.match(/\d+{6,}/g)
}

const validator = {
rules: strategy,
verify: (rule, val) => this.rules[rule](val)
}

function formValidate(formData) {
const { name, age, password, introduction } = formData;
return validator.verify('name', name) &&
validator.verify('age', age) &&
validator.verify('password', password) &&
validator.verify('notEmpty', introduction);
}

策略模式的优势在扩展性良好,同时避免了if else以及switch语句。它的问题也很明显,策略类增多时,会不好维护,同时所有策略类都需要对外暴露。当策略类数目膨胀时需要考虑到这些问题。同时,现在在哪些时机如何组合策略并没有严格定义,实际实现时,会参考建造者模式里一样,定义一个导演类,把常用的组合方式定义出来。减少策略类的暴露。

适配器模式

适配器模式即,把一个类的接口变成客户端期待的另一个接口,从而使原先不匹配的两个类能够一起工作。简单点说,就是加了一个中间层做翻译工作。这种模式下有三种角色:

  • 目标角色,即期望接口
  • 源角色,即原始接口
  • 适配器角色,即转换类

在实现上,通常使用一个同时继承两个类的类作为中转。因为对象很轻量级,JS中就更容易实现了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public interface Target {
public void request();
}

public class Target1 extends Target {
public void request() {
// 目标逻辑
}
}

public class Adaptee {
// 原有逻辑
public void foo() {
// ...
}
}

public class Adapter extends Adaptee implements Target {
public void request() {
super.foo();
}
}

它的优点在可以将两个不相关的类在一起运行,提高了类的复用程度,它通常用来救火,完成拓展。由于实际工程中,业务变更较常出现,适配器模式也很常用。

拓展

如需要多适配一的场景,此时无法多继承(Java),在适配类中引用多个类即可。

迭代器模式

迭代器模式,顾名思义是提供一种方法按顺序访问容器中的每个元素,而无需暴露容器的细节。在实现时,通常要自己实现一个迭代器。Java中通过拓展java.util.Iterator实现,JavaScript中,则通过封装数组实现。实现时,要考虑下面几个方法:

  • 判断是否到达尾部
  • 返回下一个元素
  • 删除当前元素

像下面这样

1
2
3
4
5
6
7
8
9
10
11
12
// 迭代器
public interface Iterator {
public Object next();
public boolean hasNext();
public boolean remove();
}
// 容器
public interface Demo {
public void add(Object o);
public void remove(Object o);
public Iterator iterator();
}

现在所有高级语言基本都有这个接口或基础实现。这个模式已经很少用到。

组合模式

组合模式用在表示树状结构的数据中,使用户对单个对象和组合对象使用具有一致性。组合模式下有三种角色:

  • Component,节点抽象角色,参与组合对象的共有方法和属性
  • Leaf,叶子对象,遍历的最小单位
  • Composite,树枝节点

用JavaScript表示,就像下面这样。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const node = {
commonMethod() {}
}

const branchNode = Object.create(node);
const leafNode = Object.create(node);

branchNode = Object.create(branch, {
children: [],
add(node) {
children.push(node);
},
remove(index) {
children.splice(index,1);
},
print() {
children.forEach(child => { console.log(child); });
}
})

leafNode = Object.create(leafNode, {
// ...重写node的同名方法
})

组合模式在表示整合局部的关系时很有用,但是由于树枝节点和叶子节点的使用方式不同,在使用两种节点时,需要直接调用接口的实现类,这一点违背了依赖导致原则(面向接口编程)。使用上分为透明模式和安全模式,后者就像上面一样,在实现类上区分开树枝和叶子节点,透明模式下,所有的方法均抽象到抽象类中实现,而在叶子节点调用不合法方法时抛出异常。

综上来看,组合模式即使用用数据结构描述一颗多叉树。

观察者模式

观察者模式,也叫发布订阅模式,可以说是前端最熟悉也是最常见的一种设计模式了。小到页面事件监听,大到Vue的设计原理都能看到观察者模式的影子。它的内涵在将信息流从原来的pull变成push。从而不需要使用whilesetInterval这种很消耗资源的方式。代价是,需要硬编码到被监听者中,在状态改变时,push信息到监听者那里。通常实现时,这个过程可以抽象到很底层完成,如Vue使用Object.defineProperty,并不影响整体实现。另外,为了实现多对多的监听,往往需要在被监听者和监听者之间增加spy(或者叫probe)这样的角色进行中转和广播通知。

这时候可以定义Observable接口,当然了,它需要可以增删监听者和在发生事件后提醒监听者。

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
public interface Observable {
public void add(Observer o);
public void delete(Observer o);
public void notify(String text);
}

public interface IFoo {
public void work();
public void sleep();
}

public interface Observer {
public void update(String text);
}

public class Victim implements Observable, IFoo {
private ArrayList<Observer> oList = new ArrayList<Observer>();
public void add(Observer o) {
this.oList.add(o);
}

public void delete(Observer o){
this.oList.remove(o);
}

public void notify(String text) {
for (Observer o: oList) {
o.update(text);
}
}

public void work() {
// ...
this.notify("Working...");
}

public void sleep() {
// ...
this.notify("Sleeping...");
}
}

一个简单的JavaScript实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var EventUtil = {
// 全局事件管理
var events = {},
// 注册事件
on = function (type, handler) {
if (events[type]) {
events[type].push(handler);
} else {
events[type] = [handler];
}
},
// 触发事件
emit = function (type) {
if (!events[type]) return;
for (var i = 0, len = events[type].length; i < len; i++) {
events[type][i];
}
};
};

观察者模式的优势在于在两个对象有频繁信息交互或希望监听特定时机时很有用,当信息很多时,可以考虑增加中间层,设计消息队列处理。Java本身也提供java.util.Observerjava.util.Observable用来实现这种模式。

建立在观察者模式的基础上,有响应式编程这样新的编程范式出现,ReactiveX就是在这种范式基础上推出的多语言框架。JavaScript版本的叫做RxJS,相信看完这个简介对你会非常有帮助。

门面模式

门面模式又叫外观模式,它要求所有子系统与外部通信时必须使用统一的对象进行,提供高层的接口,尽量掩盖不必要的业务细节,使得子系统更易用。因为实现起来重点在于统一通信数据格式和封装业务细节。这种模式也非常常用。比如在通常的前后端协调时,后端回传前端请求的数据通常都是统一的格式,避免错误同时减少前端工作量。比如下面这样。同样,有时后端也会要求前端在请求时使用统一的数据格式(不常见)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const successRet = {
// 错误码
code: 0,
// 返回数据
data: {
userInfo: {
id: 0000001,
// ...
}
}
}

const errorRet = {
code: 0002,
// 错误原因
message: "请求过于频繁!"
}

门面模式可以极大地提升封装性,增加系统的高内聚,同时也减少了系统内外的耦合,提高了灵活度和安全性。劣势在于对扩展不利,所有的改动几乎都要对应到门面(Facade)类的硬编码。因此门面模式的使用场景是:

  • 为一个复杂的系统或模块提供对外接口
  • 子系统间相对独立

通常情况下,门面类只负责聚合,不参与具体的子系统逻辑。另外,在系统庞大时,很可能有不止一个门面入口。后端接口微服务化的趋势下,在系统内,拆分原来庞大的接口,同时面向前端不同设备,设计不同的服务汇合接入点正是门面模式的体现。

备忘录模式

备忘录模式即,在不破坏封装性的前提下,捕获一个对象的内部状态,在对象外保存,并在合适的时候可以将对象恢复到保存的状态。这个概念很简单,涉及到三个角色:

  • 发起人,需要记录状态的对象
  • 备忘录, 用来储存状态
  • 备忘录管理者,对备忘录进行管理,保存和恢复
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
class Memorial {
state: ''

memorize() {
return new Memento(this.state);
}
restore(memento) {
this.state = memento.state;
}
}

class Memento {
state: ''

constructor(state) {
this.state = state;
}
}

const Manager = {
memento: null
}

const m = new Memorial();
Manager.memento = Memorial.memorize();
Memorize.restore(Manager.memento);

它的优点很明显,可以保存和回复状态,支持回滚事务。但在使用时,通常是结合了别的设计模式的变种。

拓展

结合原型模式,可以直接用clone对象的方式保存状态。这么做问题是当状态对象较大时,会有时间和空间的开销,优势是可以直接将状态存储在类内部,避免了其余类的定义。

在多状态存储上,Java可以借用BeanUtil工具类(书中所说),JavaScript中就灵活很多了(还是因为轻量级的对象)。同样的多备忘录模式就不再赘述。另外,需要保证备忘录的保密性时,封装成业务类的内置类,设置权限为private即可,JS中同理。

访问者模式

访问者模式指,封装作用在数据结构上各元素的操作,它可以在不改变数据的前提下定义新的对于元素的操作。实现原理上,

  • 被访问类新增访问方法(如accept),注入访问类,同时将自己交给访问类
  • 访问类根据得到的被访问类对象,执行想要的操作
  • 场景类中通过调用访问方法访问被访问类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public abstract class Element {
// ...
public void accept(IVisitor v);
}

public class Element {
public void foo() {
}

// 注入访问类
public void accept(IVisitor v) {
visitor.visit(this);
}
}

public interface IVisitor() {
// 通过重载对不同元素定义不同的访问方式
public void visit(Element e);
public void visit(OtherElement e);
}

当然,为了保证依赖倒置原则,被访问类和访问类都可以再抽象出抽象类和接口。另外,访问者模式通常和组合模式、迭代器模式一同出现。访问者模式的优点:

  • 符合单一职责原则
  • 拓展性优秀

缺点在于,被访问者要暴露细节给访问者,通常会增加很多约定,使代码不稳定。另外重载中依赖的是具体元素违背了依赖倒置原则。

访问器模式的应用场景通常是使用迭代器模式已经不能满足的场合。比如对不同的元素有不同的遍历操作,甚至涉及到元素内部的逻辑。使用访问器模式可以封装和掩盖这种差异性。

拓展

在访问器模式和迭代器模式一同出现时,可以增加统计功能,在每次访问元素时收集统计信息。在往深处拓展,甚至可以抽象访问器为接口,从而拓展不同类型的访问器,如用来展示数据和用来统计数据的。

状态模式

这种模式就很好理解了。即将客体抽象成一个有限状态机,客体有一个初始状态,在某特定时机下会跃迁到另一状态,且状态间的跳转是有规律可循的。这种模式在编程中非常常见,自然语言分析、所有可以用马尔科夫过程描述的事物变化都可以抽象成状态模式实现。在实现时,主要要完成三方面工作:

  • 定义所有状态,根据状态的薄厚程度,用常量或类定义
  • 定义修改状态的行为,在方法内往往要根据上一时刻状态做判断,这些行为定义在状态内部
  • 在上下文中调用这些行为

在实现时,为了避免switch语句,会使用一个上下文类,托管当前状态,在状态切换时,先调用状态类中的行为,再通知上下文类更换托管的状态。两者相互注入。

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 State {
// 注入上下文对象
protected Context c;
// 设置上下文
public void setContext(Context c) {
this.context = c;
}
// 行为
public abstract action1();
public abstract action2();
// ...
}

public class State1 {
@Override
public void action1() {
// ...
}

@Override
public void action2() {
super.context.setState(Context.STATE2);
// 切换到state2
super.context.handleAction2();
}
}

public class Context {
// 注入所有状态
public final static state STATE1 = new State1();
public final static state STATE2 = new State2()

private State CurrState;

public State getState() {
return this.CurrState;
}

// 设置当前状态
public void setState(State currState) {
this.currState = currState;
// 切换当前状态
this.CurrState.setContext(this);
}

// 行为委托
public void handleAction1() {
this.CurrState.action1();
}
public void handleAction2() {
this.CurrState.action2();
}
}

增加了上下文类Context后,避免了大量的switch语句,问题是,状态较多时,定义的类也会较多。状态模式在工作流开发中很常用。

解释器模式

解释器模式顾名思义,即定义一个解释器,去按文法解释一种语言中的句子。当然这个语言以科学运算和编程语句居多。它的应用场景比较特殊,即需要语句解析介入的场景,比如自然语言分析、或者真的编写一个语言的解释器。通常开发解释器工程量和难度都较大,且会遇到效率问题。一般应用较少。

在这个模式下。主要有下面这些角色;

  • 抽象解释器,用来派生具体的表达式解释器
  • 终结符解释器,即不需要解释的,字面意义的符号,比如1a
  • 非终结符解释器,和两边表达式相关联的符号解释器,比如+*
  • 上下文角色

享元模式

享元模式是一种重要的池技术,原理上指使用共享对象支持大量的细粒度的对象。我们可以将这些对象内的状态拆分成可共享状态和不可共享状态。对象往往可以按可共享状态拆分为细粒度较大的若干部分,放在共享对象池中,再贴上自己的不可共享状态。

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
// 享元
public abstract class SharedObject {
private String intrinsic;
// 共享的状态作为享元的key
protected final String Extrinsic;
// 共享的状态需要可以设置
public SharedObject(String E) {
this.Extrinsic = E;
}
// 业务方法
public abstract foo();
// 不可共享状态的getter/setter
public String getIntrinsic() {
return intrinsic;
}
public void setIntrinsic(String intrinsic) {
this.intrinsic = intrinsic;
}
}

// 享元工厂
public class SharedObjectFactory {
// 共享池
private static HashMap<String, SharedObject> pool = new HashMap<String, SharedObject>();
// 工厂方法
public static SharedObject getSharedObject(String Extrinsic) {
SharedObject o = null;
// 从池中寻找
if (pool.containsKey(Extrinsic)) {
o = pool.get(Extrinsic);
} else {
o = new SharedObject1(Extrinsic);
// 放到池中
pool.put(Extrinsic, o);
}
return o;
}
}

在共享对象池中,建议使用可共享状态的简单组合构成池内元素的key值(最好是基本类型),一方面减少编程负担,另一方面还可以提高工作效率。享元模式主要使用在下面场景下:

  • 系统中存在大量相似对象
  • 对象具备相近的外部状态和与环境无关的内部状态

桥梁模式

桥梁模式又叫桥接模式,是比较轻量级的一种设计模式,意为将抽象和实现解耦,使两者可以独立变化。角色上,桥梁模式分为主体和客体,在主体内注入客体的接口/抽象类,并在主体的方法内使用,提供setter或构造函数方法。这样继承自主体的类就可以根据传入setter/构造函数的客体实现类的不同得到不同的实现结果。

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
public interface Object {
// 基本方法
public void foo()
}

public class Object1 implements Object {
public void foo() {
// 自己的逻辑
}
}

public class Object2 implements Object {
public void foo() {
// 自己的逻辑
}
}

public abstract class Subject {
// 必须有一个注入的客体
private Object o;

// 必须可以通过构造函数/setter注入
public Subject(Object o) {
this.o = o;
}

// 获得客体行为
public void bar() {
this.o.foo();
}

// 获得客体
public Object getObject() {
return o;
}
}

public class Subject1 extends Subject {
// 覆写构造函数
public Subject1(Object o) {
super(o);
}
// 自身业务
@Override
public void bar() {
super.bar();
// ... 其余业务
}
}

public class Scene {
public static void main(String[] args) {
Object o = new Object1();
Subject s = new Subject1(o);
s.bar();
}
}

桥梁模式的扩展能力很强,它是对普通的继承的一种灵活的补充,避免了父类对子类的强侵入,可以将父类可能会变化的部分抽出去,通过注入的方式引入,方便子类随时更换。

设计模式的比较

创建类

和创建类相关的模式:

  • 工厂模式
  • 抽象工厂模式
  • 建造者模式
  • 单例模式
  • 原型模式

后两者容易理解。重点对比上面三个模式:

  • 工厂模式注重整体的构建过程,成产出的对象具有很强的相似性
  • 建造者模式注重建造的过程,希望在每一步最好都可定制,根据步骤的不同生产出差异化的对象,通常伴生导演类
  • 抽象工厂模式通常和产品族关系密切,尤其是一组事物有很明显的两个或多个划分方法时使用,其余等同工厂模式

结构类

结构类指调整或组合类产生更大结构来适应更高层次逻辑需求,大多通过增加中间层解决问题,有下面的相关模式:

  • 适配器模式
  • 桥梁模式
  • 组合模式
  • 装饰模式
  • 门面模式
  • 享元模式
  • 代理模式

其中简单明了的有桥梁模式、组合模式、门面模式、享元模式,下面对比下其他的几种模式:

  • 代理模式即在原对象和用户间增加了一个中间层,在不改变原接口的情况下,增加准入和限定操作
  • 装饰模式是代理模式的加强,装饰类并不起中间层的作用,不做准入判断,它单纯地在原接口上增强或削弱功能
  • 适配器模式和它们差别较大,它也起包装作用,作用于两个不同的对象,重点在伪装和转换

行为类

这一批模式重点在修饰类的行为:

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

下面比较一些类似的模式

  • 命令模式强调把动作解耦,将其分为执行对象和执行行为,在行为类内部注入执行对象,使用执行者操作命令
  • 策略模式强调包装对等的可替换的多个算法,通常有一个上下文类,封装所有的算法

比如在表单验证时,策略模式会将所有的验证规则对等地定义出来,再由一个表单验证上下文对象包裹起来;命令模式下,首先要明确所有验证规则的接受者(Receiver),再定义所有的验证“命令”,最后由执行者(Invoker)操作命令完成工作,具体工作是在接受者那里完成的,命令只负责组织。

关于策略模式和状态模式,

  • 策略模式没有状态的概念,虽然有上下文类Context,但是切换的状态只是不同的算法而已
  • 状态模式重点关注状态,它同样有上下文类Context,但相同的行为在不同的状态下产生的结果不同。在实现上体现在,上下文类内保存了当前状态,虽然状态间都有相同方法,但实现不同。

至于观察者模式和责任链模式,

  • 观察者模式重点在观察和被观察的关系(想想事件监听),被观察者中需要注入监听者(Observable),再由监听者告知观察者(Observer),整条链是有回调的,链上传递的信息可以自由变化,即最后返回给用户数据的总是第一个被观察者
  • 责任链模式重点在事务链条化处理的过程(想想中间件),每个处理者都必须通过next属性明确指定下一个目标,整条链是责任链,链上角色相互平等,传递的信息一般不会改变结构,最终由最后一个角色返回结果

其他

首先先来比较策略模式和桥梁模式。它俩的共同点在都有一个注入依赖关系。策略模式中算法封装被注入到上下文类Context中,桥梁模式中差异化继承的部分被单独抽出再注入父类里。它们的区别主要在:

  • 策略模式着重于封装一系列不同的行为
  • 桥梁模式在不破坏封装的情况下将差异化的实现部分从抽象部分抽取出来,因此必然有抽象化角色和实现化角色

门面模式和中介者模式就比较好区分了,它们的应用场景有很大不同;

  • 门面模式用来掩盖下面复杂的子系统,提供统一的高层接口(“金玉其外”),它并不管下面的子系统间怎样耦合(“败絮其中”)
  • 中介者模式要用在同事对象间(通常至少3个)复杂交互时,化网状结构为星型结构,减少耦合

最后,代理模式、装饰模式、适配器模式(不严格)、桥梁模式、门面模式都可以总结为包装模式,它们并没有为原来的类增加新的功能,只是增加了新的包装或插件。

设计模式的组合

shell命令解释demo

主要采用命令模式、责任链模式、模板方法模式。

银行扣款demo

主要采用策略模式、工厂方法模式、门面模式

产品消费事件demo

产品创建时摄影工厂模式,保证产品和工厂的紧耦合,避免创建事件不触发的可能性

新模式

MVC

MVC其实算不上一种新模式,但是从上世纪90年代起到现在实在是太流行了。它的目的是通过C(Controller)将模型M(Model)和视图V(View)分离开。书中具体在讲MVC在Java Web开发中的实现,这里从略。

规格书模式

规格书模式多用在描述一个规范或条件的场合下,比如“性别男年龄大于20且位于北京”。通过定义抽象类以及ANDORNOT等的组合,可以得到更复杂的规格书对象。

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
public interface ISpec() {
public boolean isSatisfied(Object o);
public ISpec and(ISpec s);
public ISpec or(ISpec s);
public ISpec not(ISpec s);
}

public abstract Spec implements ISpec {
public abstract isSatisfied(Object o);

public ISpec and(ISpec s) {
return new AndSpec(this, spec);
}

public ISpec or(ISpec s) {
return newOrSpec(this, spec);
}

public ISpec not() {
return notSpec(this);
}
}

public class AndSpec extends Spec {
private ISpec left;
private ISpec right;

public AndSpec(ISpec left, ISpec right) {
this.left = left;
this.right = right;
}

@Override
public boolean isSatisfied(Object o) {
return left.isSatisfied(o) && right.isSatisfied(o);
}
}

public class OrSpec extends Spec {
private ISpec left;
private ISpec right;

public OrSpec(ISpec left, ISpec right) {
this.left = left;
this.right = right;
}

@Override
public boolean isSatisfied(Object o) {
return left.isSatisfied(o) || right.isSatisfied(o);
}
}

public class NotSpec extends Spec {
private ISpec spec;

public NotSpec(ISpec s) {
this.spec = s;
}

@Override
public boolean isSatisfied(Object o) {
return !this.spec.isSatisfied(o);
}
}

public class bizSpec extends Spec {
private Object obj;

public bizSpec(Object o) {
this.obj = o;
}

public boolean isSatisfied(Object o) {
// 根据业务逻辑决定真值判断
// ...
}
}

规格模式应用场景比较局限,在LINQ(Language INtegrated Query,语言集成查询)中常见,用来构造WHERE子句。

对象池模式

对象池模式和享元模式相似,都是循环使用对象,但是对象池模式是整个直接取出到池中,避免初始化和释放资源时的消耗。如连接池和线程池就是常见的例子。

雇工模式

雇工模式是常见的“接口-实现”的倒转实现。比如,小学老师教小学学生,中学老师教中学学生,大学老师教大学学生。在实现时,我们先定义学习接口,再实现所有的学生类。再实现老师时,直接调用学生接口的学习方法就完成了教学的实现。雇工模式为一组类提供通用的功能,不需要类实现这些功能,而在客体上实现功能。它和命令模式有相似之处。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public interface IService {
public void serving();
}

public class Object implements IService {
public void serving() {
// 服务完成
}
}

public class Servant {
public void service(IService s) {
s.serving();
}
}

黑板模式

黑板模式是观察者模式的拓展,它允许消息有多个读写者,且同时进行。消息生产者通过“黑板”这个总线,将消息传递给对应的消息消费者。这可以是一种宏观的设计理念,使用数据库或者消息队列当做“黑板”都是可以的。

空对象模式

空对象通过实现一个无意义的默认类避免程序出现null值。

1
2
3
4
5
class NullAnimal implements Animal {
public void makeSound {
// 什么都不写
}
}

–END–