【UniApp领域驱动开发】工厂模式 + 自动扫描,彻底告别购物车 if-else 地狱

6 阅读8分钟

适配场景:UniApp 商城 / 点餐系统 | 领域驱动设计 | Vue2/Vue3+Vite/Webpack

核心技术:抽象工厂 + 策略模式 + 装饰器 + 自动化扫描注册


🔖 目录

  1. 引言|为什么要重构购物车?
  2. 架构设计图|一眼看懂核心流程
  3. 背景与痛点|传统写法的致命问题
  4. 设计目标|DDD架构改造标准
  5. 核心代码实现|全量可复制
  6. 领域模型重构|纯净DDD落地
  7. 实战使用示例|开箱即用
  8. 方案优势总结|工业级架构价值
  9. 常见问题FAQ|避坑指南

1. 引言|为什么要重构购物车?

在 UniApp 多端商城、外卖点餐类项目开发中,购物车是承载核心业务的领域模型。随着业务迭代,商品类型越来越丰富:普通单品、特价商品、规格商品(辣度/甜度/尺寸)、组合套餐、预售商品等。

传统开发模式下,购物车添加商品充斥着大量 if-else/switch 硬编码,不仅代码臃肿、难以维护,更违反开闭原则单一职责,每次新增商品类型都要改动购物车核心代码,极易引发线上故障。

本篇基于领域驱动设计 DDD )思想,结合抽象工厂+策略模式+装饰器+自动化扫描,打造一套可无限扩展、零耦合、自动注册的购物车架构,真正实现新增商品=新建文件,无需改动一行旧代码


2. 架构设计图|一眼看懂核心流程

核心流程泳道图

graph TD
    A[业务层:ICart购物车] -->|调用create| B[CartItemFactory工厂]
    B -->|遍历规则| C[策略匹配:按优先级筛选]
    C -->|匹配成功| D[自动创建对应子类:ICartItem/SkuCartItem...]
    C -->|无匹配| E[抛出异常提示]
    F[子类装饰器@Register] -->|自动注册| G[工厂规则队列]
    H[自动化扫描] -->|加载文件| F
    style A fill:#f9f,stroke:#333
    style B fill:#9ff,stroke:#333
    style G fill:#ff9,stroke:#333

UML类图(核心模块)

classDiagram
    class IComponent{
        <<基础组件>>
    }
    class IFactory{
        #_rules: FactoryRule[]
        +register()
        +create()
    }
    class CartItemFactory{
        <<单例工厂>>
    }
    class ICart{
        #_cartItemMap
        +addGoodsItem()
        +removeGoodsItem()
    }
    class ICartItem{
        #_price
        #_num
        +match()
        +getTotalPrice()
    }
    class SkuCartItem{
        #_sku
        +match()
    }
    IComponent <|-- IFactory
    IFactory <|-- CartItemFactory
    IComponent <|-- ICart
    IComponent <|-- ICartItem
    ICartItem <|-- SkuCartItem
    ICart --> CartItemFactory: 依赖

3. 背景与痛点|传统写法的致命问题

业务场景

购物车需支持多种商品类型,不同商品的价格计算、匹配规则、UI逻辑完全不同,规格商品还需携带额外参数(如辣度、配料)。

传统坏代码示例

// 坏味道:大量if-else,违反开闭原则
addGoodsItem(goodsItem: any, num: number = 1, options?: any): void {
  let cartItem;
  // 判定规格商品
  if (options) {
    cartItem = new SkuCartItem(goodsItem, options);
  } 
  // 判定组合商品
  else if (goodsItem.type === 'combo') {
    cartItem = new ComboCartItem(goodsItem, options);
  } 
  // 判定特价商品
  else if (goodsItem.isSpecial) {
    cartItem = new SpecialCartItem(goodsItem, options);
  } 
  // 默认普通商品
  else {
    cartItem = new ICartItem(goodsItem, options);
  }
}

核心痛点

  1. 硬编码严重,新增商品必须修改购物车源码
  2. 逻辑嵌套复杂,排查bug成本极高
  3. 违背DDD理念,领域模型不纯净
  4. 扩展性极差,多人协作易冲突
  5. 无统一规范,代码难以维护

