领域驱动设计初步应用
领域驱动设计初步应用
领域驱动设计(Domain-Driven Design,DDD)是一种软件开发方法,强调通过深入理解业务领域来构建复杂系统。在Koa2框架中应用DDD,可以帮助我们更好地组织代码结构,提高可维护性和扩展性。
为什么要在Koa2中使用DDD
Koa2作为Node.js的轻量级框架,本身不强制任何特定的架构模式。但随着业务复杂度增加,传统的MVC架构可能难以应对:
- 业务逻辑分散在控制器和服务层
- 领域概念不清晰,代码难以理解
- 修改一处可能影响多个不相关功能
// 传统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需要注意:
- 避免在领域对象中创建过多实例
- 考虑使用Data Mapper模式减少内存占用
- 复杂查询可以直接使用基础设施层
// infrastructure/orderRepository.js
class OrderRepository {
// ...其他方法
async findRecentOrders(limit = 10) {
// 直接返回POJO而不是领域对象
return this.db('orders')
.orderBy('created_at', 'desc')
.limit(limit);
}
}
与现有代码的渐进式迁移
从传统架构迁移到DDD的建议步骤:
- 先识别核心领域对象
- 将业务逻辑从控制器移到领域类
- 逐步引入仓储模式
- 最后处理跨聚合逻辑
// 迁移中的过渡代码示例
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时:
- 在代码中使用业务术语命名
- 保持领域模型与数据库模型的分离
- 使用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
上一篇:微服务架构下的 Koa2 应用
下一篇:代码组织与架构演进策略