在《说说那些我们在项目中无意识应用的设计模式》一文中,介绍了我在项目已经应用过的四种前端设计模式。今天,将介绍三种虽未在项目中应用过但在合适场景中值得去应用的设计模式。
一、装饰器模式
- 模式理解
装饰器模式涉及被装饰对象(类、函数、组件)、装饰器、装饰后的对象三个主体。装饰器在不修改被装饰对象的情况下,扩展对象的行为和功能,装饰后的对象支持被装饰对象的所有属性和方法。而且,通过组合不同功能的装饰器,可以动态产生新对象,是实现功能扩展的一种有效模式。
- 前端实现
假设我们现在要做一个购物车功能,其中一个需求是要计算购物车里商品的总价,我们可以写下如下函数。
js
/**
* 被装饰函数 shoppingCart
* @param productList 购物车商品对象集合
*/
const shoppingCart = (productList)=> {
return {
getTotalPrice: (productList)=> {
return productList.reduce((total, item) => total + item.price, 0);
}
}
}
但是商品总价的计算并非如此简单,通常会涉及到折扣、优惠券等。如果把这些逻辑都加到shoppingCart函数中,该函数将会变得非常复杂。此时,就可以利用装饰器模式了。
按照装饰器模式的思想,我们分别实现了折扣装饰器discountDecorator和优惠券装饰器couponDecorator。
js
/**
* 折扣装饰器
* @param shoppingCart 被装饰函数
* @param discount 折扣
*/
const discountDecorator = funciton(shoppingCart,discount) {
return {
getTotalPrice: ()=> {
const totalPrice = shoppingCart.getTotalPrice();
return totalPrice * discount
}
}
}
/**
* 优惠券装饰器
* @param shoppingCart 被装饰函数
* @param coupon 优惠券
*/
const couponDecorator = funciton(shoppingCart,coupon) {
return {
getTotalPrice: ()=> {
const totalPrice = shoppingCart.getTotalPrice();
return totalPrice - coupon
}
}
}
我们可以根据用户下单实际情况组合多种不同的装饰器。
js
const productList = [{name: '玩具1',price: 100},{name: '玩具2',price: 200},]
const shoppingCart = shoppingCart(productList)
const discountCart = discountDecorator(shoppingCart, 0.8)
const couponCart = couponDecorator(shoppingCart, 10)
const total = couponCart.getTotalPrice()
可见,通过装饰器模式,我们可以动态地给购物车对象添加多个不同的装饰器,每个装饰器都可以添加不同的行为或功能,但是我们没有修改购物车对象的任何代码。通过这种方式,我们可以灵活地扩展和定制对象的功能,实现更复杂的功能组合。
- 应用场景
1.需要动态地为对象添加功能
2.有多个独立的扩展功能
3.避免修改已有代码,如在第三方库中添加自己的功能,而不影响库的原始代码。
- 模式优缺点
1.优点
1)装饰器模式可以动态地为对象添加新的行为,而无需修改原有的代码
2)装饰器模式遵循开闭原则,即对扩展开放、对修改关闭
3)装饰器模式可以将多个装饰器串联起来,从而以一种更灵活的方式组合对象的行为
2.缺点
1)如果过度使用装饰器模式,会导致程序变得复杂和难以理解。因此,需要在设计时慎重考虑是否使用该模式。
2)装饰器模式增加了许多小型的对象,这些对象可能会增加内存消耗和性能损失。
二、代理模式
- 模式理解
代理模式允许通过引入一个代理对象来控制对另一个对象的访问。代理对象充当了客户端和真实对象之间的中介,通过代理对象可以访问真实对象,并在访问前后进行一些额外的处理。
代理对象可预先处理请求,再决定是否转交给本体;代理和本体对外显示接口保持一致性;代理对象仅对本体做一次包装。
代理模式主要目的是控制访问,而非加强功能。这是它跟装饰器模式最大的不同。
- 前端实现
假设现在需要对一个老函数加一些后置逻辑,利用代理模式思想,可写下如下代码:
js
const formatMoney = function(money) {
// 老功能代码
};
const _formatMoney = formatMoney;
formatMoney = function(money) {
_formatMoney();
//新功能代码
}
- 应用场景
1.缓存代理:在访问实际对象之前,代理对象先检查缓存中是否存在结果,并返回缓存的结果,从而减少重复计算或请求。
2.虚拟代理:延迟加载大型资源,例如在需要时才加载并显示图片。
3.安全代理:控制对实际对象的访问权限,例如检查用户权限或身份验证。
- 日志代理:记录实际对象的方法调用日志,用于调试或监控。
- 模式优缺点
1.优点
1)函数功能复杂度降低,符合 "单一职责原则"
2)可额外添加扩展功能,而不修改本体对象,符合 "开发-封闭原则"
3)可拦截和监听外部对本体对象的访问
2.缺点:
1)额外代理对象的创建,增加部分内存开销
2)处理请求速度可能有差别,非直接访问存在开销,但 "虚拟代理" 及 "缓存代理" 均能提升性能
三、 适配器模式
- 模式理解
适配器模式通常包含目标接口、适配器、被适配者三个角色。其中,适配器是一个具体的函数、类或对象,它实现了目标接口,解决了被适配者接口不兼容的问题,使得不同接口的对象能够协同工作。
- 前端实现
假设这样一个场景,后端接口中所有的时间字段返回格式均为"YYYY-MM-DD hh:mm:ss",但是产品要求在页面上显示的格式为"YYYY/MM/DD hh:mm"。
一般思路是在每个需要显示时间的地方调用一个时间格式转换方法,比较麻烦,但如果应用适配器设计模式,封装一个timeAdaptor适配器,在获取后端数据后就直接调用timeAdaptor适配器进行转换,就方便多了,具体代码如下:
js
import moment from 'moment'
function timeAdaptor(data) {
//将接口中的 创建时间和更新时间 格式化成我们想要的格式
['createTime', 'updateTieme'].forEach(key => {
if (data[key]) {
data[key] = moment(data[key]).format('YYYY-MM-DD HH:mm');
}
})
return data;
}
- 应用场景
1.旧接口适配新接口
2.多个类的接口统一
3.封装有缺陷的接口
- 模式优缺点
1.优点
1)可以让原本不兼容的接口协同工作,提高代码的复用性和灵活性。
2)可以将适配器类和目标类解耦,使得适配器类和目标类可以独立变化
2.缺点
1)适配器模式需要增加一个额外的适配器类,增加了代码的量
2)如果设计不当,可能会导致适配器类的滥用,增加代码的混乱程度
在其他的一些博文中,总是用类的写法去举例。但是对于我或大多数前端来说,基本没有在实际的前端项目中用到过类的写法。所以本文中设计模式都是基于js函数实现的,有不足之处,欢迎在评论区指出。