考试系统DDD(领域驱动设计)实现步骤详解
一、DDD核心概念通俗解读
🎯 什么是DDD?
传统开发思维:
需求文档 → 设计数据库 → 写代码实现功能
DDD开发思维:
业务专家 + 技术专家 → 共同理解业务 → 抽象出"领域模型" → 代码就是业务模型
📦 DDD核心概念通俗比喻
| 概念 | 通俗解释 | 考试系统示例 |
|---|---|---|
| 领域 | 业务范围,你要解决什么问题 | 考试管理、阅卷评分 |
| 子域 | 大业务里的小业务模块 | 题库管理、考试安排 |
| 限界上下文 | 独立的小王国,有自己的语言和规则 | 考试上下文、阅卷上下文 |
| 实体 | 有唯一ID的"人物" | 学生、老师、试卷 |
| 值对象 | 没有ID的"道具" | 考试时间、分数、地址 |
| 聚合 | 一群有组织关系的实体 | 试卷(聚合根)+ 试题(实体) |
| 聚合根 | 群里的老大,唯一对外接口 | 试卷 |
| 仓储 | 仓库管理员,存取数据 | 试卷仓储、学生仓储 |
| 领域服务 | 协调多个实体的复杂业务 | 考试安排服务 |
二、DDD实现步骤图文详解
第一步:领域分析 - 理解业务本质
领域分析
识别核心业务
划分子域
确定限界上下文
核心业务: 组织考试
支撑业务: 题库管理
通用业务: 用户管理
核心子域: 考试流程
支撑子域: 阅卷评分
通用子域: 基础数据
考试上下文
阅卷上下文
题库上下文
通俗解释:
- 问自己:这个系统到底要解决什么问题?(组织在线考试)
- 再问:这个业务可以分成几个独立的部分?(考试、阅卷、题库)
- 再问:每部分有自己的语言和规则吗?(是,考试有自己的状态流转)
第二步:领域建模 - 画出业务蓝图
参加
使用
包含
1 1 1 * 1 * Exam
-id: ExamId
-title: String
-status: ExamStatus
-time: ExamTime
-config: ExamConfig
+publish()
+start()
+end()
Student
-id: StudentId
-name: String
-class: ClassInfo
+participate(exam)
Paper
-id: PaperId
-questions: List<Question>
-totalScore: Score
Question
-id: QuestionId
-content: String
-answer: Answer
ExamTime
-startTime: DateTime
-endTime: DateTime
+isInProgress()
Score
-value: BigDecimal
+calculateFinal()
建模方法:
- 找出实体:有唯一ID,会变化的(学生、试卷、考试)
- 找出值对象:没有ID,不变的(分数、时间、地址)
- 找出聚合:紧密关联的实体群(试卷聚合:试卷+试题)
第三步:限界上下文划分 - 建立"小王国"
共享内核
题库上下文
阅卷上下文
考试上下文
考试实体
考试记录
考试服务
考试仓储
阅卷任务
评分记录
阅卷服务
阅卷仓储
试题
知识点
组卷服务
题库仓储
用户实体
班级值对象
通俗解释:
- 每个上下文就像一个小公司:
- 考试公司:专门负责组织考试
- 阅卷公司:专门负责批改试卷
- 题库公司:专门管理试题
- 各公司有自己的"方言"(领域语言)
- 共享用户信息,就像共用同一个客户数据库
第四步:领域模型映射到代码结构
src/
├── domain/ # 领域层
│ ├── exam/ # 考试上下文
│ │ ├── model/ # 领域模型
│ │ │ ├── aggregate/
│ │ │ │ ├── Exam.java # 考试聚合根
│ │ │ │ ├── Student.java # 学生实体
│ │ │ │ └── Paper.java # 试卷聚合根
│ │ │ ├── valueobject/
│ │ │ │ ├── ExamTime.java # 考试时间值对象
│ │ │ │ ├── ExamConfig.java # 考试配置值对象
│ │ │ │ └── Score.java # 分值对象
│ │ │ └── vo/
│ │ │ ├── ExamId.java # 考试ID值对象
│ │ │ └── StudentId.java # 学生ID值对象
│ │ ├── service/ # 领域服务
│ │ │ ├── ExamService.java # 考试服务
│ │ │ └── PaperService.java # 试卷服务
│ │ ├── repository/ # 仓储接口
│ │ │ ├── ExamRepository.java
│ │ │ └── StudentRepository.java
│ │ └── event/ # 领域事件
│ │ ├── ExamPublishedEvent.java
│ │ └── ExamFinishedEvent.java
│ ├── marking/ # 阅卷上下文
│ │ ├── model/
│ │ ├── service/
│ │ └── repository/
│ └── questionbank/ # 题库上下文
│ ├── model/
│ ├── service/
│ └── repository/
├── application/ # 应用层
├── interfaces/ # 接口层
└── infrastructure/ # 基础设施层
三、DDD代码实现详细步骤
步骤1:定义值对象(Value Objects)
java
// 考试ID值对象 - 有唯一标识
public class ExamId implements ValueObject {
private final Long value;
public ExamId(Long value) {
if (value == null || value <= 0) {
throw new IllegalArgumentException("考试ID不能为空");
}
this.value = value;
}
public Long getValue() { return value; }
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof ExamId)) return false;
return value.equals(((ExamId) o).value);
}
}
// 考试时间值对象 - 无唯一标识
public class ExamTime implements ValueObject {
private final LocalDateTime startTime;
private final LocalDateTime endTime;
private final Duration duration;
public ExamTime(LocalDateTime startTime, LocalDateTime endTime) {
if (startTime.isAfter(endTime)) {
throw new IllegalArgumentException("开始时间不能晚于结束时间");
}
this.startTime = startTime;
this.endTime = endTime;
this.duration = Duration.between(startTime, endTime);
}
public boolean isInProgress() {
LocalDateTime now = LocalDateTime.now();
return !now.isBefore(startTime) && !now.isAfter(endTime);
}
}
步骤2:定义实体(Entities)
java
// 学生实体
public class Student implements Entity<StudentId> {
private StudentId id;
private String name;
private String account;
private ClassInfo classInfo; // 值对象
private List<ExamRecord> examRecords = new ArrayList<>();
// 参加考试
public ExamRecord participate(Exam exam) {
if (!exam.isAvailableFor(this)) {
throw new IllegalStateException("学生不能参加此考试");
}
ExamRecord record = new ExamRecord(
ExamRecordId.generate(),
this.id,
exam.getId(),
LocalDateTime.now()
);
examRecords.add(record);
return record;
}
}
步骤3:定义聚合根(Aggregate Roots)
java
// 考试聚合根
public class Exam implements AggregateRoot<ExamId> {
private ExamId id;
private String title;
private ExamStatus status;
private ExamTime time;
private ExamConfig config;
private List<StudentId> studentIds; // 引用学生ID
private PaperId paperId; // 引用试卷ID
// 发布考试
public void publish() {
if (status != ExamStatus.DRAFT) {
throw new IllegalStateException("只有草稿状态的考试可以发布");
}
this.status = ExamStatus.PUBLISHED;
// 发布领域事件
DomainEventPublisher.publish(new ExamPublishedEvent(
this.id,
this.title,
this.studentIds
));
}
// 开始考试
public void start() {
if (status != ExamStatus.PUBLISHED) {
throw new IllegalStateException("只有已发布的考试可以开始");
}
if (!time.isInProgress()) {
throw new IllegalStateException("未到考试开始时间");
}
this.status = ExamStatus.IN_PROGRESS;
}
// 业务规则封装在聚合内
public boolean isAvailableFor(Student student) {
return studentIds.contains(student.getId()) &&
status == ExamStatus.IN_PROGRESS;
}
}
步骤4:定义领域服务(Domain Services)
java
// 考试安排服务 - 协调多个聚合的业务逻辑
@Service
public class ExamArrangementService {
private final ExamRepository examRepository;
private final StudentRepository studentRepository;
private final PaperRepository paperRepository;
public Exam arrangeExam(CreateExamCommand command) {
// 1. 验证试卷是否存在
Paper paper = paperRepository.findById(command.getPaperId());
if (paper == null) {
throw new IllegalArgumentException("试卷不存在");
}
// 2. 创建考试
Exam exam = new Exam(
ExamId.generate(),
command.getTitle(),
new ExamTime(command.getStartTime(), command.getEndTime()),
new ExamConfig(
command.isAllowPause(),
command.isAllowRetake(),
command.getMaxAttempts()
),
paper.getId()
);
// 3. 添加学生
for (StudentId studentId : command.getStudentIds()) {
Student student = studentRepository.findById(studentId);
if (student != null) {
exam.addStudent(studentId);
}
}
// 4. 保存考试
examRepository.save(exam);
return exam;
}
}
步骤5:定义仓储接口(Repository Interfaces)
java
// 考试仓储接口 - 定义在领域层
public interface ExamRepository {
Exam findById(ExamId id);
List<Exam> findByStatus(ExamStatus status);
List<Exam> findByTeacher(TeacherId teacherId);
void save(Exam exam);
void delete(ExamId id);
}
// 仓储实现 - 在基础设施层
@Repository
public class ExamRepositoryImpl implements ExamRepository {
@Autowired
private ExamMapper examMapper; // MyBatis Mapper
@Override
public Exam findById(ExamId id) {
ExamDO examDO = examMapper.selectById(id.getValue());
if (examDO == null) return null;
// 转换为领域对象
return ExamConverter.toDomain(examDO);
}
@Override
public void save(Exam exam) {
ExamDO examDO = ExamConverter.toDO(exam);
if (examDO.getId() == null) {
examMapper.insert(examDO);
} else {
examMapper.updateById(examDO);
}
}
}
步骤6:定义应用服务(Application Services)
java
// 应用服务 - 协调领域服务,处理事务
@Service
@Transactional
public class ExamApplicationService {
private final ExamArrangementService examArrangementService;
private final ExamRepository examRepository;
private final EventPublisher eventPublisher;
public ExamDTO createExam(CreateExamCommand command) {
// 1. 调用领域服务安排考试
Exam exam = examArrangementService.arrangeExam(command);
// 2. 发布考试创建事件
eventPublisher.publish(new ExamCreatedEvent(
exam.getId(),
command.getTeacherId()
));
// 3. 返回DTO
return ExamDTO.from(exam);
}
public void publishExam(PublishExamCommand command) {
Exam exam = examRepository.findById(command.getExamId());
if (exam == null) {
throw new ExamNotFoundException(command.getExamId());
}
exam.publish();
examRepository.save(exam);
}
}
步骤7:实现防腐层(Anti-Corruption Layer)
java
// 用户上下文防腐层
@Component
public class UserContextAdapter {
@Autowired
private UserFeignClient userFeignClient;
// 获取学生信息
public Student getStudent(StudentId studentId) {
UserDTO userDTO = userFeignClient.getUser(studentId.getValue());
// 转换为当前上下文的领域对象
return new Student(
new StudentId(userDTO.getId()),
userDTO.getName(),
new ClassInfo(userDTO.getClassId(), userDTO.getClassName())
);
}
}
四、DDD完整工作流程示例
数据库 仓储 聚合根 领域服务 应用服务 用户界面 数据库 仓储 聚合根 领域服务 应用服务 用户界面 封装业务规则验证 1. 创建考试请求 2. 调用考试安排服务 3. 查询试卷 4. 查询试卷数据 5. 返回试卷 6. 返回试卷对象 7. 创建考试聚合 8. 保存考试 9. 插入考试数据 10. 保存成功 11. 返回考试对象 12. 返回考试DTO
五、DDD与传统开发对比
| 方面 | 传统开发 | DDD开发 |
|---|---|---|
| 设计起点 | 数据库表结构 | 业务领域模型 |
| 代码组织 | 按技术分层 | 按业务上下文 |
| 业务逻辑 | 分散在Service | 集中在领域对象 |
| 可维护性 | 修改困难 | 修改容易 |
| 团队沟通 | 技术术语 | 业务语言 |
| 适合场景 | 简单CRUD | 复杂业务系统 |
六、DDD实战小贴士
🎯 开始DDD的实用建议
- 从小处开始:先从一个核心业务场景开始实践
- 建立通用语言:和业务人员一起定义术语表
- 先画图再编码:用UML或Mermaid画领域模型
- 边界清晰:明确每个上下文的职责范围
- 逐步演进:DDD是持续重构的过程
🚨 常见陷阱
- ❌ 把DDD当成银弹,过度设计
- ❌ 把数据库表直接当领域对象
- ❌ 领域服务变成贫血的Service
- ❌ 忽略限界上下文边界
✅ 成功标志
- ✓ 新功能添加时,代码修改范围很小
- ✓ 业务专家能看懂核心领域模型
- ✓ 测试用例能清晰表达业务规则
- ✓ 团队沟通用业务术语而非技术术语
七、考试系统DDD架构总结
基础设施层
领域层 - 核心
应用层
用户界面层
Web前端
移动端
管理后台
考试应用服务
阅卷应用服务
题库应用服务
考试上下文
阅卷上下文
题库上下文
数据库
缓存
消息队列
外部API
通俗总结 :
DDD就像盖房子:
- 打地基(领域分析):搞清楚要盖什么样的房子
- 画图纸(领域建模):设计房子的结构
- 分区域(限界上下文):划分客厅、卧室、厨房
- 建框架(聚合、实体):建梁柱、墙体
- 做装修(应用层):刷墙、铺地
- 通水电(基础设施层):接通水电网络
通过DDD,你的代码就像是房子的设计图,清晰表达了业务意图,而不是一堆杂乱的材料。