前端编程之道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 ,添加务必注明掘金

相关推荐
AlienZHOU6 小时前
AI Coding 时代下,我的技术面试实践分享
前端·后端·面试
Captaincc9 小时前
AI用量v0.1.11更新发布 新增 jusage doctor 诊断指令 托盘展示token 和余额 新增 AutoClaw 支持
前端·后端·vibecoding
计算机魔术师10 小时前
德国Wiki被黑后两周,OpenAI终于把模型失控的账本摊开了
前端
kyriewen11 小时前
我让 AI 当面试官面了我一轮:第 3 个追问我就卡住了(附 10 道追问清单)
前端·面试·ai编程
IT_陈寒11 小时前
Python的GIL把我坑惨了,多线程跑得比单线程还慢
前端·人工智能·后端
前端snow12 小时前
ai agent --- 多agent框架之图编排引擎-langgraph
前端
竹林81812 小时前
OmniPic Studio v3.2.1 核心技术架构与全平台发版解析文档
前端·浏览器
JamesZhang8007812 小时前
页面内存只涨不跌? 一次泄漏排查, 牵出 WeakMap 的诞生
前端
Z小明12 小时前
第 6 章 组件进阶
前端·vue.js
江华森12 小时前
HTTP请求的完整过程详解:从DNS解析到TCP挥手的微秒级实战分析
前端