前言
前 6 篇我们把 DDD 的核心概念都过了一遍。从这一篇开始,我们看这些概念如何在代码里组织。
这就是分层架构要做的事。
一、为什么要分层?
在单体时代,最常见的架构是三层架构:
┌──────────────┐
│ Controller │ 表现层
├──────────────┤
│ Service │ 业务层
├──────────────┤
│ DAO │ 数据访问层
└──────────────┘
简单、直接、好理解。但当业务复杂起来:
- Service 越来越胖,几千行代码。
- 一个 Service 调几十个 DAO,什么都干。
- 业务逻辑、数据访问、事务控制混在一起。
- 想把某段业务抽出来复用,抽不出来。
分层的本质是“用边界来组织代码”。边界越清晰,代码越可维护。
二、DDD 四层架构
DDD 提出的分层架构是这样的:
┌────────────────────┐
│ 用户接口层 │ Web/Controller、API 适配
├────────────────────┤
│ 应用层 │ 用例编排、事务控制
├────────────────────┤
│ 领域层 │ 业务核心:实体、值对象、聚合、领域服务
├────────────────────┤
│ 基础层 │ 数据库、缓存、消息、文件
└────────────────────┘
依赖关系:上层依赖下层,下层不依赖上层。
每一层都有明确的职责。
三、每一层都在干什么
1. 用户接口层(User Interface Layer)
职责:接收用户输入,返回结果。
包含:
- REST Controller
- GraphQL Resolver
- gRPC Service
- 消息消费者(消费外部消息后转换为内部命令)
- DTO(数据传输对象)
- 装配器(Assembler):DO ↔ DTO 转换
这个层的代码应该很薄,只做参数接收、调用应用层、返回结果。
@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 的核心。包含:
- 实体(Entity)
- 值对象(Value Object)
- 聚合(Aggregate)+ 聚合根
- 领域服务(Domain Service)
- 仓储接口(Repository Interface)
- 领域事件(Domain Event)
领域层不应该依赖任何技术框架(不依赖 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 Impl)
- 数据库访问(MyBatis/JPA)
- 消息中间件客户端
- 缓存客户端
- 文件存储
- 第三方 API 客户端
- 通用工具类
@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 { // 基础层实现
// ...
}
效果:
- 领域层只依赖接口,不依赖实现。
- 换数据库只需要重写
CourseRepositoryImpl,业务代码完全不动。 - 业务层是稳定的(核心业务逻辑),数据层是易变的(技术实现)。
这就是依赖倒置的本质:让稳定的依赖易变的,让易变的依赖稳定的。
五、严格分层 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/ // 外部服务
演进的核心动作:
- 业务代码从 Service 拆到领域层(Service 变薄,只做编排)。
- DAO 改名 Repository,接口在领域层,实现在基础层。
- PO ↔ DO 转换:DO 是领域对象,PO 是数据库对象,通过 Assembler 转换。
- 引入 DTO:Controller 只用 DTO,不用 DO。
七、聚合作为演进的最小单位
DDD 分层架构的最大优势是以聚合为单位演进。
举个例子:随着业务发展,“选课聚合”被频繁访问,性能瓶颈凸显。
演进前:CourseService(包含 3 个聚合)
↓
演进后:把“选课聚合”拆成独立微服务
├── enrollment-service(选课聚合)
└── course-service(课程聚合)
怎么拆?
- 把
enrollment/目录整体抽到新项目。 CourseRepository接口保留在原项目,新服务只引用接口的客户端(Feign 等)。- 数据库单独建表或独立 schema。
- 部署独立。
因为聚合的边界已经划清,拆起来非常顺。如果业务代码混在一起,拆起来会要命。
八、严格分层的一个常见困惑
“应用层真的不能有业务规则吗?”
应用层可以有业务流程相关的判断,但不应该有业务规则。
举例:
// ✅ 应用层可以有:流程判断
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 四层架构:用户接口层、应用层、领域层、基础层。
- 依赖倒置:接口在领域层,实现在基础层。
- 严格分层:每层只依赖直接下方的层。
- 业务规则属于领域层,业务流程属于应用层。
- 聚合是架构演进的最小单位。
下一篇我们讲整洁架构和六边形架构——DDD 分层之外的另外两种选择,以及它们的关系。