1. 设计模式简介
[设计模式](Design pattern) 是解决软件开发某些特定问题而提出的一些解决方案也可以理解成解决问题的一些思路。通过设计模式可以帮助我们增强代码的[可重用性]、可扩充性、 可维护性、灵活性好。我们使用设计模式最终的目的是实现代码的 [高内聚] 和 低 耦合。通俗一点讲的话 打比方面试官经常会问你如何让代码有健壮性。其实把代码中的变与不变分离,确保变化的部分灵活、不变的部分稳定,这样的封装变化就是代码健壮性的关键。而设计模式的出现,就是帮我们写出这样的代码。 设计模式就是解决编程里某类问题的通用模板,总结出来的代码套路就是设计模式。本文章总结下JS在工作中常用的设计模式 ,以帮助大家提高代码性能,增强工作效率!
设计模式原则
-
S -- Single Responsibility Principle 单一职责原则
- 一个程序只做好一件事
- 如果功能过于复杂就拆分开,每个部分保持独立
-
O -- OpenClosed Principle 开放/封闭原则
- 对扩展开放,对修改封闭
- 增加需求时,扩展新代码,而非修改已有代码
-
L -- Liskov Substitution Principle 里氏替换原则
- 子类能覆盖父类
- 父类能出现的地方子类就能出现
-
I -- Interface Segregation Principle 接口隔离原则
- 保持接口的单一独立
- 类似单一职责原则,这里更关注接口
-
D -- Dependency Inversion Principle 依赖倒转原则
- 面向接口编程,依赖于抽象而不依赖于具体
- 使用方只关注接口而不关注具体类的实现
SO体现较多,举个例子:(比如Promise)
- 单一职责原则:每个then中的逻辑只做好一件事
- 开放封闭原则(对扩展开放,对修改封闭):如果新增需求,扩展then
再举个例子:
typescript
//checkType('165226226326','mobile')
//result:false
let checkType=function(str, type) {
switch (type) {
case 'email':
return /^[\w-]+(.[\w-]+)*@[\w-]+(.[\w-]+)+$/.test(str)
case 'mobile':
return /^1[3|4|5|7|8][0-9]{9}$/.test(str);
case 'tel':
return /^(0\d{2,3}-\d{7,8})(-\d{1,4})?$/.test(str);
default:
return true;
}
}
有以下两个问题:
- 如果想添加其他规则就得在函数里面增加 case 。添加一个规则就修改一次!这样违反了开放-封闭原则(对扩展开放,对修改关闭)。而且这样也会导致整个 API 变得臃肿,难维护。
- 比如A页面需要添加一个金额的校验,B页面需要一个日期的校验,但是金额的校验只在A页面需要,日期的校验只在B页面需要。如果一直添加 case 。就是导致A页面把只在B页面需要的校验规则也添加进去,造成不必要的开销。B页面也同理。
建议的方式是给这个 API 增加一个扩展的接口:
typescript
let checkType=(function(){
let rules={
email(str){
return /^[\w-]+(.[\w-]+)*@[\w-]+(.[\w-]+)+$/.test(str);
},
mobile(str){
return /^1[3|4|5|7|8][0-9]{9}$/.test(str);
}
};
//暴露接口
return {
//校验
check(str, type){
return rules[type]?rules[type](str):false;
},
//添加规则
addRule(type,fn){
rules[type]=fn;
}
}
})();
//调用方式
//使用mobile校验规则
console.log(checkType.check('188170239','mobile'));
//添加金额校验规则
checkType.addRule('money',function (str) {
return /^[0-9]+(.[0-9]{2})?$/.test(str)
});
//使用金额校验规则
console.log(checkType.check('18.36','money'));
2. 设计模式分类(23种设计模式)
-
创建型
- 单例模式(Singleton Pattern)
- 原型模式(Prototype Pattern)
- 工厂模式(Factory Method Pattern)
- 抽象工厂模式(Abstract Factory Pattern)
- 建造者模式(Builder Pattern)
-
结构型
- 适配器模式(Adapter Pattern)
- 装饰器模式(Decorator Pattern)
- 代理模式(Proxy Pattern)
- 外观模式(Facade Pattern)
- 桥接模式(Bridge Pattern)
- 组合模式(Composite Pattern)
- 享元模式(Flyweight Pattern)
-
行为型
- 观察者模式(Observer Pattern)
- 迭代器模式(Iterator Pattern)
- 策略模式(Strategy Pattern)
- 模板方法模式(Template Method Pattern)
- 责任链模式(Chain of Responsibility)
- 命令模式(Command)
- 备忘录模式(Memento)
- 状态模式(State)
- 访问者模式(Visitor)
- 中介者模式(Mediator)
- 解释器模式(Interpreter)
2.1 工厂方法模式(Factory Method Pattern)
工厂模式定义一个用于创建对象的接口,这个接口由子类决定实例化哪一个类。该模式使一个类的实例化延迟到了子类。而子类可以重写接口方法以便创建的时候指定自己的对象类型。
javascript
class Product {
constructor(name) {
this.name = name
}
init() {
console.log('init')
}
fun() {
console.log('fun')
}
}
class Factory {
create(name) {
return new Product(name)
}
}
// use
let factory = new Factory()
let p = factory.create('p1')
p.init()
p.fun()
适用场景
- 如果你不想让某个子系统与较大的那个对象之间形成强耦合,而是想运行时从许多子系统中进行挑选的话,那么工厂模式是一个理想的选择
- 将new操作简单封装,遇到new的时候就应该考虑是否用工厂模式;
- 需要依赖具体环境创建不同实例,这些实例都有相同的行为,这时候我们可以使用工厂模式,简化实现的过程,同时也可以减少每种对象所需的代码量,有利于消除对象间的耦合,提供更大的灵活性
优点
- 创建对象的过程可能很复杂,但我们只需要关心创建结果。
- 构造函数和创建者分离, 符合"开闭原则"
- 一个调用者想创建一个对象,只要知道其名称就可以了。
- 扩展性高,如果想增加一个产品,只要扩展一个工厂类就可以。
缺点
- 添加新产品时,需要编写新的具体产品类,一定程度上增加了系统的复杂度
- 考虑到系统的可扩展性,需要引入抽象层,在客户端代码中均使用抽象层进行定义,增加了系统的抽象性和理解难度
什么时候不用
当被应用到错误的问题类型上时,这一模式会给应用程序引入大量不必要的复杂性.除非为创建对象提供一个接口是我们编写的库或者框架的一个设计上目标,否则我会建议使用明确的构造器,以避免不必要的开销。
由于对象的创建过程被高效的抽象在一个接口后面的事实,这也会给依赖于这个过程可能会有多复杂的单元测试带来问题。
例子
- 曾经我们熟悉的JQuery的$()就是一个工厂函数,它根据传入参数的不同创建元素或者去寻找上下文中的元素,创建成相应的jQuery对象
javascript
class jQuery {
constructor(selector) {
super(selector)
}
add() {
}
// 此处省略若干API
}
window.$ = function(selector) {
return new jQuery(selector)
}
- vue 的异步组件
在大型应用中,我们可能需要将应用分割成小一些的代码块,并且只在需要的时候才从服务器加载一个模块。为了简化,Vue 允许你以一个工厂函数的方式定义你的组件,这个工厂函数会异步解析你的组件定义。Vue 只有在这个组件需要被渲染的时候才会触发该工厂函数,且会把结果缓存起来供未来重渲染。例如:
javascript
Vue.component('async-example', function (resolve, reject) {
setTimeout(function () {
// 向 `resolve` 回调传递组件定义
resolve({
template: '<div>I am async!</div>'
})
}, 1000)
})
2.2 单例模式(Singleton Pattern)
一个类只有一个实例,并提供一个访问它的全局访问点。
javascript
class LoginForm {
constructor() {
this.state = 'hide'
}
show() {
if (this.state === 'show') {
alert('已经显示')
return
}
this.state = 'show'
console.log('登录框显示成功')
}
hide() {
if (this.state === 'hide') {
alert('已经隐藏')
return
}
this.state = 'hide'
console.log('登录框隐藏成功')
}
}
LoginForm.getInstance = (function () {
let instance
return function () {
if (!instance) {
instance = new LoginForm()
}
return instance
}
})()
let obj1 = LoginForm.getInstance()
obj1.show()
let obj2 = LoginForm.getInstance()
obj2.hide()
console.log(obj1 === obj2)
优点
- 划分命名空间,减少全局变量
- 增强模块性,把自己的代码组织在一个全局变量名下,放在单一位置,便于维护
- 且只会实例化一次。简化了代码的调试和维护
缺点
- 由于单例模式提供的是一种单点访问,所以它有可能导致模块间的强耦合 从而不利于单元测试。无法单独测试一个调用了来自单例的方法的类,而只能把它与那个单例作为一个单元一起测试。
场景例子
- 定义命名空间和实现分支型方法
- 登录框
- vuex 和 redux中的store
2.3 建造者模式(Builder Pattern)
建造者模式是一种对象创建设计模式,它旨在通过一步步的构建流程来创建复杂对象
。其代码示例如下:
kotlin
// 创建 Product 类
class Sandwich {
constructor() {
this.ingredients = [];
}
addIngredient(ingredient) {
this.ingredients.push(ingredient);
}
toString() {
return this.ingredients.join(', ');
}
}
// 创建一个建造者类
class SandwichBuilder {
constructor() {
this.sandwich = new Sandwich();
}
reset() {
this.sandwich = new Sandwich();
}
putMeat(meat) {
this.sandwich.addIngredient(meat);
}
putCheese(cheese) {
this.sandwich.addIngredient(cheese);
}
putVegetables(vegetables) {
this.sandwich.addIngredient(vegetables);
}
get result() {
return this.sandwich;
}
}
// 创建用户(director)使用的 builder
class SandwichMaker {
constructor() {
this.builder = new SandwichBuilder();
}
createCheeseSteakSandwich() {
this.builder.reset();
this.builder.putMeat('ribeye steak');
this.builder.putCheese('american cheese');
this.builder.putVegetables(['peppers', 'onions']);
return this.builder.result;
}
}
// 建造一个三明治
const sandwichMaker = new SandwichMaker();
const sandwich = sandwichMaker.createCheeseSteakSandwich();
console.log(`Your sandwich: ${sandwich}`); // Output: Your sandwich: ribeye steak, american cheese, peppers, onions
2.4 抽象工厂模式(Abstract Factory Pattern)
抽象工厂模式提供了一种封装一组具有相同主题的单个工厂的方式
。它有一个接口,用于创建相关或依赖对象的家族,而不需要指定实际实现的类
。其代码示例如下:
scala
// 创建一组主题对象类型的抽象类
class AnimalFood {
provide() {
throw new Error('This method must be implemented.');
}
}
class AnimalToy {
provide() {
throw new Error('This method must be implemented.');
}
}
// 创建一组具体代表家族的对象
class HighQualityDogFood extends AnimalFood {
provide() {
return 'High quality dog food';
}
}
class HighQualityDogToy extends AnimalToy {
provide() {
return 'High quality dog toy';
}
}
class CheapCatFood extends AnimalFood {
provide() {
return 'Cheap cat food';
}
}
class CheapCatToy extends AnimalToy {
provide() {
return 'Cheap cat toy';
}
}
// 创建一个抽象工厂
class AnimalProductsAbstractFactory {
createFood() {
throw new Error('This method must be implemented.');
}
createToy() {
throw new Error('This method must be implemented.');
}
}
// 创建具体工厂类
class HighQualityAnimalProductsFactory extends AnimalProductsAbstractFactory {
createFood() {
return new HighQualityDogFood();
}
createToy() {
return new HighQualityDogToy();
}
}
class CheapAnimalProductsFactory extends AnimalProductsAbstractFactory {
createFood() {
return new CheapCatFood();
}
createToy() {
return new CheapCatToy();
}
}
// 使用具体工厂类来创建相关的对象
const highQualityAnimalProductsFactory = new HighQualityAnimalProductsFactory();
console.log(highQualityAnimalProductsFactory.createFood().provide()); // Output: High quality dog food
console.log(highQualityAnimalProductsFactory.createToy().provide()); // Output: High quality dog toy
const cheapAnimalProductsFactory = new CheapAnimalProductsFactory();
console.log(cheapAnimalProductsFactory.createFood().provide()); // Output: Cheap cat food
console.log(cheapAnimalProductsFactory.createToy().provide()); // Output: Cheap cat toy
2.5 适配器模式(Adapter Pattern)
将一个类的接口转化为另外一个接口,以满足用户需求,使类之间接口不兼容问题通过适配器得以解决。
javascript
class Plug {
getName() {
return 'iphone充电头';
}
}
class Target {
constructor() {
this.plug = new Plug();
}
getName() {
return this.plug.getName() + ' 适配器Type-c充电头';
}
}
let target = new Target();
target.getName(); // iphone充电头 适配器转Type-c充电头
优点
- 可以让任何两个没有关联的类一起运行。
- 提高了类的复用。
- 适配对象,适配库,适配数据
缺点
- 额外对象的创建,非直接调用,存在一定的开销(且不像代理模式在某些功能点上可实现性能优化)
- 如果没必要使用适配器模式的话,可以考虑重构,如果使用的话,尽量把文档完善
场景
- 整合第三方SDK
- 封装旧接口
php
// 自己封装的ajax, 使用方式如下
ajax({
url: '/getData',
type: 'Post',
dataType: 'json',
data: {
test: 111
}
}).done(function() {})
// 因为历史原因,代码中全都是:
// $.ajax({....})
// 做一层适配器
var $ = {
ajax: function (options) {
return ajax(options)
}
}
- vue的computed
xml
<template>
<div id="example">
<p>Original message: "{{ message }}"</p> <!-- Hello -->
<p>Computed reversed message: "{{ reversedMessage }}"</p> <!-- olleH -->
</div>
</template>
<script type='text/javascript'>
export default {
name: 'demo',
data() {
return {
message: 'Hello'
}
},
computed: {
reversedMessage: function() {
return this.message.split('').reverse().join('')
}
}
}
</script>
原有data 中的数据不满足当前的要求,通过计算属性的规则来适配成我们需要的格式,对原有数据并没有改变,只改变了原有数据的表现形式。
不同点
适配器与代理模式相似
- 适配器模式: 提供一个不同的接口(如不同版本的插头)
- 代理模式: 提供一模一样的接口
2.6 装饰器模式(Decorator Pattern)
- 动态地给某个对象添加一些额外的职责,,是一种实现继承的替代方案
- 在不改变原对象的基础上,通过对其进行包装扩展,使原有对象可以满足用户的更复杂需求,而不会影响从这个类中派生的其他对象
javascript
class Cellphone {
create() {
console.log('生成一个手机')
}
}
class Decorator {
constructor(cellphone) {
this.cellphone = cellphone
}
create() {
this.cellphone.create()
this.createShell(cellphone)
}
createShell() {
console.log('生成手机壳')
}
}
// 测试代码
let cellphone = new Cellphone()
cellphone.create()
console.log('------------')
let dec = new Decorator(cellphone)
dec.create()
场景例子
- 比如现在有4 种型号的自行车,我们为每种自行车都定义了一个单 独的类。现在要给每种自行车都装上前灯、尾 灯和铃铛这3 种配件。如果使用继承的方式来给 每种自行车创建子类,则需要 4×3 = 12 个子类。 但是如果把前灯、尾灯、铃铛这些对象动态组 合到自行车上面,则只需要额外增加3 个类
- ES7 Decorator
- core-decorators
优点
- 装饰类和被装饰类都只关心自身的核心业务,实现了解耦。
- 方便动态的扩展功能,且提供了比继承更多的灵活性。
缺点
- 多层装饰比较复杂。
- 常常会引入许多小对象,看起来比较相似,实际功能大相径庭,从而使得我们的应用程序架构变得复杂起来
2.7 代理模式(Proxy Pattern)
是为一个对象提供一个代用品或占位符,以便控制对它的访问
假设当A 在心情好的时候收到花,小明表白成功的几率有 60%,而当A 在心情差的时候收到花,小明表白的成功率无限趋近于0。 小明跟A 刚刚认识两天,还无法辨别A 什么时候心情好。如果不合时宜地把花送给A,花 被直接扔掉的可能性很大,这束花可是小明吃了7 天泡面换来的。 但是A 的朋友B 却很了解A,所以小明只管把花交给B,B 会监听A 的心情变化,然后选 择A 心情好的时候把花转交给A,代码如下:
javascript
let Flower = function() {}
let xiaoming = {
sendFlower: function(target) {
let flower = new Flower()
target.receiveFlower(flower)
}
}
let B = {
receiveFlower: function(flower) {
A.listenGoodMood(function() {
A.receiveFlower(flower)
})
}
}
let A = {
receiveFlower: function(flower) {
console.log('收到花'+ flower)
},
listenGoodMood: function(fn) {
setTimeout(function() {
fn()
}, 1000)
}
}
xiaoming.sendFlower(B)
场景
- HTML元 素事件代理
xml
<ul id="ul">
<li>1</li>
<li>2</li>
<li>3</li>
</ul>
<script>
let ul = document.querySelector('#ul');
ul.addEventListener('click', event => {
console.log(event.target);
});
</script>
- ES6 的 proxy
- jQuery.proxy()方法
优点
- 代理模式能将代理对象与被调用对象分离,降低了系统的耦合度。代理模式在客户端和目标对象之间起到一个中介作用,这样可以起到保护目标对象的作用
- 代理对象可以扩展目标对象的功能;通过修改代理对象就可以了,符合开闭原则;
缺点
处理请求速度可能有差别,非直接访问存在开销
不同点
装饰者模式实现上和代理模式类似
- 装饰者模式: 扩展功能,原有功能不变且可直接使用
- 代理模式: 显示原有功能,但是经过限制之后的
2.8 外观模式(Facade Pattern)
为子系统的一组接口提供一个一致的界面,定义了一个高层接口,这个接口使子系统更加容易使用
- 兼容浏览器事件绑定
rust
let addMyEvent = function (el, ev, fn) {
if (el.addEventListener) {
el.addEventListener(ev, fn, false)
} else if (el.attachEvent) {
el.attachEvent('on' + ev, fn)
} else {
el['on' + ev] = fn
}
};
- 封装接口
ini
let myEvent = {
// ...
stop: e => {
e.stopPropagation();
e.preventDefault();
}
};
场景
- 设计初期,应该要有意识地将不同的两个层分离,比如经典的三层结构,在数据访问层和业务逻辑层、业务逻辑层和表示层之间建立外观Facade
- 在开发阶段,子系统往往因为不断的重构演化而变得越来越复杂,增加外观Facade可以提供一个简单的接口,减少他们之间的依赖。
- 在维护一个遗留的大型系统时,可能这个系统已经很难维护了,这时候使用外观Facade也是非常合适的,为系系统开发一个外观Facade类,为设计粗糙和高度复杂的遗留代码提供比较清晰的接口,让新系统和Facade对象交互,Facade与遗留代码交互所有的复杂工作。
参考: 大话设计模式
优点
- 减少系统相互依赖。
- 提高灵活性。
- 提高了安全性
缺点
- 不符合开闭原则,如果要改东西很麻烦,继承重写都不合适。
2.9 观察者模式(Observer Pattern)
定义了一种一对多的关系,让多个观察者对象同时监听某一个主题对象,这个主题对象的状态发生变化时就会通知所有的观察者对象,使它们能够自动更新自己,当一个对象的改变需要同时改变其它对象,并且它不知道具体有多少对象需要改变的时候,就应该考虑使用观察者模式。
- 发布 & 订阅
- 一对多
javascript
// 主题 保存状态,状态变化之后触发所有观察者对象
class Subject {
constructor() {
this.state = 0
this.observers = []
}
getState() {
return this.state
}
setState(state) {
this.state = state
this.notifyAllObservers()
}
notifyAllObservers() {
this.observers.forEach(observer => {
observer.update()
})
}
attach(observer) {
this.observers.push(observer)
}
}
// 观察者
class Observer {
constructor(name, subject) {
this.name = name
this.subject = subject
this.subject.attach(this)
}
update() {
console.log(`${this.name} update, state: ${this.subject.getState()}`)
}
}
// 测试
let s = new Subject()
let o1 = new Observer('o1', s)
let o2 = new Observer('02', s)
s.setState(12)
场景
- DOM事件
javascript
document.body.addEventListener('click', function() {
console.log('hello world!');
});
document.body.click()
- vue 响应式
优点
- 支持简单的广播通信,自动通知所有已经订阅过的对象
- 目标对象与观察者之间的抽象耦合关系能单独扩展以及重用
- 增加了灵活性
- 观察者模式所做的工作就是在解耦,让耦合的双方都依赖于抽象,而不是依赖于具体。从而使得各自的变化都不会影响到另一边的变化。
缺点
过度使用会导致对象与对象之间的联系弱化,会导致程序难以跟踪维护和理解
2.10 状态模式(State)
允许一个对象在其内部状态改变的时候改变它的行为,对象看起来似乎修改了它的类
javascript
// 状态 (弱光、强光、关灯)
class State {
constructor(state) {
this.state = state
}
handle(context) {
console.log(`this is ${this.state} light`)
context.setState(this)
}
}
class Context {
constructor() {
this.state = null
}
getState() {
return this.state
}
setState(state) {
this.state = state
}
}
// test
let context = new Context()
let weak = new State('weak')
let strong = new State('strong')
let off = new State('off')
// 弱光
weak.handle(context)
console.log(context.getState())
// 强光
strong.handle(context)
console.log(context.getState())
// 关闭
off.handle(context)
console.log(context.getState())
场景
- 一个对象的行为取决于它的状态,并且它必须在运行时刻根据状态改变它的行为
- 一个操作中含有大量的分支语句,而且这些分支语句依赖于该对象的状态
优点
- 定义了状态与行为之间的关系,封装在一个类里,更直观清晰,增改方便
- 状态与状态间,行为与行为间彼此独立互不干扰
- 用对象代替字符串来记录当前状态,使得状态的切换更加一目了然
缺点
- 会在系统中定义许多状态类
- 逻辑分散
2.11 迭代器模式(Iterator Pattern)
提供一种方法顺序一个聚合对象中各个元素,而又不暴露该对象的内部表示。
kotlin
class Iterator {
constructor(conatiner) {
this.list = conatiner.list
this.index = 0
}
next() {
if (this.hasNext()) {
return this.list[this.index++]
}
return null
}
hasNext() {
if (this.index >= this.list.length) {
return false
}
return true
}
}
class Container {
constructor(list) {
this.list = list
}
getIterator() {
return new Iterator(this)
}
}
// 测试代码
let container = new Container([1, 2, 3, 4, 5])
let iterator = container.getIterator()
while(iterator.hasNext()) {
console.log(iterator.next())
}
场景例子
- Array.prototype.forEach
- jQuery中的$.each()
- ES6 Iterator
特点
- 访问一个聚合对象的内容而无需暴露它的内部表示。
- 为遍历不同的集合结构提供一个统一的接口,从而支持同样的算法在不同的集合结构上进行操作
总结
对于集合内部结果常常变化各异,不想暴露其内部结构的话,但又想让客户代码透明的访问其中的元素,可以使用迭代器模式
2.12 桥接模式(Bridge Pattern)
桥接模式(Bridge)将抽象部分与它的实现部分分离,使它们都可以独立地变化。
javascript
class Color {
constructor(name){
this.name = name
}
}
class Shape {
constructor(name,color){
this.name = name
this.color = color
}
draw(){
console.log(`${this.color.name} ${this.name}`)
}
}
//测试
let red = new Color('red')
let yellow = new Color('yellow')
let circle = new Shape('circle', red)
circle.draw()
let triangle = new Shape('triangle', yellow)
triangle.draw()
优点
- 有助于独立地管理各组成部分, 把抽象化与实现化解耦
- 提高可扩充性
缺点
- 大量的类将导致开发成本的增加,同时在性能方面可能也会有所减少。
团队介绍
「三翼鸟数字化技术平台-ToC服务平台」以用户行为数据为基础,利用推荐引擎为用户提供"千人千面"的个性化推荐服务,改善用户体验,持续提升核心业务指标。通过构建高效、智能的线上运营系统,全面整合数据资产,实现数据分析-人群圈选-用户触达-后效分析-策略优化的运营闭环,并提供可视化报表,一站式操作提升数字化运营效率。