阿里云主机折上折
  • 微信号
您当前的位置:网站首页 > 领域驱动设计初步应用

领域驱动设计初步应用

作者:陈川 阅读数:11611人阅读 分类: Node.js

领域驱动设计初步应用

领域驱动设计(Domain-Driven Design,DDD)是一种软件开发方法,强调通过深入理解业务领域来构建复杂系统。在Koa2框架中应用DDD,可以帮助我们更好地组织代码结构,提高可维护性和扩展性。

为什么要在Koa2中使用DDD

Koa2作为Node.js的轻量级框架,本身不强制任何特定的架构模式。但随着业务复杂度增加,传统的MVC架构可能难以应对:

  1. 业务逻辑分散在控制器和服务层
  2. 领域概念不清晰,代码难以理解
  3. 修改一处可能影响多个不相关功能
// 传统Koa2控制器示例
router.post('/orders', async (ctx) => {
  const { userId, productId } = ctx.request.body;
  
  // 验证用户
  const user = await userService.findById(userId);
  if (!user) throw new Error('用户不存在');
  
  // 验证商品
  const product = await productService.findById(productId);
  if (!product.stock <= 0) throw new Error('库存不足');
  
  // 创建订单
  const order = await orderService.create({
    userId,
    productId,
    status: 'pending'
  });
  
  ctx.body = order;
});

这种写法将业务逻辑完全放在控制器中,随着功能增加会变得难以维护。

DDD核心概念在Koa2中的实现

实体(Entity)

实体是具有唯一标识的领域对象。在订单系统中,Order就是一个典型实体:

// domain/order.js
class Order {
  constructor({ id, userId, productId, status }) {
    this._id = id;
    this._userId = userId;
    this._productId = productId;
    this._status = status;
  }
  
  get id() { return this._id; }
  get status() { return this._status; }
  
  cancel() {
    if (this._status !== 'pending') {
      throw new Error('只有待处理订单可以取消');
    }
    this._status = 'cancelled';
  }
  
  toJSON() {
    return {
      id: this._id,
      userId: this._userId,
      productId: this._productId,
      status: this._status
    };
  }
}

值对象(Value Object)

值对象没有唯一标识,通过属性值定义:

// domain/address.js
class Address {
  constructor({ province, city, district, detail }) {
    this.province = province;
    this.city = city;
    this.district = district;
    this.detail = detail;
  }
  
  equals(other) {
    return this.province === other.province &&
           this.city === other.city &&
           this.district === other.district &&
           this.detail === other.detail;
  }
  
  toString() {
    return `${this.province}${this.city}${this.district}${this.detail}`;
  }
}

聚合根(Aggregate Root)

聚合根是外部访问聚合的入口点:

// domain/order.js
class Order {
  // ...之前的代码
  
  addPayment(payment) {
    if (this._payments.some(p => p.id === payment.id)) {
      throw new Error('支付已存在');
    }
    this._payments.push(payment);
  }
  
  get totalPaid() {
    return this._payments.reduce((sum, p) => sum + p.amount, 0);
  }
}

分层架构实现

在Koa2中实现典型DDD分层:

src/
├── application/    # 应用层
├── domain/         # 领域层
├── infrastructure/ # 基础设施层
└── interfaces/     # 接口层

应用层示例

// application/orderService.js
class OrderService {
  constructor({ orderRepository, userRepository }) {
    this.orderRepository = orderRepository;
    this.userRepository = userRepository;
  }
  
  async createOrder(userId, productId) {
    const user = await this.userRepository.findById(userId);
    if (!user) throw new Error('用户不存在');
    
    const order = new Order({
      userId,
      productId,
      status: 'pending'
    });
    
    await this.orderRepository.save(order);
    return order;
  }
}

基础设施层

// infrastructure/orderRepository.js
class OrderRepository {
  constructor({ db }) {
    this.db = db;
  }
  
  async findById(id) {
    const data = await this.db('orders').where({ id }).first();
    return data ? new Order(data) : null;
  }
  
