之前没认真看设计模式,感觉只是一些构造函数的定义,结果真被问到的时候发现,因为不太清楚所以不知道要做什么
就像前几天的面试,面试官问了我vue双向绑定的原理,然后我说了数据劫持和发布-订阅模式的结合。然后又说了Object.defineProperty()设置set、get实现的数据劫持,Event.addEventListener()设置的发布订阅,然后面试官就叫我写一下发布订阅是怎么实现的,我以为是要写个dom绑定事件,结果面试官强调是写这个addEventListener是怎么实现的。。。当时因为对设计模式不太清楚,以为是要定义一个类,然后添加一个类似update这种的方法,然后创建一个类的实例,当实例的值更新时,就会自动触发这个update方法什么的。。。然后就没写出来。。。
序
什么是设计模式?设计模式是可重用的用于解决软件设计中一般问题的方案。设计模式如此让人着迷,以至在任何编程语言中都有对其进行的探索。
其中一个原因是它可以让我们站在巨人的肩膀上,获得前人所有的经验,保证我们以优雅的方式组织我们的代码,满足我们解决问题所需要的条件。
设计模式同样也为我们描述问题提供了通用的词汇。这比我们通过代码来向别人传达语法和语义性的描述更为方便。
一个模式就是一个可重用的方案,可应用于在软件设计中的常见问题 。模式的另一种解释就是一个我们如何解决问题的模板,那些可以在许多不同的情况里使用的模板。 设计模式有以下三点好处:
-
模式是行之有效的解决方法:他们提供固定的解决方法来解决在软件开发中出现的问题,这些都是久经考验的反应了开发者的经验和见解的使用模式来定义的技术。
-
可以很容易地重用:一个模式通常反映了一个可以适应自己需要的开箱即用的解决方案。这个特性让它们很健壮。
-
善于表达:当我们看到一个提供某种解决方案的模式时,一般有一组结构和词汇可以非常优雅地帮助表达相当大的解决方案。
设计模式的种类
创建型设计模式
- 创建型设计模式关注于对象创建的机制方法,通过该方法,对象以适应工作环境的方式被创建。基本的对象创建方法可能会给项目增加额外的复杂性,而这些模式的目的就是为了通过控制创建过程解决这个问题。
- 属于这一类的一些模式是:构造器模式(Constructor),工厂模式(Factory),抽象工厂模式 (Abstract),原型模式 (Prototype),单例模式 (Singleton) 以及 建造者模式(Builder)。
结构设计模式
- 结构模式关注于对象组成和通常识别的方式实现不同对象之间的关系。该模式有助于在系统-的某一部分发生改变的时候,整个系统结构不需要改变。该模式同样有助于对系统中某部分没有达到某一目的的部分进行重组。
- 在该分类下的模式有:装饰模式,外观模式,享元模式,适配器模式和代理模式。
行为设计模式
SN | 描述 |
---|---|
Creational | 根据创建对象的概念分成下面几类。 |
Class | |
Factory Method(工厂方法) | 通过将数据和事件接口化来构建若干个子类。 |
Object | |
Abstract Factory(抽象工厂) | 建立若干族类的一个实例,这个实例不需要具体类的细节信息。(抽象类) |
Builder (建造者) | 将对象的构建方法和其表现形式分离开来,总是构建相同类型的对象。 |
Prototype(原型) | 一个完全初始化的实例,用于拷贝或者克隆。 |
Singleton(单例) | 一个类只有唯一的一个实例,这个实例在整个程序中有一个全局的访问点。 |
— | — |
Structural | 根据构建对象块的方法分成下面几类。 |
Class | |
Adapter(适配器) | 将不同类的接口进行匹配,调整,这样尽管内部接口不兼容但是不同的类还是可以协同工作的。 |
Bridge(桥接模式) | 将对象的接口从其实现中分离出来,这样对象的实现和接口可以独立的变化。 |
Composite(组合模式) | 通过将简单可组合的对象组合起来,构成一个完整的对象,这个对象的能力将会超过这些组成部分的能力的总和,即会有新的能力产生。 |
Decorator(装饰器) | 动态给对象增加一些可替换的处理流程。 |
Facada(外观模式) | 一个类隐藏了内部子系统的复杂度,只暴露出一些简单的接口。 |
Flyweight(享元模式) | 一个细粒度对象,用于将包含在其它地方的信息 在不同对象之间高效地共享。 |
Proxy(代理模式) | 一个充当占位符的对象用来代表一个真实的对象。 |
— | — |
Behavioral | 基于对象间作用方式来分类。 |
Class | |
Interpreter(解释器) | 将语言元素包含在一个应用中的一种方式,用于匹配目标语言的语法。 |
Template Method(模板方法) | 在一个方法中为某个算法建立一层外壳,将算法的具体步骤交付给子类去做。 |
Object | |
Chain of Responsibility(响应链) | 一种将请求在一串对象中传递的方式,寻找可以处理这个请求的对象。 |
Command(命令) | 封装命令请求为一个对象,从而使记录日志,队列缓存请求,未处理请求进行错误处理 这些功能称为可能。 |
Iterator(迭代器) | 在不需要知道集合内部工作原理的情况下,顺序访问一个集合里面的元素。 |
Mediator(中介者模式) | 在类之间定义简化的通信方式,用于避免类之间显式的持有彼此的引用。 |
Observer(观察者模式) | 用于将变化通知给多个类的方式,可以保证类之间的一致性。 |
State(状态) | 当对象状态改变时,改变对象的行为。 |
Strategy(策略) | 将算法封装到类中,将选择和实现分离开来。 |
Visitor(访问者) | 为类增加新的操作而不改变类本身。 |
观察者模式
观察者模式是这样一种设计模式。一个被称作被观察者的对象,维护一组被称为观察者的对象,这些对象依赖于被观察者,被观察者自动将自身的状态的任何变化通知给它们。
一个或者更多的观察者对一个被观察者的状态感兴趣,将这种兴趣通过附着自身的方式注册在被观察者身上。当被观察者发生变化,而这种变化也是观察者所关心的,就会产生一个通知,这个通知将会被送出去,最后将会调用每个观察者的更新方法。当观察者不在对被观察者的状态感兴趣的时候,它们只需要简单的将自身剥离即可。 ———— 《设计模式:可重用的面向对象软件的元素》
观察者模式确实很有用,但是在javascript时间里面,通常我们使用一种叫做发布/订阅模式的变体来实现观察者模式。这两种模式很相似,但是也有一些值得注意的不同。
- 观察者模式要求想要接受相关通知的观察者必须到发起这个事件的被观察者上注册这个事件(观察者需要对特定事件进行注册)。
- 发布/订阅模式使用一个主题/事件频道,这个频道处于想要获取通知的订阅者和发起事件的发布者之间。这个事件系统允许代码定义应用相关的事件,这个事件可以传递特殊的参数,参数中包含有订阅者所需要的值(事件可以传参)。这种想法是为了避免订阅者和发布者之间的依赖性。
- 这种和观察者模式之间的不同,使订阅者可以实现一个合适的事件处理函数,用于注册和接受由发布者广播的相关通知。
一个简单的观察者模式js代码实现
//被观察者以及其增加,删除,通知在观察者列表中的观察者的能力进行建模
function Subject() {
this.observerList = [];
}
Subject.prototype.addObserver = function (observer) {
this.observerList.push(observer)
}
Subject.prototype.removeObserver = function (observer) {
let index = this.observerList.indexOf(observer)
this.observerList.splice(index, 1)
}
Subject.prototype.notify = function (context) {
for (let i in this.observerList) {
this.observerList[i].update(context);
}
}
//观察者的一个框架。这里的update 函数之后会被具体的行为覆盖。
function Observer() {
this.update = function () {
// ...
};
}
一个简单的发布/订阅模式代码实现
class Publisher {
constructor() {
this.subscribers = {}
}
on(topic, event) {
if (!this.subscribers[topic]) {
this.subscribers[topic] = []
}
if(typeof event !== 'function'){
event = ()=>{}
}
this.subscribers[topic].push(event)
}
remove(topic, event) {
let events = this.subscribers[topic]
if (events) {
let i = events.indexOf(event)
if (i !== -1) {
events.splice(i, 1)
}
}
}
emit(topic, ...args) {
let events = this.subscribers[topic]
if (events) {
for (let key in events) {
events[key](...args)
}
}
}
}
单例模式
单例模式的定义:单例模式之所以这么叫,是因为它限制一个类只能有一个实例化对象。实现的方法为先判断实例存在与否,如果存在则直接返回,如果不存在就创建了再返回,这就确保了一个类只有一个实例对象。
一个简单的单例模式实现
var Singleton = (function () {
function myObject(name, age) {
this.name = name
this.age = age
}
myObject.prototype.getName = function () {
console.log(this.name)
}
var instance;
return function (...args) {
if (!instance) {
instance = new myObject(...args)
}
return instance
}
})()
var a = Singleton('小明', 11)
var b = Singleton('小刚', 12)
a.getName() //小明
b.getName() //小明
console.log(a===b) //true
工厂模式
工厂起到的作用就是隐藏了创建实例的复杂度,只需要提供一个接口,简单清晰。有点类似与函数柯里化的感觉。
一个简单的工厂模式实现
//可以生产各种各样的齿轮
class GearFactory {
constructor(radius, thickness, material) {
this.radius = radius
this.thickness = thickness
this.material = material
}
}
//生产半径5cm、厚度2mm的铁齿轮的接口
function createGear(){
return new GearFactory('5cm','Fe','2mm')
}
中介者模式
中介者模式的作用就是解除对象与对象之间的紧耦合关系。增加一个中介者对象后,所有的 相关对象都通过中介者对象来通信,而不是互相引用,所以当一个对象发生改变时,只需要通知 中介者对象即可。中介者使各对象之间耦合松散,而且可以独立地改变它们之间的交互。中介者模式使网状的多对多关系变成了相对简单的一对多关系,避免了对象之间相互的引用关系导致的逻辑混乱的问题。
让人联想到了星型拓扑结构
中介者模式聊天室示例
class User {
constructor(name) {
this.name = name
this.log = []
this.room = null
}
showLog() {
this.log.forEach(x => console.log(x))
}
into(chatRoom) {
this.room = chatRoom
this.room.into(this)
}
send(msg, user) {
if (this.room) {
this.room.send(this, msg, user)
this.log.push(`you send ${user&&user.name||'all'}:${msg}`)
}
}
receive(msg, from) {
this.log.push(`${from.name} to you:${msg}`)
}
}
class ChatRoom {
constructor() {
this.user = []
this.log = []
}
into(user) {
this.user.push(user)
}
send(from, msg, to) {
if (to) {
this.log.push(`${from.name} to ${to.name}:${msg}`)
if (this.user.indexOf(to) !== -1) {
to.receive(msg, from)
}
} else {
this.log.push(`${from.name} to all:${msg}`)
this.user.forEach(user => {
if (user !== from) {
user.receive(msg, from)
}
})
}
}
showLog() {
this.log.forEach(x => console.log(x))
}
}
var room = new ChatRoom()
var a = new User('A')
var b = new User('B')
var c = new User('C')
a.into(room)
a.send('有人吗?')
b.into(room)
c.into(room)
b.send('有人吗?')
c.send('有人吗?')
a.send('有', b)
c.send('有')
console.log('--room-')
room.showLog()
console.log('---a---')
a.showLog()
console.log('---b---')
b.showLog()
console.log('---c---')
c.showLog()
适配器模式
适配器用来解决两个接口不兼容的情况,不需要改变已有的接口,通过包装一层的方式实现两个接口的正常协作。
这个例子可能没表达好,主要作用就是包装接口对象,在不改变原对象的情况,将对象包装一层,来保证其能适应不同的接口
class Person {
constructor(name, brithday) {
this.name = name
this.brithday = brithday
}
}
//Person不能直接使用
function getAge(brithYear, brithMonth, brithDay) {
let now = new Date()
let nowYear = now.getFullYear(),
nowMonth = now.getMonth(),
nowDay = now.getDay()
let age = nowYear - brithYear
if (nowMonth < brithMonth) {
age--
} else if (nowMonth === brithMonth) {
if (nowDay < brithDay) {
age--
}
}
return age
}
class AgePerson {
constructor(name, brithday) {
this.person = new Person(name, brithday)
this.brithday = brithday.split('/').map(d => +d)
}
age() {
console.log(getAge(...this.brithday))
}
}
var ming = new AgePerson('小明', '1999/9/9')
ming.age()
装饰模式
装饰器模式并不去深入依赖于对象是如何创建的,而是专注于扩展它们的功能这一问题上。不同于只依赖于原型继承,我们在一个简单的基础对象上面逐步添加能够提供附加功能的装饰对象。它的想法是,不同于子类划分,我们向一个基础对象添加(装饰)属性或者方法,因此它会是更加轻巧的。
AOP(Aspect-Oriented Programming)装饰函数
// 前置代码
Function.prototype.before = function (fn) {
const self = this
return function () {
fn.apply(this, arguments)
return self.apply(this, arguments)
}
}
// 后置代码
Function.prototype.after = function (fn) {
const self = this
return function () {
let res = self.apply(this, arguments)
fn.apply(this, arguments)
return res
}
}
const wear1 = function () {
console.log('穿上第一件衣服')
}
const wear2 = function () {
console.log('穿上第二件衣服')
}
const wear3 = function () {
console.log('穿上第三件衣服')
}
const wear = wear2.before(wear1).after(wear3)
wear()
// 穿上第一件衣服
// 穿上第二件衣服
// 穿上第三件衣服
外观模式
外观模式提供了一个接口,隐藏了内部的逻辑,更加方便外部调用。
比如jquery的$ ,不论何时我们使用jQuery的 ( e l ) . c s s 或 者 (el).css或者 (el).css或者(el).animate()方法,我们实际上都是在使用一个门面——更加简单的公共接口让我们避免为了使得行为工作起来而不得不去手动调用jQuery核心的内置方法。
比如红宝书中定义的createXHR就是一个使用外观模式的示例
function createXHR() {
if (typeof XMLHttpRequest != "undefined") {
return new XMLHttpRequest();
} else if (typeof ActiveXObject != "undefined") {
if (typeof arguments.callee.activeXString != "string") {
var versions = ["MSXML2.XMLHttp.6.0", "MSXML2.XMLHttp.3.0", "MSXML2.XMLHttp"],
i, len;
for (i = 0, len = versions.length; i < len; i++) {
try {
new ActiveXObject(versions[i]);
arguments.callee.activeXString = versions[i];
break;
} catch (ex) {
//跳过
}
}
}
return new ActiveXObject(arguments.callee.activeXString);
} else {
throw new Error("No XHR object available.");
}
}
代理模式
在我们需要在一个对象后多次进行访问控制访问和上下文,代理模式是非常有用处的。
当实例化一个对象开销很大的时候,它可以帮助我们控制成本,提供更高级的方式去关联和修改对象,就是在上下文中运行一个特别的方法。
JS中的事件委托(事件代理)就是使用了代理模式。
<ul id="ul">
<li>1</li>
<li>2</li>
<li>3</li>
<li>4</li>
<li>5</li>
</ul>
<script>
let ul = document.querySelector('#ul')
ul.addEventListener('click', (event) => {
console.log(event.target);
})
</script>
资料参考:JavaScript 设计模式