4. 设计目标|DDD架构改造标准

✅ 完全解耦:购物车不依赖任何具体CartItem子类

✅ 无限扩展:新增商品仅需新建文件+装饰器

✅ 无if-else:策略自动匹配,优先级可控

✅ 自动注册:Vite/Webpack自动扫描,零手动配置

✅ 运行时动态:支持热插拔、动态替换

✅ 遵循开闭原则:对扩展开放,对修改关闭

✅ 跨平台兼容:UniApp全端、Vue2/Vue3通用


5. 核心代码实现|全量可复制

5.1 抽象工厂基类 IFactory

import { IComponent } from "@/th4/mall/core/IComponent";

/**
 * 工厂规则类型定义
 */
export interface FactoryRule<T> {
  rule: (data: any, options?: any) => boolean;
  constructor: new (data: any, options?: any) => T;
  order: number;
}

/**
 * 通用抽象工厂基类
 * 支持策略注册、优先级排序、自动去重
 */
export class IFactory<T extends mall.Component> extends IComponent implements mall.Factory<T> {
  protected _rules: FactoryRule<T>[] = [];

  // 内置日志,便于调试
  protected _logger = {
    info: console.info,
    debug: console.debug,
    warn: console.warn,
    error: console.error,
  };

  constructor() {
    super({});
    this._rules = [];
  }

  /**
   * 注册子类构造器+匹配规则+优先级
   * @param ctor 子类构造函数
   * @param rule 匹配规则函数
   * @param order 优先级,数字越小越先匹配
   */
  register(
    ctor: new (data: any, options?: any) => T,
    rule: (data: any, options?: any) => boolean,
    order: number = 999
  ): void {
    // 自动去重,避免重复注册
    const exist = this._rules.some((x) => x.constructor === ctor);
    if (exist) return;

    this._rules.push({ rule, constructor: ctor, order });
    // 按优先级升序排序
    this._rules.sort((a, b) => a.order - b.order);
    this._logger.debug(`[Factory] 注册成功: ${ctor.name}`);
  }

  /**
   * 统一创建入口,自动匹配子类
   */
  create(data: any, options?: any): T {
    for (const rule of this._rules) {
      if (rule.rule(data, options)) {
        return new rule.constructor(data, options);
      }
    }
    throw new Error(`[Factory] 无匹配的CartItem类型,请检查注册规则`);
  }
}

5.2 通用注册装饰器 Register

/**
 * 通用自动注册装饰器
 * 修饰子类,自动将规则注册到对应工厂
 * @param factory 目标工厂实例
 * @param rule 匹配规则
 * @param order 优先级
 */
export const Register = <T>(
  factory: mall.Factory<T>,
  rule?: (data: any, options?: any) => boolean,
  order?: number
) => {
  return <C extends new (...args: any[]) => T>(constructor: C) => {
    factory.register(constructor, rule || (() => true), order);
  };
}

5.3 购物车商品工厂(单例)

import { IFactory } from "@/th4/mall/core/IFactory";

/**
 * 购物车商品专属工厂
 * 继承通用抽象工厂,无额外逻辑,职责单一
 */
class CartItemFactory extends IFactory<mall.CartItem> {}

// 全局单例,保证唯一注册入口
export const cartItemFactory = new CartItemFactory();

5.4 自动化扫描启动器(全平台适配)

5.4.1 Vite / UniApp Vue3 版本

// 文件路径:src/th4/mall/starter.ts
/**
 * Vite自动扫描:匹配所有CartItem结尾的ts文件
 * 自动加载并触发@Register装饰器
 */
(() => {
  try {
    const modules = import.meta.glob("./cart/**/*CartItem.ts", { eager: true });
    console.log(`✅ 商城购物车自动扫描完成,共加载 ${Object.keys(modules).length} 个组件`);
  } catch (err) {
    console.error("❌ 购物车自动扫描失败", err);
  }
})();

5.4.2 Webpack / Vue CLI / UniApp Vue2 版本

// 文件路径:src/th4/mall/starter.ts
/**
 * Webpack自动扫描:require.context实现
 */