  async save(order) {
    if (order.id) {
      await this.db('orders')
        .where({ id: order.id })
        .update(order.toJSON());
    } else {
      const [id] = await this.db('orders').insert(order.toJSON());
      order._id = id;
    }
  }
}

Koa2控制器改造

将业务逻辑移到领域层后,控制器变得简洁:

// interfaces/controllers/orderController.js
const router = require('koa-router')();
const OrderService = require('../../application/orderService');

router.post('/orders', async (ctx) => {
  const { userId, productId } = ctx.request.body;
  const order = await OrderService.createOrder(userId, productId);
  ctx.body = order.toJSON();
});

router.post('/orders/:id/cancel', async (ctx) => {
  const order = await OrderService.cancelOrder(ctx.params.id);
  ctx.body = order.toJSON();
});

复杂业务场景处理

处理跨聚合的业务逻辑时,可以使用领域服务:

// domain/services/orderPaymentService.js
class OrderPaymentService {
  constructor({ orderRepository, paymentRepository }) {
    this.orderRepository = orderRepository;
    this.paymentRepository = paymentRepository;
  }
  
  async processPayment(orderId, paymentInfo) {
    const order = await this.orderRepository.findById(orderId);
    if (!order) throw new Error('订单不存在');
    
    const payment = new Payment(paymentInfo);
    order.addPayment(payment);
    
    if (order.totalPaid >= order.totalAmount) {
      order.complete();
    }
    
    await this.orderRepository.save(order);
    await this.paymentRepository.save(payment);
    
    return { order, payment };
  }
}

事件驱动架构集成

在领域模型中引入事件:

// domain/order.js
class Order {
  constructor() {
    this._events = [];
  }
  
  complete() {
    this._status = 'completed';
    this._events.push(new OrderCompletedEvent(this));
  }
  
  get events() {
    return [...this._events];
  }
  
  clearEvents() {
    this._events = [];
  }
}

// application/orderService.js
async function createOrder() {
  // ...之前的代码
  const order = new Order(/* ... */);
  
  await this.orderRepository.save(order);
  
  // 处理领域事件
  order.events.forEach(event => {
    if (event instanceof OrderCompletedEvent) {
      eventBus.emit('order.completed', event.order);
    }
  });
  
  order.clearEvents();
  return order;
}

测试策略调整

DDD使单元测试更聚焦领域逻辑:

// test/domain/order.test.js
describe('Order', () => {
  it('应该允许取消待处理订单', () => {
    const order = new Order({ status: 'pending' });
    order.cancel();
    expect(order.status).toBe('cancelled');
  });
  
  it('应该禁止取消非待处理订单', () => {
    const order = new Order({ status: 'completed' });
    expect(() => order.cancel()).toThrow('只有待处理订单可以取消');
  });
});

// test/application/orderService.test.js
describe('OrderService', () => {
  it('创建订单时应验证用户存在', async () => {
    const mockUserRepo = { findById: jest.fn().mockResolvedValue(null) };
    const service = new OrderService({ userRepository: mockUserRepo });
    
    await expect(service.createOrder('invalid', 'product1'))
      .rejects
      .toThrow('用户不存在');
  });
});

与Koa2中间件集成

将DDD架构与Koa2中间件结合:

// interfaces/middlewares/domainContext.js
async function domainContext(ctx, next) {
  // 初始化领域服务
  ctx.services = {
    orderService: new OrderService({
      orderRepository: new OrderRepository({ db: ctx.db }),
      userRepository: new UserRepository({ db: ctx.db })
    })
  };
  
  await next();
}

// app.js
app.use(async (ctx, next) => {
  ctx.db = getDbConnection(); // 初始化数据库连接
  await next();
});

app.use(domainContext);

性能考量

在Node.js环境下实现DDD需要注意:

  1. 避免在领域对象中创建过多实例
  2. 考虑使用Data Mapper模式减少内存占用
  3. 复杂查询可以直接使用基础设施层
// infrastructure/orderRepository.js
class OrderRepository {
  // ...其他方法
  
  async findRecentOrders(limit = 10) {
    // 直接返回POJO而不是领域对象
    return this.db('orders')
      .orderBy('created_at', 'desc')
      .limit(limit);
  }
}

