前言
设计模式就是符合某种场景下某个问题的解决方案,通过设计模式可以增加代码的可重用性,可扩展性,可维护性,最终使得我们的代码高内聚、低耦合。设计模式,无处不在。
六大原则
单一职责原则(Single responsibility principle)
一个对象(方法)只做一件事情
开放-封闭原则(Open Close Principle)
对扩展开放,对修改关闭。
里氏代换原则(Liskov Substitution Principle)
子类能覆盖父类,父类能出现的地方子类也能出现。
接口隔离原则(Interface Segregation Principle)
保持接口的单一独立,降低类之间的耦合度
依赖倒置原则(Dependence Inversion Principle)
面向接口编程,依赖于抽象而不依赖于具体。
迪米特法则,又称最少知识原则(Demeter Principle)
一个实体应当尽量少地与其他实体之间发生相互作用,使得系统功能模块相对独立。
简介
单例模式
- 定义:保证一个类仅有一个实例,并提供一个访问它的全局访问点(线程池、全局缓存、浏览器window),核心是确保只有一个实例,并提供全局使用访问
- 实现过程简单说就是用一个变量来标志是否创建过对象,如果是,下次直接返回已创建好的对象
function GetSingle() {}
GetSingle.getInstance = (function() {
let instance
return function() {
if(!instance) {
instance = new GetSingle()
}
return instance
}
})()
const a = new GetSingle.getInstance()
const b = new GetSingle.getInstance()
a === b // true
- 惰性单例:在需要的时候才创建对象实例,创建对象和管理单例的职责被分布在两个不同的方法中
const getSingle = function(fn) {
let result
return function() {
return result || (result = fn.apply(this, arguments))
}
}
策略模式
- 定义一系列算法,把他们一个个封装起来,并且使他们可以相互替换
- 由两部分组成:一组策略类,封装具体算法,并负责具体计算过程;环境类Context,接受客户的请求,随后把请求委托给某一个策略类,维持对某个策略对象的引用
const stratigies = {
one: (val) => {},
two: (val) => {},
three: (val) => {},
}
const ret = stratigies[type](val)
- 优点
- 利用组合、委托、多态等思想,有效避免多重条件选择语句
- 提供了对开放-封闭原则的完美支持,将算法独立封装起来,使得易于切换、易于理解、易于扩展
- 策略算法可以复用在系统内其他地方
- 利用组合和委托让Context有执行算法的能力,也是继承的一种更轻便的替代方案
- 缺点
- 会在程序中增加许多策略类或策略对象
- 必须了解所有的strategy及其不同点,才能选择合适的strategy
代理模式
- 定义:为一个对象提供一个代用品或占位符,以便控制对他的访问,当客户不方便直接访问一个对象或者不满足需要的时候,提供一个替身对象来控制它的访问
- 代理的意义:单一职责原则:就一个类而言,应该仅有一个引起它变化的原因
- 好处:用户可以放心地请求代理,只关心是否能得到想要的结果;在任何使用本体的地方都可以替换成使用代理
- 事件代理:一个父元素下有n个子元素,点击子元素时显示对象文字,给父元素绑定事件即可,不需要在子元素绑定n次
// 获取父元素
const father = document.getElementById('father')
// 给父元素安装一次监听函数
father.addEventListener('click', function(e) {
// 识别是否是目标子元素
if(e.target.tagName === 'A') {
// 以下是监听函数的函数体
e.preventDefault()
alert(`我是${e.target.innerText}`)
}
} )
- 虚拟代理:把一些开销很大的对象,延迟到真正需要的时候才创建
// 在图片资源请求到之前添加本地loading图
const myImage = (function(){
const node = document.createElement('img')
document.body.appendChild(node)
return {
setSrc: function(src) {
node.src = src
}
}
})()
const proxyImage = (function(){
const img = new Image()
img.onload = function() {
myImage.setSrc(this.src)
}
return {
setSrc: function(src) {
myImage.setSrc('./loading.gif')
img.src = src
}
}
})()
proxyImage.setSrc('图片url')
- 缓存代理:可以为一些开销大的运算结果提供暂时的存储,在下次运算时,如果传递进来的参数跟之前的一致,则可以直接返回存储的计算结果,斐波那契用缓存存上一个计算结果也类比属于缓存代理
迭代器模式
- 提供一种方法顺序访问一个聚合对象中的各个元素,而不需要暴露该对象的内部表示
function iteratorGenerator(list) {
let idx = 0
const len = list.length
return {
next: function() {
const done = idx >= len
const value = !done ? list[idx++] : undefined
return {
done,
value
}
}
}
}
- 内部迭代器:内部已经定义好了迭代规则,它完全接受整个迭代过程,外部只需一次初始调用,例如forEach
- 外部迭代器:必须显示的请求迭代下一个元素,它增加了调用的复杂度,但也增加了迭代器的灵活性,可以手动控制迭代的过程,例如:Generator
- ES6约定,任何数据结构只要具备Symbol.iterator属性(这个属性就是Iterator的具体实现,它本质上是当前数据结构默认的迭代器生成函数),就可以被forof循环或迭代器next方法遍历
发布订阅模式(观察者模式)
- 推模型:事件发生时,发布者一次性把所有更改的状态和数据都推送给订阅者
- 拉模型:发布者仅通知订阅者事件已发生,还提供一些公开的接口供订阅者来主动拉取数据。好处:让订阅者按需获取,坏处是有可能让发布者变成一个门户大开的对象,增加代码量和复杂度
- 一般选择推模型
- 优点:时间解耦,对象之间解耦
- 缺点:当订阅者比较多的时候,同时通知所有的订阅者可能会造成性能问题;过度使用会导致对象之间的联系弱化,会导致程序难以跟踪维护和理解
// 定义发布者类
class Publisher {
constructor() {
this.observers = []
console.log('Publisher created')
}
// 增加订阅者
add(observer) {
console.log('Publisher.add')
this.observers.push(observer)
}
// 移除订阅者
remove(observer) {
console.log('Publisher.remove')
this.observers.forEach((item, i) => {
if (item === observer) {
this.observers.splice(i, 1)
}
})
}
// 通知所有订阅者
notify() {
console.log('Publisher.notify invoked')
this.observers.forEach((observer) => {
observer.update(this)
})
}
}
// 定义订阅者类
class Observer {
constructor() {
console.log('Observer created')
}
update() {
console.log('Observer.update')
}
}
---
const pub = new Publisher()
const ob1 = new Observer()
const ob2 = new Observer()
pub.add(ob1)
pub.add(ob2)
pub.notify()
// Publisher.notify invoked
// 2 Observer.update invoked
pub.remove(ob1)
Publisher.remove invoked
pub.notify()
// Publisher.notify invoked
// Observer.update invoked
- 区别
- 观察者模式和发布-订阅模式之间的区别,在于是否存在第三方、发布者能否直接感知订阅者
- 发布者直接触及订阅者叫观察者模式
- 发布者不触及订阅者,由第三方统一处理叫发布-订阅模式
命令模式
- 命令指的是一个执行某些特定事情的指令,还可以完成撤销、排队等功能,js中命令模式的其实是回调函数的一个面向对象的替代品
- 优点:命令发出者和接受者解耦,使发出者不需要知道命令的具体执行过程即可执行,易扩展
- 缺点:使用命令模式可能会导致某些系统有过多的具体命令
const btn = document.getElementById('btn')
const A = {
a:function(){
console.log('A-a')
}
}
function setCommand(e,command){
e.onclick = function(){
command.execute()
}
}
class MiddlewareCommand{
constructor(receiver,key){
this.receiver = receiver
this.key = key
}
execute(){
this.receiver[this.key]()
}
}
setCommand(btn,new MiddlewareCommand(A,'a'))
组合模式
- 将对象组合成树形结构,以表示“部分-整体”的层次结构;通过对象的多态性表现,使得用户对单个对象和组合对象的使用具有一致性;一种HAS-A(聚合)关系,组合对象把请求委托给它所包含的所有叶对象
- 优点:可以一致的对待组合对象和基本对象
- 缺点:创建过多对象,会造成性能负担;需要保证叶对象和组合对象有同样的方法
// 购物车下单
class ShoppingCart {
constructor() {
this.list = []
}
addCart(item) {
this.list.push(item)
return this
}
buy() {
this.list.forEach(item => {
item.buy()
})
return this
}
}
class BuyCar {
buy() {
console.log('买车')
}
}
class BuyHouse {
buy() {
console.log('买房')
}
}
const cart = new ShoppingCart()
const car = new BuyCar()
const house = new BuyHouse()
cart.add(car).add(house).buy()
模板方法模式
- 两部分组成:抽象父类,具体的实现子类,子类放弃了对自己的控制权,改为父类通知子类,典型的通过封装变化提高系统扩展性的设计模式
- 好莱坞原则:允许底层组件将自己挂钩到高层组件中,而高层组件会决定什么时候、以何种方式使用这些底层组件
- 优点:提供公共的代码便于维护
- 缺点:每个子类需具体实现,导致类的个数增加,增加了系统复杂度
// 把动物放进冰箱的过程
class Template {
constructor({ putAnimal }) {
this.putAnimal = putAnimal
}
openRefrigerator() { console.log('打开冰箱=== 共用') }
closeRefrigerator() { console.log('关闭冰箱===共用') }
/* 模板方法 */
init() {
this.openRefrigerator()
this.putAnimal()
this.closeRefrigerator()
}
}
/* 猫 */
const cat = new Template({
putAnimal: function() { console.log('猫放进冰箱') }
})
cat.init()
/* 狗 */
const dog = new Template({
putAnimal: function() { console.log('狗放进冰箱') }
})
dog.init()
享元模式
- 用于性能优化的模式,核心是运用共享技术有效支持大量细粒度的对象
- 目标是尽量减少共享对象的数量:内部状态存储于对象内部;内部状态可以被一些对象共享;内部状态独立于具体的场景,通常不会改变;外部状态取决于具体的场景,并根据场景变化,不能被共享
- 用时间换空间的模式,关键在于怎么区别内部状态和外部状态
- 以下情况可使用享元模式:
- 一个程序中使用了大量的相似对象
- 由于使用了大量对象,造成很大的内存开销
- 对象的大多数状态都可以变为外部状态
- 剥离出对象的外部状态后,可以用相对较少的共享对象取代大量对象
- 优点:减少了大批量对象的创建,降低系统内存
- 缺点:提高了系统的复杂度,需要分离出外部状态和内部状态
const Model = function(sex) {
this.sex = sex
}
Model.prototype.takephoto = function() {
console.log(this.sex, this.underwear)
}
const menModel = new Model('men')
const womenModel = new Model('women')
for(let i = 0; i < 50; i++) {
menModel.underwear = 'men wear' + i
menModel.takephoto()
}
for(let j = 0; j < 50; j++) {
womenModel.underwear = 'women wear' + j
womenModel.takephoto()
}
// 只创建了menModel、womenModel两个对象便完成所有对象的创建
职责链模式
- 使多个对象都有机会处理请求,从而避免请求的发送者和接受者之间的耦合关系,将这些对象连成一条链,并沿着这条链传递该请求,直到有一个对象处理它为止(类比原型链、作用域链等)
- 优点:解耦请求发送者和N个接受者之间的复杂关系,由于不知道链中的哪个节点可以处理请求,所以只需把请求传递给第一个节点即可;可以手动指定起始节点;节点数量和顺序是可以自由变化的
- 缺点:不能保证链条中的所有方法都会处理请求,过长的职责链会带来性能损耗
class Chain {
constructor(chain) {
this.chain = chain
}
setChain(chain) {
this.chain = chain
if(this.chain === 'end') {
console.log('end')
}
return this
}
}
const a = new Chain()
a.setChain('start').setChain('ing').setChain('end')
中介者模式
- 以中介者和对象之间的一对多关系取代了对象之间的网状多对多关系,各个对象只需关注自身功能的实现,关系交给中介者实现
- 优点:将一对多转化成了一对一,各个类之间解耦
- 缺点:系统中会新增一个中介者对象,对象之间交互的复杂性转移成了中介者对象的复杂性
- 如果对象之间的复杂耦合确实导致调用和维护出现了困难,且这些耦合度随项目的变化呈指数增长曲线,可以用中介者模式重构代码
class A {
constructor() {
this.foo = 0
}
setFoo(num, m) {
this.foo = num
if (m) {
m.setB()
}
}
}
class B {
constructor() {
this.foo = 0
}
setFoo(num, m) {
this.foo = num
if (m) {
m.setA()
}
}
}
class Mediator {
constructor(a, b) {
this.a = a
this.b = b
}
setA() {
let foo = this.b.foo
this.a.setFoo(foo * 10)
}
setB() {
let foo = this.a.foo
this.b.setFoo(foo / 10)
}
}
let a = new A()
let b = new B()
let m = new Mediator(a, b)
a.setFoo(10, m)
console.log(a.foo, b.foo) // 10 1
b.setFoo(10, m)
console.log(a.foo, b.foo) // 100 10
装饰者模式
- 可以动态的给某个对象添加一些额外的职责,而不会影响从这个类中派生的其他对象
- Redux中的connect,Mobx中的inject等都是装饰者模式的例子
@inject('store')
@observer
class App {}
- 优点:装饰类和被装饰类之间可以相互独立发展,不会相互耦合,装饰器模式是继承的一个替代模式,提供了比继承更多的灵活性
- 缺点:多层装饰会显得程序比较复杂
- AOP装饰函数
Function.prototype.before = function(beforeFn) {
const self = this
return funtion() {
beforeFn.apply(this, arguments)
return self.apply(this, arguments)
}
}
Function.prototype.after = function(afterFn) {
const self = this
return funtion() {
const ret = self.apply(this, arguments)
afterFn.apply(this, arguments)
return ret
}
}
状态模式
- 允许一个对象在其内部状态改变时,对象看起来似乎修改了它的类
- 优点:
- 定义了状态与行为之间的关系,并将它们封装在一个类里,通过增加新的状态类,很容易增加新的状态和转换
- 避免Context无限膨胀,去掉了Context中原本过多的条件分支
- 用对象代替字符串来记录当前状态,使得状态一目了然
- Context中的请求动作和状态类中的封装行为可以非常容易的独立变化而互不影响
- 缺点:在系统中定义很多状态类,增加不少对象,在逻辑分散的状态类中,虽然避开了条件分支语句,但也造成了逻辑分散的问题
- 性能优化点:最好一开始创建好所有state对象,没必要销毁,因为很快就会再次用到
// 以登录网页时发送验证码为例,
// 状态(未发送、发送成功并开始倒计时、发送失败、倒计时结束)
const action = {
unSent: function () {},
sendSuccess: function () {},
sendFailed: function () {},
countdownEnd: function () {},
}
class State {
constructor(state) {
this.state = state
}
handle(context) {
console.log(`this is ${this.state}`)
context.setState(this)
action[this.state]()
}
}
class Context {
constructor() {
this.state = null
}
getState() {
return this.state
}
setState(state) {
this.state = state
}
}
let context = new Context()
let unSent = new State('unSent')
let sendSuccess = new State('sendSuccess')
let sendFailed = new State('sendFailed')
let countdownEnd = new State('countdownEnd')
unSent.handle(context) // { state: 'unSent' }
sendSuccess.handle(context) // { state: 'sendSuccess' }
sendFailed.handle(context) // { state: 'sendFailed' }
countdownEnd.handle(context) // { state: 'countdownEnd' }
适配器模式
- 作用是解决两个软件实体间的接口不兼容的问题,如果现有的接口能正常工作,则永远不会用到适配器模式
- 适配器模式主要用来解决两个已有接口之间不匹配的问题,不考虑接口是怎么实现的,不考虑它们如何演化,不需要改变已有接口,就能使它们协同作用
- 一般应用场景有适配第三方SDK、整合旧接口等
// 比如要把项目中全部用axios请求的代码全部替换成fetch,那么写一个axiosToFetchAdaptor适配器方法即可
function axiosHttp(type, url, data, success, failed) {
axios[type]({
url,
data,
}).then((res) => {
success(res)
}).catch((err) => {
failed(err)
})
}
function fetchHttp(type, url, data) {
return new Promise((resolve, reject) => {
fetch(url, {
method: type,
body: data,
})
.then(res => res.json())
.then(res => resolve(res))
.catch(err => reject(err))
})
}
async function axiosToFetchAdaptor(type, url, data, success, failed) {
let result
try {
result = await fetchHttp(type, url, data)
// 假设code为0为请求成功
result.code === 0 && success ? success(result) : failed(result.code)
} catch(err) {
// 捕捉网络错误
if(failed){
failed(err.code);
}
}
}
---
function axiosHttp(type, url, data, success, failed) {
return axiosToFetchAdaptor(type, url, data, success, failed)
}
原型模式
- 原型模式不仅是一种设计模式,它还是一种编程范式,是 JavaScript 面向对象系统实现的根基
- 当我们想要创建一个对象时,会先找到一个对象作为原型,然后通过克隆原型的方式来创建出一个与原型一样(共享一套数据/方法)的对象。在 JavaScript 里,Object.create方法就是原型模式的天然实现——准确地说,只要我们还在借助Prototype来实现对象的创建和原型的继承,那么我们就是在应用原型模式
外观模式
- 主要是为子系统的一组接口提供一个简单便利的访问入口,对客户屏蔽了子系统的复杂性
- 经常被用于处理高级游览器的和低版本游览器的一些接口的兼容处理
function addEvent(el, type, fn){
if(el.addEventlistener){
el.addEventlistener(type, fn, false)
}else if(el.attachEvent){
el.attachEvent(`on${type}`, fn)
}else {
el[type] = fn
}
}
区别
状态模式和策略模式的区别
- 相同点:都有一个上下文,一些策略类,上下文把请求委托给这些类执行
- 策略模式中的各个策略类之间是平等又平行的,之间没有任何关系
- 状态模式中状态对应的行为是早已被封装好的,状态间的切换也早被规定完成,改变行为发生在状态模式内部
代理模式和装饰者模式的区别
- 代理模式目的是,当直接访问本体不方便或者不符合需要时,为这个本体提供一个替代者;装饰者模式作用是为对象动态加入行为
- 代理模式强调一种关系,这种关系可以静态的表达,这种关系在一开始就可以被确定;装饰者模式用于一开始不能确定对象的全部功能时
- 代理模式通常只有一层代理-本体的引用,装饰者模式会形成一条长长的装饰链
- 装饰者模式是实实在在的为对象增加新的职责和行为,代理做的还是跟本体一样
总结
回顾这些设计模式,再结合平时做的项目,可能我们多多少少已经写过类似的代码。 设计模式是要长时间深入研究的一个方向,需要结合实际的场景练习模仿,不断地思考总结,使得自己的项目真正通过应用具体设计模式的方式得到实质性的优化。 当然,也不是所有的场景都需要设计模式,强行应用只会增加系统迭代的心智负担。