装饰器模式,也有人称为装饰者模式,是我们非常常见的一个设计模式。我们先看下装饰器模式的定义
装饰器模式是指允许在不修改现有对象 的情况下,动态地向对象添加额外的行为或功能。装饰器模式通过将对象包装在一个装饰器对象中,从而在运行时动态地添加新的行为或修改现有行为。
装饰器模式核心要求是在不修改现有对象(类、函数、组件等)的情况下,扩展对象的行为或功能,很明显,这符合我们前面讲过的一个重要的软件设计原则:开闭原则(对扩展开发,对修改关闭)。
我们通过三个场景来加深对装饰器模式的理解。
扩展第三方库方法
假设我们现在想要给第三方库lodash的方法加一个日志功能,即在某个方法被调用之前,将调用信息存储到日志中。
由于lodash是个第三方库,我们并不能修改其源码,此时,可以创建一个日志装饰器,通过拦截lodash的方法调用实现这个需求。
javascript
function logDecorator(obj, methodName) {
const originalMethod = obj[methodName];
obj[methodName] = function (...args) {
console.log(`${methodName}方法被调用`);
return originalMethod.apply(this, args);
};
}
logDecorator(lodash, 'get');
lodash.get({a: 5}, 'a') //这里会打印 get方法被调用
这种方式在前端开发中经常会用到,比如在我的开源库 vue-office 中就多次使用该方法扩展xSpreadSheet的功能。
javascript
//监听excel底部页签的切换事件
//在切换事件后刷新页面数据
let swapFunc = xs.bottombar.swapFunc;
xs.bottombar.swapFunc = function (index) {
swapFunc.call(xs.bottombar, index);
//刷新页面数据
};
扩展类
上述示例我感觉并不够经典,不能完全反应装饰器模式的定义,因为装饰器模式中要求不能修改现有对象,在上述示例中,虽然没有修改现有对象所属的Class类,但是修改了对象实例的方法。
我们来看一个更经典的示例。
假设我们有一个简单的购物车对象,包含一个items数组来存储购物车中的商品,拥有一个计算购物车中物品价格的方法 getTotalPrice。
javascript
class ShoppingCart {
constructor() {
this.items = [];
}
addItem(item) {
this.items.push(item);
console.log(`Item added: ${item}`);
}
getTotalPrice() {
return this.items.reduce((total, item) => total + item.price, 0);
}
}
当然了,在购物时计算价格并不是这么简单,比如在有商家打折、优惠券、Vip会员折扣、满减等优惠场景叠加时,价格的计算就会很复杂,如果把所有计算逻辑都放到购物车类中,那么ShoppingCart的实现会非常复杂,这时可以把各种优惠方式做成一个个装饰器,通过嵌套装饰器来完成复杂的逻辑计算。
我们先来实现一个折扣装饰器DiscountDecorator,装饰器的构造参数中包含要装饰的对象,即购物车实例cart。
javascript
class DiscountDecorator {
constructor(cart, discount) {
this.cart = cart;
this.discount = discount; // 折扣
}
//实现购物车的方法
addItem(item) {
this.cart.addItem(item);
}
getTotalPrice() {
const totalPrice = this.cart.getTotalPrice();
const discountedPrice = totalPrice * this.discount;
return discountedPrice;
}
}
在折扣装饰器中实现了购物车对象的全部方法,这样就能在任何使用购物车对象的地方,替换成被折扣装饰器装饰后的对象。
现在,我们可以使用装饰器类来装饰原始的购物车对象:
javascript
const cart = new ShoppingCart();
const discountCart = new DiscountDecorator(cart, 0.9);
discountCart.addItem({ name: 'Product 1', price: 10 });
discountCart.addItem({ name: 'Product 2', price: 20 });
console.log(discountCart.getTotalPrice()); //( 10 + 20 )*0.9 = 27
如果还有优惠券功能,我们再实现一个优惠券装饰器 CouponDecorator:
javascript
class CouponDecorator {
constructor(cart, coupon) {
this.cart = cart;
this.coupon = coupon; // 优惠券金额
}
addItem(item) {
this.cart.addItem(item);
}
getTotalPrice() {
const totalPrice = this.cart.getTotalPrice();
const totalPriceWithCoupon = totalPrice - this.coupon;
return totalPriceWithCoupon;
}
}
现在有两个装饰器了,我们可以根据用户下单实际情况组合多种不同的装饰器。
javascript
const cart = new ShoppingCart();
const discountCart = new DiscountDecorator(cart, 0.9);
const couponCart = new CouponDecorator(discountCart, 5);
couponCart.addItem({ name: 'Product 1', price: 10 });
couponCart.addItem({ name: 'Product 2', price: 20 });
console.log(couponCart.getTotalPrice()); // (10 + 20)* 0.9 - 5 = 22
可以看到,通过装饰器模式,我们可以动态地给购物车对象添加多个不同的装饰器,每个装饰器都可以添加不同的行为或功能,但是我们没有修改购物车对象的任何代码。通过这种方式,我们可以灵活地扩展和定制对象的功能,实现更复杂的功能组合。
这里要注意一点,装饰器要实现原有对象的全部方法,确保所有使用被装饰对象的地方,都可以无缝替换为装饰后的对象。
扩展组件
装饰器模式同样适用于组件开发。
以Vue组件开发为例,假设我们想在每个表单的底部添加一个提交按钮,在点击提交按钮之后进行表单验证,并在验证失败时显示错误消息。我们可以使用装饰者模式来实现这个需求。
首先,我们创建一个装饰者组件FormValidatorDecorator.vue,它接受一个原始组件作为插槽,并在原始组件的基础上添加额外的行为:
vue
<template>
<div class="form-validator-decorator">
<slot></slot>
<button @click="handleSubmit">提交</button>
<div v-if="showError" class="error-message">{{ errorMessage }}</div>
</div>
</template>
<script>
export default {
data() {
return {
showError: false,
errorMessage: '',
};
},
methods: {
handleSubmit() {
// 进行表单验证
if (this.validateForm()) {
// 验证通过,执行原始组件的提交逻辑
this.$slots.default[0].submitForm();
} else {
// 验证失败,显示错误消息
this.showError = true;
this.errorMessage = 'Form validation failed';
}
},
validateForm() {
// 进行表单验证的逻辑,返回验证结果
return this.$slots.default[0].validate();
},
},
};
</script>
现在,我们可以使用装饰者组件来装饰原始的表单组件:
vue
<template>
<form-validator-decorator>
<Form></Form>
</form-validator-decorator>
</template>
<script>
import Form from './Form.vue';
import FormValidatorDecorator from './FormValidatorDecorator.vue';
export default {
components: {
Form,
FormValidatorDecorator,
},
};
</script>
这样,我们就成功地给表单组件添加了一个额外的行为,即表单验证功能。在用户点击提交按钮时,会先进行表单验证,如果验证通过,则执行原始组件的提交逻辑;如果验证失败,则显示错误消息。
通过装饰者模式,我们可以动态地给Vue组件添加多个不同的装饰器,每个装饰器都可以添加不同的行为或样式,而不会影响原始组件的代码。
类似的场景很多,比如给每个组件的右上角增加全屏操作按钮、给图片增加预览按钮或者给组件增加拖拽缩放功能、展开收起功能等,都可以通过装饰器模式进行开发。
vue
<template>
<!--全屏装饰器,右上角显示全屏按钮-->
<full-screen-decorator>
<!--拖拽缩放缩放装饰器,让组件可以通过拖拽调节宽高-->
<drag-scale-decorator>
<!--展开收起装饰器,让组件可以收缩成一个小图标-->
<collapse-decorator>
<component/>
</collapse-decorator>
</drag-scale-decorator>
</full-screen-decorator>
</template>
通过组合多种装饰器,可以为组件增加各种不同的功能,试想下,如果不采用装饰器模式,如何实现这个功能呢,是不是要封装大量的组件,比如支持全屏的A组件、支持全屏和缩放的A组件、支持缩放和展开收起的B组件、支持全屏和缩放的B组件...这样势必会造成组件数量爆炸,而通过装饰器模式,我们只需要实现有限的几个装饰器,然后通过各种组合,就可以产生支持各种不同行为的新组件。
总结
- 装饰器模式设计三个主体:被装饰对象(class、函数、或者组件)、装饰器和装饰后的对象
- 装饰器在不修改被装饰对象的情况下,扩展对象的行为和功能,装饰后的对象支持被装饰对象的所有属性和方法,可以平替被装饰对象
- 通过装饰器模式,可以避免出现对象爆炸现象,通过组合不同功能的装饰器,动态产生新对象
- 装饰器模式符合开闭原则,是实现功能扩展的一个有效的模式
如果你读到这里了,觉得搞明白装饰器模式了,麻烦给点个赞,写了很多篇前端编程之道文章了,都没什么回应,不知道是不是应该继续下去~~
Hi,我是前端林叔,正在编写前端编程之道系列,欢迎关注,github.com/501351981/H... 欢迎前端同学微信交流: hit757 ,添加务必注明掘金