(() => {
  try {
    const ctx = require.context("./cart/", true, /CartItem.ts$/);
    ctx.keys().forEach(ctx);
    console.log(`✅ 商城购物车自动扫描完成,共加载 ${ctx.keys().length} 个组件`);
  } catch (err) {
    console.error("❌ 购物车自动扫描失败", err);
  }
})();

入口引入(main.ts)

// 项目启动时自动执行扫描
import "@/th4/mall/starter";

6. 领域模型重构|纯净DDD落地

6.1 购物车领域模型 ICart

import { IComponent } from "@/th4/mall/core/IComponent";
import { cartItemFactory } from "@/th4/mall/cart/factory/CartItemFactory";

/**
 * 购物车事件常量
 */
export const CartEvent = {
  CalculatePrice: "CalculatePrice",
};

/**
 * 购物车领域核心类
 * 无任何具体子类依赖,完全解耦
 */
export class ICart extends IComponent implements mall.Cart {
  protected _cartItemMap: Record<string, mall.CartItem> = {};
  protected _totalPrice: number = 0;

  get totalPrice(): number {
    return this._totalPrice;
  }

  /**
   * 添加商品:统一入口,无if-else
   */
  addGoodsItem(goodsItem: any, num: number = 1, options?: any): void {
    let cartItem = this.loadCartItem(goodsItem, options);

    if (!cartItem) {
      // 工厂自动创建对应子类,屏蔽创建细节
      cartItem = cartItemFactory.create(goodsItem, options);
      this._cartItemMap[cartItem.id] = cartItem;
    }

    cartItem.plus(num);
    this._logger.debug("购物车添购商品", goodsItem, cartItem);
    // 重新计算总价
    this._calculatePrice();
  }

  /**
   * 移除商品
   */
  removeGoodsItem(goodsItem: any, num: number = 1, options?: any): void {
    const cartItem = this.loadCartItem(goodsItem, options);
    if (!cartItem) return;

    cartItem.minus(num);
    if (cartItem.num <= 0) {
      delete this._cartItemMap[cartItem.id];
    }
    this._logger.debug("购物车减购商品", goodsItem, cartItem);
    this._calculatePrice();
  }

  /**
   * 加载已存在的购物车商品
   */
  loadCartItem(goodsItem: any, options?: any): mall.CartItem | null {
    return Object.values(this._cartItemMap).find((item) =>
      item.match(goodsItem, options)
    ) || null;
  }

  /**
   * 计算总价并派发事件
   */
  protected _calculatePrice(): void {
    this._totalPrice = Object.values(this._cartItemMap).reduce(
      (total, item) => total + item.getTotalPrice(),
      0
    );
    this.emit(CartEvent.CalculatePrice, this._totalPrice, this);
  }

  // 省略:清空购物车、获取数量、获取列表等方法
}

6.2 基础购物车商品 ICartItem

import { Register } from "@/th4/mall/core/Register";
import { cartItemFactory } from "./factory/CartItemFactory";

/**
 * 默认购物车商品:兜底策略
 * 优先级999,最后匹配
 */
@Register<mall.CartItem>(cartItemFactory, () => true, 999)
export class ICartItem extends IComponent implements mall.CartItem {
  protected _price: number;
  protected _num: number;

  constructor(data: any, options?: any) {
    super(data, options);
    this._price = data.price || 0;
    this._num = 0;
  }

  get price(): number {
    return this._price;
  }

  get num(): number {
    return this._num;
  }

  /**
   * 计算总价
   */
  getTotalPrice(): number {
    return this._price * this._num;
  }

  /**
   * 分组匹配
   */
  group(goodsItem: any): boolean {
    return this._data.id === goodsItem.id;
  }

  /**
   * 唯一匹配规则
   */
  match(goodsItem: any, options?: any): boolean {
    return this._data.id === goodsItem.id;
  }

  /**
   * 减少数量
   */
  minus(num: number = 1): void {
    this._num = Math.max(0, this._num - num);
  }

  /**
   * 增加数量
   */
  plus(num: number = 1): void {
    this._num += num;
  }

