总结一下在日常开发中比较常用到的设计模式,从使用场景的来体验其作用
意义
软件越来越复制,实现方式千万种。业务可以实现但是如果完全不考虑可维护性、可扩展性。那么以后的版本就真的要多痛苦就有多痛苦。但我们可以尽量不变与变的功能代码分离,确保灵活性的同时不变的部分又可以稳定,于是我们部分不变的代码进行封装,而设计模式就是为了帮助我们写出这样的封装。
策略模式
- 策略模式可看作为if/else判断的另一种表现形式;
- 将一个个算法|逻辑封装起来,提高代码复用率,减少代码冗余;
- 减少了代码量以及代码维护成本。
使用场景
假设有一活动需求
- 当类型为“预售”时,满 100 - 10,不满 100 打 8 折
- 当类型为“大促”时,满 100 - 20,不满 100 打 7 折
- 当类型为“返场”时,满 200 - 40,不叠加
- 当类型为“尝鲜”时,直接打 4 折
常规写法
function answerPrice(tag, originPrice) {
// 处理预热
if (tag === 'pre') {
if (originPrice >= 100) {
return originPrice - 10;
}
return originPrice * 0.8;
}
// 处理大促
if (tag === 'onSale') {
if (originPrice >= 100) {
return originPrice - 20;
}
return originPrice * 0.7;
}
// 处理返场
if (tag === 'back') {
if (originPrice >= 200) {
return originPrice - 40;
}
return originPrice;
}
// 处理尝鲜
if (tag === 'fresh') {
return originPrice * 0.4;
}
}
违背了“单一功能”原则,万一其中一行代码出了 Bug,那么整个询价逻辑都会崩坏;与此同时出了 Bug 你很难定位到底是哪个代码块坏了事;再比如说单个能力很难被抽离复用
违背了“开放封闭”原则,假设产品要要他加一个满 100 - 50 的“新人价”怎么办?他只能继续 if-else。
策略模式写法
function prePrice(originPrice) {
if (originPrice >= 100) {
return originPrice - 10;
}
return originPrice * 0.8;
}
// 处理大促价
function onSalePrice(originPrice) {
if (originPrice >= 100) {
return originPrice - 20;
}
return originPrice * 0.7;
}
// 处理返场价
function backPrice(originPrice) {
if (originPrice >= 200) {
return originPrice - 40;
}
return originPrice;
}
// 处理尝鲜价
function freshPrice(originPrice) {
return originPrice * 0.4;
}
function answerPrice(tag, originPrice) {
// 处理预热价
if (tag === 'pre') {
return prePrice(originPrice);
}
// 处理大促价
if (tag === 'onSale') {
return onSalePrice(originPrice);
}
// 处理返场价
if (tag === 'back') {
return backPrice(originPrice);
}
// 处理尝鲜价
if (tag === 'fresh') {
return freshPrice(originPrice);
}
}
// 定义一个询价处理器对象
const priceProcessor = {
pre(originPrice) {
if (originPrice >= 100) {
return originPrice - 10;
}
return originPrice * 0.8;
},
onSale(originPrice) {
if (originPrice >= 100) {
return originPrice - 20;
}
return originPrice * 0.7;
},
back(originPrice) {
if (originPrice >= 200) {
return originPrice - 40;
}
return originPrice;
},
fresh(originPrice) {
return originPrice * 0.4;
},
};
// 询价函数
function askPrice1(tag, originPrice) {
return priceProcessor[tag](originPrice);
}
// 加入要添加一个新人价
priceProcessor.newUser = function(originPrice) {
if (originPrice >= 100) {
return originPrice - 50;
}
return originPrice;
};
- 获得更好的扩展性
- 减少if/else 或 Switch的使用
单例模式
使用场景
实现一个全局的模态框
常规写法
一般情况下,当我们创建了一个类(本质是构造函数)后,可以通过new关键字调用构造函数进而生成任意多的实例对象
class Bigfish {
say() {
console.log('hi! Bigfish');
}
}
const c1 = new Bigfish();
const c2 = new Bigfish();
这里先 new 了一个 c1,又 new 了一个 c2,很明显 c1 和 c2 之间没有任何瓜葛,两者是相互独立的对象,各占一块内存空间。而模态框全局只能出现一个,常规写法无论是性能还是对于两个实例的维护都不好。那么我们可以使用单例模式只维护一个实例,不管我们尝试去创建多少次,它都只给你返回第一次所创建的那唯一的一个实例。
单例模式写法
class Bigfish {
say() {
console.log('hi! Bigfish')
}
static getInstance() {
// 判断是否已经new过1个实例
if (!Bigfish.instance) {
// 若这个唯一的实例不存在,那么先创建它
Bigfish.instance = new Bigfish()
}
// 如果这个唯一的实例已经存在,则直接返回
return Bigfish.instance
}
}
const c1 = Bigfish.getInstance() // init
const c2 = Bigfish.getInstance()
这里 c1 === c2,于是模态框的完整实现是
// 核心逻辑,这里采用了闭包思路来实现单例模式
const Modal = (function() {
let modal = null;
return function() {
if (!modal) {
modal = document.createElement('div');
modal.innerHTML = '我是一个全局唯一的Modal';
modal.id = 'modal';
modal.style.display = 'none';
document.body.appendChild(modal);
}
return modal;
};
})();
// 点击打开按钮展示模态框
document.getElementById('open').addEventListener('click', function() {
// 未点击则不创建modal实例,避免不必要的内存占用;此处不用 new Modal 的形式调用也可以,和 Storage 同理
const modal = new Modal();
modal.style.display = 'block';
});
// 点击关闭按钮隐藏模态框
document.getElementById('close').addEventListener('click', function() {
const modal = new Modal();
if (modal) {
modal.style.display = 'none';
}
});
装饰器模式
在不改变原对象的基础上,通过对其进行包装拓展,使原有对象可以满足用户的更复杂需求。
(高阶函数)
应用:react中=>高阶组件HOC,redux connect、类组件装饰器
使用场景
临时需求,修改样式,产品要求打开模态框的时候上报埋点
// 将展示Modal的逻辑单独封装
function openModal() {
const modal = new Modal();
modal.style.display = 'block';
}
// 埋点逻辑
function fetchPoint() {
// xxxxx
}
// 新版本功能逻辑整合
function changeButtonStatus() {
openModal();
fetchPoint();
}
如此一来,我们就实现了“只添加,不修改”的装饰器模式。相当于给手机套上一个手机壳。
适配器模式
适配器模式通过把一个类的接口变换成客户端所期待的另一种接口,可以帮我们解决不兼容的问题。生活中比如耳机接口,充电宝的兼容
使用场景
ajax请求
常规写法
axios.get(url地址, 入参).then((data) => {...})
// 发送get请求
Ajax('get', url地址, post入参, function (data) {
// 成功的回调逻辑
}, function (error) {
// 失败的回调逻辑
})
适配器写法
// Ajax适配器函数,入参与旧接口保持一致
async function AjaxAdapter(type, url, data, success, failed) {
const type = type.toUpperCase()
let result
try {
// 实际的请求全部由新接口发起
if (type === 'GET') {
result = await HttpUtils.get(url) || {}
} else if (type === 'POST') {
result = await HttpUtils.post(url, data) || {}
}
// 假设请求成功对应的状态码是1
result.statusCode === 1 && success ? success(result) : failed(result.statusCode)
} catch (error) {
// 捕捉网络错误
if (failed) {
failed(error.statusCode);
}
}
}
// 用适配器适配旧的Ajax方法
async function Ajax(type, url, data, success, failed) {
await AjaxAdapter(type, url, data, success, failed)
}
代理模式
在某些情况下,出于种种考虑/限制,一个对象不能直接访问另一个对象,需要一个第三者(代理)牵线搭桥从而间接达到访问目的,这样的模式就是代理模式
使用场景
事件委托/代理
<body>
<div id="father">
<a href="#">1</a>
<a href="#">2</a>
<a href="#">3</a>
<a href="#">4</a>
</div>
<script>
// 获取父元素
// 给父元素安装一次监听函数
father.addEventListener('click', function (e) {
// 识别是否是目标子元素
if (e.target.tagName === 'A') {
// 以下是监听函数的函数体
e.preventDefault()
alert(`我是${e.target.innerText}`)
}
})
</script>
</body>
虚拟代理——图片预加载
为了避免网络不好、或者图片太大时,页面长时间给用户留白的尴尬。常见的操作是先让这个 img 标签展示一个占位图,然后创建一个 Image 实例,让这个 Image 实例的 src 指向真实的目标图片地址、观察该 Image 实例的加载情况 —— 当其对应的真实图片加载完毕后,即已经有了该图片的缓存内容,再将 DOM 上的 img 元素的 src 指向真实的目标图片地址。此时我们直接去取了目标图片的缓存,所以展示速度会非常快
class PreLoadImage {
// 占位图的url地址
static LOADING_URL = 'xxxxxx'
constructor(imgNode) {
// 获取该实例对应的DOM节点
this.imgNode = imgNode;
}
// 该方法用于设置真实的图片地址
setSrc(targetUrl) {
// img节点初始化时展示的是一个占位图
this.imgNode.src = PreLoadImage.LOADING_URL;
// 创建一个帮我们加载图片的Image实例
const image = new Image();
// 监听目标图片加载的情况,完成时再将DOM上的img节点的src属性设置为目标图片的url
image.onload = () => {
this.imgNode.src = targetUrl;
};
// 设置src属性,Image实例开始加载图片
image.src = srcUrl;
}
}
缓存代理——斐波那契数列
一些计算量较大的场景里。在这种场景下,我们需要“用空间换时间”——当我们需要用到某个已经计算过的值的时候,不想再耗时进行二次计算,而是希望 能从内存里去取出现成的计算结果。
// 为求和方法创建代理
const proxyFn = function(fn) {
// 求和结果的缓存池
const resultCache = {};
return function() {
// 将入参转化为一个唯一的入参字符串
const args = Array.prototype.join.call(arguments, ',');
// 检查本次入参是否有对应的计算结果
if (args in resultCache) {
// 如果有,则返回缓存池里现成的结果
return resultCache[args];
}
return resultCache[args] = fn(...arguments);
};
};
保护代理
所谓“保护代理”,就是在访问层面做文章,在 getter 和 setter 函数里去进行校验和拦截,确保一部分变量是安全的。
const protector = new Proxy(obj, {
get: function(obj, key) {
if (baseInfo.indexOf(key) !== -1 && !user.isValidated) {
alert('您还没有完成验证哦');
return;
}
// ...(此处省略其它有的没的各种校验逻辑)
// 此处我们认为只有验证过的用户才可以购买VIP
if (user.isValidated && privateInfo.indexOf(key) && !user.isVIP) {
alert('只有VIP才可以查看该信息哦');
return;
}
},
set: function(obj, key, val) {
// 赋值拦截
if (key === 'msg') {
if (val.value === '') {
alert('sorry,信息为空');
return;
}
// 如果没有拒收,则赋值成功
obj[msg] = val;
}
},
});