Sorry, your browser cannot access this site
This page requires browser support (enable) JavaScript
Learn more >

设计模式收集使用JS实现,参考JS设计模式Github仓库以及书《JavaScript设计模式与开发实践》曾探著

设计模式

设计模式通常是前任在开发中总结而来的经验,通常我们在看别人代码的时候,会产生:“为什么这么写”的疑问。而学习设计模式的目的在于帮助我们更好地理解代码。

本文沿用GoF四人组(Erich Gamma、Richard Helm、Ralph Johnson、John Vlissides)的分类方式将设计模式分为:

  • 创建型模式
    • 创建对象
  • 结构型模式
    • 处理类或对象的组合
  • 行为型模式
    • 描述类或对象怎样交互和怎样分配职责

三类并使用JS进行实现

设计模式六大原则

在描述设计模式之前,先来聊聊设计原则吧,因为稍后介绍的设计模式都或多或少的应用了这些设计原则,而且这些原则能够帮助我们提高自己代码的健壮性。

本章参考知乎——六大设计模式

六大设计原则包括:

  • 单一责任原则(Single Responsibility Principle)
    • 定义:应该有,且仅有一个原因引起类的变更
    • 理解:即一个接口或类或方法,应该具有单一职责,例如一个接口要么进行协议管理要么进行数据传输,不应同时具备上述两种职责。
    • 优点:
      • 类复杂性低
      • 可读性,可维护性高
      • 变更分析低
    • 总结:一个类或接口只承担一个职责。
  • 开闭原则(Open Closed Principle)
    • 定义:一个软件实体(类,模块,函数)对扩展开放,对修改封闭
    • 理解:开闭原则是其他五大原则的精神领袖,其他五大原则都是实现开闭原则的具体型态
    • 优点:
      • 提高可复用性与可维护性
    • 总结:对软件实体的改动,最好用扩展而非修改的方式。
  • 里氏替换原则(Liskov Substitution Principle)(LSP❌)
    • 定义:如果对每一个类型为S的对象o1,都有类型为T的对象o2,使得以T定义的所有程序P中,使用对象o2替换所有的对象o1时,程序P的行为没有发生变化,那么类型S是类型T的子类型。
    • 理解:就是只要父类能出现的地方,子类就可以出现,而且替换为子类也不会产生任何错误或异常。符合该原则的继承可以被定义为良好的继承
      • 子类必须完全实现父类的方法
      • 子类可以有自己的个性
      • 覆盖或实现父类的方法时,输入参数可以被放大,即子类重载父类方法时,参数范围要\geq父类方法参数
      • 覆盖或实现父类的方法时,输出结果可以被缩小,即子类重载父类方法时,返回值范围要\leq父类方法返回值
    • 总结:在继承类时,务必重写(override)父类中所有的方法,尤其需要注意父类的protected方法(它们往往是让你重写的),子类尽量不要暴露自己的public方法供外界调用。
  • 迪米特法则(Law of Demeter)(也叫最少知道原则)
    • 定义:一个对象应该对其他对象有最少的了解
    • 理解:
      • 只和直接的朋友交流:出现在成员变量、方法的输入输出参数中的类称为成员朋友类,一个类的方法中只能和朋友类进行数据交流,不能涉及其他类
      • 朋友间也是有距离的:开发中尽量不要对外公布太多public方法和非静态的public变量,尽量内敛,可以整合为一个方法的需要进行整合。
      • 是自己的就是自己的:如果一个方法放在本类中,既不增加类间关系,也对本类不产生负面影响,那就放置在本类中。
    • 总结:尽量减少对象之间的交互,从而减小类之间的耦合。
  • 接口隔离原则(Interface Segregation Principle)
    • 定义:户端不应该依赖它不需要的接口,或者说类间的依赖关系应该建立在最小的接口上。
    • 理解:把一个臃肿的接口拆分为多个独立的接口
    • 总结:不要对外暴露没有实际意义的接口。
  • 依赖倒置原则(Dependence Inversion Principle)
    • 定义:即面向接口编程
      • 模块间的依赖通过抽象发生,实现类之间不直接发生依赖关系,其依赖关系是通过接口或抽象类产生的
      • 接口或抽象类不依赖于实现类
      • 实现类依赖接口或抽象类
    • 理解:
      • 每个类尽量都有接口或抽象类,或者接口和抽象类两者都具备。
      • 变量的字面类型尽量是接口或抽象类。
      • 任何类都不应该从具体类派生。
      • 尽量不要重写基类的方法。如果基类是一个抽象类,而且这个方法已经实现了,子类尽量不要重写。
    • 优点:
      • 减少类间的耦合性,提高系统的稳定性
      • 降低并行开发引起的风险
      • 提高代码的可读性和可维护性。
    • 总结:高层模块不应该依赖于低层模块,而应该依赖于抽象。抽象不应依赖于细节,细节应依赖于抽象。

我们把这六大原则的首字母放进一个Set中,并保持原有顺序,就能得到字符串SOLID,预示着六大原则结合后的好处:健壮、稳定

创建型模式

创建型模式 (Creational Pattern) 对类的实例化过程进 行了抽象,能够 将软件模块中对象的创建和对象的使用 分离 。为了使软件的结构更加清晰,外界对于这些对象 只需要知道它们共同的接口,而不清楚其具体的实现细 节,使整个系统的设计更加符合单一职责原则。 创建型模式在 创建什么 (What) , 由谁创建 (Who) , 何 时创建 (When) 等方面都为软件设计者提供了尽可能大 的灵活性。创建型模式 隐藏了类的实例的创建细节,通 过隐藏对象如何被创建和组合在一起达到使整个系统独 立的目的 。

单例模式(Singleton Pattern)

在面向对象设计的语言中,单例模式通常为我们提供了一种避免多次进行实例化的方法,从而降低构造带来的性能消耗,在基于ES6的语法中,我们也可以实现这一功能。

另外单例模式分为:

  • 懒汉式
    • 使用时才进行实例化
  • 饿汉式
    • 先初始化,用的时候直接返回

由于js不需要考虑线程安全,所以推荐使用懒汉式写法,饿汉在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
class SingleObject {
constructor () {
// TODO: 防止使用new创建对象
if(new.target !== undefined) {
const errorMsg = "This is single object,Can't use keyword new!";
const tipMsg = "You should use method getInstance to get instance。";
throw new Error(`\n${errorMsg}\n${tipMsg}`)
}
}

// TODO: getInstance获取单例
static getInstance() {
// TODO: 如果存在单例直接返回
if(SingleObject.instance) {
return SingleObject.instance
}
// TODO: 否则创建
SingleObject.instance = {}
SingleObject.instance.__proto__ = SingleObject.prototype
return SingleObject.instance
}

// TODO: 实际功能
doSomething() {
console.log("..doing")
}
}

const instance1 = SingleObject.getInstance();
const instance2 = SingleObject.getInstance();

instance1.doSomething() // ...doing
console.log(instance1 === instance2) // true

但上述方法仍然存在一些不足,比如不满足1.单一责任原则,上述代码中进行单例是否存在判断的代码与实例化的代码是融合在一起的。另外上述代码也没能很好的满足2.开闭原则,因为如果在后续的场景中,我们需要创建多个对象时,就需要修改关于new方式的控制。

因此我们可以通过代理的方式将实现单例的代码抽离出来,他需要实现一下几个功能:

  1. 单例判断
  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
function getSingle(classFun) {
let result;
return () => {
return result || (result = new classFun())
}
}

class Object {
constructor() {}

doSomething() {
console.log("...doing")
}

// 支持函数式调用
__call__(){
return new Object()
}
}

const singleObjectProxy = getSingle(Object)
const instance1 = singleObjectProxy()
const instance2 = singleObjectProxy()

const instance3 = new Object()

console.log(instance1 === instance2) // true
console.log(instance1 === instance3) // false
instance1.doSomething() // ...doing
instance3.doSomething() // ...doing

单例设计模式远不止能够在面向对象中使用,利用单例模式,创建某些唯一的div浮窗来实现登陆框,iframe,script标签解决跨域问题时候,我们希望整个项目,或者某个页面中仅使用一个实例来减少创建实例的开销以优化性能。

并且懒汉式的单例模式能够避免页面在第一次加载的时候就将该元素加载。等到真正要用到这个元素时才创建该元素并加载DOM

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
let getSingle = function(fn) {
let result;
return (...args) => {
return result || (result = fn.apply(this, args))
}
}

let createLoginLayer = () => {
let dir = document.createElement('div')
div.innerHtml = 'Login Window'
div.style.display = 'none'
document.body.appendChild(div)
return div
}

let createSingleLoginLayer = getSingle(createLoginLayer)

document.getElementById('LoginBtn').onclick = function() {
let loginLayer = createSingleLoginLayer()
loginLayer.style.display = 'block'
}

工厂设计模式

工厂设计模式常用来实例化对象,某些对象的实例化逻辑非常复杂,我们希望将其封装到一个函数中,当我们想要实例化时,调用函数即可,那么这个用来封装的函数就可以被视为一个工厂,工厂设计模式按照抽象程度不同可以分为:

  • 简单工厂模式
  • 工厂模式
  • 抽象工厂模式

简单工厂模式

简单工厂又叫静态工厂,核心思想是将某一种产品类的实例化交给工厂对象处理,主要用来创建同一类对象,这些类通常具体相同的父类,例如在进行用户鉴权时,可以根据用户类型渲染不同的页面:

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
class User {
// 构造函数
constructor(opt) {
this.name = opt.name
this.viewPages = opt.viewPages
}

// 静态方法实现工厂
static userFactory(role) {
switch (role) {
case 'superAdmin':
return new User({name:"超管", viewPages: ['首页','作业编辑','账号管理', '阅卷']})
break;
case 'teacher':
return new User({name:"教师", viewPages: ['首页','作业编辑', '阅卷']})
break;
case 'student':
return new User({name:"学生", viewPages: ['首页'})
break;
default:
throw new Error("参数错误")
}
}
}

const superAdmin = User.userFactory('superAdmin')
const teacher = User.userFactory('teacher')
const student = User.userFactory('student')

简单工厂的优势在于我们只需要传递正确的参数即可得到想要的实例化对象,无需关注实例化的过程。当函数包含了所有对象创建的逻辑,随着项目的增大,构造函数的增多,我们不仅需要修改方法工厂中的逻辑代码,该函数还会变成一个体量庞大的超级函数,因此只适用于实例化对象较少的场景。

工厂模式

工厂方法实际上是对静态工厂的进一步抽象,在简单工厂模式中,如果我们希望工厂生产型的同类产品,比如admin类别的用户,那么我们需要修改工厂中的代码,这违反了开闭原则,因此工厂模式使用抽象工厂类或接口来定义工厂需要具备的方法,然后为每个产品类构造对应的工厂,实现一个工厂对应一个产品的形式。

事实上这一设计利用了单一责任原则,将类的实例化充工厂中抽离,交由子类实现

这我们只需要添加一个新的createAdmin工厂函数即可,而不需要修改代码

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
class User {
constructor(name = '', viewPages = []) {
if (new.target === User) {
throw Error("抽象类无法实例化")
}
this.name = name
this.viewPages = viewPages
}
}

class UserFactory extends User {
constructor(name, viewPages) {
super(name, viewPages)
}

createSuperAdmin() {
return new UserFactory({name:"超管", viewPages: ['首页','作业编辑','账号管理', '阅卷']})
}

createTeacher() {
return new UserFactory({name:"教师", viewPages: ['首页','作业编辑', '阅卷']})
}
createStudent() {
return new UserFactory({name:"学生", viewPages: ['首页']})
}
}

const userFactory = new UserFactory();
const superAdmin = userFactory.createSuperAdmin()
const teacher = userFactory.createTeacher()
const student = userFactory.createStudent()

抽象工厂模式

抽象工厂模式这是对工厂模式的进一步抽象,考虑如果我们想要生产除了User类以外的其他类,我们仍然需要修改工厂中的create方法,仍然会破坏开闭原则,因此我们需要更高级的抽象,需要一个抽象工厂,来管理其余的所有工厂:

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
class User {
constructor(name = '', viewPages = []) {
if (new.target === User) {
throw Error("抽象类无法实例化")
}
this.name = name
this.viewPages = viewPages
}
}

class Problem {
constructor(title = '', desc = '', score = 0) {
if(new.target === Problem) {
throw Error("抽象类无法实例化")
}
this.title = title
this.desc = desc
this.score = score
}
}

class ProblemFactory extends Problem {
constructor(title, desc, score) {
super(title, desc, score)
}
createEasyProblem() {
return new ProblemFactory("简单题", "简单", "1")
}

createHardProblem() {
return new ProblemFactory("困难题", "困难", "5")
}
}

class UserFactory extends User {
constructor(name, viewPages) {
super(name, viewPages)
}

createSuperAdmin() {
return new UserFactory({name:"超管", viewPages: ['首页','作业编辑','账号管理', '阅卷']})
}

createTeacher() {
return new UserFactory({name:"教师", viewPages: ['首页','作业编辑', '阅卷']})
}
createStudent() {
return new UserFactory({name:"学生", viewPages: ['首页']})
}
}

class EduFactory {
static getUserFactory() {
return new UserFactory()
}

static getProblemFactory() {
return new ProblemFactory()
}
}

const userFactory = EduFactory.getUserFactory()

const teacher = userFactory.createTeacher()
const student = userFactory.createStudent()

const problemFactory = EduFactory.getProblemFactory()

const easyProblem = problemFactory.createEasyProblem()
const hardProblem = problemFactory.createHardProblem()

这样一来,日后如果需要生产新的商品,只需要添加型的工厂即可,不需要修改工厂中的代码

实际引用

最直接的应用就是在处理路由上了,假设现在我们有多个页面,需要让不同的用户根据路径进入不同的页面,通常我们会在route/index.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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
// index.js
import Vue from 'vue'
import Router from 'vue-router'
import Login from '../components/Login.vue'
import SuperAdmin from '../components/SuperAdmin.vue'
import Teacher from '../components/Teacher.vue'
import Student from '../components/Student.vue'
import NotFound404 from '../components/404.vue'

Vue.use(Router)

export default new Router({
routes: [
//重定向到登录页
{
path: '/',
redirect: '/login'
},
//登陆页
{
path: '/login',
name: 'Login',
component: Login
},
//超管页面
{
path: '/super-admin',
name: 'SuperAdmin',
component: SuperAdmin
},
//教师页面
{
path: '/teacher',
name: 'Teacher',
component: Teacher
},
//学生页面
{
path: '/student',
name: 'Student',
component: Student
},
//404页面
{
path: '*',
name: 'NotFound404',
component: NotFound404
}
]
})

此时,如果学生用户知道了教师用户的path,可以直接通过url进入教师页面,这样显然是不科学的,因此我们需要在登录的时候根据权限使用vue-routeraddRoutes方法动态赋予权限,此处就可以利用工厂设计模式进行设计。

首先我们只需要在index.js中添加Login的路由:

点击查看代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import Vue from 'vue'
import Router from 'vue-router'
import Login from '../components/Login.vue'

Vue.use(Router)

export default new Router({
routes: [
//重定向到登录页
{
path: '/',
redirect: '/login'
},
//登陆页
{
path: '/login',
name: 'Login',
component: Login
},
]
})

随后我们在router目录下创建一个路由工厂routerFactory.js来根据登录状态动态地添加路由,我们可以利用数组的slice方法,将不属于该类用户的路由剔除,仅保留运行访问的路由:

点击查看代码
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
// routerFactory.js

import SuperAdmin from '../components/SuperAdmin.vue'
import Teacher from '../components/Teacher.vue'
import Student from '../components/Student.vue'
import NotFound404 from '../components/404.vue'

let AllRouter = [
{
//超管页面
{
path: '/super-admin',
name: 'SuperAdmin',
component: SuperAdmin
},
//教师页面
{
path: '/teacher',
name: 'Teacher',
component: Teacher
},
//学生页面
{
path: '/student',
name: 'Student',
component: Student
},
//404页面
{
path: '*',
name: 'NotFound404',
component: NotFound404
}
}
]

let routerFactory = (role) => {
switch(role) {
case "superAdmin":
return {
name: "SuperAdmin",
route: AllRouter
};
break;
case "Teacher":
return {
name: "Teacher",
// 将不属于该类型用户的导航去掉
route: AllRouter.slice(1)
};
break;
case "Student":
return {
name: "Student",
route: AllRouter.slice(2)
};
break;
default:
throw new Error('参数错误! 可选参数: superAdmin, Teacher, Student')
}
}

export { routerFactory }

在登录组件的事件中调用路由工厂得到路由:

点击查看代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// Login.vue

<script lang='js' setup>
import { routerFactory } from "../router/routerFactory.js"

const router = useRouter()

const getRouter = (role) => {
let routerObj = routerFactory(role)
// 动态添加路由
router.addRoute(routerObj)
// 进行页面跳转
router.push({name: routerObj.name})
}
</script>

但在实际项目中,因为使用addRoute方法添加的路由刷新后不能保存,所以会导致路由无法访问。通常的做法是本地加密保存用户信息,在刷新后获取本地权限并解密,根据权限重新添加路由。

实际上JQuery中的$(selector),React中的createElement()都是工厂方法。

$()利用我们提供的信息创建JQuery对象并返回

createElement()利用我们提供的信息创建Vnode对象并返回

原型设计模式

对于前端程序员而言,原型设计模式可谓非常熟悉了,JS的原型链就是以原型设计模式设计的。

因此对一这部分的理解可以直接借用JS原型链的知识,原型编程泛型的基本规则是:

