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. 生成器模式也能与桥接模式很好的组合,例如我们可以让主管类负责抽象类的工作,让各种不用的生成器类负责实现类的工作。

行为型模式

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

评论