一、领域驱动设计为什么能减少重复代码
咱们程序员最头疼的就是写重复代码。今天改这里,明天发现那边也要改同样的逻辑,维护起来简直要命。而领域驱动设计(DDD)的核心思想,就是把业务逻辑集中到"领域层",让相同的业务规则只在一个地方实现。
举个电商系统的例子(使用C#/.NET技术栈):
// 传统写法:订单验证逻辑分散在各处
public class OrderService
{
public void CreateOrder(OrderDto dto)
{
// 验证库存
if(dto.Items.Any(item => item.Stock < 1))
throw new Exception("库存不足");
// 其他业务逻辑...
}
}
public class OrderController : Controller
{
public IActionResult Submit(OrderDto dto)
{
// 重复的库存验证
if(dto.Items.Any(item => item.Stock < 1))
return BadRequest("库存不足");
// 调用服务...
}
}
// DDD改造后:将验证规则封装在领域对象中
public class Order : Entity
{
private readonly List<OrderItem> _items = new();
public static Order Create(IEnumerable<OrderItem> items)
{
// 业务规则集中在此
if(items.Any(item => item.Stock < 1))
throw new DomainException("库存不足");
return new Order { _items = items.ToList() };
}
}
看到区别了吗?传统写法需要在每个用到订单的地方重复验证逻辑,而DDD把规则封装在Order领域对象中,所有调用方都复用同一套规则。
二、聚合根模式的实际应用
DDD有个超级实用的模式叫"聚合根",它就像业务对象的组长,负责维持一组相关对象的一致性。继续用电商例子:
// 订单聚合根示例
public class Order : AggregateRoot
{
public Guid Id { get; private set; }
public OrderStatus Status { get; private set; }
private List<OrderLine> _lines = new();
// 核心业务方法
public void AddProduct(Product product, int quantity)
{
// 业务规则校验
if(Status != OrderStatus.Draft)
throw new DomainException("已提交订单不能修改");
if(quantity <= 0)
throw new DomainException("数量必须大于0");
// 查找或创建订单项
var line = _lines.FirstOrDefault(x => x.ProductId == product.Id);
if(line != null)
{
line.IncreaseQuantity(quantity);
}
else
{
_lines.Add(new OrderLine(product.Id, quantity));
}
}
// 提交订单
public void Submit()
{
if(_lines.Count == 0)
throw new DomainException("订单不能为空");
Status = OrderStatus.Submitted;
AddDomainEvent(new OrderSubmittedEvent(this));
}
}
// 配套的值对象
public class OrderLine : Entity
{
public Guid ProductId { get; }
public int Quantity { get; private set; }
public OrderLine(Guid productId, int quantity)
{
ProductId = productId;
Quantity = quantity;
}
public void IncreaseQuantity(int amount)
{
Quantity += amount;
}
}
这个设计妙在哪?所有订单相关的修改都必须通过Order聚合根完成,它保证了:
- 添加商品时自动校验状态
- 数量修改必须符合业务规则
- 提交时进行完整性检查
- 生成领域事件通知其他系统
三、领域服务与仓储模式实战
有些业务逻辑不适合放在实体里,这时候就需要领域服务。再结合仓储模式,可以大幅减少数据访问重复代码:
// 领域服务示例:优惠券核销
public class CouponService
{
private readonly ICouponRepository _couponRepo;
private readonly IOrderRepository _orderRepo;
public CouponService(
ICouponRepository couponRepo,
IOrderRepository orderRepo)
{
_couponRepo = couponRepo;
_orderRepo = orderRepo;
}
public void ApplyCoupon(Guid orderId, string couponCode)
{
// 获取领域对象
var order = _orderRepo.GetById(orderId);
var coupon = _couponRepo.GetByCode(couponCode);
// 业务规则校验
if(coupon.IsExpired())
throw new DomainException("优惠券已过期");
if(!coupon.IsApplicable(order.TotalAmount))
throw new DomainException("不满足使用条件");
// 执行业务操作
order.ApplyDiscount(coupon.DiscountAmount);
coupon.MarkAsUsed();
// 持久化
_orderRepo.Update(order);
_couponRepo.Update(coupon);
}
}
// 仓储接口定义
public interface IOrderRepository
{
Order GetById(Guid id);
void Add(Order order);
void Update(Order order);
}
// EF Core实现
public class OrderRepository : IOrderRepository
{
private readonly AppDbContext _db;
public OrderRepository(AppDbContext db) => _db = db;
public Order GetById(Guid id)
{
return _db.Orders
.Include(o => o.Lines)
.FirstOrDefault(o => o.Id == id);
}
// 其他实现...
}
这种设计带来三个好处:
- 业务逻辑集中在领域层,不分散在Controller或Service中
- 数据访问接口统一,更换ORM时只需修改仓储实现
- 业务规则可复用,比如优惠券校验逻辑
四、CQRS模式进阶实践
对于复杂系统,推荐使用CQRS(命令查询职责分离)模式。看这个订单查询优化案例:
// 命令端(写操作)
public class CreateOrderCommandHandler
: ICommandHandler<CreateOrderCommand>
{
private readonly IOrderRepository _repo;
public CreateOrderCommandHandler(IOrderRepository repo)
=> _repo = repo;
public async Task Handle(CreateOrderCommand cmd)
{
var order = Order.Create(
cmd.UserId,
cmd.Items.Select(i =>
new OrderItem(i.ProductId, i.Quantity)));
await _repo.AddAsync(order);
// 生成领域事件
order.AddDomainEvent(new OrderCreatedEvent(order.Id));
}
}
// 查询端(读操作)
public class OrderQueryService
{
private readonly IOrderReadModelRepository _readRepo;
public OrderQueryService(IOrderReadModelRepository readRepo)
=> _readRepo = readRepo;
public OrderDto GetOrderDetails(Guid orderId)
{
// 直接从优化的读模型获取
return _readRepo.GetOrderWithDetails(orderId);
}
}
// 读模型仓储(使用Dapper)
public class OrderReadModelRepository : IOrderReadModelRepository
{
private readonly IDbConnection _db;
public OrderReadModelRepository(IDbConnection db) => _db = db;
public OrderDto GetOrderWithDetails(Guid orderId)
{
const string sql = @"
SELECT o.*,
ol.Id as LineId, ol.ProductId, ol.Quantity
FROM Orders o
LEFT JOIN OrderLines ol ON o.Id = ol.OrderId
WHERE o.Id = @orderId";
using var multi = _db.QueryMultiple(sql, new { orderId });
var order = multi.Read<OrderDto>().Single();
order.Items = multi.Read<OrderLineDto>().ToList();
return order;
}
}
CQRS的精髓在于:
- 写操作走严格领域模型,保证业务正确性
- 读操作可以绕过领域模型直接查询,提升性能
- 读写可以使用不同的数据库技术(如写用SQL Server,读用Redis缓存)
五、实战注意事项与经验总结
经过多个项目实践,我总结了这些经验:
聚合设计原则:
- 一个聚合应该代表一个业务一致性边界
- 聚合间通过ID引用,不要直接持有对象引用
- 聚合应该尽可能小,大聚合会导致性能问题
领域事件使用技巧:
// 领域事件发布示例
public class Order : AggregateRoot
{
public void Cancel(string reason)
{
Status = OrderStatus.Cancelled;
AddDomainEvent(new OrderCancelledEvent(Id, reason));
}
}
// 事件处理器
public class OrderCancelledHandler
: IDomainEventHandler<OrderCancelledEvent>
{
public Task Handle(OrderCancelledEvent @event)
{
// 发送通知、更新报表等
return Task.CompletedTask;
}
}
技术选型建议:
- 中小项目:EF Core + 内存事件总线
- 大型项目:专用事件存储(如EventStore) + 消息队列
- 查询优化:考虑使用Elasticsearch或Redis作为读存储
团队协作要点:
- 一定要先和领域专家统一语言(Ubiquitous Language)
- 使用清晰的分层架构(表现层、应用层、领域层、基础设施层)
- 为复杂业务编写测试用例,验证领域模型正确性
最后记住,DDD不是银弹。适合的场景包括:
- 业务逻辑复杂的核心系统
- 长期演进的战略项目
- 需要多团队协作的大型工程
而对于简单CRUD应用,传统三层架构可能更合适。关键在于根据实际情况灵活选择,不要为了用DDD而用DDD。
评论