一、领域驱动设计为什么能减少重复代码

咱们程序员最头疼的就是写重复代码。今天改这里,明天发现那边也要改同样的逻辑,维护起来简直要命。而领域驱动设计(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聚合根完成,它保证了:

  1. 添加商品时自动校验状态
  2. 数量修改必须符合业务规则
  3. 提交时进行完整性检查
  4. 生成领域事件通知其他系统

三、领域服务与仓储模式实战

有些业务逻辑不适合放在实体里,这时候就需要领域服务。再结合仓储模式,可以大幅减少数据访问重复代码:

// 领域服务示例:优惠券核销
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);
    }
    
    // 其他实现...
}

这种设计带来三个好处:

  1. 业务逻辑集中在领域层,不分散在Controller或Service中
  2. 数据访问接口统一,更换ORM时只需修改仓储实现
  3. 业务规则可复用,比如优惠券校验逻辑

四、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的精髓在于:

  1. 写操作走严格领域模型,保证业务正确性
  2. 读操作可以绕过领域模型直接查询,提升性能
  3. 读写可以使用不同的数据库技术(如写用SQL Server,读用Redis缓存)

五、实战注意事项与经验总结

经过多个项目实践,我总结了这些经验:

  1. 聚合设计原则

    • 一个聚合应该代表一个业务一致性边界
    • 聚合间通过ID引用,不要直接持有对象引用
    • 聚合应该尽可能小,大聚合会导致性能问题
  2. 领域事件使用技巧

// 领域事件发布示例
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;
    }
}
  1. 技术选型建议

    • 中小项目:EF Core + 内存事件总线
    • 大型项目:专用事件存储(如EventStore) + 消息队列
    • 查询优化:考虑使用Elasticsearch或Redis作为读存储
  2. 团队协作要点

    • 一定要先和领域专家统一语言(Ubiquitous Language)
    • 使用清晰的分层架构(表现层、应用层、领域层、基础设施层)
    • 为复杂业务编写测试用例,验证领域模型正确性

最后记住,DDD不是银弹。适合的场景包括:

  • 业务逻辑复杂的核心系统
  • 长期演进的战略项目
  • 需要多团队协作的大型工程

而对于简单CRUD应用,传统三层架构可能更合适。关键在于根据实际情况灵活选择,不要为了用DDD而用DDD。