在React中使用DDD(领域驱动设计)可以显著提升复杂前端应用的可维护性和可扩展性。以下是详细的实践方案:
1. 领域模型设计
实体和值对象
typescript
// 值对象 - 金额
export class Money {
constructor(
public readonly amount: number,
public readonly currency: string = 'CNY'
) {
if (amount < 0) throw new Error('金额不能为负数');
}
add(other: Money): Money {
if (this.currency !== other.currency) {
throw new Error('货币单位不一致');
}
return new Money(this.amount + other.amount, this.currency);
}
multiply(factor: number): Money {
return new Money(this.amount * factor, this.currency);
}
toString(): string {
return `¥${this.amount.toFixed(2)}`;
}
}
// 实体 - 购物车项目
export class CartItem {
constructor(
public readonly productId: string,
public readonly productName: string,
public readonly price: Money,
public readonly quantity: number
) {}
getSubtotal(): Money {
return this.price.multiply(this.quantity);
}
updateQuantity(newQuantity: number): CartItem {
if (newQuantity <= 0) {
throw new Error('数量必须大于0');
}
return new CartItem(
this.productId,
this.productName,
this.price,
newQuantity
);
}
}
// 聚合根 - 购物车
export class ShoppingCart {
private items: CartItem[] = [];
constructor(
public readonly customerId: string,
public readonly createdAt: Date = new Date()
) {}
addItem(item: CartItem): void {
const existingItem = this.items.find(i => i.productId === item.productId);
if (existingItem) {
const updatedItem = existingItem.updateQuantity(
existingItem.quantity + item.quantity
);
this.items = this.items.map(i =>
i.productId === item.productId ? updatedItem : i
);
} else {
this.items.push(item);
}
}
removeItem(productId: string): void {
this.items = this.items.filter(item => item.productId !== productId);
}
updateItemQuantity(productId: string, quantity: number): void {
if (quantity <= 0) {
this.removeItem(productId);
return;
}
this.items = this.items.map(item =>
item.productId === productId
? item.updateQuantity(quantity)
: item
);
}
getTotalAmount(): Money {
return this.items.reduce(
(total, item) => total.add(item.getSubtotal()),
new Money(0)
);
}
getItems(): readonly CartItem[] {
return [...this.items];
}
isEmpty(): boolean {
return this.items.length === 0;
}
}
2. 领域服务
typescript
// 领域服务 - 价格计算服务
export class PricingService {
static calculateDiscount(cart: ShoppingCart, customer: Customer): Money {
const total = cart.getTotalAmount();
let discount = new Money(0);
// 会员折扣
if (customer.isVip()) {
discount = discount.add(total.multiply(0.1));
}
// 满减优惠
if (total.amount >= 1000) {
discount = discount.add(new Money(100));
}
return discount;
}
static calculateFinalAmount(cart: ShoppingCart, customer: Customer): Money {
const total = cart.getTotalAmount();
const discount = this.calculateDiscount(cart, customer);
const finalAmount = new Money(total.amount - discount.amount);
return finalAmount.amount > 0 ? finalAmount : new Money(0);
}
}
// 领域服务 - 表单验证服务
export class OrderValidationService {
static validateOrder(order: Order): ValidationResult {
const errors: string[] = [];
if (!order.shippingAddress) {
errors.push('收货地址不能为空');
}
if (order.items.length === 0) {
errors.push('订单项不能为空');
}
if (order.getTotalAmount().amount <= 0) {
errors.push('订单金额必须大于0');
}
return new ValidationResult(errors.length === 0, errors);
}
}
3. React Hooks封装领域逻辑
typescript
// 自定义Hook - 购物车管理
import { useState, useCallback } from 'react';
import { ShoppingCart, CartItem, Money } from '../domain/models';
export const useShoppingCart = (customerId: string) => {
const [cart] = useState(() => new ShoppingCart(customerId));
const [items, setItems] = useState<CartItem[]>([]);
const addItem = useCallback((item: CartItem) => {
cart.addItem(item);
setItems([...cart.getItems()]);
}, [cart]);
const removeItem = useCallback((productId: string) => {
cart.removeItem(productId);
setItems([...cart.getItems()]);
}, [cart]);
const updateQuantity = useCallback((productId: string, quantity: number) => {
try {
cart.updateItemQuantity(productId, quantity);
setItems([...cart.getItems()]);
} catch (error) {
console.error('更新数量失败:', error);
}
}, [cart]);
const getTotalAmount = useCallback((): Money => {
return cart.getTotalAmount();
}, [cart]);
return {
items,
addItem,
removeItem,
updateQuantity,
getTotalAmount,
isEmpty: cart.isEmpty()
};
};
// 自定义Hook - 订单管理
import { Order, OrderItem } from '../domain/models/Order';
export const useOrder = () => {
const [order, setOrder] = useState<Order | null>(null);
const createOrder = useCallback((customerId: string) => {
const newOrder = new Order(customerId);
setOrder(newOrder);
return newOrder;
}, []);
const addOrderItem = useCallback((item: OrderItem) => {
if (!order) throw new Error('请先创建订单');
try {
order.addItem(item);
setOrder({ ...order }); // 触发重新渲染
} catch (error) {
throw error;
}
}, [order]);
const validateOrder = useCallback(() => {
if (!order) throw new Error('订单不存在');
return OrderValidationService.validateOrder(order);
}, [order]);
return {
order,
createOrder,
addOrderItem,
validateOrder
};
};
4. 领域事件处理
typescript
// 领域事件
export abstract class DomainEvent {
public readonly timestamp: Date = new Date();
public readonly eventId: string = crypto.randomUUID();
}
export class CartItemAddedEvent extends DomainEvent {
constructor(
public readonly customerId: string,
public readonly productId: string,
public readonly quantity: number
) {
super();
}
}
// 事件处理器Hook
export const useDomainEvents = () => {
const [events, setEvents] = useState<DomainEvent[]>([]);
const publishEvent = useCallback((event: DomainEvent) => {
setEvents(prev => [...prev, event]);
// 处理事件
handleEvent(event);
}, []);
const handleEvent = (event: DomainEvent) => {
switch (event.constructor.name) {
case 'CartItemAddedEvent':
// 发送统计埋点
analytics.track('cart_item_added', event);
break;
case 'OrderCreatedEvent':
// 发送通知
notificationService.send('订单创建成功');
break;
}
};
return { publishEvent, events };
};
5. React组件中的DDD应用
typescript
// 购物车组件
import React, { memo } from 'react';
import { useShoppingCart } from '../hooks/useShoppingCart';
import { CartItem } from '../domain/models';
interface ShoppingCartProps {
customerId: string;
}
export const ShoppingCart: React.FC<ShoppingCartProps> = memo(({ customerId }) => {
const { items, removeItem, updateQuantity, getTotalAmount } = useShoppingCart(customerId);
return (
<div className="shopping-cart">
<h2>购物车</h2>
{items.map(item => (
<CartItemRow
key={item.productId}
item={item}
onRemove={removeItem}
onUpdateQuantity={updateQuantity}
/>
))}
<div className="cart-summary">
<div className="total">
总计: {getTotalAmount().toString()}
</div>
<button
disabled={items.length === 0}
onClick={() => {/* 结算逻辑 */}}
>
去结算
</button>
</div>
</div>
);
});
// 购物车项目行组件
interface CartItemRowProps {
item: CartItem;
onRemove: (productId: string) => void;
onUpdateQuantity: (productId: string, quantity: number) => void;
}
const CartItemRow: React.FC<CartItemRowProps> = ({
item,
onRemove,
onUpdateQuantity
}) => {
const [quantity, setQuantity] = useState(item.quantity);
const handleQuantityChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const newQuantity = parseInt(e.target.value) || 1;
setQuantity(newQuantity);
onUpdateQuantity(item.productId, newQuantity);
};
return (
<div className="cart-item-row">
<span className="product-name">{item.productName}</span>
<span className="price">{item.price.toString()}</span>
<input
type="number"
min="1"
value={quantity}
onChange={handleQuantityChange}
/>
<span className="subtotal">{item.getSubtotal().toString()}</span>
<button onClick={() => onRemove(item.productId)}>
删除
</button>
</div>
);
};
6. 状态管理集成
typescript
// 使用Zustand进行状态管理
import { create } from 'zustand';
import { ShoppingCart, CartItem } from '../domain/models';
interface CartState {
cart: ShoppingCart;
addItem: (item: CartItem) => void;
removeItem: (productId: string) => void;
updateQuantity: (productId: string, quantity: number) => void;
getItems: () => CartItem[];
getTotalAmount: () => Money;
}
export const useCartStore = create<CartState>((set, get) => ({
cart: new ShoppingCart('anonymous'),
addItem: (item: CartItem) => set(state => {
const newCart = new ShoppingCart(state.cart.customerId);
// 复制现有项目
state.cart.getItems().forEach(cartItem => newCart.addItem(cartItem));
newCart.addItem(item);
return { cart: newCart };
}),
removeItem: (productId: string) => set(state => {
const newCart = new ShoppingCart(state.cart.customerId);
state.cart.getItems().forEach(cartItem => {
if (cartItem.productId !== productId) {
newCart.addItem(cartItem);
}
});
return { cart: newCart };
}),
updateQuantity: (productId: string, quantity: number) => set(state => {
const newCart = new ShoppingCart(state.cart.customerId);
state.cart.getItems().forEach(cartItem => {
if (cartItem.productId === productId) {
try {
newCart.addItem(cartItem.updateQuantity(quantity));
} catch (error) {
newCart.addItem(cartItem);
}
} else {
newCart.addItem(cartItem);
}
});
return { cart: newCart };
}),
getItems: () => get().cart.getItems(),
getTotalAmount: () => get().cart.getTotalAmount()
}));
7. 表单处理中的DDD
typescript
// 订单表单领域模型
export class OrderForm {
private formData: Partial<OrderFormData> = {};
private errors: Map<string, string> = new Map();
setField<K extends keyof OrderFormData>(
field: K,
value: OrderFormData[K]
): void {
this.formData[field] = value;
this.validateField(field, value);
}
private validateField<K extends keyof OrderFormData>(
field: K,
value: OrderFormData[K]
): void {
const error = this.getFieldError(field, value);
if (error) {
this.errors.set(field, error);
} else {
this.errors.delete(field);
}
}
private getFieldError<K extends keyof OrderFormData>(
field: K,
value: OrderFormData[K]
): string | null {
switch (field) {
case 'recipientName':
if (!value) return '收货人姓名不能为空';
if ((value as string).length > 50) return '姓名长度不能超过50个字符';
break;
case 'phone':
if (!value) return '手机号不能为空';
if (!/^1[3-9]\d{9}$/.test(value as string)) return '手机号格式不正确';
break;
// 其他字段验证...
}
return null;
}
isValid(): boolean {
return this.errors.size === 0;
}
getErrors(): Map<string, string> {
return new Map(this.errors);
}
getData(): OrderFormData {
return { ...this.formData } as OrderFormData;
}
}
// React Hook for form handling
export const useOrderForm = () => {
const [form] = useState(() => new OrderForm());
const [errors, setErrors] = useState<Map<string, string>>(new Map());
const setField = useCallback(<K extends keyof OrderFormData>(
field: K,
value: OrderFormData[K]
) => {
form.setField(field, value);
setErrors(form.getErrors());
}, [form]);
const validate = useCallback((): boolean => {
const isValid = form.isValid();
setErrors(form.getErrors());
return isValid;
}, [form]);
return {
setField,
validate,
errors,
formData: form.getData()
};
};
8. 优势和最佳实践
优势:
- 业务逻辑清晰:核心业务规则集中在领域模型中
- 易于测试:领域模型独立于React组件,便于单元测试
- 可维护性强:业务变化时只需修改领域模型
- 团队协作:前后端可以共享领域概念
最佳实践:
- 保持领域模型纯净:不要在领域模型中引入React特定的依赖
- 合理使用Hook:将领域逻辑封装在自定义Hook中
- 事件驱动:使用领域事件处理副作用
- 渐进式应用:从复杂的业务场景开始应用DDD
这种DDD在React中的应用方式,特别适合电商、金融、企业管理系统等业务逻辑复杂的前端应用。