与现有代码的渐进式迁移

从传统架构迁移到DDD的建议步骤:

  1. 先识别核心领域对象
  2. 将业务逻辑从控制器移到领域类
  3. 逐步引入仓储模式
  4. 最后处理跨聚合逻辑
// 迁移中的过渡代码示例
class LegacyOrderService {
  constructor() {
    // 暂时兼容新旧两种方式
    this.domainService = new OrderService(/* ... */);
  }
  
  async createOrder(userId, productId) {
    // 暂时保留旧逻辑
    if (global.USE_DDD) {
      return this.domainService.createOrder(userId, productId);
    } else {
      // 旧实现
    }
  }
}

常见问题与解决方案

问题1:领域对象与数据库模型如何映射?

解决方案:在仓储层实现映射逻辑:

class OrderRepository {
  async findById(id) {
    const data = await this.db('orders').where({ id }).first();
    if (!data) return null;
    
    // 转换数据库模型到领域对象
    return new Order({
      id: data.id,
      userId: data.user_id, // 字段名转换
      productId: data.product_id,
      status: data.status
    });
  }
}

问题2:如何处理复杂事务?

使用工作单元模式:

class UnitOfWork {
  constructor(db) {
    this.db = db;
    this.repositories = new Map();
  }
  
  getRepository(repoClass) {
    if (!this.repositories.has(repoClass)) {
      this.repositories.set(repoClass, new repoClass(this.db));
    }
    return this.repositories.get(repoClass);
  }
  
  async commit() {
    await this.db.transaction(async trx => {
      for (const repo of this.repositories.values()) {
        if (repo.commit) await repo.commit(trx);
      }
    });
  }
}

// 使用示例
const uow = new UnitOfWork(db);
const orderRepo = uow.getRepository(OrderRepository);
const order = await orderRepo.findById(1);
order.cancel();
await uow.commit();

领域模型与RESTful API的对应关系

设计API时反映领域模型:

GET /orders/{id}        -> Order聚合根
POST /orders/{id}/payments -> 在Order下创建Payment
GET /products           -> 独立的Product聚合

避免设计出不符合领域概念的端点,如:

POST /orders/create-payment  # 不符合,payment应属于order

团队协作与统一语言

在Koa2项目中实践DDD时:

  1. 在代码中使用业务术语命名
  2. 保持领域模型与数据库模型的分离
  3. 使用TypeScript增强领域模型表达力
// 使用TypeScript定义领域模型
interface Order {
  id: string;
  status: 'pending' | 'completed' | 'cancelled';
  cancel(): void;
}

class Order implements Order {
  private status: OrderStatus;
  
  cancel() {
    if (this.status !== 'pending') {
      throw new Error('Invalid status');
    }
    this.status = 'cancelled';
  }
}

监控与日志增强

在DDD架构中增强可观测性:

class LoggingOrderRepository {
  constructor(innerRepository, logger) {
    this.inner = innerRepository;
    this.logger = logger;
  }
  
  async findById(id) {
    this.logger.debug('Finding order', { id });
    const result = await this.inner.findById(id);
    this.logger.debug('Found order', { id, exists: !!result });
    return result;
  }
}

// 使用装饰器模式
const orderRepo = new LoggingOrderRepository(
  new OrderRepository({ db }),
  logger
);

领域模型与前端协作

定义共享的领域类型:

// shared-types/order.ts
export type OrderStatus = 
  | 'pending'
  | 'processing'
  | 'completed'
  | 'cancelled';

export interface OrderDTO {
  id: string;
  userId: string;
  status: OrderStatus;
  createdAt: string;
}

// 前端和后端都可以使用相同类型定义

性能敏感场景的优化

对于性能关键路径,可以绕过领域模型:

class OrderRepository {
  // ...其他方法
  
  async getOrderStatus(id) {
    // 直接查询所需字段
    const result = await this.db('orders')
      .where({ id })
      .select('status')
      .first();
    
    return result?.status;
  }
}

与微服务架构结合