  // 折扣相关:预留扩展
  unUseDiscount(discount?: any): void {}
  useDiscount(discount: any): void {}
}

6.3 规格商品 SkuCartItem

import { Register } from "@/th4/mall/core/Register";
import { cartItemFactory } from "./factory/CartItemFactory";
import { ICartItem } from "./ICartItem";

/**
 * 规格购物车商品
 * 优先级1,优先匹配(存在options则命中)
 */
@Register<mall.CartItem>(
  cartItemFactory,
  (data: any, options: any) => !!options,
  1
)
export class SkuCartItem extends ICartItem {
  protected _sku: string;

  constructor(data: any, options?: any) {
    super(data, options);
    // 序列化规格参数,用于唯一匹配
    this._sku = JSON.stringify(options || {});
  }

  /**
   * 重写匹配规则:商品ID+规格参数双重匹配
   */
  match(goodsItem: any, options?: any): boolean {
    const baseMatch = super.match(goodsItem, options);
    const skuMatch = options && JSON.stringify(options) === this._sku;
    return baseMatch && skuMatch;
  }
}

7. 实战使用示例|开箱即用

// 初始化购物车
const cart = new ICart();

// 1. 添加普通商品(自动匹配 ICartItem)
cart.addGoodsItem({ id: 1001, price: 18 });

// 2. 添加规格商品(自动匹配 SkuCartItem)
cart.addGoodsItem(
  { id: 1002, price: 22 },
  1,
  { 辣度: "中辣", 甜度: "七分糖", 配料: "加珍珠" }
);

// 3. 获取购物车商品列表
const cartItems = cart.getCartItems();
console.log("购物车列表", cartItems);

// 4. 获取总价
console.log("购物车总价", cart.totalPrice);

8. 方案优势总结|工业级架构价值

  1. 彻底 解耦:购物车领域完全独立,不依赖任何子类
  2. 极致扩展:新增商品仅需2步(新建类+加装饰器),零旧代码改动
  3. 代码整洁:彻底消灭if-else,可读性、可维护性拉满
  4. 自动运维:扫描注册全自动化,无需手动维护导入
  5. 优先级可控:精确规则优先,兜底规则保障,适配复杂场景
  6. DDD 规范:领域边界清晰,职责单一,符合大型项目架构标准
  7. 全端兼容:UniApp、Vue2/Vue3、Vite/Webpack无缝适配

9. 常见问题FAQ|避坑指南

❓ 1. 装饰器不生效,子类没有自动注册?

  1. 检查 tsconfig.json 是否开启:
{
  "experimentalDecorators": true,
  "emitDecoratorMetadata": true
}
  1. 确认 starter.ts 已在 main.ts 引入
  2. 检查文件命名是否符合扫描规则(*CartItem.ts

❓ 2. 多个子类规则冲突,匹配错误?

调整 @Register 装饰器的 order 参数,数字越小优先级越高,精确匹配规则设小值,兜底规则设999。

❓ 3. Webpack环境报错:require.context未定义?

  1. 确认是Vue CLI/Webpack工程,不要混用Vite语法
  2. 检查扫描路径是否正确,建议使用相对路径
  3. 重启项目重新编译

❓ 4. 新增子类后,控制台提示未找到匹配类型?

  1. 检查子类文件是否放在 cart/** 目录下
  2. 确认装饰器参数正确,绑定了 cartItemFactory
  3. 重启项目,让自动扫描重新加载文件

❓ 5. 如何实现动态注销/替换子类?

在 IFactory 中新增 unregister 方法,通过构造函数移除对应规则即可,支持运行时动态调整。

❓ 6. 这套架构能扩展到其他模块吗?

完全可以,商品、优惠券、订单、营销活动等模块,均可复用 IFactory 和 @Register,只需新建对应工厂和扫描规则即可。


📢 文末互动 + 引流

本文已收录于个人专栏  《UniApp实战架构进阶》 ,持续输出UniApp/DDD/前端架构干货,欢迎关注~

✅ 觉得有用记得点赞、收藏、关注三连,防止迷路!

💬 评论区留下你的实战场景/遇到的问题,看到都会一一回复