  1. 所有数据都是对象
  2. 要得到一个对象,不是通过实例化类,而是找到一个对象作为原型,并克隆他它
  3. 对象会记住它的原型
  4. 如果对象无法响应某个请求,则会把这个请求委托给自己的原型

是不是很熟悉,new方法实际上是一个很好的例子,我们可以借助new方法源码的方法实现一个这个克隆的过程:

点击查看代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class shape {
constructor() {
this.id = null
this.type = null
}
getType(){
return this.type;
}
getId() {
return this.id;
}
setId(id) {
this.id = id;
}
clone() {
// 创建空对象
let res = {}
// 绑定原型
res.__proto__ = this.__proto__
// 改变this指向
this.__proto__.constructor.call(res)
return 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
function Rectangle() {
this.type = "Rectangle"
}

// 绑定原型
Rectangle.prototype.__proto__ = new Shape()
// 绑定静态方法
Rectangle.prototype.draw = () => {console.log("Rectangle!")}


function Square() {
this.type = "Square"
}

// 绑定原型
Square.prototype.__proto__ = new Shape()
// 绑定静态方法
Square.prototype.draw = () => {console.log("Square!")}

function Circle() {
this.type = "Circle"
}

// 绑定原型
Circle.prototype.__proto__ = new Shape()
// 绑定静态方法
Circle.prototype.draw = () => {console.log("Circle!")}

创建好了类以后我们就可以尝试对类进行实例化,由上面的描述我们知道实例化的过程是通过找到一个对象,然后克隆得到的,为了实现’找到一个对象’的过程,我们需要模拟一个cache:

点击查看代码
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
class ShapeCache {
static getShape(shapeId) {
const cachedShape = ShapeCache.shapeMap.get(shapeId);
return cachedShape.clone();
}
static loadCache() {
const circle = new Circle();
circle.setId("1");
ShapeCache.shapeMap.set(circle.getId(),circle);

const square = new Square();
square.setId("2");
ShapeCache.shapeMap.set(square.getId(),square);

const rectangle = new Rectangle();
rectangle.setId("3");
ShapeCache.shapeMap.set(rectangle.getId(),rectangle);
}
}
ShapeCache.shapeMap = new Map();


ShapeCache.loadCache();

const clonedShape = ShapeCache.getShape("1");
console.log("Shape : " + clonedShape.getType());

const clonedShape2 = ShapeCache.getShape("2");
console.log("Shape : " + clonedShape2.getType());

const clonedShape3 = ShapeCache.getShape("3");
console.log("Shape : " + clonedShape3.getType());
/**
* output:
* Shape : Circle
* Shape : Square
* Shape : Rectangle
*/

在其它编程中使用原型模式的优势是使用更小的代价来创建对象,通过原型引用的方式而不是开辟新的空间。

JS在设计之初就是采用的原型链继承的方式,直接new就进行了克隆的操作,所以对比其它语言创建大对象的性能,能高出不少。

生成器设计模式

使用简单的对象组合成复杂的对象

这里我们用快餐举例,如果我们希望但开一家快餐店,那么各种套餐都是由不同种类的冷饮和汉堡组合而成。同时冷饮需要瓶子装,汉堡需要纸盒包住,那么我们可以先定义冷饮和汉堡类和它们所需要的瓶子和纸盒。

下面是生成器模式的类图:

  1. 主管(Director)类定义调用构造步骤的顺序,这样就可以创建和复用特定的产品配置。
  2. 生成器(Builder)接口声明在所有类型生成器中通用的产品构造步骤。
  3. 具体生成器(Concrete Builders)提供构造过程的不同实现。 具体生成器也可以构造不遵循通用接口的产品。
点击查看代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 纸盒
class Wrapper {
pack() {
return "Wrapper"
}
}
// 水杯
class Bottle{
pack() {
return "Bottle"
}
}
// 汉堡需要纸盒包住
class Burger {
packing() {
return new Wrapper()
}
}
// 饮料需要水杯
class Drink {
packing() {
return new Bottle()
}
}

如果在这个基础上,我们想要提供不同类别的汉堡与饮料我们可以根据基类进行派生:

点击查看代码
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
// 各种生成器
class VegBurger extends Burger {
price() {
return 25.0
}
name() {
return "Veg Burger"
}
}

class ChickenBurger extends Burger {
price() {
return 50.0
}
name() {
return "Chicken Burger"
}
}

class Coke extends Drink {
price() {
return 3.0
}
name() {
return "Coke"
}
}

之后我们可以在这个的基础上构建一些套餐,以便用户进行选择:

点击查看代码
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
class Meal {
constructor() {
const items = []
// 使用defineProperty代理定义属性操作
Reflect.defineProperty(this, 'items', {
get: () => {
if(this.__proto__ !== Meal.prototype) {
throw new Error('items is private!');
}
return items
}
})
}
addItem(item) {
this.items.push(item)
}
getCost() {
let cost = 0.0
this.items.forEach((item) => {
cost += item.price()
})
}

showItems() {
this.items.forEach((item) => {
console.log(`Item: ${item.name()};Packing: ${item.packing.pack};Price: ${item.price()}`)
})
}
}

接下来我们可以利用工厂设计模式,为套餐创建工厂,避免繁杂的对象构建:

点击查看代码
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
// 主管类
class MealBuilder {
prepareVegMeal() {
const meal = new Meal()
meal.addItem(new VegBurger());
meal.addItem(new Coke());
return meal
}
prepareChickenMeal() {
const meal = new Meal()
meal.addItem(new ChickenBurger());
meal.addItem(new Coke());
return meal
}
}


// 使用
const mealBuilder = new MealBuilder()
const vegMeal = mealBuilder.prepareVegMeal()
console.log("Veg Meal:")
vegMeal.showItems()
console.log("Total Cost: " +vegMeal.getCost());


const chickenMeal = mealBuilder.prepareChickenMeal()
console.log("Chicken Meal:")
chickenMeal.showItems()
console.log("Total Cost: " +chickenMeal.getCost());

/**
* output:
* Veg Meal
* Item : Veg Burger,Packing : Wrapper,Price : 25
* Item : Coke,Packing : Bottle,Price : 3
* Total Cost: 28
*
* Chicken Meal
* Item : Chicken Burger,Packing : Wrapper,Price : 50
* Item : Coke,Packing : Bottle,Price : 3
* Total Cost: 53
*/

这是一种创建复杂对象的最佳实践。尤其是复杂对象多变的情况下,通过基础组件来组合,在基础组件变更时,多种依赖于基础组件的复杂组件也能方便变更,而不需要更改多种不同的复杂组件。

结构型模式

其描述 如何将类或者对 象结合在一起形成更大的结构 ,就像搭积木,可以通过 简单积木的组合形成复杂的、功能更为强大的结构。 结构型模式可以分为 类结构型模式 和 对象结构型模式 : • 类结构型模式关心类的组合 ,由多个类可以组合成一个更大的系统,在类结构型模式中一般只存在继承关系和实现关系。 • 对象结构型模式关心类与对象的组合,通过关联关系使得在一个类中定义另一个类的实例对象,然后通过该对象调用其方法。 根据“合成复用原则”,在系统中尽量使用关联关系来替代继承关系,因此大部分结构型模式都是 对象结构型模式 。

适配器模式

适配器模式是一种结构型设计模式, 它能使接口不兼容的对象能够相互合作。

我们从实际问题出发理解适配器设计模式:

现在我们有一个数据展示平台,有一个需求是将股票数据展示的业务整合到原有平台中,我们决定使用第三方库对股票信息进行分析后进行展示。

那么问题来了,我们使用的第三方库要求以Json格式输入,但从数据提供方取得的数据是XML格式的,我们该怎么办

我们当然可以修改第三方库中的代码让他支持,但有时第三方库的原码并没有那么好取得,也没有那么好修改。

于是,我们想到可以创建一个适配器,对格式进行转换后,输入给第三方库。

适配器不仅可以转换不同格式的数据, 其还有助于采用不同接口的对象之间的合作。 它的运作方式如下:

  1. 适配器实现与其中一个现有对象兼容的接口。
  2. 现有对象可以使用该接口安全地调用适配器方法。
  3. 适配器方法被调用后将以另一个对象兼容的格式和顺序将请求传递给该对象。

有时你甚至可以创建一个双向适配器来实现双向转换调用。

对象适配器

实现时使用了构成原则: 适配器实现了其中一个对象的接口, 并对另一个对象进行封装。 所有流行的编程语言都可以实现适配器。

类适配器

下列适配器模式演示基于经典的 “方钉和圆孔” 问题。

适配器假扮成一个圆钉 (Round­Peg), 其半径等于方钉 (Square­Peg) 横截面对角线的一半 (即能够容纳方钉的最小外接圆的半径)。

使用场景

  1. 当你希望使用某个类, 但是其接口与其他代码不兼容时, 可以使用适配器类
    • 适配器模式允许你创建一个中间层类, 其可作为代码与遗留类、 第三方类或提供怪异接口的类之间的转换器。
  2. 如果您需要复用这样一些类, 他们处于同一个继承体系, 并且他们又有了额外的一些共同的方法, 但是这些共同的方法不是所有在这一继承体系中的子类所具有的共性。
    • 你可以扩展每个子类, 将缺少的功能添加到新的子类中。 但是, 你必须在所有新子类中重复添加这些代码, 这样会使得代码有坏味道。将缺失功能添加到一个适配器类中是一种优雅得多的解决方案。 然后你可以将缺少功能的对象封装在适配器中, 从而动态地获取所需功能。 如要这一点正常运作, 目标类必须要有通用接口, 适配器的成员变量应当遵循该通用接口。 这种方式同装饰模式非常相似。

下面大件一个场景来写一个简单的demo吧。

我们知道音频文件有vlc,mp3,mp4等多种不同的文件,不同的文件需要使用不同的播放器大概并播放,但随着文件编码方式的进步,未来将会有更多编码类型的音频文件流行,那么我们希望设计一个播放器可以同时播放不同格式的文件。

通过上面的分析我们知道,此处是使用适配器模式的好时候,我们为播放器设计一个适配器来处理不同格式的文件:

点击查看代码
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
class VlcPlayer {
playVlc(fileName) {
console.log(`Vlc file:${fileName} is playing`)
}
}

class Mp3Player {
playMp3(fileName) {
console.log(`Mp3 file:${fileName} is playing`)
}
}

class Mp4Player {
playMp4(fileName) {
console.log(`Mp4 file:${fileName} is playing`)
}
}

class MediaAdapter {
constructor(audioType) {
switch(audioType) {
case 'vlc':
MediaAdapter.MusicPlayer = new VlcPlayer()
break;
case 'mp3':
MediaAdapter.MusicPlayer = new Mp3Player()
break;
case 'mp4':
MediaAdapter.MusicPlayer = new Mp4Player()
break;
}
}
play(audioType, fileName) {
switch(audioType) {
case 'vlc':
MediaAdapter.MusicPlayer.playVlc(fileName)
break;
case 'mp3':
MediaAdapter.MusicPlayer.playMp3(fileName)
break;
case 'mp4':
MediaAdapter.MusicPlayer.playMp4(fileName)
break;
}
}
}

class AudioPlayer {
play(audioType, fileName) {
switch(audioType) {
case 'vlc':
case 'mp3':
case 'mp4':
AudioPlayer.mediaAdapter = new MediaAdapter(audioType)
AudioPlayer.mediaAdapter.play(audioType, fileName)
break;
default:
console.log(`sorry this type is not supported yet`)
break;
}
}
}

const audioPlayer = new AudioPlayer();

audioPlayer.play('mp3', 'fly me to the moon.mp3') // Mp3 file: fly me to the moon.mp3 is playing
audioPlayer.play('vlc', 'high way to hell.vlc') // Vlc file: high way to hell.vlc is playing
audioPlayer.play('mp4', 'help.mp4') // Mp4 file: help.mp4 is playing

桥接模式

桥接模式也叫桥梁模式,将实现与抽象放在两个不同的层次中,使得两者可以独立地变化。(最主要的将实现和抽象两个层次划分开来)

桥接模式将类分为了抽象与实现两个层次,其中抽象类作为桥梁定义角色的行为并保存一个实现类的引用。然后为了规范实现类具备的功能,实现类需要实现一个统一的interface

就好比GUI和API,用户操作的是GUI,GUI充当用户和系统之间的桥梁,调用各种API来处理具体的事物。

下面我们用一个小例子来体会一下,现在我们有两套播放设备,一套是tv,另一套是radio,客户希望可以对他们进行控制。此时我们通过创建一个抽象类RemoteControl来向用户提供一些操作接口,然后在这些接口中调用实现类的方法完成这些操作,这样就可以将用户和设配联系在一起了。

该场景下类图如下:

点击查看代码
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
// 实现类 的接口
interface Device {
isEnabled(): boolean;
enable(): void;
disable(): void;
getVolume(): number;
setVolume(number): void;
getChannel(): number;
setChannel(number): void;
getName(): string;
}

// 实现类1
class Tv implements Device {
private name: "Tv";
private state: "close" | "open";
private volume: number;
private channel: number;
constructor() {
this.name = "Tv";
this.state = "close";
this.volume = 0;
this.channel = 0;
}
isEnabled(): boolean {
return this.state === "open";
}
enable(): void {
this.state = "open";
}
disable(): void {
this.state = "close";
}
getVolume(): number {
return this.volume;
}
setVolume(number: any): void {
this.volume += number;
this.volume = this.volume > 100 ? 100 : this.volume;
this.volume = this.volume < 0 ? 0 : this.volume;
}
getChannel(): number {
return this.channel;
}
setChannel(number: any): void {
this.channel += number;
this.channel = this.channel < 0 ? 0 : this.channel;
}
getName(): string {
return this.name;
}
}

// 实现类2
class Radio implements Device {
private name: "Radio";
private state: "close" | "open";
private volume: number;
private channel: number;
constructor() {
this.name = "Radio";
this.state = "close";
this.volume = 0;
this.channel = 0;
}
isEnabled(): boolean {
return this.state === "open";
}
enable(): void {
this.state = "open";
}
disable(): void {
this.state = "close";
}
getVolume(): number {
return this.volume;
}
setVolume(number: any): void {
this.volume += number;
this.volume = this.volume > 100 ? 100 : this.volume;
this.volume = this.volume < 0 ? 0 : this.volume;
}
getChannel(): number {
return this.channel;
}
setChannel(number: any): void {
this.channel += number;
this.channel = this.channel < 0 ? 0 : this.channel;
}
getName(): string {
return this.name;
}
}

// 抽象类
// 它定义了两个类层次结构中“控制”部分的接口。
// 它管理着一个指向实现类对象的引用,并会将所有真实工作委派给该对象。
// 它扮演了一个桥梁的作用,将用户和device连接起来
class RemoteControl {
private device: Device;
constructor(device: Device) {
this.device = device;
}

togglePower() {
console.log(`toggle ${this.device.getName()} power`);
if (this.device.isEnabled()) this.device.disable();
else this.device.enable();
}

volumeDown() {
this.device.setVolume(this.device.getVolume() - 10);
}

volumeUp() {
this.device.setVolume(this.device.getVolume() + 10);
}

channelDown() {
this.device.setChannel(this.device.getChannel() - 10);
}

channelUp() {
this.device.setChannel(this.device.getChannel() + 10);
}
}

const tv = new Tv();
const tvRemote = new RemoteControl(tv);

tvRemote.togglePower(); // toggle Tv power

const radio = new Radio();
const radioRemote = new RemoteControl(radio);

radioRemote.togglePower(); // toggle Radio power

适用场景

  1. 如果你想要拆分或重组一个具有多重功能的庞杂类 (例如能与多个数据库服务器进行交互的类), 可以使用桥接模式。
    • 这样一来能够减少类的代码行数,提升代码可读性,此后,可以修改任意一个类层次结构而不会影响到其他类层次结构。
  2. 如果你希望在几个独立维度上扩展一个类,可使用该模式。
    • 桥接建议将每个维度抽取为独立的类层次。初始类将相关工作委派给属于对应类层次的对象, 无需自己完成所有工作。
  3. 如果你需要在运行时切换不同实现方法,可使用桥接模式。
    • 桥接模式可替换抽象类中的实现类对象。

缺点

  1. 对高内聚的类使用该模式可能会让代码更加复杂。

Combo

  1. 当抽象类只与特定的实现类组合时,可以将桥接模式与抽象工厂模式进行结合,抽象工厂负责对某些关系进行封装,对客户段隐藏其复杂性
  2. 生成器模式也能与桥接模式很好的组合,例如我们可以让主管类负责抽象类的工作,让各种不用的生成器类负责实现类的工作。

过滤器模式

过滤器模式用来操作一组对象,允许开发者使用不同标准对这组对象进行过滤从而得到不同的集合,该模式通过逻辑运算以解耦的方式把它们连接起来

参与过滤器设计模式中的角色包括:

  1. 抽象过滤器角色(AbstractFilter): 负责定义过滤器的实现接口
  2. 具体过滤器角色(ConcreteFilter): 负责具体的过滤器实现,最后返回一个过滤后的数据集合,标准过滤器只对数据进行过滤,当然也能对集合中的数据进行预处理,在将处理好的数据行程集合返回
  3. 被过滤对象(Subject): 具体的被过滤对象

类图中Criteria扮演抽象过滤器的角色Person扮演被过滤对象,其余均为实现对象。

我们根据上述类图来实现一个过滤器模式,首先我们来实现一个被过滤对象:

点击查看代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 被过滤对象
class Person {
constructor(name, gender, maritalStatus) {
this.name = name;
this.gender = gender;
this.maritalStatus = maritalStatus;
}
getName() {
return this.name
}
getGender() {
return this.gender
}
getMaritalStatus() {
return this.maritalStatus
}
}

接下来我们对过滤器对象进行实现,这个过程我们需要实现一个接口,随后根据结构实现一系列具体过滤器,我们还可以利用处理结合的方式,例如与、或操作,构造更为复杂的筛选条件:

点击查看代码
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
interface Criteria {
/**
* meetCriteria
*/
meetCriteria(persons: Array<Person>): Array<Person>;
}

class CriteriaMale implements Criteria {
public meetCriteria(persons: Array<Person>) {
return persons.filter((person: Person) => {
if (person.getGender().toUpperCase() === "MALE") {
return true;
}
});
}
}

class CriteriaFemale implements Criteria {
public meetCriteria(persons: Array<Person>) {
return persons.filter((person: Person) => {
if (person.getGender().toUpperCase() === "FEMALE") {
return true;
}
});
}
}

class CriteriaSingle implements Criteria {
public meetCriteria(persons: Array<Person>): Person[] {
return persons.filter((person: Person) => {
if (person.getMaritalStatus().toUpperCase() === "SINGLE") {
return true;
}
});
}
}

// 与筛选
class AndCriteria implements Criteria {
private criteria: Criteria;
private otherCriteria: Criteria;
constructor(criteria: Criteria, otherCriteria: Criteria) {
this.criteria = criteria;
this.otherCriteria = otherCriteria;
}

public meetCriteria(persons: Array<Person>): Person[] {
const middleArray = this.criteria.meetCriteria(persons);
return this.otherCriteria.meetCriteria(middleArray);
}
}

// 或筛选
class OrCriteria implements Criteria {
private criteria: Criteria;
private otherCriteria: Criteria;

constructor(criteria: Criteria, otherCriteria: Criteria) {
this.criteria = criteria;
this.otherCriteria = otherCriteria;
}

public meetCriteria(persons: Array<Person>): Person[] {
const result: Array<Person> = [];
const firstArray = this.criteria.meetCriteria(persons);
const secondArray = this.otherCriteria.meetCriteria(persons);

firstArray.forEach((person: Person) => {
result.push(person);
});

secondArray.forEach((person: Person) => {
result.push(person);
});

return result
}
}

随后我们就可以使用这些过滤器来处理一个目标集合了:

点击查看代码
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
const showPersons = (persons: Array<Person>) => {
persons.forEach((person, index) => {
console.log(person.getName());
});
};

const personList = new Array<Person>();

personList.push(new Person("Robert", "Male", "Single"));
personList.push(new Person("John", "Male", "Married"));
personList.push(new Person("Laura", "Female", "Married"));
personList.push(new Person("Diana", "Female", "Single"));
personList.push(new Person("Mike", "Male", "Single"));
personList.push(new Person("Bobby", "Male", "Single"));

const maleCriteria = new CriteriaMale();
const femaleCriteria = new CriteriaFemale();
const singleCriteria = new CriteriaSingle();
const singleAndMale = new AndCriteria(singleCriteria, maleCriteria);
const singleOrFemale = new OrCriteria(singleAndMale, femaleCriteria);

console.log("Males: ");
showPersons(maleCriteria.meetCriteria(personList));
/**
* Males:
* Robert
* John
* Mike
* Bobby
*/
console.log("Females: ");
showPersons(femaleCriteria.meetCriteria(personList));
/**
* Females:
* Laura
* Diana
*/
console.log("Singles: ");
showPersons(singleCriteria.meetCriteria(personList));
/**
* Singles:
* Robert
* Diana
* Mike
* Bobby
*/
console.log("Single And Males: ");
showPersons(singleAndMale.meetCriteria(personList));
/**
* Single And Males:
* Robert
* Mike
* Bobby
*/
console.log("Single Or Females: ");
showPersons(singleOrFemale.meetCriteria(personList));
/**
* Robert
* Mike
* Bobby
* Laura
* Diana
*/

优势

在需要做类的筛选的时候,通过每次单一功能的筛选,再做聚合能极大的降低筛选功能的复杂性。

组合设计模式

组合模式是一种结构型设计模式, 你可以使用它将对象组合成树状结构, 并且能像使用独立对象一样使用它们。

下面我们假设这样一个场景:我们有一些产品和一些盒子,一个盒子里可以装多个产品,或多个较小的盒子,以此类推。现在我们的订单系统需要支持无盒子包装的单一产品,也需要支持一个包含内部所有东西的盒子。

对于后者我们应该如何得到这个盒子的订单总价呢?

我们当然可以使用DFS等算法计算,但更好的方法是利用类似记忆化搜索的方法让每个盒子负责计算他内部的产品的价格,以此类推。

这样设计最大优点在于无需了解构成树状结构的对象的具体类。也无需了解对象是简单的产品还是复杂的盒子。 你只需调用通用接口以相同的方式对其进行处理即可。当你调用该方法后,对象会将请求沿着树结构传递下去。

组合结构类图如下:

  1. 组件Component描述了树结构上每个节点共有的方法
  2. 叶节点Leaf是树的基本结构,不包含子项,树中实际完成工作的单元,无法将工作指派给其他人
  3. 容器Composite用来包含其他容器和叶节点,容器无需了解组成自己的子项的具体类型,收到工作请求后,将其派发给子项完成,然后处理中间结果返回给上级

接下来我们用组合设计模式来实现一个文件树的小案例。

首先我们有文件和文件夹两周类型,文件夹里可以包含其他文件夹。另外为了方便地查看文件,每个文件夹和文件需要包含:

  1. 文件名
  2. 文件大小
  3. 一个getName方法来输出自身的名称
  4. 一个getSize方法来计算自身的体积
  5. display方法来输出自身以及自身包含的文件或文件夹

可以想到displaygetSize两个方法是需要根节点派发任务到子节点才能实现的。

那么首先我们可以实现一个接口来定义文件、文件夹等组件的共有方法:

点击查看代码
1
2
3
4
5
6
7
// 组件接口
interface Component {
getName(): string;
getSize(): number;
// 缩进数量
display(indentation: number): void;
}

随后利用组件接口我们来实现一下文件类和文件夹类,文件夹类需要一个Component数组:

点击查看代码
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
// 叶子节点:文件
class MyFile implements Component {
private name: string;
private size: number;
constructor(name: string, size: number) {
this.name = name;
this.size = size;
}
getName(): string {
return this.name;
}

getSize(): number {
return this.size;
}

display(indentation: number): void {
let tabLine = "";
for (let i = 0; i < indentation; i++) tabLine += " ";
console.log(tabLine + this.name, " Size: " + this.getSize());
}
}

// 组合节点:文件夹
class Folder implements Component {
private name: string;
private size: number;
private children: Array<Component> = [];

constructor(name: string, children: Array<Component> = []) {
this.name = name;
this.children = children;
this.size = this.getSize();
}

getName(): string {
return this.name;
}

getSize(): number {
this.size = this.children.reduce((pre, cur) => pre + cur.getSize(), 0);
return this.size;
}

add(child: Component): void {
this.children.push(child);
}

remove(child: Component): void {
const childIndex = this.children.indexOf(child);
if (childIndex !== -1) {
this.children.splice(childIndex, 1);
}
}

display(indentation: number): void {
let tabLine = "";
for (let i = 0; i < indentation; i++) tabLine += " ";
console.log(tabLine + this.name, " Size: " + this.getSize());
this.children.forEach((component: Component, index) => {
component.display(indentation + 4);
});
}
}

最后我们就可以利用这些类来构造一个目录了,利用display方法可以显示出层级结构以及文件大小:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
const file1 = new MyFile("file1.txt", 12);
const file2 = new MyFile("file2.txt", 13);
const folder1 = new Folder("Folder 1");
const folder2 = new Folder("Folder 2");

folder1.add(file1);
folder1.add(file2);
folder2.add(new MyFile("file3.txt", 30));

const rootFolder = new Folder("Root");
rootFolder.add(folder1);
rootFolder.add(folder2);

console.log("File system structure:");
rootFolder.display(0);
/**
* File system structure:
* Root Size: 55
* Folder 1 Size: 25
* file1.txt Size: 12
* file2.txt Size: 13
* Folder 2 Size: 30
* file3.txt Size: 30
*/

优势

让相互关联的对象产生了结构性,无论是在关系修改或者是关系直观性上,都只需要关心当前下级的关系,这样能更好的降低关系和关系之间的复杂度,加强单对象关系结构的可维护性。[引用自zy445566的design-pattern-in-javascript库]

由此可以引申出两种应用场景:

  1. 实现特定的树形对象结构时
  2. 需要使用相同的操作处理对象时,共用同一接口,此时就无需关注对象的具体实现

组合设计模式符合开闭原则。其无需更改现有代码,就可以在应用中添加新元素,使其成为对象树的一部分。[引用自重构大师]

Combo

  1. 在使用组合设计模式构造结构复杂的树时,可以使用生成器模式让树的构造以递归的形式进行
  2. 责任链模式通常和组合模式结合使用。在这种情况下,叶组件接收到请求后, 可以将请求沿包含全体父组件的链一直传递至对象树的底部。
  3. 可以使用迭代器模式来遍历组合树。
  4. 你可以使用访问者模式对整个组合树执行操作。
  5. 你可以使用享元模式实现组合树的共享叶节点以节省内存。
  6. 组合装饰模式的结构图很相似, 因为两者都依赖递归组合来组织无限数量的对象。
    1. 装饰类似于组合, 但其只有一个子组件。 此外还有一个明显不同: 装饰为被封装对象添加了额外的职责, 组合仅对其子节点的结果进行了 “求和”。
    2. 但是, 模式也可以相互合作: 你可以使用装饰来扩展组合树中特定对象的行为。
  7. 大量使用组合和装饰的设计通常可从对于原型模式的使用中获益。可以通过该模式来复制复杂结构,而非从零开始重新构造。

装饰设计模式

装饰模式是一种结构型设计模式, 允许你通过将对象放入包含行为的特殊封装对象中来为原对象绑定新的行为。

我们考虑以下场景:

  1. 首先公司原本有一个用于发送通知到邮箱的Notifier
  2. 现在由于社会的发展,社交媒体开始流行,手机得到普及,公司需要丰富通知类的功能,需要新增:
    1. 发送通知到短信的类
    2. 发送通知到微信的类
    3. 发送通知到QQ的类
    4. 能同时发送到微信和QQ的类
    5. 能同时发送到短信和微信的类

我们当然可以为每种发送方式基于Notifier类创建子类,但每种情况都需要创建特殊的子类,这样会是的子类数量爆炸:

除了继承以外,面向对象中还有两个重要的概念:聚合以及组合,两者都是使一个对象包含另一个对象的引用,并将部分任务委托给引用对象完成。

而继承则是子类自身完全负责完成工作。

聚合: 对象 A 包含对象 B; B 可以独立于 A 存在。

组合: 对象 A 由对象 B 构成; A 负责管理 B 的生命周期。 B 无法独立于A 存在。

在装饰设计模式中真正实现上述功能的对象我们称之为装饰器,一个装饰器需要与其分装对象实现相同的接口,这样在用户的视角下,装饰器和引用成员变量是完全一样的减少心智负担。而装饰器中的对象,可以是实现了相同接口的任意对象,这样就可以将一个对象放入多个封装器中,并在对象中添加所有这些装饰器的组合行为。

那上文提到的通知类来举例,我们可以将邮件通知行为放在基类中,将其他通知方式放入装饰器中:

当我们需要使用不同装饰器组合的时候,我们可以用嵌套的方式用装饰器构造装饰器,结构就像一个栈:

最终用户会拿到最后一个构造的装饰器,而由于所有装饰器均实现了同一接口,用户并不需要知道这个装饰器内部有多少层嵌套,直接使用send()方法即可。

下面我们再通过装饰设计模式实现一个文件读取写入的类,加深理解。

点击查看代码
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
// 部件 (Component): 数据源
// 声明封装器和被封装对象的公用接口。
interface DataSource {
writeData(data: string): void;
readData(): string;
}

// 具体部件 (Concrete Component) 类:文件数据源
// 被封装对象所属的类。 它定义了基础行为, 但装饰类可以改变这些行为。
class FileDataSource implements DataSource {
private filename: string;
constructor(filename: string) {
this.filename = filename;
}

writeData(data: string): void {
console.log(data + " 数据已写入");
}

readData(): string {
return this.filename;
}
}

// 装饰器基类
class DataSourceDecorator implements DataSource {
protected wrapper: DataSource;
constructor(wrapper: DataSource) {
this.wrapper = wrapper;
}

writeData(data: string): void {
this.wrapper.writeData(data);
}

readData(): string {
return this.wrapper.readData();
}
}

// 具体装饰器: 加密器
class EncryptionDecorator extends DataSourceDecorator {
writeData(data: string): void {
data = data + " 数据已加密";
this.wrapper.writeData(data);
// this.wrapper.writeData(data)
}

readData(): string {
let readData = this.wrapper.readData();
readData = readData + " 数据已解密";
return readData;
}
}

// 具体装饰器: 压缩器
class CompressionDecorator extends DataSourceDecorator {
writeData(data: string): void {
data = data + " 数据已压缩";
this.wrapper.writeData(data);
}

readData(): string {
let zipData = this.wrapper.readData();
zipData = zipData + " 数据已解压";
return zipData;
}
}

// 具体引用类:薪资文件管理
class SalaryManager {
private source: DataSource;
constructor(source: DataSource) {
this.source = source;
}

load() {
return this.source.readData();
}

save(salaryRecords: string) {
this.source.writeData(salaryRecords);
}
}

const getDataSource = (
enabledEncryption: boolean,
enabledCompression: boolean
) => {
let source: DataSource = new FileDataSource("data.txt");
if (enabledEncryption) {
source = new EncryptionDecorator(source);
}
if (enabledCompression) {
source = new CompressionDecorator(source);
}
return source;
};

// 构造薪资管理器,要求在存储之前进行加密和压缩,在读取之前进行解密和解压缩
const salaryLogger = new SalaryManager(getDataSource(true, true));

salaryLogger.save("salary"); // salary 数据已压缩 数据已加密 数据已写入
console.log(salaryLogger.load()); // data.txt 数据已解密 数据已解压

适用场景

  1. 在无需修改代码的情况下为对象添加额外行为
  2. 使用继承对对象进行扩展非常复杂时

js中已经增加了对装饰器语法的支持

优点

  1. 无需创建子类即可扩展行为
  2. 可以在运行时为对象添加功能
  3. 支持多装饰器的组合
  4. 符合单一责任原则(可以将一个大类拆分为数个小类)

缺点

  1. 嵌套装饰器难以删除其中的某一个
  2. 实现一个不受嵌套顺序影响的装饰器比较困难
  3. 各层的初始化配置代码比较复杂

Combo

  1. 适配器模式相比,装饰器可以在不修改对象接口的前提下强化对象,另外装饰器支持嵌套
  2. 适配器、装饰器与代理
    1. 适配器可以为被封装对象提供不同接口
    2. 代理模式能为对象提供相同的接口
    3. 装饰器能为对象提供强化的接口
  3. 组合模式装饰模式的结构图很相似,因为两者都依赖递归组合来组织无限数量的对象。
    1. 装饰类只有一个子组件。
    2. 装饰为被封装对象添加了额外的职责,而组合仅对其子节点的结果进行了 “求和”。
    3. 可以使用装饰来扩展组合树中特定对象的行为。
  4. 装饰可让你更改对象的外表,策略模式则让你能够改变其本质。
  5. 装饰代理
    1. 二者相似的结构,但是意图不同。两者都基于组合原则
    2. 两者之间的不同之处在于代理通常自行管理其服务对象的生命周期,而装饰的生成则总是由客户端进行控制。

外观设计模式

外观模式是一种结构型设计模式, 能为程序库、 框架或其他复杂类提供一个简单的接口。

假设一下场景:我们需要在项目中使用某个第三方库的众多对象,并按顺序组织他们的初始化工作。这样我们的业务逻辑会和第三方类的实现纠缠在一起形成紧密的耦合,使得维护难度增加

二外观设计模式就是用来处理这种情况的,我们可以为包含许多活动的复杂子系统提供一个简单的接口来降低用户的心智负担。虽然这一接口提供的功能有限,但他更关心用户需要的功能。

例如显示生活中的网购,我们只需要点击下单并付款,购物平台将为我们完成支付处理、调货、包装、纳税、送货等等操作。

外观模式类图如下所示:

例如我们使用外观设计模式来实现一个第三方视频转换框架,我们需要向客户暴露一个上传接口,其中需要读取视频文件,获得视频文件的源码,得到视频的比特流,随后使用对应的编码器将比特流编码为视频流,最后进行混音后返回。

首先我们来准备实际处理这些操作的类,包含:

  1. 视频文件类
  2. MPEG4视频编码类
  3. Ogg视频编码类
  4. 源码提取类
  5. 比特流读取类
  6. 混音类
点击查看代码
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
class VideoFile {
private name: string
constructor(name: string) {
this.name = name
}
read(): string {
return this.name
}
}

interface Compression {
compression(buffer: string): string
}

class MPEG4Compression implements Compression {
compression(buffer: string): string {
console.log("MPEG4编码文件: ", file.read())
return "MPEG4编码文件"
}
}

class OggCompression implements Compression {
compression(buffer: string): string {
console.log("OGG编码文件: ", file.read())
return "OGG编码文件"
}
}

// 读取视频源码
class CodeFactory {
extract(file: VideoFile): string {
console.log("读取视频源码: ", file.read())
return file.read() + "的源码"
}
}

// 读取bit流
class BitrateReader {
static read(filename: string, source: string): string {
console.log("将名为: ", filename,",的源码: ", source, ",读取为比特流")
return filename + "的二进制代码"
}

static convert(buffer: string, compression: Compression): string {
let result = compression.compression(buffer)
console.log("将文件: ", buffer, ", 编码为: ", result)
return "编码后的" + filename
}
}

// 混音类
class AudioMixer {
fix(codeFile: string): string {
console.log("对文件: ", codeFile, " 进行混音")
return "混音后的文件" + codeFile
}
}

随后我们定义一个类来对外暴露一个接口,在接口中实现整个业务逻辑,用户使用该接口即可:

点击查看代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class VideoConvert {
convert(filename: string, format: string): string {
let file = new VideoFile(filename)
let sourceCode = (new CodeFactory()).extract(file)
let compression = null
if(format === "mp4") {
compression = new MPEG4Compression()
} else {
compression = new OggCompression()
}
let buffer = BitrateReader.read(filename, sourceCode)
let res = BitrateReader.convert(buffer, compression)
res = (new AudioMixer()).fix(res)
return res
}
}

// 使用

convert = new VideoConvert()
mp4 = convert.convert("funny-cats-video.ogg", "mp4")
console.log(mp4)

使用场景

  1. 如果你需要一个指向复杂子系统的直接接口,且该接口的功能有限,则可以使用外观模式。
  2. 如果需要将子系统组织为多层结构,可以使用外观。创建外观来定义子系统中各层次的入口。可以要求子系统仅使用外观来进行交互,以减少子系统之间的耦合。
    1. 例如上述上传视频的功能,我们可以可以拆分为两个层次:音频相关和视频相关。可以为每个层次创建一个外观,然后要求各层的类必须通过这些外观进行交互。

优点

  1. 可以让自己的代码独立于复杂子系统。

缺点

  1. 外观可能成为与程序中所有类都耦合的上帝对象

Combo

  1. 外观模式在实现时与代理模式较为类似,他们都缓存了一个复杂实体并自行对其进行初始化,但不同之处在于代理与其服务对象遵循同一接口,使得自己和服务对象可以互换。
  2. 外观类通常可以转换为单例模式,大部分情况下一个外观对象就够用了
  3. 抽象工厂模式外观模式看起来也很相似,但他们的使用场景对他们进行了区分,如果只需要对用户隐藏子系统的对象创建,则使用抽象工厂模式
  4. 适配器模式只封装一个对象,并运用已有接口。外观模式作用于整个对象子系统上,并为现有对象定义了一个新的接口。

享元模式

享元模式是一种结构型设计模式, 它摒弃了在每个对象中保存所有数据的方式, 通过共享多个对象所共有的相同状态, 让你能在有限的内存容量中载入更多对象。

考虑如下场景:

  1. 由一个FPS游戏,需要设计特殊的粒子系统,希望弹片可以在战场上自由飞翔
  2. 那么粒子需要一个自己的类,应该包含方向、速度、颜色、精灵图等信息
  3. 希望战争场景尽量真实,因此需要让每颗子弹、导弹、炸弹爆炸时都生成数个粒子

但这样做显然是非常耗费内存的,每个粒子对象都需要在内存中保存自己的信息,包括精灵图

而事实上大部分粒子的颜色和精灵图都是相同的,不需要为每个粒子对象都存储一遍

在面向对象中,我们通常将对象中的状态分为两类:

  1. 内在状态——其他对象能读但不能修改
  2. 外在状态——能被其他对象充外部修改

而享元模式建议:不在对象中存储外在状态

那么,根据这一建议我们可以将原本的粒子对象分为两个对象:

  1. 粒子享元:Particle
    1. color
    2. sprite
  2. 移动粒子:MovingParticle
    1. particle
    2. coords
    3. vector
    4. speed

让每个移动中的,颜色,精灵图相同的粒子共用一个粒子对象,这一对象我们称其为享元

从图中我们可以看出,除了需要上述两个对象外,我们还需要添加以下几个部分:

  1. 在容器Game中添加一个数组存储并索引不同的粒子
  2. 在容器Game中添加一个数组来存储并索引每个粒子的坐标、方向矢量和速度
  3. 为容器Game添加一个方法addParticle来生成不同状态的对象

更优雅的,我们可以构造一个独立的情景类来管理享元对象和外在属性对象,这样容器Game就只需要一个数组就可以了。

由于享元对象需要被多个其他对象复用因此,它最好是不可被修改的,这样就不会出现牵一发而动全身的情况。

另外为了能够方便的访问各种享元,可以创建一个工厂来管理已有的享元对象的缓存池。

下面一起来看看更泛华的享元模式类图:

  1. 享元(Flyweight)类: 包含原始对象中部分能共享的状态,被称为内在状态
  2. 情景(Context)类: 包含原始对象中各部相同的状态,与其中的享元对象一起组成原始对象
    1. 通常原始对象的行为会被保存在享元对象中,但也可以将其保存在情景对象中
  3. 享元工厂(Flyweight Factory)类: 管理享元缓存池,工厂会根据客户提供的参数从缓存池中查找特定享元,有则直接用,无则创建并加入缓存池

示例

下面我们以绘制树形对象为例来使用享元模式设计一系列对象:

我们的树具有一些类型,他们包含一下状态:

  1. name
  2. color
  3. texture

另外,每棵树需要画在不同的地方,因此需要:

  1. x
  2. y

两个坐标。而使用这些树的容器我们称其为森林Forest

那么我们可以得到这样一幅类图:

这样一来,我们在渲染这个森林时,就能尽量少的占用内存。

根据这张图我们可以尝试写一个简单的Demo。

点击查看代码
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
class TreeType {
private name: string;
private color: string;
private texture: string;

constructor(name, color, texture) {
this.name = name;
this.color = color;
this.texture = texture;
}

draw(canvas: string, x: number, y: number) {
console.log(
`Draw a ${this.name} tree with ${this.color} color & ${this.texture} texture at (${x},${y}) on ${canvas}`
);
}

getName(): string {
return this.name;
}

getColor(): string {
return this.color;
}

getTexture(): string {
return this.texture;
}
}

class Tree {
public x: number;
public y: number;
public type: TreeType;

constructor(
x: number,
y: number,
name: string,
color: string,
texture: string
) {
this.x = x;
this.y = y;
this.type = TreeFactory.getTreeType(name, color, texture);
}
draw(canvas: string) {
this.type.draw(canvas, this.x, this.y);
}
}

class TreeFactory {
static types: TreeType[] = [];

constructor() {
TreeFactory.types = [];
}

static getTreeType(name: string, color: string, texture: string): TreeType {
let res = TreeFactory.types.filter((type: TreeType, index: number) => {
if (
type.getColor() === color &&
type.getName() === name &&
type.getTexture() === texture
) {
return true;
} else {
return false;
}
});

if (res !== null && res.length !== 0) {
return res[0];
} else {
let newType = new TreeType(name, color, texture);
TreeFactory.types.push(newType);
return newType;
}
}
}

class Forest {
public trees: Tree[];
constructor() {
this.trees = [];
}

plantTree(x, y, name, color, texture): Tree {
let newTree = new Tree(x, y, name, color, texture);
this.trees.push(newTree);
return newTree;
}

draw(canvas: string) {
this.trees.forEach((tree: Tree, index: number) => {
tree.draw(canvas);
});
}
}

let forest = new Forest();

forest.plantTree(0, 0, "Trio", "red", "shadow");
forest.plantTree(0, 1, "Trio", "yellow", "solid");
forest.plantTree(0, 2, "Trio", "blue", "dash");
forest.plantTree(1, 0, "Trio", "red", "shadow");
forest.plantTree(1, 1, "B+", "green", "solid");
forest.plantTree(1, 2, "B+", "green", "solid");

forest.draw("Window");

// Draw a Trio tree with red color & shadow texture at (0,0) on Window
// Draw a Trio tree with yellow color & solid texture at (0,1) on Window
// Draw a Trio tree with blue color & dash texture at (0,2) on Window
// Draw a Trio tree with red color & shadow texture at (1,0) on Window
// Draw a B+ tree with green color & solid texture at (1,1) on Window
// Draw a B+ tree with green color & solid texture at (1,2) on Window

使用场景

  1. 程序支持大量对象且没有足够的内存容量时使用

优点

  1. 对于具有相似状态的大量对象,可以节约内存

缺点

  1. 时间换空间,享元工厂挑选享元的过程需要花费额外的时间
  2. 代码复杂,一个实体将被拆分为多个实体

Combo

  1. 使用享元模式实现组合模式树的共享叶节点以节省内存。
  2. 享元展示了如何生成大量的小型对象,外观模式则展示了如何用一个对象来代表整个子系统。
  3. 如果将对象的所有共享状态简化为一个享元对象,那么享元就和单例模式类似了。 但这两个模式有两个根本性的不同:
    1. 单例只有一个实体,享元可以有多个,且内在状态不同
    2. 单例对象可变,享元不可变

代理模式

代理模式是一种结构型设计模式, 让你能够提供对象的替代品或其占位符。 代理控制着对于原对象的访问, 并允许在将请求提交给对象前后进行一些处理。

考虑如下场景,当我们在使用某个第三方类时:

  1. 构造该类对象将消耗大量系统资源
  2. 对该对象的使用并不频繁

我们可能会想到对该类实现延迟初始化,即在使用时再进行实例化对象。

我们希望可以将延时初始化的逻辑写入类中来避免冗余代码,但如果第三方库闭源,我们就不得在每个使用该对象的文件中实现延时初始化逻辑。

当然,我们可以使用代理模式解决这一问题。我们可以设计一个代理类,让它与原服务对象接口相同,于是我们就能在代理类中,在实际调用接口之前或之后进行一些操作来实现特殊的功能。

  1. ServiceInterface服务接口,为代理类以及服务类提供接口规范,但从服务类中抽取接口并非总是可行的,因为需要对服务的所有客户端进行修改,让它们使用接口。备选计划是将代理作为服务类的子类, 这样代理就能继承服务的所有接口了。
  2. Service服务类,实际处理业务逻辑的类
  3. Proxy代理类,包含一个指向服务对象的引用成员变量,完成服务对象进行业务处理前后的工作。通常情况下,代理负责创建服务并对其整个生命周期进行管理。 在一些特殊情况下,客户端会通过构造函数将服务传递给代理。
  4. 可以考虑新建一个构建方法来判断客户端可获取的是代理还是实际服务。可以在代理类中创建一个简单的静态方法,也可以创建一个完整的工厂方法。
  5. 可以考虑为服务对象实现延迟初始化。

下面我们以一个使用第三方视频工具(例如腾讯视频TencentVideo,TV)库,尝试为其添加延迟初始化和缓存功能的案例,来感受一下代理设计模式。

首先我们需要一个腾讯视频类充当服务类,进行实际的视频业务,随后我们需要实现一个接口,其中包含腾讯视频类中的一些方法。最后实现一个代理类,以腾讯视频对象作为属性,并实现需要代理的方法。

点击查看代码
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
// 模拟下载队列
const downLoadList: Array<Video> = [];

// 判断当前视频是否已在下载队列
const downLoadExists = (id: number) => {
let judgeList = downLoadList.filter((value: Video) => {
return id === value.id;
});
return judgeList.length > 0;
};

interface VideoLib {
listVideos();
getVideoInfo(id: number);
downloadVideo(id: number);
}

type Video = {
id: number;
name: string;
};

class TVClass implements VideoLib {
videoList: Array<Video>;
constructor() {
this.videoList = [
{ id: 1, name: "1.mp4" },
{ id: 2, name: "2.mp4" },
];
}

listVideos(): string {
return JSON.stringify(this.videoList);
}

getVideoInfo(id: number): string {
return JSON.stringify(this.videoList[id]);
}

downloadVideo(id: number): void {
let video = this.getVideoInfo(id);
console.log("视频正在下载...");
console.log(video);
console.log("下载成功!");
}
}

class CacheTVClass implements VideoLib {
private service: TVClass;
private listCache: string;
private videoCache: string;
private needReset;

constructor(service: TVClass) {
this.service = service;
this.needReset = false;
}

listVideos() {
if (this.listCache === null || this.needReset) {
this.listCache = this.service.listVideos();
}
return this.listCache;
}

getVideoInfo(id: number) {
if (this.videoCache === null || this.needReset) {
this.videoCache = this.service.getVideoInfo(id);
}
return this.videoCache;
}

downloadVideo(id: number) {
if (downLoadExists(id) || this.needReset) {
this.service.downloadVideo(id);
}
}
}

// 实际使用服务的类
class TVManager {
protected service: VideoLib;

constructor(service: VideoLib) {
this.service = service;
}

renderVideoPage(id: number) {
let info = this.service.getVideoInfo(id);
// 渲染视频页面。
console.log("视频详情页面被渲染!");
}

renderListPanel() {
let list = this.service.listVideos();
// 渲染视频缩略图列表。
console.log("视频缩略图列表被渲染!");
}

reactOnUserInput(id: number) {
console.log(`响应用户点击,并渲染视频 ${id} 的详情页`);
this.renderVideoPage(id);
this.renderListPanel();
}
}

const tyService = new TVClass();
const tyProxy = new CacheTVClass(tyService);
const manager = new TVManager(tyProxy);
manager.reactOnUserInput(1);

// 响应用户点击,并渲染视频 1 的详情页
// 视频详情页面被渲染!
// 视频缩略图列表被渲染!

试用场景

  1. 延迟初始化(虚拟代理)。如果有一个偶尔使用的重量级服务对象,一直保持该对象运行会消耗系统资源时,可使用代理模式。
    1. 无需在程序启动时就创建该对象,可将对象的初始化延迟到真正有需要的时候。
  2. 访问控制 (保护代理)。如果只希望特定客户端使用服务对象,这里的对象可以是操作系统中非常重要的部分,而客户端则是各种已启动的程序 包括恶意程序),此时可使用代理模式。
    1. 代理可仅在客户端凭据满足要求时将请求传递给服务对象。
  3. 本地执行远程服务(远程代理)。适用于服务对象位于远程服务器上的情形。
    1. 在这种情形中,代理通过网络传递客户端请求,负责处理所有与网络相关的复杂细节。
  4. 记录日志请求(日志记录代理)。适用于当需要保存对于服务对象的请求历史记录时。
  5. 缓存请求结果(缓存代理)。适用于需要缓存客户请求结果并对缓存生命周期进行管理时,特别是当返回结果的体积非常大时。
  6. 智能引用。可在没有客户端使用某个重量级对象时立即销毁该对象。
    1. 代理会可以将所有获取了指向服务对象或其结果的客户端记录在案。代理可以时不时地遍历各个客户端,检查它们是否仍在运行。如果相应的客户端列表为空,代理就会销毁该服务对象,释放底层系统资源。
  7. 代理还可以记录客户端是否修改了服务对象。 其他客户端还可以复用未修改的对象。

优点

  1. 可以在客户端毫无察觉的情况下控制服务对象。
  2. 如果客户端对服务对象的生命周期没有特殊要求,你可以对生命周期进行管理。
  3. 即使服务对象还未准备好或不存在,代理也可以正常工作。
  4. 开闭原则。可以在不对服务或客户端做出修改的情况下创建新代理。

缺点

  1. 代码可能会变得复杂,因为需要新建许多类。
  2. 服务响应可能会延迟。

Combo

  1. 适配器模式能为被封装对象提供不同的接口,代理模式能为对象提供相同的接口,装饰模式则能为对象提供加强的接口。
  2. 外观模式代理的相似之处在于它们都缓存了一个复杂实体并自行对其进行初始化。代理与其服务对象遵循同一接口,使得自己和服务对象可以互换,在这一点上它与外观不同。
  3. 装饰代理有着相似的结构,但是其意图却非常不同。这两个模式的构建都基于组合原则,也就是说一个对象应该将部分工作委派给另一个对象。两者之间的不同之处在于代理通常自行管理其服务对象的生命周期,而装饰的生成则总是由客户端进行控制

行为型模式

行为型模式 (Behavioral Pattern) 是对 在不 同的对象之间划分责任和算法的抽象化 。 行为型模式不仅仅关注类和对象的结构,而 且 重点关注它们之间的相互作用 。 通过行为型模式,可以更加清晰地 划分类与 对象的职责 ,并 研究系统在运行时实例对象 之间的交互 。在系统运行时,对象并不是孤 立的,它们可以通过相互通信与协作完成某 些复杂功能,一个对象在运行时也将影响到 其他对象的运行。 行为型模式分为 类行为型模式 和 对象行为型模式 两种: • 类行为型模式 :类的行为型模式 使用继承关系在几个类之间分配行为 ,类行为型模式主要通过多态等方式来分配父类与子类的职责。 • 对象行为型模式 :对象的行为型模式则 使用对象的聚合关联关系来分配行为 ,对象行为型模式主要是通过对象关联等方式来分配两个或多个类的职责。根据“合成复用原则”,系统中要尽量使用关联关系来取代继承关系,因此大部分行为型设计模式都属于对象行为型设计模式

责任链模式(Chain of Responsibility Pattern)

责任链模式是一种行为设计模式, 允许你将请求沿着处理者链进行发送。 收到请求后, 每个处理者均可对请求进行处理, 或将其传递给链上的下个处理者。

我们来看以下场景:

  1. 现在有一个在线订购系统
  2. 希望限制:有认证的用户才能创建订单
  3. 希望限制:有管理员权限的用户拥有所有订单的完全访问权限

接下来有同事提出这个系统存在安全隐患,需要增加一些功能:

  1. 需要增加额外的验证步骤来清理请求中的数据
  2. 需要添加一个检测步骤来过滤来自同一IP地址的重复错误请求,避免暴力密码破解
  3. 需要增加一个缓存查找功能,包含同样数据的重复请求返回缓存中的结果,提高响应速度

但我们还把这一大堆东西加入原本的身份认证机制中后,将得到一大坨复杂、难以复用且难以维护的代码。

这时,责任链模式可以优雅的解决上述问题。

责任链模式会将上述功能,按照单一责任原则,才分为一个一个的处理者,然后将这些处理者按照输入输出一个一个链接起来,每个处理者都有一个成员变量来保存对于下一处理者的引用。这些处理者除了处理请求外,还要负责沿着责任链传递请求。处理者可以决定不再沿着链传递请求, 这可高效地取消所有后续处理步骤

还有一种更为经典的责任链设计模式,我们在JS中触发事件时,最外层对象Window对象会率先处理事件,最后会一层一层向内存对象传递,最后到达能够处理该事件的对象,则响应该事件,并将结果一层一层冒泡场地到最外层window对象。

所有处理者类均实现同一接口是关键所在。每个具体处理者仅关心下一个包含 execute执行方法的处理者。 这样一来, 就可以在运行时使用不同的处理者来创建链,而无需将相关代码与处理者的具体类进行耦合。

责任链模式类图如下:

  1. 处理者Handler:为处理者声明接口。
  2. 基础处理者BaseHandler:可选类,将所有处理者共同的代码保存在这里,例如确定下一个处理者存在后再将请求传递给它。
  3. 具体处理者ConcreteHandler: 实际处理请求的类,接收到请求后举动直接处理或传递给下一个处理者。

下面我们用责任链模式设计一个投喂系统,不同动物会根据请求给出的不同食物各取所需。那么我们需要如下几个部分:

  1. 处理者接口
    1. 责任链构建方法
    2. 请求处理方法
  2. 基础处理者抽象类
    1. 构建方法:接受一个处理者,保存,然后返回该处理者以实现链式调用
    2. 处理方法:处理者非空判断
  3. 具体处理者
    1. 处理方法:若能处理则返回结果,否则调用父类处理方法
  4. 客户端
    1. 构建责任链
    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
/**
* 处理者接口
* 包含一个责任链构建方法
* 和一个请求处理方法
*/
interface Handler<Request = string, Result = string> {
setNext(handler: Handler<Request, Result>): Handler<Request, Result>;

handle(request: Request): Result | null;
}

/**
* 基础处理者抽象类
*/
abstract class AbstractHandler implements Handler {
private nextHandler: Handler;

public setNext(handler: Handler): Handler {
this.nextHandler = handler;
// 返回处理者,以支持链式调用
// monkey.setNext(squirrel).setNext(dog);
return handler;
}

public handle(request: string): string | null {
if (this.nextHandler) {
return this.nextHandler.handle(request);
}
return null;
}
}

/**
* 具体处理者
*/
class MonkeyHandler extends AbstractHandler {
public handle(request: string): string | null {
if (request === "Banana") {
return `Monkey: I'll eat the ${request}.`;
}
return super.handle(request);
}
}

class SquirrelHandler extends AbstractHandler {
public handle(request: string): string | null {
if (request === "Nut") {
return `Squirrel: I'll eat the ${request}.`;
}
return super.handle(request);
}
}

class DogHandler extends AbstractHandler {
public handle(request: string): string | null {
if (request === "MeatBall") {
return `Dog: I'll eat the ${request}.`;
}
return super.handle(request);
}
}

/**
* 客户端首先创建责任链.
*/
const monkey = new MonkeyHandler();
const squirrel = new SquirrelHandler();
const dog = new DogHandler();

monkey.setNext(squirrel).setNext(dog);

/**
* 随后构造客户端调用方法的行为
*/
function clientCode(handler: Handler) {
const foods = ["Nut", "Banana", "Cup of coffee"];

for (const food of foods) {
console.log(`Client: Who wants a ${food}?`);

const result = handler.handle(food);
if (result) {
console.log(` ${result}`);
} else {
console.log(` ${food} was left untouched.`);
}
}
}

/**
* 可以将任意处理者传入调用方法
*/
console.log("Chain: Monkey > Squirrel > Dog\n");
clientCode(monkey);
console.log("");

console.log("Subchain: Squirrel > Dog\n");
clientCode(squirrel);

/**
* Chain: Monkey > Squirrel > Dog
*
* Client: Who wants a Nut?
* Squirrel: I'll eat the Nut.
* Client: Who wants a Banana?
* Monkey: I'll eat the Banana.
* Client: Who wants a Cup of coffee?
* Cup of coffee was left untouched.
*
* Subchain: Squirrel > Dog
*
* Client: Who wants a Nut?
* Squirrel: I'll eat the Nut.
* Client: Who wants a Banana?
* Banana was left untouched.
* Client: Who wants a Cup of coffee?
* Cup of coffee was left untouched.
*/

试用场景

  1. 当程序需要使用不同方式处理不同种类请求,而且请求类型和顺序预先未知时,可以使用责任链模式。
  2. 当必须按顺序执行多个处理者时,可以使用该模式。
  3. 如果所需处理者及其顺序必须在运行时进行改变,可以使用责任链模式。

优点

  1. 允许处理请求顺序。
  2. 符合单一责任原则。可对发起操作和执行操作的类进行解耦。
  3. 符合开闭原则。可以在不更改现有代码的情况下在程序中新增处理者。

缺点

  1. 部分请求可能未被处理。

Combo

  1. 责任链模式命令模式中介者模式观察者模式都是用于处理请求发送者和接收者之间的不同连接方式:
    1. 责任链按照顺序将请求传递给潜在接收者。直到有一名处理
    2. 命令在发送者和接收者之间建立单向连接🔗
    3. 中介者消除了发送者和接收者之间的直接连接🔗,强制他们与一个中介对象进行沟通
    4. 观察者允许接收者动态地订阅或取消接收请求
  2. 责任链通常和组合模式结合使用。在这种情况下,叶组件接收到请求后,可以将请求沿包含全体父组件的链一直传递至对象树的底部。
  3. 责任链的管理者可使用命令模式实现。在这种情况下,你可以对由请求代表的同一个上下文对象执行许多不同的操作。另一种是请求自身就是一个命令对象。在这种情况下,可以对由一系列不同上下文连接而成的链执行相同的操作。
  4. 责任链装饰模式的类结构非常相似。两者都依赖递归组合将需要执行的操作传递给一系列对象。但是,两者有几点重要的不同之处。
    1. 责任链的管理者可以相互独立地执行一切操作,还可以随时停止传递请求。
    2. 各种装饰可以在遵循基本接口的情况下扩展对象的行为。
    3. 装饰无法中断请求的传递。

命令模式

命令模式将请求的所有相关信息转化为一个独立对象,这一操作可以允许我们根据不同请求将方法参数化、延迟请求执行或者将其放入队列,且能支持撤销操作。

现在我们有一个构建GUI的业务,他也许包含以下特征:

  1. 需要创建一个多个按钮的工具栏
  2. 每个按钮对应不同的功能,例如打开、保存、打印
  3. 同一功能,例如复制,除了试用按钮完成,还需要支持右键菜单复制与ctrl + C完成

对于第一项需求,我们也许会设计一个简洁的按钮基类Btn,随后设计一些按钮类的子类,来支持不同的功能,例如OpenBtn,SaveBtn。

而对于第三个需求,我们有两个方案:

  1. 将复制按钮中的逻辑代码复制一份到菜单类中
  2. 让菜单依赖于按钮

前者造成了不小的代码重复,而后者将导致我们的类继承关系混乱。

此时,命令模式可以很好地解决上述问题。

命令模式将类的行为抽象成了命令类如图所示:

  1. 发送者Sender: 负责对请求进行初始化操作,需要包含一个成员变量来存储命令对象的引用,发送者只负责触发命令,而不需要向接收者发送请求。(注意:发送者并不负责创建命令对象,而是通过构造函数获得预先生成的命令)。
  2. 命令Command: 命令接口,通常只需要声明一个执行函数。
  3. 具体命令ConcreteCommand: 实现请求,并不完成具体的业务逻辑,而是委派给一个业务逻辑对象,请求参数可保存在成员变量中。
  4. 接收者Receiver: 实际处理业务逻辑的类,几乎所有对象都能作为接收者。

下面我们来实现一个简单的DEMO来感受一下每个类的作用:

  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
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
// TODO: 命令接口,声明执行方法
interface Command {
execute(): void;
}

// TODO: 接收者,处理实际业务逻辑
class Receiver {
doSomething(a: string): void {
console.log(`Receiver: Working on (${a}.)`);
}

doSomethingElse(b: string): void {
console.log(`Receiver: Working on (${b}.)`);
}
}

// TODO: 简单命令
class SimpleCommand implements Command {
private payload: string;
constructor(payload: string) {
this.payload = payload;
}

execute(): void {
console.log(
`SimpleCommand: See, I can do simple things like printing (${this.payload})`
);
}
}

// TODO: 复杂命令,需要委派给接收者
class ComplexCommand implements Command {
private receiver: Receiver;

// TODO: 上下文信息,用于保存请求参数
private a: string;
private b: string;

constructor(receiver: Receiver, a: string, b: string) {
this.receiver = receiver;
this.a = a;
this.b = b;
}

execute(): void {
console.log(
"ComplexCommand: Complex stuff should be done by a receiver object."
);
this.receiver.doSomething(this.a);
this.receiver.doSomethingElse(this.b);
}
}

// TODO: 与命令相关联,向命令发送请求
class Invoke {
private onStart: Command;
private onFinish: Command;

setOnStart(command: Command) {
this.onStart = command;
}

setOnFinish(command: Command) {
this.onFinish = command;
}

private isCommand(object): object is Command {
return object.execute !== undefined;
}

// Invoke类并不需要依赖具体的命令或是接收者,Invoke通过执行命令来调用接收者
doSomethingImportant(): void {
console.log(
"Invoker: Does anybody want something done before I begin?"
);
if (this.isCommand(this.onStart)) {
this.onStart.execute();
}
console.log("Invoker: ...doing something really important...");

console.log(
"Invoker: Does anybody want something done after I finish?"
);

if (this.isCommand(this.onFinish)) {
this.onFinish.execute();
}
}
}

const invoker = new Invoke();

invoker.setOnStart(new SimpleCommand("Say Hi!"));
const receiver = new Receiver();

invoker.setOnFinish(
new ComplexCommand(receiver, "Send Message", "Save report")
);

invoker.doSomethingImportant();
// Invoker: Does anybody want something done before I begin?
// SimpleCommand: See, I can do simple things like printing (Say Hi!)
// Invoker: ...doing something really important...
// Invoker: Does anybody want something done after I finish?
// ComplexCommand: Complex stuff should be done by a receiver object.
// Receiver: Working on (Send Message.)
// Receiver: Working on (Save report.)

试用场景

  1. 当需要通过操作来参数化对象时
  2. 当需要将操作放入队列中或者计划命令执行时间等等需要序列化命令时
  3. 当需要实现命令回滚时

优点

  1. 符合单一责任原则。可将触发和执行操作类解耦
  2. 符合开闭原则。可以在不修改已有代码的情况下构建新的命令
  3. 可以实现撤销和恢复功能
  4. 可以实现操作延迟执行
  5. 可以将一组简单的命令组合为复杂的命令

缺点

  1. 代码可能会因为全新的层次:命令层的引入而变得复杂

Combo

  1. 责任链的管理者可使用命令模式实现。在这种情况下,可以对由请求代表的同一个上下文对象执行许多不同的操作。另外一种实现方式,是请求自身就是一个命令对象。这样可以对由一系列不同上下文连接而成的链执行相同的操作。
  2. 可以同时使用命令和备忘录模式来实现 “撤销”。在这种情况下,命令用于对目标对象执行各种不同的操作,备忘录用来保存一条命令执行前该对象的状态。
  3. 命令策略模式看上去很像,因为两者都能通过某些行为来参数化对象。但是,它们的意图有非常大的不同:
    1. 可以使用命令来将任何操作转换为对象。可以通过转换来延迟操作的执行、将操作放入队列、保存历史命令或者向远程服务发送命令等。
    2. 策略通常可用于描述完成某件事的不同方式,让你能够在同一个上下文类中切换算法。
  4. 原型模式可用于保存命令的历史记录。
  5. 可以将访问者模式视为命令模式的加强版本,其对象可对不同类的多种对象执行操作。

迭代器模式

迭代器模式是一种行为设计模式,能让你在不暴露集合底层表现形式(如列表、栈、队列)的情况下遍历集合中所有的元素

我们考虑以下场景:

  1. 在一个系统中我们有一个树形集合容器
  2. 需要为这一容器设计一些遍历算法
  3. 但可能今天需要用DFS遍历,明天需要用BFS遍历

在这一场景中我们可能需要配合业务不断的增加新的遍历算法,这将造成一下几点问题:

  1. 模糊集合“高效存储数据”的主要职责。
  2. 根据特定应用定制的算法加入泛型集合类造成不必要的内存开销
  3. 客户端不得不关心当前集合类是否能够进行某些元素访问方式,使得代码与特定集合耦合。

迭代器模式就是为了解决这一问题而生。迭代器模式的主要思想是将集合的遍历行为抽取为单独的迭代器对象。

迭代器类主要包含以下内容:

  1. 实现自身迭代算法
  2. 封装遍历操作的所有细节
  3. 获取集合元素的基本方法

另外我们需要实现一个迭代器接口,来保证迭代器方法一致。迭代器模式类图如下:

  1. 迭代器Iterator: 迭代器接口,声明了遍历集合所需要的操作,例如获取下一个元素、获取当前位置、重新开始迭代
  2. 具体迭代器ConcreteIterator: 实现遍历集合的一种算法。迭代器对象必须追踪自身遍历的进度。这使得多个迭代器可以相互独立地遍历同一集合
  3. 集合Collection: 集合接口,声明一个或多个方法来获取与集合兼容的迭代器。方法返回的类型必须为迭代器接口,因此具体集合可以返回各种不同的迭代器
  4. 具体集合ConcreteCollection: 实现集合接口中的方法,为客户端返回一个特定的具体迭代器对象。
  5. 客户端Client: 客户端通常不会执行创建迭代器,而是从集合中获取。但特定情况下,例如当客户端要自定义特殊迭代器,客户端可以直接创建一个迭代器。

下面我们尝试为一个字符串集合构建迭代器:

  1. 迭代器接口:需要支持获取当前元素、获取下一元素、获取当前元素key值、检测当前指针位置是否合法、重置指针的操作
  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
// TODO: 迭代器接口
interface MyIterator<T> {
// 获取当前元素
getCurrent(): T;

// 返回当前元素并将指针移动到下一元素
getNext(): T;

// 返回当前预算的key
getCurrentKey(): number;

// 检测当前位置是否有效
checkValid(): boolean;

// 将迭代器指针指向第一个元素
rewind(): void;
}

// TODO: 集合接口,定义获得迭代器的方法
interface Aggregator {
getIterator(): MyIterator<string>;
}

// TODO: 具体集合类
class WordsCollection implements Aggregator {
private items: string[] = [];

// 获得集合
public getItems(): string[] {
return this.items;
}

// 获得长度
public getCount(): number {
return this.items.length;
}

public addItem(item: string) {
this.items.push(item);
}

// 获得迭代器
public getIterator(): MyIterator<string> {
return new AlphabeticalOrderIterator(this);
}

// 获得方向迭代器
public getReverseIterator(): MyIterator<string> {
return new AlphabeticalOrderIterator(this, true);
}
}

// TODO: 具体迭代器类
class AlphabeticalOrderIterator implements MyIterator<string> {
private collection: WordsCollection;

private position: number = 0;

// 遍历方向
private reverse: boolean = false;

constructor(collection: WordsCollection, reverse: boolean = false) {
this.collection = collection;
this.reverse = reverse;
if (reverse) {
this.position = collection.getCount() - 1;
}
}

public rewind() {
this.position = this.reverse ? this.collection.getCount() - 1 : 0;
}

public getCurrent(): string {
return this.collection.getItems()[this.position];
}

public getCurrentKey(): number {
return this.position;
}

public getNext(): string {
const item = this.collection.getItems()[this.position];
this.position += this.reverse ? -1 : 1;
return item;
}

public checkValid(): boolean {
return this.reverse
? this.position >= 0
: this.position < this.collection.getCount();
}
}

const collection = new WordsCollection();
collection.addItem("first");
collection.addItem("second");
collection.addItem("third");

const iterator = collection.getIterator();

console.log("Straight traversal:");
while (iterator.checkValid()) {
console.log(iterator.getNext());
}

console.log("");
console.log("Reverse traversal:");
const reverseIterator = collection.getReverseIterator();
while (reverseIterator.checkValid()) {
console.log(reverseIterator.getNext());
}
/**
* Straight traversal:
* first
* second
* third
*
* Reverse traversal:
* third
* second
* first
*/

应用场景

  1. 当集合背后为复杂的数据结构,且你希望对客户端隐藏其复杂性时(出于使用便利性或安全性的考虑),可以使用迭代器模式。
  2. 使用该模式可以减少程序中重复的遍历代码。
  3. 如果希望代码能够遍历不同的甚至是无法预知的数据结构,可以使用迭代器模式。

优点

  1. 符合单一责任原则。将遍历算法抽离为独立的类
  2. 符合开闭原则。无需修改代码,就能创建新的集合与迭代器
  3. 可以并行遍历同一集合,因为迭代器对象都包含其自身的遍历状态。
  4. 可以暂停遍历,并在需要时继续

缺点

  1. 如果程序只与简单的集合进行交互,应用该模式可能会矫枉过正。
  2. 对于某些特殊集合,使用迭代器可能比直接遍历的效率低。

Combo

  1. 可以使用迭代器模式来遍历组合模式树
  2. 可以同时使用工厂方法模式迭代器来让子类集合返回不同类型的迭代器, 并使得迭代器与集合相匹配。
  3. 可以同时使用备忘录模式迭代器来获取当前迭代器的状态, 并且在需要的时候进行回滚。
  4. 可以同时使用访问者模式迭代器来遍历复杂数据结构,并对其中的元素执行所需操作,即使这些元素所属的类完全不同。

解释器模式Interpreter Pattern

给分析对象定义一个语言,并定义该语言的文法表示,再设计一个解析器来解释语言中的句子。也就是说,用编译语言的方式来分析应用中的实例。这种模式实现了文法表达式处理的接口,该接口解释一个特定的上下文。

中介模式

中介模式是一种行为设计模式,他能减少对象之间混乱无序的依赖关系。通过限制对象之间的直接交互,强迫他们通过一个中介对象进行合作

中介模式建议我们停止组件之间的直接交流,使其相互独立,这样就不会出现一个组件与多个其他组件相互耦合的情况,一个组件只会依赖于一个中介类。中介模式类图如下

  1. 组件Component: 包含业务逻辑的类,依赖于一个中介对象,保存其引用。组件无需知道中介对象的具体类,只需要知道其实现了中介接口既可以,这样便于连接不同中介类,并在程序中复用
  2. 中介接口Mediator: 声明了中介与组件交流的方法,通常包含一个通知方法,以上下文作为参数,将接收组件与发送组件解耦
  3. 具体中介ConcreteMediator: 封装组件间的关系,保存所有组件的引用并对其进行管理,甚至可以管理其生命周期
  4. 对于组件而言,中介者是一个黑箱,组件并不知道其发送的消息将由谁来处理,也不会知道是谁给它发送了消息。

下面我们来写一个Demo感受一下中介模式,我们需要以下接口:

  1. 中介接口: 声明一个消息提醒接口,它接收一个参数sender代表发送信息的组件,一个上下文信息event,这里用string简单演示
  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
// TODO: 中介接口

interface Mediator {
/**
* notify
* @param:
* sender: object
* event: string
* @returns:
* void
*/
notify(sender: object, event: string): void;
}

// TODO: 实现组件基类
class BaseComponent {
// TODO: 组件需要保存中介对象
protected mediator: Mediator;
constructor(mediator?: Mediator) {
this.mediator = mediator!;
}

public setMediator(mediator: Mediator) {
this.mediator = mediator;
}
}

// TODO: 实际组件
class Component1 extends BaseComponent {
constructor(mediator?: Mediator) {
super(mediator);
}

// TODO: 一些业务逻辑
public doSomeA(): void {
console.log("Component 1 does A.");
this.mediator.notify(this, "A");
}

public doSomeB(): void {
console.log("Component 1 does B.");
this.mediator.notify(this, "B");
}
}

// TODO: 实际组件
class Component2 extends BaseComponent {
constructor(mediator?: Mediator) {
super(mediator);
}

// 一些业务逻辑
public doSomeC(): void {
console.log("Component 2 does C.");
this.mediator.notify(this, "C");
}

public doSomeD(): void {
console.log("Component 2 does D.");
this.mediator.notify(this, "D");
}
}

// TODO: 实际中介类
class ConcreteMediator implements Mediator {
// TODO: 保存所有组件
private component1: Component1;
private component2: Component2;

constructor(component1: Component1, component2: Component2) {
this.component1 = component1;
this.component1.setMediator(this);
this.component2 = component2;
this.component2.setMediator(this);
}

// TODO: 实现通知接口,根据事件名称执行不同的业务,sender用于保存信息发送者
notify(sender: object, event: string): void {
if (event === "A") {
console.log(
"Mediator reacts on A and triggers following operations:"
);
this.component2.doSomeC();
}
if (event === "D") {
console.log(
"Mediator reacts on D and triggers following operations:"
);
this.component1.doSomeB();
this.component2.doSomeC();
}
}
}

const c1 = new Component1();
const c2 = new Component2();
const mediator = new ConcreteMediator(c1, c2);

console.log("Client triggers operation A.");
c1.doSomeA();

console.log("");
console.log("Client triggers operation D.");
c2.doSomeD();

// Client triggers operation A.
// Component 1 does A.
// Mediator reacts on A and triggers following operations:
// Component 2 does C.

// Client triggers operation D.
// Component 2 does D.
// Mediator reacts on D and triggers following operations:
// Component 1 does B.
// Component 2 does C.

使用场景

  1. 当一些对象与其他对象紧密耦合以至于难以对其进行修改时,可以使用中介模式
  2. 当组件因过于依赖于其他组件而无法在不同应用中复用时,可以使用中介模式
  3. 如果为了能在不同场景下复用一些基本行为,导致不得不创建大量组件子类时,可以使用中介模式

优点

  1. 符合单一责任原则,将多个组件间的交流抽取到同一位置,使其便于理解与维护
  2. 符合开闭原则,无需修改实际组件就能增加新的中介者
  3. 缓解应用中的耦合情况
  4. 更方便地复用组件

缺点

  1. 一段时间后中介者可能变为上帝对象

Combo

  1. 责任链模式命令模式中介者模式观察者模式都是用于处理请求发送者和接收者之间的不同连接方式:
    1. 责任链按照顺序将请求传递给潜在接收者。直到有一名处理
    2. 命令在发送者和接收者之间建立单向连接🔗
    3. 中介者消除了发送者和接收者之间的直接连接🔗,强制他们与一个中介对象进行沟通
    4. 观察者允许接收者动态地订阅或取消接收请求
  2. 外观模式中介模式的职责类似: 它们都尝试在大量紧密耦合的类中组织起合作。
    1. 外观为子系统中的所有对象定义了一个简单接口, 但是它不提供任何新功能。子系统本身不会意识到外观的存在。子系统中的对象可以直接进行交流。
    2. 中介者将系统中组件的沟通行为中心化。各组件只知道中介者对象,无法直接相互交流。
  3. 中介者观察者大部分情况下只能使用其中一种模式,但有时可以同时使用:
    1. 中介者的主要目标是消除一系列系统组件之间的相互依赖。观察者的目标是在对象之间建立动态的单向连接,使得部分对象可作为其他对象的附属发挥作用。
    2. 而有一种流行的中介模式融合了二者:中介者对象担当发布者的角色,其他组件则作为订阅者

备忘录模式

备忘录模式是一种行为设计模式,允许在不暴露对象实现细节的情况下保存和恢复对象之前的状态。

我们先来思考以下场景:

  1. 我们想要设计一个文字编辑器app,需要支持撤销操作
  2. 这意味着用户每次进行操作时,需要将操作之前的所有对象的状态进行保存
  3. 但这也意味着,负责记录的类要访问所有需要被记录的所有成员。
  4. 这就意味着:要么会暴露类的所有内部细节而使其过于脆弱;要么会限制对其状态的访问权限而无法生成快照。

而备忘录模式为我们提供了解决办法:

  1. 原发射器Originator: 备忘录将创建快照的工作委托给实际状态拥有者,称为原发射器对象,拥有对备忘录所有成员的访问权限,可以保存自身状态的快照,也能在需要时通过快照恢复自身状态。
  2. 备忘录Memento: 备忘录模式建议将对象状态存储在备忘录对象中。除了创建备忘录的对象可以访问其内容,其余对象均不可访问其中的内容。其他对象必须使用受限接口与备忘录进行交互,它们可以获取快照的元数据(创建时间和操作名称等),但不能获取快照中原始对象的状态。
  3. 负责人Caretakers: 备忘录将被保存在负责人对象中,负责人对象仅通过受限接口访问备忘录,因此无法对其进行修改。

备忘录模式具体有三种主流结构,下面我们来一一介绍

基于嵌套实现

  1. 在这一方法中备忘录类将被嵌套在原发射器中
  2. 这样原发射器可以访问备忘录的所有成员变量和方法
  3. 负责人对备忘录的访问权限非常有限,只能在栈中保存备忘录,而无法修改其状态
  4. 负责人通过备忘录栈来保存历史状态,当原发送者需要回溯时,负责人从栈顶取出备忘录,传递给原发射器来恢复状态

基于中间接口实现

  1. 适用于不支持嵌套类的语言(比如PHP)
  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
// 原发射器
class Originator {
private state: string;
constructor(state: string) {
this.state = state;
}

// 模拟业务
doSomething(): void {
console.log("Originator: I'm doing something important.");
this.state = this.generateRandomString(30);
console.log(`Originator: and my state has changed to: ${this.state}`);
}

// 产生随机字符串
generateRandomString(length: number = 10) {
const charSet = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
return Array.apply(null, { length })
.map(() =>
charSet.charAt(Math.floor(Math.random() * charSet.length))
)
.join("");
}

// 保存快照
public save(): Memento {
return new ConcreteMemento(this.state);
}

// 恢复状态
public restore(m: Memento) {
this.state = m.getState();
console.log(`Originator: My state has changed to: ${this.state}`);
}
}

// 备忘录接口
interface Memento {
getState(): string;
getName(): string;
getDate(): string;
}

// 实际备忘录
class ConcreteMemento implements Memento {
// 快照
private state: string;
// 保存时间
private date: string;
constructor(state: string) {
this.state = state;
this.date = new Date().toISOString().slice(0, 19).replace("T", " ");
}

public getState(): string {
return this.state;
}

public getName(): string {
return `${this.date} / (${this.state.slice(0, 9)}...)`;
}

public getDate(): string {
return this.date;
}
}

// 负责人
class Caretaker {
private mementos: Memento[] = [];
private originator: Originator;

constructor(originator: Originator) {
this.originator = originator;
}

// 备份时创建新的备忘录
public backup(): void {
console.log("\nCaretaker: Saving Originator's state...");
this.mementos.push(this.originator.save());
}

// 撤销时取出栈定备忘
public undo(): void {
if (!this.mementos.length) {
return;
}
const memento = this.mementos.pop() as Memento;
console.log(`Caretaker: Restoring state to: ${memento.getName()}`);
this.originator.restore(memento);
}

// 查看保存历史
public showHistory(): void {
console.log("Caretaker: Here's the list of mementos:");
for (const memento of this.mementos) {
console.log(memento.getName());
}
}
}

// 客户端代码

const originator = new Originator("Super-duper-super-puper-super.");

const caretaker = new Caretaker(originator);

caretaker.backup();
originator.doSomething();

caretaker.backup();
originator.doSomething();

console.log("");
caretaker.showHistory();

console.log("\nClient: Now, let's rollback!\n");
caretaker.undo();

console.log("\nClient: Once more!\n");
caretaker.undo();

// Caretaker: Saving Originator's state...
// Originator: I'm doing something important.
// Originator: and my state has changed to: jeSdYDdKubiayBMNwGKzkmVeloqZUz

// Caretaker: Saving Originator's state...
// Originator: I'm doing something important.
// Originator: and my state has changed to: JrMFnUuPEBgdTGNzBaeduRDUHlrmax

// Caretaker: Here's the list of mementos:
// 2024-06-28 10:59:37 / (Super-dup...)
// 2024-06-28 10:59:37 / (jeSdYDdKu...)

// Client: Now, let's rollback!

// Caretaker: Restoring state to: 2024-06-28 10:59:37 / (jeSdYDdKu...)
// Originator: My state has changed to: jeSdYDdKubiayBMNwGKzkmVeloqZUz

// Client: Once more!

// Caretaker: Restoring state to: 2024-06-28 10:59:37 / (Super-dup...)
// Originator: My state has changed to: Super-duper-super-puper-super.

更为严格的实现

  1. 不会让其他类有任何机会访问原发射器状态。
  2. 允许存在多种不同形式的原始发射器,每种发射器与其相应的备忘录进行交互,原发射器和备忘录不会暴露任何状态。
  3. 负责人禁止修改存储在备忘录中的状态,负责人独立于原发射器存在,因此恢复方法被保存在了备忘录中。
  4. 每个备忘录与创建了自身的原发射器连接,原发射器将自身状态传递给备忘录的构造函数,这样只需要原发射器定义了合适的设置器(setter),备忘录就能恢复原本状态。

试用场景

  1. 当需要创建对象状态快照来恢复其之前的状态时,可以使用备忘录模式。
  2. 当直接访问对象的成员变量、获取器或设置器将导致封装被突破时,可以使用该模式。
    1. 备忘录让对象自行负责创建其状态的快照。任何其他对象都不能读取快照,这有效地保障了数据的安全性。

优点

  1. 可以在不破坏对象封装情况的前提下创建对象状态快照。
  2. 可以通过让负责人维护原发器状态历史记录来简化原发器代码。

缺点

  1. 如果客户端过于频繁地创建备忘录,程序将消耗大量内存。
  2. 负责人必须完整跟踪原发器的生命周期,这样才能销毁弃用的备忘录。
  3. 绝大部分动态编程语言(例如 PHP、Python 和 JavaScript)不能确保备忘录中的状态不被修改。

Combo

  1. 可以同时使用命令模式和备忘录模式来实现 “撤销”。在这种情况下,命令用于对目标对象执行各种不同的操作,备忘录用来保存一条命令执行前该对象的状态。
  2. 你可以同时使用备忘录和迭代器模式来获取当前迭代器的状态,并且在需要的时候进行回滚。
  3. 有时候原型模式可以作为备忘录的一个简化版本,其条件是需要在历史记录中存储的对象的状态比较简单,不需要链接其他外部资源,或者链接可以方便地重建。

观察者设计模式(事件订阅者)

观察者模式是一种行为设计模式,允许你定义一种订阅机制,可在对象事件发生时通知多个观察该对象的其他对象

订阅这模式脱胎于一个很日常的问题:

  1. 某个商店将要上一批产品,但目前并不知道具体的开售时间。
  2. 部分顾客对该产品感兴趣,但部分用户不感兴趣

那么如果我们不采用任何措施,也许对产品感兴趣的顾客,可能得连续几天都到店询问是否到货。

商店还能采取一种措施:到货了之后,向所有注册用户发送邮件。

第一种方案会造成不必要的时间浪费,也很大程度上会导致顾客错过商品;第二种方案,对产品不感兴趣的用户也会受到邮件,这将造成不必要的骚扰。

于是观察者模式应运而生。

商店可以先对顾客收集意向,当产品到货之后,只向有意向的顾客发送邮件。这就避免了时间的浪费,以及不必要的骚扰。

观察者模式类图如下:

  1. 发布者Publisher: 会向其他对象发送事件通知。包含一个加入订阅和离开订阅的框架。事件会在发布者自身状态改变或执行特定行为后发生。
    1. 当新事件发生时,发送者会遍历订阅列表,并调用每个订阅这的更新方法。
  2. 订阅者Subscriber: 订阅这接口,声明了通知接口。在绝大多数情况下包含一个update更新方法。该方法可以拥有多个参数,使发布者能在更新时传递时间的详细信息
  3. 具体订阅者ConcreteSubscribers: 可以执行一些操作来回应发布者的通知。所有具体订阅者类都实现了同样的接口,以达到将发布者与具体的类解耦的目的。
    1. 订阅者通常需要一些上下文信息来正确处理更新。硬吃发布者会将上下文传递给更新方法,发布者甚至可以将自身作为参数传递给更新方法,使订阅者直接获取所需的数据。

我们熟知的Vue中,数据双向绑定就是使用观察者模式实现的。下面我们也来简单的实现一下观察者模式:

  1. 首先我们需要一个发布者接口,它定义了一个普通对象要被当做发布者应该具有的方法:
    1. 增加订阅
    2. 删除订阅
    3. 通知订阅者
  2. 随后我们实现一个带有状态的类,然后让他实现发布者接口,成为发布者,此外他需要一个可以修改自身状态的业务方法
  3. 其次我们需要一个订阅者接口,它定义了一个普通对象成为订阅者需要实现的方法:
    1. 更新函数(副作用函数)
  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
// TODO: 发布者接口,定义收集,分发,通知方法
interface Publisher {
attach(observer: Observer): void;

detach(observer: Observer): void;

notify(): void;
}

// TODO: 观察者接口,定义更新方法(副作用函数)
interface Observer {
update(publish: Publisher): void;
}

class ConcretePublisher implements Publisher {
// 发布者具有的状态
public state: number;
// 收集订阅者
private observers: Observer[];

constructor(state: number) {
this.state = state;
this.observers = [];
}

// 增加
public attach(observer: Observer): void {
const isExit = this.observers.indexOf(observer) === -1;
if (isExit)
return console.log(
"Publisher: Observer has been attached already."
);
console.log("Publisher: Attached an observer.");
this.observers.push(observer);
}

// 删除
public detach(observer: Observer): void {
const observerIndex = this.observers.indexOf(observer);
if (observerIndex === -1)
return console.log("Publisher: Nonexistent observer.");
this.observers.splice(observerIndex, 1);
console.log("Publisher: Detached an observer.");
}

// 通知
public notify(): void {
console.log("Publisher: Notifying observers...");
for (const observer of this.observers) {
observer.update(this);
}
}

// TODO: 模拟业务逻辑改变发布者的状态
public doSomething(): void {
console.log("\nPublisher: I'm doing something important.");
this.state = Math.floor(Math.random() * (10 + 1));
console.log(`Publisher: My state has just changed to: ${this.state}`);
this.notify();
}
}

class ConcreteObserverA implements Observer {
public update(publish: Publisher): void {
if (publish instanceof ConcretePublisher && publish.state < 3) {
console.log("ConcreteObserverA: Reacted to the event.");
}
}
}

class ConcreteObserverB implements Observer {
public update(publish: Publisher): void {
if (publish instanceof ConcretePublisher && publish.state < 3) {
console.log("ConcreteObserverB: Reacted to the event.");
}
}
}

const publisher = new ConcretePublisher(3);
const observer1 = new ConcreteObserverA();
publisher.attach(observer1);
const observer2 = new ConcreteObserverB();
publisher.attach(observer2);

publisher.doSomething();
publisher.doSomething();

publisher.detach(observer2);

publisher.doSomething();

// Publisher: Observer has been attached already.
// Publisher: Observer has been attached already.

// Publisher: I'm doing something important.
// Publisher: My state has just changed to: 8
// Publisher: Notifying observers...

// Publisher: I'm doing something important.
// Publisher: My state has just changed to: 5
// Publisher: Notifying observers...
// Publisher: Nonexistent observer.

// Publisher: I'm doing something important.
// Publisher: My state has just changed to: 3
// Publisher: Notifying observers...

适用场景

  1. 当一个对象状态的改变需要改变其他对象,或实际对象是事先未知的或动态变化的时,可使用观察者模式。
    1. 后者的一个例子是:你创建了自定义按钮类并允许客户端在按钮中注入自定义代码,这样当用户按下按钮时就会触发这些代码。
  2. 当应用中的一些对象必须观察其他对象时,可使用该模式。但仅能在有限时间内或特定情况下使用。

优点

  1. 符合开闭原则。无需修改发布者代码就能引入新的订阅者类 (如果是发布者接口则可轻松引入发布者类)。
  2. 可以在运行时建立对象之间的联系。

缺点

  1. 订阅者的通知顺序是随机的。

Combo

  1. 责任链模式、命令模式、中介者模式和观察者模式用于处理请求发送者和接收者之间的不同连接方式。
  2. 中介者观察者之间的区别往往很难记住。在大部分情况下,可以使用其中一种模式,而有时可以同时使用。

状态模式

状态模式是一种行为设计模式,让你能在一个对象的内部状态变化时改变其行为,使其看上去就像改变了自身所属的类一样。

状态模式实际上与**有限状态机**这一概念息息相关。JS种的Promise就是一个具有四种状态的有限状态机。这里不做展开。我们通过下面的例子来感受一下:

  1. 我们需要实现一个文档类
  2. 文档类可能处于三种状态:
    1. 草稿
    2. 审核中
    3. 已发布
  3. 文档类需要包含一个publish发布方法,但该方法在不同的状态时逻辑不同:
    1. 处于草稿状态时,调用publish,文档变为审核中状态
    2. 处于审核中状态时,调用publish,会进行身份认证,若为管理员者将文档变为发布状态
    3. 处于发布状态时,调用publish,不会进行任何操作

上述文档类就能看作一个三个状态的有限状态机。因此我们简单概括一下就能得到有限状态机的定义:

程序在任意时刻处于几种有限的状态中,在任何状态下程序的行为并不相同,并且可瞬间从一个状态变换到另一个,或保持不变。这种状态切换称为转移

接下来我们在回到上面的例子,如果我们想要实现这一案例,我能该怎么做?

最简单的方法一定是通过if...else或者switch等条件运算符实现。但当我们逐步在文档类中添加更多状态和依赖于状态的行为后,大部分方法中将会包含复杂的条件语句。修改其转换逻辑可能会涉及到修改所有方法中的状态条件语句,导致代码维护困难。

状态模式建议为对象的所有可能状态新建一个类, 然后将所有状态的对应行为抽取到这些类中。

  1. 上下文Context: 有限状态机本身,包含一个对当前状态对象的引用,将与当前状态相关的工作委派给状态对象,并提供一个改变当前状态对象的接口。
  2. 状态State: 状态接口,声明特定于这一状态的方法。
  3. 具体状态ConcreteStates: 实现特定于状态的方法。为了避免多个状态中包含相似代码,可以提供一个封装有部分通用行为的中间抽象类。状态对象可存储对于上下文对象的反向引用。状态可以通过该引用从上下文处获取所需信息,并且能触发状态转移。
  4. 上下文和具体状态都可以设置上下文的下个状态,并可通过替换连接到上下文的状态对象来完成实际的状态转换。

根据上述类图我们可以尝试实现一个状态模式Demo:

点击查看代码
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
// TODO: 状态接口
abstract class State {
protected context: Context;

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

public abstract handle1(): void;

public abstract handle2(): void;
}

// TODO: 上下文
class Context {
private state: State;

constructor(state: State) {
this.transitionTo(state);
}

// 状态转移
public transitionTo(state: State) {
console.log(`Context: Transition to ${(<any>state).constructor.name}.`);
this.state = state;
this.state.setContext(this);
}

// 业务1
public request1(): void {
this.state.handle1();
}

// 业务2
public request2(): void {
this.state.handle2();
}
}

// TODO: 具体状态
class ConcreteStateA extends State {
public handle1(): void {
console.log("ConcreteStateA handles request1.");
console.log("ConcreteStateA wants to change the state of the context.");
// 转移到下一状态
this.context.transitionTo(new ConcreteStateB());
}

public handle2(): void {
console.log("ConcreteStateA handles request2.");
}
}

class ConcreteStateB extends State {
public handle1(): void {
console.log("ConcreteStateB handles request1.");
}

public handle2(): void {
console.log("ConcreteStateB handles request2.");
console.log("ConcreteStateB wants to change the state of the context.");
// 转移到下一状态
this.context.transitionTo(new ConcreteStateA());
}
}

const context = new Context(new ConcreteStateA());
context.request1();
context.request2();

// Context: Transition to ConcreteStateA.
// ConcreteStateA handles request1.
// ConcreteStateA wants to change the state of the context.
// Context: Transition to ConcreteStateB.
// ConcreteStateB handles request2.
// ConcreteStateB wants to change the state of the context.
// Context: Transition to ConcreteStateA.

试用场景

  1. 如果对象需要根据自身当前状态进行不同行为,同时状态的数量非常多且与状态相关的代码会频繁变更的话,可使用状态模式。
  2. 如果某个类需要根据成员变量的当前值改变自身行为,从而需要使用大量的条件语句时,可使用该模式。
  3. 当相似状态和基于条件的状态机转换中存在许多重复代码时,可使用状态模式。

优点

  1. 符合单一职责原则。将与特定状态相关的代码放在单独的类中。
  2. 符合开闭原则。无需修改已有状态类和上下文就能引入新状态。
  3. 通过消除臃肿的状态机条件语句简化上下文代码。

缺点

  1. 如果状态机只有很少的几个状态,或者很少发生改变,那么应用该模式可能会显得小题大作。

Combo

  1. 桥接模式、状态模式和策略模式(在某种程度上包括适配器模式)模式的接口非常相似。实际上,它们都基于组合模式——即将工作委派给其他对象, 不过也各自解决了不同的问题。
  2. 状态可被视为策略的扩展。两者都基于组合机制: 它们都通过将部分工作委派给 “帮手” 对象来改变其在不同情景下的行为。
    1. 策略使得这些对象相互之间完全独立,它们不知道其他对象的存在。
    2. 状态模式没有限制具体状态之间的依赖,且允许它们自行改变在不同情景下的状态。

策略模式

评论