在Koa2中实现跨服务领域交互:

class OrderService {
  constructor({ 
    orderRepository,
    inventoryService 
  }) {
    this.orderRepository = orderRepository;
    this.inventoryService = inventoryService;
  }
  
  async createOrder(userId, productId) {
    // 调用库存服务
    const available = await this.inventoryService.checkStock(productId);
    if (!available) throw new Error('库存不足');
    
    // 创建订单
    const order = new Order({ userId, productId });
    await this.orderRepository.save(order);
    
    // 预留库存
    await this.inventoryService.reserveStock(productId);
    
    return order;
  }
}

领域模型的版本兼容性

处理模型变更时的向后兼容:

class Order {
  constructor(data) {
    // 兼容旧版本数据
    this._id = data.id || data._id;
    this._userId = data.userId || data.user_id;
    
    // 新字段提供默认值
    this._version = data.version || 1;
  }
  
  toJSON() {
    return {
      id: this._id,
      userId: this._userId,
      version: this._version,
      // 新旧API都支持的格式
      status: this._status,
      _status: this._status
    };
  }
}

安全考虑

在领域模型中实施安全规则:

class Order {
  // ...其他代码
  
  canBeViewedBy(user) {
    return user.isAdmin || this._userId === user.id;
  }
}

// 应用层使用
async function getOrder(ctx) {
  const order = await orderRepository.findById(ctx.params.id);
  if (!order.canBeViewedBy(ctx.state.user)) {
    ctx.status = 403;
    return;
  }
  ctx.body = order.toJSON();
}

领域模型与缓存集成

class CachedOrderRepository {
  constructor(innerRepository, cache) {
    this.inner = innerRepository;
    this.cache = cache;
  }
  
  async findById(id) {
    const cacheKey = `order:${id}`;
    let order = await this.cache.get(cacheKey);
    
    if (!order) {
      order = await this.inner.findById(id);
      if (order) {
        await this.cache.set(cacheKey, order, { ttl: 3600 });
      }
    }
    
    return order;
  }
}

处理领域模型与第三方服务的集成

class ExternalPaymentService {
  constructor(httpClient, config) {
    this.http = httpClient;
    this.config = config;
  }
  
  async processPayment(payment) {
    const response = await this.http.post(
      `${this.config.baseUrl}/payments`,
      {
        amount: payment.amount,
        currency: payment.currency,
        reference: payment.orderId
      }
    );
    
    return new PaymentResult({
      success: response.status === 'success',
      transactionId: response.id
    });
  }
}

领域模型与数据验证

将验证逻辑放在领域对象中:

class Email {
  constructor(value) {
    if (!this.validate(value)) {
      throw new Error('Invalid email');
    }
    this.value = value;
  }
  
  validate(email) {
    return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
  }
  
  toString() {
    return this.value;
  }
}

class User {
  constructor(email) {
    this.email = new Email(email);
  }
}

领域事件的实际应用

实现完整的事件处理流程:

// domain/events/orderCreated.js
class OrderCreatedEvent {
  constructor(order) {
    this.order = order;
    this.occurredOn = new Date();
  }
}

// infrastructure/eventHandlers/sendOrderConfirmation.js
class SendOrderConfirmationHandler {
  constructor(emailService) {
    this.emailService = emailService;
  }
  
  async handle(event) {
    await this.emailService.send({
      to: event.order.userEmail,
      subject: '订单确认',
      text: `您的订单 #${event.order.id} 已创建`
    });
  }
}

// 事件注册
eventBus.register(
  OrderCreatedEvent,
  new SendOrderConfirmationHandler(emailService)
);

本站部分内容来自互联网,一切版权均归源网站或源作者所有。

如果侵犯了你的权益,请来信告知我们删除。邮箱:cc@cccx.cn

前端川

前端川,陈川的代码茶馆🍵,专治各种不服的Bug退散符💻,日常贩卖秃头警告级的开发心得🛠️,附赠一行代码笑十年的摸鱼宝典🐟,偶尔掉落咖啡杯里泡开的像素级浪漫☕。‌