从“调接口仔”到“业务合伙人”:前端的 DDD 初体验

从"调接口仔"到"业务合伙人":前端的 DDD 初体验

一、引言:熟悉的场景,共同的痛点

想象一下,是否有这样的日常:

  • 产品经理:"我们要做一个下单功能",然后甩来一张原型图。
  • 后端同学:"接口写好了",结果你一对接,字段名对不上、状态逻辑不清晰。
  • 测试同学:"这里有个 Bug!"结果一查,发现是需求理解不一致......

最后,反复沟通、返工,前端像救火队一样,疲于应付。

这就是传统协作模式下的常态:

  • 沟通成本高
  • 需求边界模糊
  • 前端被动实现
  • 测试沦为"需求侦探"

直到我接触到 领域驱动设计(DDD) ,才发现它能从根源缓解这些痛点。更重要的是,它不是后端专利,前端同样能受益。

二、什么是 DDD?(给前端的极简解释)

DDD 的核心思想只有一句话:

让代码成为业务的精准映射。

它追求的是:代码结构能反映真实的业务规则,而不仅仅是技术实现。

几个关键概念(人话版):

  • 领域(Domain) :软件要解决的问题空间,比如电商、支付、社交。
  • 通用语言(Ubiquitous Language) :团队达成共识的业务术语,比如"订单""优惠券"。
  • 领域模型(Domain Model) :用类/对象实现这些业务概念,不只是数据,还包含规则和行为。

可以把领域模型类比成 地图:它不是现实本身,但能精准表达"去哪里、怎么走"。

一句话总结:DDD 是一套沟通与设计的方法论,最终会在代码里落地

三、订单功能的故事:传统模式 vs DDD 模式

1. 最初需求:一个简单的下单

产品一开始说:

"做一个下单功能,用户点确认就行。"

前端心想:"好嘛,按钮 + 请求接口就能搞定。"

2. 需求膨胀:逻辑开始滚雪球

开发到一半,产品补充:

  • "下单要支持优惠券。"
  • "要校验库存,不然会超卖。"
  • "部分用户是黑名单,不能下单。"
  • "订单金额超过 1000 元要审批。"

业务复杂度直线上升。

3.传统模式:逻辑塞满 Store

typeScript 复制代码
// stores/order.ts
import { defineStore } from 'pinia';

export const useOrderStore = defineStore('order', {
  state: () => ({
    cartItems: [] as { id: string; price: number; quantity: number }[],
    coupon: null as { id: string; discount: number } | null,
    user: { id: 'u1', isBlocked: false, level: 'normal' },
    stockMap: {} as Record<string, number>,
  }),
  actions: {
    async placeOrder() {
      if (this.user.isBlocked) throw new Error('用户被禁止下单');

      for (const item of this.cartItems) {
        if (this.stockMap[item.id] < item.quantity) throw new Error('库存不足');
      }

      let total = this.cartItems.reduce((sum, i) => sum + i.price * i.quantity, 0);
      if (this.coupon) total -= this.coupon.discount;

      if (total > 1000 && this.user.level !== 'vip') {
        throw new Error('金额超过限制,需要审批');
      }

      await fakeApiCreateOrder({ items: this.cartItems, total });
    },
  },
});

问题:

  • 所有规则塞在 Store 里,方法越写越臃肿。
  • 新需求一加 → 全部改方法,Bug 风险高。
  • 测试、后端难以直接理解逻辑。

4.DDD 模式:业务规则内聚

a.领域研讨

团队围在白板前,把"下单"流程拆解:

  • 校验黑名单
  • 校验库存
  • 计算金额 → 应用优惠券
  • 超额需要审批

形成通用语言:订单、优惠券、库存、审批

b.领域模式
typeScript 复制代码
// domains/order/model/Order.ts
type CartItem = { id: string; price: number; quantity: number };
type Coupon = { id: string; discount: number };

export class Order {
  constructor(
    public items: CartItem[],
    public user: { id: string; isBlocked: boolean; level: 'normal' | 'vip' },
    public stockMap: Record<string, number>,
    public coupon?: Coupon
  ) {}

  private validateUser() {
    if (this.user.isBlocked) throw new Error('用户被禁止下单');
  }

  private validateStock() {
    for (const item of this.items) {
      if (this.stockMap[item.id] < item.quantity) {
        throw new Error(`商品 ${item.id} 库存不足`);
      }
    }
  }

  private validateApproval(total: number) {
    if (total > 1000 && this.user.level !== 'vip') {
      throw new Error('金额超过限制,需要审批');
    }
  }

  calculateTotal(): number {
    let total = this.items.reduce((sum, i) => sum + i.price * i.quantity, 0);
    if (this.coupon) total -= this.coupon.discount;
    return total;
  }

  place() {
    this.validateUser();
    this.validateStock();
    const total = this.calculateTotal();
    this.validateApproval(total);
    return { items: this.items, total };
  }
}
c.Store 只管状态
typeScript 复制代码
// stores/order.ts
import { defineStore } from 'pinia';
import { Order } from '@/domains/order/model/Order';

export const useOrderStore = defineStore('order', {
  state: () => ({
    order: null as Order | null,
  }),
  actions: {
    async placeOrder() {
      if (!this.order) throw new Error('没有订单');
      const payload = this.order.place();
      await fakeApiCreateOrder(payload);
    },
  },
});

优点:

  • 业务逻辑集中在模型里,结构清晰。
  • 新增规则只需改模型,不会污染 Store。
  • 测试、后端都能直接理解模型逻辑。

四、前端如何具体实践 DDD?

