设计模式
设计模式的定义:在面向对象软件设计过程中针对特定问题的简洁而优雅的解决方案
当然我们可以用一个通俗的说法:设计模式是解决某个特定场景下对某种问题的解决方案。因此,当我们遇到合适的场景时,我们可能会条件反射一样自然而然想到符合这种场景的设计模式。
比如,当系统中某个接口的结构已经无法满足我们现在的业务需求,但又不能改动这个接口,因为可能原来的系统很多功能都依赖于这个接口,改动接口会牵扯到太多文件。因此应对这种场景,我们可以很快地想到可以用适配器模式来解决这个问题。
开发中,我们或多或少地接触了设计模式,但是很多时候不知道自己使用了哪种设计模式或者说该使用何种设计模式。
其实我们在平时的工作中没有必要特意去用什么样的设计模式,或者你在不经意间就已经用了设计模式当中的一种
1. 单例模式
单例模式的定义:保证一个类仅有一个实例,并提供一个访问它的全局访问点。实现的方法为先判断实例存在与否,如果存在则直接返回,如果不存在就创建了再返回,这就确保了一个类只有一个实例对象。
适用场景:一个单一对象。比如:弹窗,无论点击多少次,弹窗只应该被创建一次。
单例模式优点:减少内存占用,提高效率。
单例模式举例:
eg1:特殊单例举例
Math.random();
Math.random();
console.log(Math == Math); //true
eg2:标准单例模式
//eg2:同时仅能一个用户登录
class CreateUser {
//创建静态方法(共享实例),统一接口
static shareInstance() {
if (!CreateUser.ins) {
CreateUser.ins = new CreateUser('aa')
}
return CreateUser.ins;
}
constructor(name) {
this.name = name;
this.getName();
}
getName() {
return this.name;
}
}
let c1 = CreateUser.shareInstance()
let c2 = CreateUser.shareInstance()
console.log(c1 == c2) //true
//非单例模式 实例方法(d1,d2占有两个内存地址)
let d1 = new CreateUser();
let d2 = new CreateUser();
console.log(d1 == d2); //false
2. 组合模式
组合模式类似于Dom树的生成
- 组合模式在对象间形成树形结构;
- 组合模式中基本对象和组合对象被一致对待;
- 无须关心对象有多少层, 调用时只需在根部进行调用;
想象我们现在手上有个万能遥控器, 当我们回家, 按一下开关, 下列事情将被执行:
- 煮咖啡
- 打开电视、打开音响
- 打开空调、打开电脑
我们把任务划分为 3 类, 效果图如下:
组合模式举例:
命令(任务)类
const MacroCommand = function() {
return {
lists: [],
add: function(task) {
this.lists.push(task)
},
excute: function() { // ①: 组合对象调用这里的 excute,
for (let i = 0; i < this.lists.length; i++) {
this.lists[i].excute()
}
},
}
}
const command1 = MacroCommand() // 命令1
command1.add({
excute: () => console.log('煮咖啡')
})
const command2 = MacroCommand() // 命令2
command2.add({
excute: () => console.log('打开电视')
})
command2.add({
excute: () => console.log('打开音响')
})
const command3 = MacroCommand() //命令3
command3.add({
excute: () => console.log('打开空调')
})
command3.add({
excute: () => console.log('打开电脑')
})
const macroCommand = MacroCommand()//组合对象
macroCommand.add(command1)
macroCommand.add(command2)
macroCommand.add(command3)
macroCommand.excute()
// 煮咖啡
// 打开电视
// 打开音响
// 打开空调
// 打开电脑
可以看出在组合模式中基本对象和组合对象被一致对待, 所以要保证基本对象(叶对象)和组合对象具有一致方法。
3. 观察者模式
Observer模式也叫观察者模式、订阅/发布模式,是由GoF提出的23种软件设计模式的一种。
Observer模式是行为模式之一,它的作用是当一个对象的状态发生变化时,能够自动通知其他关联对象,自动刷新对象状态,或者说执行对应对象的方法。
这种设计模式可以大大降低程序模块之间的耦合度,便于更加灵活的扩展和维护。
观察者模式包含两种角色:
- 观察者(订阅者)
- 被观察者(发布者)
核心思想:观察者只要订阅了被观察者的事件,那么当被观察者的状态改变时,被观察者会主动去通知观察者,而无需关心观察者得到事件后要去做什么,实际程序中可能是执行订阅者的回调函数。
简单的例子:
假设你是一个班长,要去通知班里的某些人一些事情,与其一个一个的手动调用触发的方法(私下里一个一个通知),不如维护一个列表(建一个群),这个列表存有你想要调用的对象方法(想要通知的人);
之后每次通知事件的时候只要循环执行这个列表就好了(群发),而不用关心这个列表里有谁。
javascript实现一个例子:
// 我们向某dom文档订阅了点击事件,当点击发生时,他会依次打印1,2
document.addEventListener('click',function(){
console.log(1)
})
document.addEventListener('click',function(){
console.log(2)
})
场景、当观察的数据对象发生变化时, 自动调用相应函数。比如 vue 的双向绑定(将某一数据与页面元素绑定,不用再操作dom,直接改变对应数据可以改变页面内容);
let obj = {
data: {
list: [] //data属性下的list属性
}
}
//给obj定义一个属性 list,defineProperty可以观察obj对象的list属性的使用。
Object.defineProperty(obj, 'list', {
get() {
console.log('get list');
return this.data.list;
},
set(val) {
console.log('set list');
console.log(val); //[2,3]
this.data['list'] = val;
//修改页面的dom(还有虚拟dom diff算法(若页面改变与原页面有相同的部分,则进行一系列比较,只改变不同的部分)以后会学)
let ul = document.querySelector('ul');
let arr = this.data.list.map(v => `
<li>${v}</li>
`)
ul.innerHTML = arr.join('');
}
})
// 获取了list属性,那么get 方法就会被调用
console.log(obj.list); //[]
obj.list = [2, 3]; //设置list属性的值时会自动执行set方法
console.log(obj.list); //[2,3]
观察者模式错误写法(爆栈):
//前者正确写法将list属性放在obj的data属性下,然后后面用defineProperty观察obj下的list属性来更改obj下的data属性下的list属性。
//错误写法直接对obj中的list属性进行观察,来获取obj中的list属性值,return this.list会导致重复对list属性进行观察,导致爆栈(爆栈:程序执行过程中由于各个流程间反复调用或无限循环地相互调用或调用时占用了太多资源,导致栈的空间不够使用了,其后引发的程序无法正常执行完毕的现象)。
let obj = {
list: []
}
//爆栈
Object.defineProperty(obj, 'list', {
get() {
return this.list; //导致重复观察obj的list属性。
}
})
obj.list //获取list属性,get方法被调用。
解决方法2:(前者将list属性放在data属性下是一种解决方法。解决方法二,监听属性和操作属性名字不同。)
let obj = {
_list: []
}
//爆栈
Object.defineProperty(obj, 'list', {
get() {
return this._list;
}
})
console.log(obj.list)//[]
4. 工厂模式
工厂:可以批量生产产品的地方
工厂模式类似于现实生活中的工厂可以产生大量相似的商品,去做同样的事情,实现同样的效果;这时候需要使用工厂模式。
js中构造函数 都是工厂
工厂模式举例:
// 文本工厂
class Text {
constructor(text) {
this.text = text
}
insert(where) {
const txt = document.createTextNode(this.text)
where.appendChild(txt)
}
}
let t=new Text('sjfdlkasldfaf');
t.insert(document.body);//结果:打印文本sjfdlkasldfaf
//链接工厂
class Link {
constructor(url) {
this.url = url
}
insert(where,text) {
const link = document.createElement('a')
link.href = this.url
link.appendChild(document.createTextNode(text))
where.appendChild(link)
}
}
let res=new Link('http://baidu.com');
res.insert(document.body,'baidu');//结果:页面有名字为baidu的百度链接,点击可以跳转到百度。
5. 抽象工厂模式
抽象工厂模式:流程==》 先设计一个抽象类,这个类不能被实例化,
只能用来派生子类,最后通过对子类的扩展实现工厂方法
// DOM工厂
class DomFactory {
constructor() {
}
// 各流水线
insert() {
}
}
// 链接工厂
class Link2 extends DomFactory {
constructor(url) {
this.url = url
}
insert(where) {
const link = document.createElement('a')
link.href = this.url
link.appendChild(document.createTextNode(this.url))
where.appendChild(link)
}
}
6. 策略模式
策略模式的定义:定义一系列的算法,把他们一个个封装起来,并且使他们可以相互替换。
策略模式的目的就是将算法的使用算法的实现分离开来。
一个基于策略模式的程序至少由两部分组成。第一个部分是一组策略类(可变),策略类封装了具体的算法,并负责具体的计算过程。第二个部分是环境类Context(不变),Context接受客户的请求,随后将请求委托给某一个策略类。要做到这一点,说明Context中要维持对某个策略对象的引用。
/*策略类*/
var levelOBJ = {
"A": function (money) {
return money * 4;
},
"B": function (money) {
return money * 3;
},
"C": function (money) {
return money * 2;
}
};
/*环境对象/类*/
var calculateBouns = function (level, money) {
return levelOBJ[level](money);
};
console.log(calculateBouns('A', 10000)); // 40000
7. 代理模式
js 中有事件委托 就是典型的代理模式
情景: 小明追女生 A
- 非代理模式: 小明 =花=> 女生A
- 代理模式: 小明 =花=> 让女生A的好友B帮忙 =花=> 女生A
在开发时候不要先去猜测是否需要使用代理模式, 如果发现直接使用某个对象不方便时, 再来优化不迟。
下面这段代码运用代理模式来实现图片预加载, 可以看到通过代理模式巧妙地将创建图片与预加载逻辑分离, 并且在未来如果不需要预加载, 只要改成请求本体代替请求代理对象就行。(懒加载:需要哪张图片加载哪张图片;预加载:提前加载所有图片)
const MyImage = function(parent) {
const imgNode = document.createElement('img')
parent.appendChild(imgNode)
// 提供一个方法 ,让外部也能修改 图片的src属性
this.setSrc = function(src) {
imgNode.src = src
}
}
// 直接添加图片 ,如果图片比较大,可能加载的比较慢,导致网页上的img一开始显示不出来
let myImage = new MyImage(document.body)
// 写个代理 提供 预加载功能
const ProxyImage = function(myImage) {
// 创建image对象(不是dom对象)
const img = new Image()
img.on
// 监听img对象的 onload 方法
img.onload = function() { // http 图片加载完毕后才会执行
// 模拟网络很慢的情况
setTimeout(() => {
myImage.setSrc(this.src)
}, 2000)
}
this.setSrc = function(src) {
// 加载时候先不加载大图,先加载一张小图(浏览器已经下载过的)
myImage.setSrc('loading.gif') // 本地 loading 图片
img.src = src
}
}
//
let proxyImage = new ProxyImage(myImage)
proxyImage.setSrc('https://www.baidu.com/img/bd_logo1.png?where=super')
8. 适配器模式
适配者模式: 主要用于解决两个接口之间不匹配的问题
// 老接口
const zhejiangCityOld = function () {
return [{
name: 'hangzhou',
id: 11,
},
{
name: 'jinhua',
id: 12
}
]
}
console.log(zhejiangCityOld()) //Array
// 新接口希望是下面形式
// {
// hangzhou: 11,
// jinhua:12
// }
// 这时候就可采用适配者模式
const adaptor = function (oldCity) {
console.log(oldCity) //Array
const obj = {}
for (let city of oldCity) {
obj[city.name] = city.id
}
return obj
}
let oldData = zhejiangCityOld();
//把老数据放在适配器中产生新数据
console.log(adaptor(oldData)) //Object
运行结果如下图: