设计模式
设计模式往往是软件设计中的最佳实践,是前人对问题解决的经验总结。 很多时候你可能会发现在别人的代码或者主流技术源码会有一些和自己想法不一样的地方,但为什么不知道为什么要这样写。
使用设计模式是为了重用代码、让代码更容易被他人理解、保证代码可靠性。
学习设计模式,有助于写出可复用和可维护性高的程序
创建型模式
- 工厂模式
- 抽象工厂模式
- 单例模式
- 建造者模式
- 原型模式
结构型模式
- 适配器模式
- 桥接模式
- 过滤器模式
- 组合模式
- 装饰器模式
- 外观模式
- 享元模式
- 代理模式
行为型模式
- 责任链模式
- 命令模式
- 解释器模式
- 迭代器模式
- 中介者模式
- 备忘录模式
- 观察者模式
- 状态模式
- 空对象模式
- 策略模式
- 模板模式
- 访问者模式
设计模式的7大原则
- 开放封闭原则(Open Close Principle)
- 单一职责原则(Single Responsibility Principle)
- 里氏代换原则(Liskov Substitution Principle) 把父类都替换成它的子类,程序的行为没有变化
- 依赖反转原则(Dependence Inversion Principle) 高层次的模块不应该依赖于低层次的模块
- 接口隔离原则(Interface Segregation Principle), 不应该依赖它不需要的接口
- 迪米特法则,又称最少知道原则(Demeter Principle)
- 合成复用原则(Composite Reuse Principle) 尽量使用对象组合,而不是继承来达到复 用的目的
核心两个原则
单一职责原则
做什么都要分开,一个程序只做好一件事,如果功能过于复杂就拆分开,每个部分都保持独立
降低耦合度,减少因为代码改动带来的Bug风险
开放封闭原则
定义:当需要改变一个程序的功能或者给这个程序增加新功能的时候,可以使用增加代码的方式,但是不允许改动程序的源代码。
对扩展开放,对修改封闭。增加需求时,扩展新代码,而非修改已有代码
一、工厂模式
传入参数即可创建实例
2种类型:
-
简单工厂模式
通过传入不同的参数来实现多态 -
抽象工厂模式 分离了具体的类,通过参数创建的对象的类
举例
function Factory(career) {
function User(career, work) {
this.career = career
this.work = work
}
let work
switch(career) {
case 'coder':
work = ['写代码', '修Bug']
return new User(career, work)
break
case 'hr':
work = ['招聘', '员工信息管理']
return new User(career, work)
break
case 'boss':
work = ['喝茶', '开会', '审批']
return new User(career, work)
break
}
}
let coder = new Factory('coder')
console.log(coder)
let boss = new Factory('boss')
console.log(boss)
什么时候会用工厂模式?
将new操作简单封装,遇到new的时候就应该考虑是否用工厂模式;
工厂模式适用于需要大量获取类似对象的场景,比如jQuery,我们在使用时可能需要获得很多dom的jQuery实例,工厂模式让使用者可以直接$(selector)这样用,而不需要new,使用起来更方便。
jQuery的工厂模式
jQuery的('div')和new ()最方便 ,这是因为$()已经是一个工厂方法了;
let slice = Array.prototype.slice
class jQuery {
constructor(selector) {
if (!selector) return this;
if (typeof selector === "string") {
let dom = slice.call(document.querySelectorAll(selector))
let length = dom ? dom.length : 0
for(let i = 0;i< length;i++){
this[i] = dom[i]
}
this.length = length
this.selector = selector
}
// ...
}
toArray () {
return slice.call(this);
}
get(num){
if ( num == null ) {
return slice.call( this );
}
return num < 0 ? this[ num + this.length ] : this[ num ];
}
each(){
//...
}
map(){
//...
}
addClass(){
//...
}
html(){
//...
}
extend(){
//...
}
}
window.$ = function(selector) {
return new jQuery(selector)
}
React的工厂模式
创建组件,React.createElement
let component = <div className={"container"}>
<h1 className={"h1"}>{title}</h1>
</div>
// ==>
React.createElement("div",{ className: 'container'},
React.createElement("h1",{className: 'h1'}, title)
)
Vue的工厂模式
组件
Vue.component('example', {
name: 'example',
template: '<div><list-overflow></list-overflow></div>'
})
Vue.component("example", function(reslove, reject){
setTimeout(()=>{
reslove({
template:'<div>i am async!</div>'
})
})
})
组件工厂函数
const AsyncComponent = (component) => ({
// 需要加载的组件 (应该是一个 `Promise` 对象)
component: component,
// 异步组件加载时使用的组件
loading: LoadingComponent,
// 加载失败时使用的组件
error: ErrorComponent,
// 展示加载时组件的延时时间。默认值是 200 (毫秒)
delay: 200,
// 如果提供了超时时间且组件加载也超时了,
// 则使用加载失败时使用的组件。默认值是:`Infinity`
timeout: 3000
})
AsyncComponent(import('./MyComponent.vue'))
二、单例模式
单例模式的核心是确保只有一个实例,并提供全局访问
设计原则的验证
单一职责原则,只实例化一个唯一的对象。
例子
// 单例模式的核心是确保只有一个实例,并提供全局访问
class SetManager {
constructor(name) {
this.name = name
}
getName(){
return this.name
}
}
const SingletonSetManager = (function() {
let manager = null;
return function(name) {
if (!manager) {
manager = new SetManager(name);
}
return manager;
}
})()
let A = new SingletonSetManager('张三')
let B = new SingletonSetManager('李四')
console.log(A === B)
console.log(A.getName())
console.log(B.getName())
适用场景
1.引用第三方库,Jquery, vuex 和 vue-router 的插件注册方法 install 判断如果系统存在实例就直接返回掉
if (window.libs != null) {
return window.libs; // 直接返回
} else {
window.libs = '...'; // 初始化
}
2.弹窗
class Tip {
constructor() {
this.ele = document.createElement('div')
this.ele.className = 'tip'
//绑定事件
this.bindEvent()
//设置回调函数
this.callback = function () {}
document.body.appendChild(this.ele)
}
//设置提示内容
//实际上应该一个节点一个节点创建,这么写,容易被注入恶意代码
setContent(txt) {
this.ele.innerHTML = `
<div class="top">
<div>标题</div>
<button id='cancle'>X</button>
</div>
<div class="content">
<p>${txt}</p>
</div>
<div class="btns">
<button id='cancle'>取消</button>
<button id='ok'>确定</button>
</div>
`
this.ele.style.display = 'block'
}
bindEvent() {
this.ele.addEventListener('click', e => {
e = e || window.event
const target = e.target || e.srcElement
if (target.id === 'cancle') {
this.ele.style.display = 'none'
} else if (target.id === 'ok') {
this.callback()
this.ele.style.display = 'none'
}
})
}
}
加入单例
const Tip = (function () {
class Tip {...}
let instance = null
return function singleTon(txt, cb) {
if (!instance) instance = new Tip()
instance.setContent(txt) //设置提示内容
instance.callback = cb //设置回调函数
return instance
}
})()
const tip1 = new Tip("你好", () => {
console.log("回调函数1")
})
const tip2 = new Tip("世界", () => {
console.log("回调函数2")
})
console.log(tip1 === tip2) //true
不论我们new多少次,只有一个实例对象,这很好地节约了内存,也省去了每次创建和删除节点了工作
3.全局状态管理 (Vuex / Redux) 命名空间 store
单例模式的作用远远不止减少内存的作用,而是保证全局对象的唯一性,每一个去实例化对象的地方拿到的是同一个实例,参考vue2的设计,或者vuex
三、装饰器模式
在不改变原对象的基础上,通过对其添加属性或方法来进行包装拓展,使得原有对象可以动态具有更多功能
传统面向对象的装饰器模式
//原始的飞机类
var Plane = function () {
};
Plane.prototype.fire = function () {
console.log('发射普通子弹');
};
//装饰类
var MissileDecorator = function (plane) {
this.plane = plane;
}
MissileDecorator.prototype.fire = function () {
this.plane.fire();
console.log('发射导弹!');
};
var plane = new Plane();
plane = new MissileDecorator(plane);
plane.fire();
应用
- vuex中Actions是一个装饰器,它包裹Mutations使之可以异步使用。对于Store对象,使用Action可以异步改变状态;
- vue:
//decorator.js
import { MessageBox, Message } from 'element-ui'
/**
* 确认框
* @param {String} title - 标题
* @param {String} content - 内容
* @param {String} confirmButtonText - 确认按钮名称
* @param {Function} callback - 确认按钮名称
* @returns
**/
export function confirm(title, content, confirmButtonText = '确定') {
return function(target, name, descriptor) {
const originValue = descriptor.value
descriptor.value = function(...args) {
MessageBox.confirm(content, title, {
dangerouslyUseHTMLString: true,
distinguishCancelAndClose: true,
confirmButtonText: confirmButtonText
}).then(originValue.bind(this, ...args)).catch(error => {
if (error === 'close' || error === 'cancel') {
Message.info('用户取消操作')
} else {
Message.info(error)
}
})
}
return descriptor
}
}
// vue代码
export default {
methods:{
@confirm('删除门店','请确认是否删除门店?')
deleteStore(id){
// 发送请求
}
}
}
- React class组件
四、适配器模式
设计原则的验证
适配器模式满足开放封闭原则,对扩展开放,对修改封闭,我们使用适配器的一个理念就是对于原有的代码不进行修改,添加适配器满足扩展的需求。
适配器模式的应用场景
1.vue computed计算属性
<template>
<div>
firstName: <input type="text" v-model="firstName"/> <br>
lastName: <input type="text" v-model="lastName"/> <br>
fullName: <span>{{fullName}}</span>
</div>
</template>
<script >
export default {
data() {
return {
firstName: "张",
lastName: "三",
}
},
computed:{
fullName: function (){
return this.firstName + this.lastName
}
}
}
</script>
-
前端网络请求中的适配器 请求拦截,通用字段的增添,headers token之类的 响应拦截,对返回数据的处理加工,data适配
-
源码中的方法
五、外观模式
多个子系统中复杂逻辑进行抽象,从而提供一个更统一、更简洁、更易用的API, 减少系统相互依赖
比如JQuery就把复杂的原生DOM操作进行了抽象和封装,并消除了浏览器之间的兼容问题,从而提供了一个更高级更易用的版本。其实在平时工作中我们也会经常用到外观模式进行开发,只是我们不自知而已
设计原则的验证
适配器模式满足开放封闭原则,对扩展开放,对修改封闭,我们使用适配器的一个理念就是对于原有的代码不进行修改,添加适配器满足扩展的需求。
应用场景
阻止冒泡兼容,去除冒泡事件和默认事件
let myEvent = {
// ...
stop: e => {
e.stopPropagation();
e.preventDefault();
}
};
六、代理模式
在某些情况下,出于种种考虑/限制,一个对象不能直接访问另一个对象,需要一个第三者(代理)牵线搭桥从而间接达到访问目的,这样的模式就是代理模式。
举例:科学上网
常见场景
- 工程化解决跨域proxy
- es6 proxy
- 代理工具
- 虚拟代理实现图片预加载
- 代理localStorage,实现本地储存时间赋能
<ul>
<li>1</li>
<li>2</li>
<li>3</li>
</ul>
</body>
<script>
let list = document.querySelectorAll('li');
list.forEach(item => {
item.addEventListener('click', event => {
console.log(event.target);
});
})
</script>
拓展一个问题
const demoComponent = () => {
return <ul>{
[1, 2, 3].map(item => <li onClick={(event) => {
console.log(event)
}}>{item}</li>)
}</ul>
}
问题解释
React 基于 Virtual DOM 实现了一个 SyntheticEvent(合成事件)层, 我们所定义的事件处理器会接收到一个 SyntheticEvent 对象的实例 同样支持事件冒泡优势:
- 减少内存小号
- 决了IE等浏览器的兼容问题(代理)
弊端(合成事件机制的坑)(已经取消事件池): React SyntheticEvent 不能异步使用
import React, { Component } from "react";
class TextInput extends Component {
state = {
editionCounter: 0,
value: this.props.defaultValue,
}
// 由于 setState 是异步操作,event.target.value 在运行时可能已经被重置了
handleChange = event =>{
// event.persist() // 方案1
// const value = event.target.value; // 方案2
this.setState(prevState => ({ value: event.target.value, editionCounter: prevState.editionCounter + 1 }));
}
render() {
return (
<span>Edited {this.state.editionCounter} times</span>
<input
type="text"
value={this.state.value}
onChange={this.handleChange} // WRONG!
/>
)
}
}
- vNode
设计原则的验证
代理类和目标类分离,隔离开目标类和使用者,符合单一职责原则和开放封闭原则。
七、状态模式
定义: 允许对象在内部状态发生改变时改变它的行为,对象看起来好像修改了它的类。
使用状态模式的目的:
状态模式主要解决的是当控制一个对象状态的条件表达式过于复杂时的情况(一个对象有状态变化,每次状态变化都会触发一个逻辑,不能总是用if…else…)。把状态的判断逻辑转移到表示不同状态的一系列类中,可以把复杂的判断逻辑简化。
判断是否适合使用状态模式
- 是一个对象且有多种私有状态
- 需要一个环境方法来对自己的状态做出变化
例子:《JS设计模式》超级玛丽 H5活动页中,通过控制超级玛丽的各种”状态“,跑,跳,蹲,来过关游戏
function changeAction(action) {
if(action=='jump'){
// 玛丽跳跃
}
if(action=='move'){
// 玛丽移动
}
if(action=='crouch'){
// 玛丽蹲下
}
}
玛丽能加速,会跑跳,边走边开枪,可能会有多种复合形态,这样才好玩。
如果按照上面写下去一下午就过去了,稍微有点需求变动,加班与你同在!
首先玛丽是一个对象,有一个私有的状态对象
const Marry = function () {
this._status = {
jump: function () {
// 跳跃
},
move: function () {
// 移动
},
shoot: function () {
// 射击
},
squat: function () {
// 蹲下
},
speed: function () {
// 加速
}
}
}
原型上增加一个环境改变状态方法
Marry.prototype.changeStatus = function (status) {
if(Array.isArray(status)){
for(let sta of status){
this._status[sta]()
}
}else{
this._status[status]()
}
}
// 简洁写法
Marry.prototype.changeStatus = function (status) {
status = Array.isArray(status) ? status : [status]
for(let sta of status){
this._status[sta]()
}
}
开枪,在移动的时候同时跳
let marry = new Marry()
marry.changeStatus("shoot")
marry.changeStatus(["move","jump"])
例子:日历期次快捷选择器
八、策略模式
状态模式的一种升级或衍生版本,策略模式更倾向于运算或策略,而状态模式更倾向于状态。
场景例子
如果在一个系统里面有许多类,它们之间的区别仅在于它们的'行为',那么使用策略模式可以动态地让一个对象在许多行为中选择一种行为。 一个系统需要动态地在几种算法中选择一种。 表单验证
定义不同的策略
class OperationAdd {
doOperation(num1, num2) {
return num1 + num2;
}
}
class OperationSubstract {
doOperation(num1, num2) {
return num1 - num2;
}
}
class OperationMultiply {
doOperation(num1, num2) {
return num1 * num2;
}
}
改变行为的上下文
class Context {
constructor(strategy){
this.strategy = strategy;
}
executeStrategy(num1, num2){
return this.strategy.doOperation(num1, num2);
}
}
那么根据策略的不同改变了上下文执行的行为
let context = new Context(new OperationAdd());
console.log("10 + 5 = " + context.executeStrategy(10, 5));
context = new Context(new OperationSubstract());
console.log("10 - 5 = " + context.executeStrategy(10, 5));
context = new Context(new OperationMultiply());
console.log("10 * 5 = " + context.executeStrategy(10, 5));
/**
* output:
* 10 + 5 = 15
* 10 - 5 = 5
* 10 * 5 = 50
*/
表单校验
<html>
<head>
<title>策略模式-校验表单</title>
<meta content="text/html; charset=utf-8" http-equiv="Content-Type">
</head>
<body>
<form id = "registerForm" method="post" action="http://xxxx.com/api/register">
用户名:<input type="text" name="userName">
密码:<input type="text" name="password">
手机号码:<input type="text" name="phoneNumber">
<button type="submit">提交</button>
</form>
<script type="text/javascript">
// 策略对象
const strategies = {
isNoEmpty: function (value, errorMsg) {
if (value === '') {
return errorMsg;
}
},
isNoSpace: function (value, errorMsg) {
if (value.trim() === '') {
return errorMsg;
}
},
minLength: function (value, length, errorMsg) {
if (value.trim().length < length) {
return errorMsg;
}
},
maxLength: function (value, length, errorMsg) {
if (value.length > length) {
return errorMsg;
}
},
isMobile: function (value, errorMsg) {
if (!/^(13[0-9]|14[5|7]|15[0|1|2|3|5|6|7|8|9]|17[7]|18[0|1|2|3|5|6|7|8|9])\d{8}$/.test(value)) {
return errorMsg;
}
}
}
// 验证类
class Validator {
constructor() {
this.cache = []
}
add(dom, rules) {
for(let i = 0, rule; rule = rules[i++];) {
let strategyAry = rule.strategy.split(':')
let errorMsg = rule.errorMsg
this.cache.push(() => {
let strategy = strategyAry.shift()
strategyAry.unshift(dom.value)
strategyAry.push(errorMsg)
return strategies[strategy].apply(dom, strategyAry)
})
}
}
start() {
for(let i = 0, validatorFunc; validatorFunc = this.cache[i++];) {
let errorMsg = validatorFunc()
if (errorMsg) {
return errorMsg
}
}
}
}
// 调用代码
let registerForm = document.getElementById('registerForm')
let validataFunc = function() {
let validator = new Validator()
validator.add(registerForm.userName, [{
strategy: 'isNoEmpty',
errorMsg: '用户名不可为空'
}, {
strategy: 'isNoSpace',
errorMsg: '不允许以空白字符命名'
}, {
strategy: 'minLength:2',
errorMsg: '用户名长度不能小于2位'
}])
validator.add(registerForm.password, [ {
strategy: 'minLength:6',
errorMsg: '密码长度不能小于6位'
}])
validator.add(registerForm.phoneNumber, [{
strategy: 'isMobile',
errorMsg: '请输入正确的手机号码格式'
}])
return validator.start()
}
registerForm.onsubmit = function() {
let errorMsg = validataFunc()
if (errorMsg) {
alert(errorMsg)
return false
}
}
</script>
</body>
</html>
优点
利用组合、委托、多态等技术和思想,可以有效的避免多重条件选择语句 提供了对开放-封闭原则的完美支持,将算法封装在独立的strategy中,使得它们易于切换,理解,易于扩展 利用组合和委托来让Context拥有执行算法的能力,这也是继承的一种更轻便的代替方案 缺点
会在程序中增加许多策略类或者策略对象 要使用策略模式,必须了解所有的strategy,必须了解各个strategy之间的不同点,这样才能选择一个合适的strategy
九、职责链模式
场景例子
JS 中的事件冒泡 作用域链
// 请假审批,需要人事审批、经理审批、老板审批
class Action {
constructor(name) {
this.name = name
this.nextAction = null
}
setNextAction(action) {
this.nextAction = action
}
handle() {
console.log( `${this.name} 审批`)
if (this.nextAction != null) {
this.nextAction.handle()
}
}
}
let a1 = new Action("人事")
let a2 = new Action("经理")
let a3 = new Action("老板")
a1.setNextAction(a2)
a2.setNextAction(a3)
a1.handle()
2.原型链 (向上链式找属性或者方法)
- Promise的意义
它的主要功用是——能让异步程序使用责任链模式。
能够把异步操作最终的成功返回值或者失败原因和相应的处理程序关联起来。 这样使得异步方法可以像同步方法那样返回值:异步方法并不会立即返回最终的值,而是会返回一个 promise,以便在未来某个时候把值交给使用者。
shell编程
需求场景: 输入账户密码
-> 返回该人员的项目列表(PP shopify,Recomsale),选择项目
-> 选择下载前端/后端代码(根据返回的仓库地址将前端/ 后端代码下载到当前文件夹)
-> 根据项目包的类型执行构建逻辑(npm run build, next build, umi build) 或者附加逻辑
-> 等待构建完成后提示选择上传服务器地址
-> 完成提示
// ...
十、观察者模式 (响应式数据原理)
观察者模式有两个对象,一个是观察者,一个是被观察者,当被观察者的状态发生改变的时候,需要通知观察者,观察者会做出相对的反应。
常见应用
- 事件绑定
<button id="btn">按钮</button>
<script>
document.getElementById("btn").addEventListener("click",function () {
console.log("btn click")
})
</script>
- Promise
加载图片示例
function loadImage(src) {
return new Promise((resolve,reject) => {
const img = document.createElement("img")
img.onload = () => resolve(img)
img.onerror = () => reject("图片加载失败")
img.src = src
})
}
十一、发布-订阅模式
发布订阅模式的核心就是一对多的关系,一个发布者发起事件,所有的订阅者都会执行(vue 事件机制), 发布者和订阅者不知道彼此的存在
常见应用
-
vue EventBus
-
node js 事件中心
const EventEmitter = require('events');
class MyEmitter extends EventEmitter {}
const myEmitter = new MyEmitter();
myEmitter.on('event', () => {
console.log('an event occurred!');
});
myEmitter.emit('event');
发布订阅模式和观察者模式是比较相似,但是很不一样的两个模式。
发布订阅与观察者的区别
1.观察者模式是高耦合,目标和观察者是直接联系起来的,基于对象
2.发布订阅模式中,双方不知道对方的存在,而观察者模式中,基于自定义事件
发布订阅模式可以被看作是观察者模式的一个进阶版
观察者模式,是1对多,观察者状态变化后通知订阅者执行对应事件,参考vue2的响应式依赖收集。
发布订阅模式是有一个中间层,消息中心,发布者只关注把自己的事件放到消息中心,谁来消费由消费者们自己去取,参考eventbus或者后端的mq思想
十二、迭代器模式
作用: 主要是用于遍历数据,比如迭代链表。在数据过大时亦可像流一样,一点一点来接数据。
方便遍历数据
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())
}
应用:
1.Jquery.each
2.ES6 Iterator
JS原生的集合类型数据结构,只有Array(数组)和Object(对象),而ES6中,又新增了Map和Set。
四种数据结构各自有着自己特别的内部实现,但我们仍期待以同样的一套规则去遍历它们,所以ES6在推出新数据结构的同时也推出了一套统一的接口机制——迭代器(Iterator)。
ES6约定,任何数据结构只要具备Symbol.iterator属性(这个属性就是Iterator的具体实现,它本质上是当前数据结构默认的迭代器生成函数),就可以被遍历——准确地说,是被for…of…循环和迭代器的next方法遍历。 事实上,for…of…的背后正是对next方法的反复调用。
在ES6中,针对Array、Map、Set、String、TypedArray、函数的 arguments 对象、NodeList 对象这些原生的数据结构都可以通过for…of…进行遍历。原理都是一样的,此处我们拿最简单的数组进行举例,当我们用for…of…遍历数组时:
Es6中的iterator迭代器
// 而在es6中可以这样使用
class NameRepositoryEs6 {
constructor() {
this.names = ["Robert" , "John" ,"Julie" , "Lora"];
this.index = 0;
}
[Symbol.iterator]() {
return {
next: () => {
let done = true;
if(this.index < this.names.length){
done = false;
}
return {value: this.names[this.index++],done};
}
};
}
}
console.log("\nES6 Iterator:");
const namesRepositoryEs6 = new NameRepositoryEs6()
for(const name of namesRepositoryEs6) {
console.log("Name : " + name);
}
const arr = [1, 2, 3]
// 通过调用iterator,拿到迭代器对象
const iterator = arr[Symbol.iterator]()
// 对迭代器对象执行next,就能逐个访问集合的成员
iterator.next()
iterator.next()
iterator.next()
体现了单一职责原则,使用者和目标对象分开,不需要了解目标对象的长度和数据结构,只管遍历或者迭代使用数据