专业的编程技术博客社区

网站首页 > 博客文章 正文

从理论到实战:领域驱动设计(DDD)的落地之道

baijin 2025-04-05 16:44:37 博客文章 14 ℃ 0 评论



"在实际项目中,领域驱动设计(DDD)往往像'对对对'设计 —— 道理我都懂,但落地还是迷糊。"



作为一名后端开发者,你是否曾经有过这样的困惑:DDD概念听了一堆,事件风暴、聚合根、领域服务、防腐层...术语记了一大堆,但真正写代码时却不知从何下手?

本文将从一线研发的角度,为你梳理DDD架构的实战落地方法。




一、解构DDD的核心价值


在开始详细讨论各层职责前,我们需要明确DDD的核心价值:它不仅仅是一种架构模式,更是一种思考问题的方式


传统的开发往往按技术职责分层(控制器、服务、DAO),而DDD则按业务领域进行划分。这种思路转变带来的好处是:


  1. 代码结构与业务语言保持一致,新人可以更快理解业务逻辑
  2. 降低跨领域耦合,避免意大利面条式代码
  3. 面向对象设计的回归,告别贫血模型




二、DDD项目层次解构




Alibaba COLA架构给我们提供了一个很好的DDD实践模板,下面让我们剖析每一层的职责和代码归属。


1. 领域层 (Domain) - DDD的核心


"如果系统只能保留一层,那么就保留领域层。"


领域层包含核心业务逻辑,与技术实现无关,具有以下特性:


public class WorkOrder {
    private String workOrderId;
    private String initiatorId;
    private WorkOrderContent content;  // 值对象
    private WorkOrderStatus status;    // 值对象
    
    // 领域行为
    public static WorkOrder start(String initiatorId, WorkOrderContent content) {
        // 业务逻辑实现...
        return new WorkOrder(...);
    }
    
    public void handle() {
        this.status = WorkOrderStatus.HANDLING;
        this.processTime = new Date();
    }
    
    public void finish(WorkOrderHandleResult result) {
        // 状态转换逻辑...
    }
}


领域层设计要点


  1. 业务对象分为实体(有ID和生命周期)和值对象(无ID,不可变)
  2. 实体和聚合根拥有业务行为,不是简单的getter/setter
  3. 定义仓储接口,但不实现
  4. 不依赖任何外部模块






2. 基础设施层 (Infrastructure)


基础设施层负责与技术细节打交道,实现领域层定义的接口。主要包含:


@Repository
public class WorkOrderRepositoryImpl implements WorkOrderRepository {
    @Autowired
    private WorkOrderMapper workOrderMapper;
    
    @Override
    public void save(WorkOrder workOrder) {
        // 将领域对象转换为数据库实体并保存
        WorkOrderDO workOrderDO = converter.to(workOrder);
        if (workOrderDO.getId() == null) {
            workOrderMapper.insert(workOrderDO);
        } else {
            workOrderMapper.updateById(workOrderDO);
        }
    }
    
    @Override
    public WorkOrder get(String workOrderId) {
        // 查询并转换回领域对象
        return converter.from(workOrderMapper.selectById(workOrderId));
    }
}


基础设施层要点


  1. 实现领域层定义的仓储接口
  2. 处理ORM映射、中间件交互
  3. 只依赖领域层,不依赖应用层
  4. 隐藏技术实现细节


这一层的设计让系统核心业务逻辑不会因为技术选型变化而改变 —— 想从MySQL换到MongoDB?只需修改基础设施层实现。


3. 应用层 (Application)


应用层是系统的指挥中心,协调各领域完成业务用例:


@Service
public class WorkOrderAppService {
    @Autowired
    private WorkOrderService workOrderService;
    
    @Autowired
    private NotificationService notificationService;
    
    @Transactional
    public WorkOrderDTO createWorkOrder(WorkOrderCreateCommand command) {
        // 1. 调用领域服务创建工单
        WorkOrder workOrder = workOrderService.startOrder(
            command.getUserId(), 
            new WorkOrderContent(command.getType(), command.getDescription())
        );
        
        // 2. 发送通知
        notificationService.notifyNewWorkOrder(workOrder);
        
        // 3. 转换结果
        return converter.toDTO(workOrder);
    }
}


应用层要点


  1. 定义系统用例和业务流程
  2. 协调多个领域服务
  3. 处理事务边界
  4. 依赖领域层和基础设施层


4. 适配层 (Adapter)


适配层负责处理外部请求,相当于传统MVC中的Controller:


@RestController
@RequestMapping("/api/workorders")
public class WorkOrderController {
    @Autowired
    private WorkOrderAppService workOrderAppService;
    
