React中使用DDD(领域驱动设计)
在React中使用DDD(领域驱动设计)可以显著提升复杂前端应用的可维护性和可扩展性。以下是详细的实践方案:
1. 领域模型设计
实体和值对象
// 值对象 - 金额
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. 领域服务
// 领域服务 - 价格计算服务
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封装领域逻辑
// 自定义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. 领域事件处理
// 领域事件
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应用
// 购物车组件
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 => (<CartItemRowkey={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><inputtype="number"min="1"value={quantity}onChange={handleQuantityChange}/><span className="subtotal">{item.getSubtotal().toString()}</span><button onClick={() => onRemove(item.productId)}>删除</button></div>);
};
6. 状态管理集成
// 使用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
// 订单表单领域模型
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中的应用方式,特别适合电商、金融、企业管理系统等业务逻辑复杂的前端应用。