跳至正文
来两杯美式
返回

DDD 复杂业务系统设计(七):分层架构,你的“业务代码”应该待在哪一层

By 来两杯美式
发布于

前言

前 6 篇我们把 DDD 的核心概念都过了一遍。从这一篇开始,我们看这些概念如何在代码里组织

这就是分层架构要做的事。

一、为什么要分层?

在单体时代,最常见的架构是三层架构

┌──────────────┐
│  Controller  │  表现层
├──────────────┤
│   Service    │  业务层
├──────────────┤
│     DAO      │  数据访问层
└──────────────┘

简单、直接、好理解。但当业务复杂起来:

分层的本质是“用边界来组织代码”。边界越清晰,代码越可维护。

二、DDD 四层架构

DDD 提出的分层架构是这样的:

┌────────────────────┐
│  用户接口层         │  Web/Controller、API 适配
├────────────────────┤
│    应用层           │  用例编排、事务控制
├────────────────────┤
│    领域层           │  业务核心:实体、值对象、聚合、领域服务
├────────────────────┤
│    基础层           │  数据库、缓存、消息、文件
└────────────────────┘

依赖关系:上层依赖下层,下层不依赖上层

每一层都有明确的职责。

三、每一层都在干什么

1. 用户接口层(User Interface Layer)

职责接收用户输入,返回结果

包含:

这个层的代码应该很薄,只做参数接收、调用应用层、返回结果。

@RestController
class CourseController {
    @Autowired
    CreateCourseAppService createCourseAppService;

    @PostMapping("/courses")
    ResponseEntity<CourseDTO> create(@RequestBody CreateCourseRequest req) {
        CreateCourseCommand cmd = assembler.toCommand(req);
        CourseId id = createCourseAppService.execute(cmd);
        return ResponseEntity.ok(new CourseDTO(id));
    }
}

2. 应用层(Application Layer)

职责编排业务流程,不包含业务规则

应用层是很薄的一层,做的事情是:

应用层不包含 if/else 业务规则,只做流程编排。

@Service
class CreateCourseAppService {
    @Autowired
    CourseRepository courseRepo;
    @Autowired
    EventPublisher eventPublisher;

    @Transactional
    public CourseId execute(CreateCourseCommand cmd) {
        // 1. 加载或创建聚合
        Course course = Course.create(cmd.getTitle(), cmd.getSyllabus());
        // 2. 持久化
        courseRepo.save(course);
        // 3. 发布事件
        eventPublisher.publish(new CourseCreatedEvent(course.getId()));
        // 4. 返回
        return course.getId();
    }
}

应用层 vs 领域层的区别

应用层领域层
流程编排业务规则
事务边界业务不变条件
调用顺序业务逻辑
不应该包含 if/else包含所有 if/else

3. 领域层(Domain Layer)

职责表达业务概念、业务状态和业务规则

这是 DDD 的核心。包含:

领域层不应该依赖任何技术框架(不依赖 Spring Data、不依赖 MyBatis、不依赖 HTTP 框架)。

// 领域层:纯 Java,不依赖任何框架
class Course {
    private final CourseId id;
    private String title;
    private Syllabus syllabus;

    void rename(String newTitle) {
        if (newTitle == null || newTitle.isBlank()) {
            throw new IllegalArgumentException("课程名不能为空");
        }
        this.title = newTitle;
    }

    void addChapter(Chapter chapter) {
        this.syllabus.addChapter(chapter);
    }
}

领域服务:跨多个实体的业务逻辑。

class CoursePricingService {
    Price calculate(Course course, Coupon coupon) {
        // 价格计算的复杂业务规则
        BigDecimal finalPrice = course.getBasePrice()
            .multiply(coupon.getDiscount())
            .setScale(2, RoundingMode.HALF_UP);
        return new Price(finalPrice);
    }
}

4. 基础层(Infrastructure Layer)

职责提供通用技术能力

包含:

@Repository
class CourseRepositoryImpl implements CourseRepository {
    @Autowired
    private CourseMapper mapper;  // MyBatis Mapper

    @Override
    public Course find(CourseId id) {
        CoursePO po = mapper.selectById(id.getValue());
        return po != null ? toDomain(po) : null;
    }

    @Override
    public void save(Course course) {
        mapper.insert(toPO(course));
    }
}

关键仓储接口在领域层,实现在基础层。这叫依赖倒置

四、依赖倒置:分层架构的灵魂

传统三层的问题:

// ❌ 传统三层:业务层直接依赖 DAO
class CourseService {
    @Autowired
    CourseDao dao;  // 业务层依赖数据访问层
}

问题:如果换数据库(MySQL → TiDB),业务层代码可能都要改。

依赖倒置的做法:

// ✅ 领域层定义接口
interface CourseRepository {  // 领域层接口
    Course find(CourseId id);
    void save(Course course);
}

// 基础层实现接口
@Repository
class CourseRepositoryImpl implements CourseRepository {  // 基础层实现
    // ...
}

效果

这就是依赖倒置的本质:让稳定的依赖易变的,让易变的依赖稳定的

五、严格分层 vs 松散分层

DDD 严格规定:每层只能依赖直接下方的层

严格分层(推荐):
用户接口层 → 应用层 → 领域层 → 基础层
   (可以依赖)
   ❌ 不能跳过依赖(如 UI 直接调领域)

松散分层(不推荐):
用户接口层 → 应用层
           ↘ 领域层 → 基础层
   UI 可以直接调领域服务

为什么推荐严格分层

反例

// ❌ 错误:Controller 直接调领域服务
@RestController
class CourseController {
    @Autowired
    CourseService courseService;  // 领域服务

    @PostMapping("/courses")
    Result publish(@RequestBody PublishRequest req) {
        courseService.publish(req.getCourseId());  // Controller 调领域服务
        return Result.ok();
    }
}

