前端编程之道10-2:装饰器模式在前端的应用

装饰器模式,也有人称为装饰者模式,是我们非常常见的一个设计模式。我们先看下装饰器模式的定义

装饰器模式是指允许在不修改现有对象 的情况下,动态地向对象添加额外的行为或功能。装饰器模式通过将对象包装在一个装饰器对象中,从而在运行时动态地添加新的行为或修改现有行为。

装饰器模式核心要求是在不修改现有对象(类、函数、组件等)的情况下,扩展对象的行为或功能,很明显,这符合我们前面讲过的一个重要的软件设计原则:开闭原则(对扩展开发,对修改关闭)。

我们通过三个场景来加深对装饰器模式的理解。

扩展第三方库方法

假设我们现在想要给第三方库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 ,添加务必注明掘金

相关推荐
时清云32 分钟前
【算法】合并两个有序链表
前端·算法·面试
小爱丨同学40 分钟前
宏队列和微队列
前端·javascript
持久的棒棒君1 小时前
ElementUI 2.x 输入框回车后在调用接口进行远程搜索功能
前端·javascript·elementui
2401_857297911 小时前
秋招内推2025-招联金融
java·前端·算法·金融·求职招聘
undefined&&懒洋洋2 小时前
Web和UE5像素流送、通信教程
前端·ue5
大前端爱好者4 小时前
React 19 新特性详解
前端
小程xy4 小时前
react 知识点汇总(非常全面)
前端·javascript·react.js
随云6324 小时前
WebGL编程指南之着色器语言GLSL ES(入门GLSL ES这篇就够了)
前端·webgl
随云6324 小时前
WebGL编程指南之进入三维世界
前端·webgl
寻找09之夏5 小时前
【Vue3实战】:用导航守卫拦截未保存的编辑,提升用户体验
前端·vue.js