从"调接口仔"到"业务合伙人":前端的 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 核心不在术语,而是 让业务和代码说同一种语言。
从下次需求评审开始,多问几个"为什么",尝试用领域模型组织核心逻辑。
那一刻,你会发现自己已经不只是"调接口仔",而是能与产品、后端并肩的 业务合伙人。