// ✅ 正确:Controller → 应用服务 → 领域服务
@RestController
class CourseController {
    @Autowired
    PublishCourseAppService appService;  // 应用服务

    @PostMapping("/courses/publish")
    Result publish(@RequestBody PublishRequest req) {
        appService.execute(req.getCourseId());
        return Result.ok();
    }
}

六、三层架构如何演进到 DDD 分层

我们之前的单体项目,代码组织是这样的:

com.example.course
├── controller/      // Controller
├── service/         // Service
├── dao/             // DAO
└── po/              // PO

演进到 DDD 分层:

com.example.course
├── interfaces/             // 用户接口层
│   ├── controller/
│   ├── dto/
│   └── assembler/
├── application/            // 应用层
│   ├── service/
│   ├── command/
│   └── event/
├── domain/                 // 领域层
│   ├── model/
│   │   ├── course/
│   │   │   ├── Course.java
│   │   │   ├── CourseId.java
│   │   │   └── Syllabus.java
│   │   └── ...
│   ├── service/
│   └── repository/         // 仓储接口
└── infrastructure/         // 基础层
    ├── persistence/        // 持久化
    ├── messaging/          // 消息
    └── external/           // 外部服务

演进的核心动作

  1. 业务代码从 Service 拆到领域层(Service 变薄,只做编排)。
  2. DAO 改名 Repository,接口在领域层,实现在基础层。
  3. PO ↔ DO 转换:DO 是领域对象,PO 是数据库对象,通过 Assembler 转换
  4. 引入 DTO:Controller 只用 DTO,不用 DO。

七、聚合作为演进的最小单位

DDD 分层架构的最大优势以聚合为单位演进

举个例子:随着业务发展,“选课聚合”被频繁访问,性能瓶颈凸显

演进前:CourseService(包含 3 个聚合)

演进后:把“选课聚合”拆成独立微服务
  ├── enrollment-service(选课聚合)
  └── course-service(课程聚合)

怎么拆?

因为聚合的边界已经划清,拆起来非常顺。如果业务代码混在一起,拆起来会要命

八、严格分层的一个常见困惑

“应用层真的不能有业务规则吗?”

应用层可以有业务流程相关的判断,但不应该有业务规则

举例:

// ✅ 应用层可以有:流程判断
if (course.isPublished()) {
    enrollmentService.enroll(...);
} else {
    throw new CourseNotPublishedException();
}

// ❌ 应用层不应该有:业务规则
if (course.getPrice().compareTo(MAX_DISCOUNT_PRICE) > 0) {  // 业务规则
    throw new PriceTooHighException();
}

业务规则属于领域层业务流程属于应用层

九、常见误区

误区 1:把业务逻辑放在应用层

// ❌ 错误:业务规则放在应用层
@Service
class CreateCourseAppService {
    public void execute(CreateCourseCommand cmd) {
        if (cmd.getTitle().length() > 100) {  // 业务规则不该在这
            throw new IllegalArgumentException();
        }
        // ...
    }
}

// ✅ 正确:业务规则在领域层
class Course {
    public void rename(String title) {
        if (title.length() > 100) {  // 业务规则
            throw new IllegalArgumentException("课程名过长");
        }
        this.title = title;
    }
}

误区 2:领域层依赖 Spring 注解

// ❌ 错误:领域层用 Spring
@Service  // Spring 注解
class CourseService { }

// ✅ 正确:领域层是纯 Java
class Course {  // 不需要任何框架注解
    void publish() { /* 业务逻辑 */ }
}

如果领域层依赖了 Spring,单元测试会很难写(要启动 Spring 容器)。

误区 3:DTO 直接返回 DO

// ❌ 错误:暴露 DO 给前端
return new CourseDTO(course);  // 直接把 DO 序列化出去

// ✅ 正确:DTO 是独立的
class CourseDTO {
    String id;
    String title;
    String instructorName;
    // 不包含 DO 的所有字段
}

DO 包含领域行为,DTO 只包含展示数据两者必须分离

总结

这一篇我们讲了:

下一篇我们讲整洁架构和六边形架构——DDD 分层之外的另外两种选择,以及它们的关系。


分享这篇文章:
通过邮件分享这篇文章✓ 链接已复制
查看系列全部文章
  1. 01.DDD 复杂业务系统设计(一):为什么从“领域”开始,比从“数据库”开始更靠谱
  2. 02.DDD 复杂业务系统设计(二):领域与子域,把“学校/课程/学员”拆成三件事再说
  3. 03.DDD 复杂业务系统设计(三):限界上下文,为什么“课程”在教务和市场是两种东西
  4. 04.DDD 复杂业务系统设计(四):实体与值对象,把“学员”和“地址”放进代码的两种姿势
  5. 05.DDD 复杂业务系统设计(五):聚合与聚合根,一个“选课单”应该装多少东西
  6. 06.DDD 复杂业务系统设计(六):领域事件,让“作业提交”自动触发批改进度
  7. 07.DDD 复杂业务系统设计(七):分层架构,你的“业务代码”应该待在哪一层
  8. 08.DDD 复杂业务系统设计(八):整洁架构 vs 六边形架构 vs DDD 分层,我选哪一种
  9. 09.DDD 复杂业务系统设计(九):中台,在线教育平台到底应该共享什么
  10. 10.DDD 复杂业务系统设计(十):从战略到微服务,一张图跑通 DDD+中台+微服务
  11. 11.DDD 复杂业务系统设计(总结):从领域到微服务,一张图看懂 DDD 落地全流程

上一篇
设计模式之桥接模式:抽象与实现分离,各自独立变化
下一篇
设计模式之适配器模式:让不兼容的接口协同工作