    @PostMapping
    public Result createWorkOrder(@RequestBody @Validated WorkOrderCreateRequest request) {
        WorkOrderCreateCommand command = converter.toCommand(request);
        WorkOrderDTO dto = workOrderAppService.createWorkOrder(command);
        return Result.success(dto);
    }
}


适配层要点


  1. 参数校验
  2. 权限控制
  3. 异常处理
  4. 结果封装
  5. 只依赖应用层


5. 客户端层 (Client)


客户端层主要用于SDK封装,供其他系统调用:


public interface WorkOrderClient {
    WorkOrderDTO createWorkOrder(WorkOrderCreateRequest request);
    
    WorkOrderDTO getWorkOrder(String workOrderId);
}


客户端层要点


  1. 定义RPC接口
  2. 封装请求和响应模型
  3. 不依赖其他层
  4. 可以包含错误码定义




三、实战案例:工单管理系统


让我们通过一个工单系统的例子,看看DDD是如何落地的:




1. 领域建模


首先识别关键概念:


  • 实体:工单(WorkOrder)
  • 值对象:工单内容(WorkOrderContent)、工单状态(WorkOrderStatus)、处理结果(WorkOrderHandleResult)
  • 领域行为:发起工单、处理工单、完成工单、驳回工单


2. 聚合设计


// 工单聚合根
public class WorkOrder {
    private String workOrderId;  // 唯一标识
    private String initiatorId;
    private Date startTime;
    private WorkOrderContent content;     // 值对象
    private WorkOrderStatus status;       // 值对象
    private WorkOrderHandleResult result; // 值对象
    
    // 工厂方法
    public static WorkOrder start(String initiatorId, WorkOrderContent content) {
        // 创建工单逻辑
    }
    
    // 领域行为
    public void handle() {
        // 处理工单逻辑
    }
    
    public void finish(WorkOrderHandleResult result) {
        // 完成工单逻辑
    }
    
    public WorkOrder reject() {
        // 驳回工单逻辑
    }
}


3. 领域服务


public interface WorkOrderService {
    WorkOrder startOrder(String userId, WorkOrderContent content);
    
    void processOrder(WorkOrderHandleResult result, String handlerId, String workOrderId);
    
    void completeOrder(String workOrderId);
    
    WorkOrder rejectOrder(String workOrderId, String reason);
}


4. 仓储接口


public interface WorkOrderRepository {
    void save(WorkOrder workOrder);
    
    WorkOrder get(String workOrderId);
    
    List findByStatus(WorkOrderStatus status);
}




四、DDD落地实践指南


经过多个项目的实践,我总结了几点关键经验:


1. 循序渐进的领域建模


不要试图一开始就做完美的领域模型。先从核心业务场景出发,识别主要实体和值对象,逐步完善:


  1. 列出核心业务术语和概念
  2. 确定实体和值对象
  3. 定义实体间的关系
  4. 划分聚合边界
  5. 确定领域服务


2. 聚焦业务语言


确保代码中的命名与业务专家使用的术语一致,这是DDD的精髓之一:


// 不好的命名
public void process(String id, int type) {}

// 好的命名(体现业务语义)
public void approveExpenseReport(String reportId, ApprovalLevel level) {}


3. 避免过度设计


不是所有概念都需要用上:


// 过度设计
class ExpenseReportFactory {
    public static ExpenseReport create() {
        return new ExpenseReport();
    }
}

// 简单直接
ExpenseReport report = new ExpenseReport();


4. 关注边界上下文


识别系统中的不同上下文,并在上下文之间建立防腐层:







五、DDD常见误区


  1. 过度使用贫血模型:实体类只有getter/setter,业务逻辑全部放在服务中
  2. 领域层依赖基础设施:在实体中直接调用Repository
  3. 混淆DTO和领域对象:直接将DTO传入领域层
  4. 忽略值对象:所有概念都建模为实体
  5. 过度拆分微服务:盲目按DDD划分的聚合来拆分服务




总结


DDD不是银弹,也不需要在所有项目中全面应用其概念。关键是根据项目复杂度选择合适的实践程度:


  • 对于简单CRUD系统,完全可以采用轻量级的分层架构
  • 对于复杂业务系统,充分利用DDD的聚合、领域服务等概念


最后,记住Eric Evans的一句话:"DDD关注的是解决领域复杂性,而非技术复杂性。"


正如文章《从Alibaba-Cola到DDD,一线研发对领域驱动的思考》中所说,没有最好的架构,如果能通过合理编排层次实现代码解耦,代码易读,这就是一种好设计,好架构。


你的团队在实施DDD时遇到过哪些挑战?欢迎在评论区分享你的经验!

Tags:

本文暂时没有评论,来添加一个吧(●'◡'●)

欢迎 发表评论:

最近发表
标签列表