1. 思维转变

  • 从"调接口"到"管业务"。
  • 不再只是页面工程师,而是业务参与者。

2. 目录结构

bash 复制代码
/domains
  /order
    /model/Order.ts
/application
/infrastructure

按业务领域拆分,而不是按"页面/组件/接口"拆分。

3. Store 写法对比

传统写法 :Store 扛下所有逻辑 → 臃肿难维护。
DDD 写法:Store 只管状态,业务规则放到模型里。

👉 最后效果:Store 变轻,逻辑内聚,协作顺畅

五、组件化 vs DDD:相似却不同的思路

有人会问:

"我平时把逻辑写到组件里,不也能减少代码量吗?和 DDD 有啥区别?"

确实,从表面看都能让页面更简洁,但出发点不同:

  • 组件化:关注 UI 复用 → 解决"代码少写点"。
  • DDD:关注业务逻辑复用 → 解决"需求别乱飞"。

一句金句总结:

组件化是 UI 的复用,DDD 是业务规则的复用。

六、DDD vs MVC:相似的分层,不同的出发点

很多同学可能会说:

"我们团队一直在用 MVC,其实也能实现高复用,和 DDD 有什么区别呢?"

1.MVC 的思路

MVC 的目标是关注点分离:

  • Model:数据存取 + 部分业务逻辑
  • View:负责展示
  • Controller:接收输入、协调流程

在理想状态下,业务逻辑可以放在 Model,Controller 只做路由转发。但在实际前端项目里,Model 往往被弱化成「数据容器」,大量的业务校验、流程判断都堆在 Controller 或 Store 里。

示例(MVC 传统写法,逻辑散落在 Controller):

typeScript 复制代码
// controllers/orderController.ts
export function placeOrder(user, items, coupon, stockMap) {
  if (user.isBlocked) throw new Error('用户被禁止下单');

  for (const item of items) {
    if (stockMap[item.id] < item.quantity) throw new Error('库存不足');
  }

  let total = items.reduce((sum, i) => sum + i.price * i.quantity, 0);
  if (coupon) total -= coupon.discount;

  if (total > 1000 && user.level !== 'vip') {
    throw new Error('金额超过限制,需要审批');
  }

  return { items, total };
}

问题

  • 业务逻辑写在 Controller 里,难复用。
  • 测试、后端要理解时,只能硬读代码,缺乏统一的业务语言。

2.DDD 的思路

DDD 在关注点分离的基础上,更进一步:把业务规则本身当作"核心资产"内聚到领域模型里

示例(DDD 写法,逻辑聚合在领域模型):

typeScript 复制代码
// domains/order/model/Order.ts
export class Order {
  constructor(
    public items: { id: string; price: number; quantity: number }[],
    public user: { id: string; isBlocked: boolean; level: 'normal' | 'vip' },
    public stockMap: Record<string, number>,
    public coupon?: { id: string; discount: number }
  ) {}

  private validateUser() {
    if (this.user.isBlocked) throw new Error('用户被禁止下单');
  }

  private validateStock() {
    for (const item of this.items) {
      if (this.stockMap[item.id] < item.quantity) throw new Error('库存不足');
    }
  }

  private validateApproval(total: number) {
    if (total > 1000 && this.user.level !== 'vip') {
      throw new Error('金额超过限制,需要审批');
    }
  }

  place() {
    this.validateUser();
    this.validateStock();
    let total = this.items.reduce((sum, i) => sum + i.price * i.quantity, 0);
    if (this.coupon) total -= this.coupon.discount;
    this.validateApproval(total);
    return { items: this.items, total };
  }
}

3.总结对比

  • MVC:关注点分离,逻辑分层,但业务语义容易丢失。
  • DDD:不仅分层,更强调业务语言一致性和规则内聚。

一句话总结:

👉 MVC 解决的是"谁负责什么",DDD 解决的是"业务规则如何被表达和演进"。

七、DDD 给前端带来的价值

  • 更少返工:逻辑集中,可复用。
  • 更高价值:前端参与业务设计,不只是搬砖。
  • 更清晰代码:结构贴合业务,易读易测。
  • 更顺畅协作:与后端、测试天然对齐。
  • 更稳架构:扩展新规则更优雅。

八、写在最后

DDD 核心不在术语,而是 让业务和代码说同一种语言

从下次需求评审开始,多问几个"为什么",尝试用领域模型组织核心逻辑。

那一刻,你会发现自己已经不只是"调接口仔",而是能与产品、后端并肩的 业务合伙人

相关推荐
东北南西3 小时前
Web Worker 从原理到实战 —— 把耗时工作搬到后台线程,避免页面卡顿
前端·javascript
Zz_waiting.3 小时前
案例开发 - 日程管理 - 第六期
前端·javascript·vue.js·路由·router
A 风3 小时前
封装日期选择器组件,带有上周,下周按钮
开发语言·javascript·vue.js
机构师4 小时前
<uniapp><指针组件>基于uniapp,编写一个自定义箭头指针组件
javascript·uni-app·vue·html
小桥风满袖4 小时前
极简三分钟ES6 - 模块化
前端·javascript
bestadc5 小时前
LeetCode 几道 Promises 和 Time 的题目
javascript·算法·leetcode
萌萌哒草头将军5 小时前
Node.js v24.8.0 新功能预览!🚀🚀🚀
前端·javascript·node.js
太空游走的鱼5 小时前
Vue3 + Vite + Element Plus web转为 Electron 应用,解决无法登录、隐藏自定义导航栏
javascript·electron·vue3·管理系统·element plsu
GISer_Jing6 小时前
Next系统学习(二)
前端·javascript·node.js