React中使用DDD(领域驱动设计)

在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. 优势和最佳实践

优势:

  1. 业务逻辑清晰:核心业务规则集中在领域模型中
  2. 易于测试:领域模型独立于React组件,便于单元测试
  3. 可维护性强:业务变化时只需修改领域模型
  4. 团队协作:前后端可以共享领域概念

最佳实践:

  1. 保持领域模型纯净:不要在领域模型中引入React特定的依赖
  2. 合理使用Hook:将领域逻辑封装在自定义Hook中
  3. 事件驱动:使用领域事件处理副作用
  4. 渐进式应用:从复杂的业务场景开始应用DDD

这种DDD在React中的应用方式,特别适合电商、金融、企业管理系统等业务逻辑复杂的前端应用。

相关推荐
excel4 小时前
📖 小说网站的预导航实战:link 预加载 + fetch + 前进后退全支持
前端
学习3人组4 小时前
React 样式隔离核心方法和最佳实践
前端·react.js·前端框架
世伟爱吗喽4 小时前
threejs入门学习日记
前端·javascript·three.js
朝阳5814 小时前
用 Rust + Actix-Web 打造“Hello, WebSocket!”——从握手到回声,只需 50 行代码
前端·websocket·rust
F2E_Zhangmo4 小时前
基于cornerstone3D的dicom影像浏览器 第五章 在Displayer四个角落显示信息
开发语言·前端·javascript
slim~5 小时前
javaweb基础第一天总结(HTML-CSS)
前端·css·html
一支鱼5 小时前
leetcode常用解题方案总结
前端·算法·leetcode
惜.己5 小时前
针对nvm不能导致npm和node生效的解决办法
前端·npm·node.js
乖女子@@@5 小时前
React笔记_组件之间进行数据传递
javascript·笔记·react.js