DDD(领域驱动设计)核心概念及落地架构全总结
本文基于订单管理场景,结合对话中的实操理解,全面梳理DDD(领域驱动设计)的核心概念、层级关系及落地架构方案,涵盖业务抽象、实现组件、架构落地等全维度细节,确保所有讨论内容无遗漏。
一、DDD核心业务概念(纯业务层面,与技术无关)
DDD的核心是围绕业务逻辑构建领域模型,所有概念均先聚焦业务本身,不涉及任何技术实现,具体可按范围从大到小梳理:
1. 领域与子域
领域是对某一具有商业价值的业务范围的抽象,是承载业务逻辑和商业规则的核心范畴。以电商场景为例,订单管理就是一个典型领域,它具备明确的商业价值,涵盖订单从创建到完成的全流程事务性业务,属于领域价值模型。
当系统规模庞大时,领域可进一步细分子域。子域本质是小范围的领域,拥有独立的业务逻辑和边界,专注解决特定业务问题,同时与其他子域协作构成完整领域。例如订单管理领域可拆分出订单创建子域、库存联动子域等。
2. 界定上下文
界定上下文是领域/子域划分的核心依据,它明确了每个子域的职责边界、业务语义及协作规则,相当于给不同业务模块划定"势力范围"。通过界定上下文,可避免业务概念混淆,确保团队对业务边界形成统一认知,同时规范子域间的交互方式。
3. 领域服务
领域服务是业务流程的协调者,专注处理不属于单个实体的跨实体、跨聚合业务逻辑。它不承载业务状态,仅按业务规则编排多个实体、值对象或其他领域服务,完成复杂业务操作。与聚合根聚焦内部管理不同,领域服务站在更高层面,协调多个聚合或实体实现大范围业务流程。例如订单创建后,协调订单实体、库存实体完成库存更新的流程,就由领域服务负责。
4. 聚合与聚合根
聚合是一组相互关联的业务对象(实体、值对象)的集合,作为一个整体处理业务逻辑,确保数据一致性和业务规则的完整性。一个领域下可包含多个聚合,每个聚合围绕一个核心对象展开。
聚合根是聚合的核心控制点,本质上是一个特殊的实体------它首先具备实体的所有特性(有唯一标识、生命周期),同时是聚合内的最高权限对象,管理聚合内其他实体和值对象的操作,对外提供统一接口,屏蔽聚合内部细节。以订单管理为例,订单本身就是聚合根,围绕它的订单商品、收货信息等实体/值对象共同构成订单聚合,订单聚合根协调内部所有对象的状态变化,保障订单业务规则的实现。
5. 实体与值对象
实体是业务载体,拥有唯一标识(如订单号)和完整生命周期(如订单从创建、支付、发货到完成的全流程),其属性会随业务状态变化而改变,且每一次状态变化都对应业务逻辑的流转。实体是聚合根的基础,聚合根本质就是核心实体。
值对象是对实体属性的描述,仅关注自身的值,无独立生命周期,也无唯一标识。它依附于实体存在,用于刻画实体在不同状态下的具体属性特征。例如订单实体的"支付金额""收货地址"就是值对象,它们仅描述订单的属性,不单独存在,当订单状态变化(如修改收货地址)时,本质是值对象的更新。
二、DDD技术实现组件(支撑业务落地,属技术层面)
上述核心概念均为纯业务抽象,需通过技术组件落地实现,这些技术实现统一归属于基础设施层,不侵入领域层的业务逻辑。
1. 仓储(Repository)
仓储是基础设施层的核心技术组件,负责实体数据的持久化存储与读取,是领域层与数据存储层的桥梁。它将领域层的实体数据转化为可存储格式(如数据库记录),同时屏蔽具体的存储技术细节(如MySQL、Redis),让领域层专注于业务逻辑。例如订单实体的创建、更新、查询,均通过仓储组件实现数据落地。
2. 其他基础设施组件
除仓储外,基础设施层还包含其他支撑业务实现的技术组件,如第三方接口调用(支付接口、物流接口)、数据库连接池、缓存服务、消息队列等。所有与技术实现相关的操作,均封装在基础设施层,确保领域层的纯粹性。
三、DDD落地架构方案(将业务概念转化为代码结构)
DDD的核心概念需通过分层架构落地,不同架构适用于不同项目场景,核心原则是职责分离、业务与技术解耦。
1. 经典四层架构(应用最广泛,易理解、好落地)
四层架构按职责自上而下/自内而外划分,各层边界清晰,是DDD落地的经典方案,其核心目录结构(代码层级)如下:
-
领域层(domain):核心业务层,封装所有纯业务概念和规则。包含实体(entity)、值对象(value object)、聚合根(aggregate root)、领域服务(domain service)、仓储接口(repository interface,仅定义接口,不涉及实现)等。
-
应用层(application):业务流程协调层,不包含核心业务规则,仅接收用户请求,协调领域层组件完成业务操作,对外提供统一的业务接口。包含应用服务(application service)、业务流程编排逻辑等。
-
基础设施层(infrastructure):技术支撑层,实现领域层所需的技术能力。包含仓储实现(repository impl)、第三方接口适配、数据库连接、缓存、消息队列等技术组件。
-
表现层(presentation):用户交互层,负责接收用户输入、展示业务结果。包含接口控制器(如API接口)、页面视图、请求参数校验等。
实操说明:四层架构可根据项目复杂度灵活调整,简单项目可合并部分层级(如表现层与应用层简化整合),核心是保留领域层与基础设施层的分离。
2. 六边形架构(端口与适配器架构)
六边形架构更强调"业务与外部依赖解耦",核心思想是领域模型处于中心,外部依赖通过适配器接入,其经典目录结构如下:
-
领域层(domain):核心不变,包含实体、值对象、聚合根、领域服务等纯业务逻辑。
-
端口(ports):定义领域层与外部交互的接口,分为输入端口(如业务服务接口,供外部调用领域逻辑)和输出端口(如仓储接口、第三方服务接口,供领域层调用外部能力)。
-
适配器(adapters):实现端口定义的接口,适配外部依赖。包含输入适配器(如API控制器、消息消费者,将外部请求转化为领域层可处理的格式)和输出适配器(如仓储实现、第三方接口适配器,将领域层请求转化为外部依赖可处理的格式)。
优势:系统扩展性极强,更换外部依赖(如将MySQL改为PostgreSQL,更换支付厂商)时,仅需修改对应适配器,不影响核心领域逻辑。
3. 整洁架构(依赖倒置架构)
整洁架构以"依赖倒置原则"为核心,所有依赖均指向核心业务,外层框架依赖内层业务,而非反之,其经典目录结构如下:
-
核心层(core):最内层,包含领域模型(实体、值对象、聚合根)、领域服务、业务规则等,完全独立于任何外部框架。
-
用例层(use cases):封装应用场景的业务流程,协调核心层组件完成具体业务用例(如"创建订单"用例、"取消订单"用例),依赖核心层。
-
接口适配层(interface adapters):将核心层、用例层的接口适配为外部框架可调用的格式,包含控制器、仓储适配器、第三方服务适配器等,依赖用例层和核心层。
-
框架与驱动层(frameworks and drivers):最外层,包含具体的框架实现(如Spring、MyBatis)、数据库、第三方服务等,依赖接口适配层。
优势:核心业务逻辑完全不受外部框架影响,可灵活替换外层框架,适合大型复杂系统长期迭代。
四、核心总结(概念与架构的关联逻辑)
DDD的核心逻辑是"先抽象业务,再落地技术":领域、子域、界定上下文定义业务边界;聚合、聚合根、实体、值对象构建业务模型;领域服务编排业务流程;仓储及基础设施层提供技术支撑;四层架构、六边形架构、整洁架构则是将这些概念转化为可落地代码结构的不同方案。
实操中,架构无需严格拘泥于标准目录,可根据项目规模、团队技术栈灵活调整,核心原则是保持"业务与技术分离""领域层纯粹性",确保系统可维护性、可扩展性与业务一致性。
案例分享
考试系统DDD架构设计与落地实现指南
基于您提供的DDD理论和考试系统表结构,我将设计一个完整的DDD落地架构方案,包含领域建模、限界上下文划分、代码结构和技术实现。
一、考试系统DDD领域建模
1. 限界上下文识别
扩展子域(增值功能)
通用子域(基础能力)
支撑子域(支持核心)
核心子域(核心价值)
考试上下文
阅卷上下文
题库上下文
试卷上下文
用户上下文
基础数据上下文
学情分析上下文
错题管理上下文
二、各限界上下文详细设计
上下文1:考试上下文(Exam Context)
核心业务:考试全生命周期管理
包含
1 * Exam
-id: ExamId
-title: String
-status: ExamStatus
-time: ExamTime
-config: ExamConfig
-paperId: PaperId
-studentIds: List<StudentId>
+publish()
+start()
+end()
+addStudent(studentId)
ExamRecord
-id: ExamRecordId
-examId: ExamId
-studentId: StudentId
-status: ExamRecordStatus
-answers: List<Answer>
+start()
+submit()
+autoSave()
<<enumeration>>
ExamStatus
DRAFT
PUBLISHED
IN_PROGRESS
ENDED
ExamTime
-startTime: LocalDateTime
-endTime: LocalDateTime
-duration: Duration
+isInProgress()
+isExpired()
领域事件:
ExamPublishedEvent- 考试发布事件ExamStartedEvent- 考试开始事件ExamSubmittedEvent- 考试提交事件
上下文2:阅卷上下文(Marking Context)
核心业务:评卷任务分配与质量控制
分配
质量控制
1 1 * 1 MarkingTask
-id: MarkingTaskId
-examId: ExamId
-status: MarkingTaskStatus
-type: MarkingType
+assign()
+start()
+complete()
MarkingAssignment
-id: MarkingAssignmentId
-taskId: MarkingTaskId
-teacherId: TeacherId
-status: MarkingStatus
+score()
+reject()
QualityControl
-id: QualityControlId
-score1: Score
-score2: Score
-difference: Decimal
+needArbitration()
+arbitrate()
上下文3:题库上下文(QuestionBank Context)
核心业务:知识点与试题管理
关联
1 * Question
-id: QuestionId
-stem: String
-type: QuestionType
-difficulty: Difficulty
-knowledgePoints: List<KnowledgePoint>
+validate()
+calculateScore()
KnowledgePoint
-id: KnowledgePointId
-name: String
-parentId: KnowledgePointId
-path: String
<<enumeration>>
QuestionType
SINGLE_CHOICE
MULTI_CHOICE
JUDGMENT
FILL_BLANK
SHORT_ANSWER
三、DDD四层架构落地实现
项目目录结构
src/
├── main/java/com/exam/
│ ├── shared/ # 共享内核
│ │ ├── kernel/
│ │ │ ├── Identifier.java # ID基类
│ │ │ ├── ValueObject.java # 值对象基类
│ │ │ └── DomainException.java # 领域异常
│ │ └── constants/ # 常量
│ │
│ ├── exam/ # 考试上下文
│ │ ├── domain/ # 领域层
│ │ │ ├── model/ # 领域模型
│ │ │ │ ├── aggregate/
│ │ │ │ │ ├── exam/ # 考试聚合
│ │ │ │ │ │ ├── Exam.java
│ │ │ │ │ │ ├── ExamTime.java
│ │ │ │ │ │ ├── ExamConfig.java
│ │ │ │ │ │ └── vo/
│ │ │ │ │ │ ├── ExamId.java
│ │ │ │ │ │ └── ExamStatus.java
│ │ │ │ │ └── examrecord/ # 考试记录聚合
│ │ │ │ │ ├── ExamRecord.java
│ │ │ │ │ ├── Answer.java
│ │ │ │ │ └── vo/
│ │ │ │ │ └── ExamRecordId.java
│ │ │ │ │
│ │ │ │ └── valueobject/ # 独立值对象
│ │ │ │ ├── Score.java
│ │ │ │ └── Duration.java
│ │ │ │
│ │ │ ├── service/ # 领域服务
│ │ │ │ ├── ExamService.java
│ │ │ │ └── ExamRecordService.java
│ │ │ │
│ │ │ ├── event/ # 领域事件
│ │ │ │ ├── ExamPublishedEvent.java
│ │ │ │ └── ExamSubmittedEvent.java
│ │ │ │
│ │ │ └── repository/ # 仓储接口
│ │ │ ├── ExamRepository.java
│ │ │ └── ExamRecordRepository.java
│ │ │
│ │ ├── application/ # 应用层
│ │ │ ├── service/ # 应用服务
│ │ │ │ ├── ExamApplicationService.java
│ │ │ │ └── command/
│ │ │ │ ├── CreateExamCommand.java
│ │ │ │ ├── PublishExamCommand.java
│ │ │ │ └── SubmitExamCommand.java
│ │ │ │
│ │ │ ├── dto/ # 应用层DTO
│ │ │ │ ├── ExamDTO.java
│ │ │ │ └── ExamRecordDTO.java
│ │ │ │
│ │ │ └── eventhandler/ # 应用层事件处理器
│ │ │ └── ExamEventHandler.java
│ │ │
│ │ ├── interfaces/ # 接口层
│ │ │ ├── web/ # Web接口
│ │ │ │ ├── ExamController.java
│ │ │ │ ├── request/
│ │ │ │ │ ├── CreateExamRequest.java
│ │ │ │ │ └── SubmitExamRequest.java
│ │ │ │ └── response/
│ │ │ │ ├── ExamResponse.java
│ │ │ │ └── ExamDetailResponse.java
│ │ │ │
│ │ │ └── rpc/ # RPC接口
│ │ │ └── ExamRpcService.java
│ │ │
│ │ └── infrastructure/ # 基础设施层
│ │ ├── persistence/ # 持久化实现
│ │ │ ├── mapper/ # MyBatis Mapper
│ │ │ │ ├── ExamMapper.java
│ │ │ │ └── ExamRecordMapper.java
│ │ │ │
│ │ │ ├── converter/ # 转换器
│ │ │ │ ├── ExamConverter.java
│ │ │ │ └── ExamRecordConverter.java
│ │ │ │
│ │ │ └── repository/ # 仓储实现
│ │ │ ├── ExamRepositoryImpl.java
│ │ │ └── ExamRecordRepositoryImpl.java
│ │ │
│ │ ├── client/ # 外部服务客户端
│ │ │ ├── PaperClient.java
│ │ │ └── UserClient.java
│ │ │
│ │ ├── message/ # 消息队列
│ │ │ └── ExamMessageProducer.java
│ │ │
│ │ └── cache/ # 缓存
│ │ └── ExamCacheService.java
│ │
│ ├── marking/ # 阅卷上下文(类似结构)
│ ├── questionbank/ # 题库上下文
│ └── user/ # 用户上下文
│
└── test/ # 测试
├── exam/
│ ├── domain/
│ ├── application/
│ └── infrastructure/
└── integration/
四、核心代码实现示例
1. 值对象实现
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 == null || getClass() != o.getClass()) return false;
ExamId examId = (ExamId) o;
return Objects.equals(value, examId.value);
}
@Override
public int hashCode() { return Objects.hash(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 == null || endTime == null) {
throw new IllegalArgumentException("考试时间不能为空");
}
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);
}
public boolean isExpired() {
return LocalDateTime.now().isAfter(endTime);
}
public Duration getRemainingTime() {
return Duration.between(LocalDateTime.now(), endTime);
}
}
2. 聚合根实现
java
// 考试聚合根
public class Exam implements AggregateRoot<ExamId> {
private ExamId id;
private String title;
private String description;
private ExamStatus status;
private ExamTime time;
private ExamConfig config;
private PaperId paperId;
private List<StudentId> studentIds;
private LocalDateTime createTime;
private LocalDateTime updateTime;
// 私有构造函数,防止外部直接创建
private Exam() {}
// 工厂方法
public static Exam create(CreateExamCommand command) {
Exam exam = new Exam();
exam.id = ExamId.nextId();
exam.title = command.getTitle();
exam.description = command.getDescription();
exam.status = ExamStatus.DRAFT;
exam.time = new ExamTime(command.getStartTime(), command.getEndTime());
exam.config = ExamConfig.of(
command.isAllowPause(),
command.isAllowRetake(),
command.isRealTimeJudge(),
command.getAutoSaveInterval(),
command.getMaxAttempts()
);
exam.paperId = command.getPaperId();
exam.studentIds = command.getStudentIds();
exam.createTime = LocalDateTime.now();
exam.updateTime = LocalDateTime.now();
// 发布领域事件
exam.registerEvent(new ExamCreatedEvent(exam.id, exam.title));
return exam;
}
// 发布考试
public void publish() {
if (status != ExamStatus.DRAFT) {
throw new DomainException("只有草稿状态的考试可以发布");
}
if (time.getStartTime().isBefore(LocalDateTime.now())) {
throw new DomainException("考试开始时间不能早于当前时间");
}
this.status = ExamStatus.PUBLISHED;
this.updateTime = LocalDateTime.now();
// 发布领域事件
registerEvent(new ExamPublishedEvent(
this.id,
this.title,
this.studentIds,
this.time.getStartTime()
));
}
// 开始考试
public void start() {
if (status != ExamStatus.PUBLISHED) {
throw new DomainException("只有已发布的考试可以开始");
}
if (!time.isInProgress()) {
throw new DomainException("未到考试开始时间");
}
this.status = ExamStatus.IN_PROGRESS;
this.updateTime = LocalDateTime.now();
registerEvent(new ExamStartedEvent(this.id, this.studentIds));
}
// 业务规则验证
public void validateForStudent(StudentId studentId) {
if (status != ExamStatus.IN_PROGRESS) {
throw new DomainException("考试未在进行中");
}
if (!studentIds.contains(studentId)) {
throw new DomainException("学生没有参加此考试的权限");
}
}
// Getters
public ExamId getId() { return id; }
public ExamStatus getStatus() { return status; }
public ExamTime getTime() { return time; }
public ExamConfig getConfig() { return config; }
public List<StudentId> getStudentIds() { return Collections.unmodifiableList(studentIds); }
}
3. 领域服务实现
java
// 考试安排领域服务
@Service
public class ExamArrangementService {
private final ExamRepository examRepository;
private final StudentRepository studentRepository;
private final PaperRepository paperRepository;
private final EventPublisher eventPublisher;
@Transactional
public Exam createExam(CreateExamCommand command) {
// 1. 验证试卷是否存在
Paper paper = paperRepository.findById(command.getPaperId());
if (paper == null) {
throw new DomainException("试卷不存在: " + command.getPaperId());
}
// 2. 验证学生是否存在
List<Student> students = studentRepository.findByIds(command.getStudentIds());
if (students.size() != command.getStudentIds().size()) {
throw new DomainException("部分学生不存在");
}
// 3. 创建考试
Exam exam = Exam.create(command);
// 4. 保存考试
examRepository.save(exam);
// 5. 发布事件
exam.getDomainEvents().forEach(eventPublisher::publish);
return exam;
}
}
4. 应用服务实现
java
// 考试应用服务
@Service
@Transactional
public class ExamApplicationService {
private final ExamArrangementService examArrangementService;
private final ExamParticipationService examParticipationService;
private final ExamRepository examRepository;
public ExamDTO createExam(CreateExamRequest request) {
// 1. 转换为命令
CreateExamCommand command = CreateExamCommand.builder()
.title(request.getTitle())
.description(request.getDescription())
.startTime(request.getStartTime())
.endTime(request.getEndTime())
.paperId(new PaperId(request.getPaperId()))
.studentIds(request.getStudentIds().stream()
.map(StudentId::new)
.collect(Collectors.toList()))
.allowPause(request.isAllowPause())
.allowRetake(request.isAllowRetake())
.realTimeJudge(request.isRealTimeJudge())
.autoSaveInterval(request.getAutoSaveInterval())
.maxAttempts(request.getMaxAttempts())
.build();
// 2. 调用领域服务
Exam exam = examArrangementService.createExam(command);
// 3. 转换为DTO
return ExamDTO.from(exam);
}
public void publishExam(PublishExamRequest request) {
Exam exam = examRepository.findById(new ExamId(request.getExamId()));
if (exam == null) {
throw new ResourceNotFoundException("考试不存在");
}
exam.publish();
examRepository.save(exam);
}
}
5. 仓储接口与实现
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
@RequiredArgsConstructor
public class ExamRepositoryImpl implements ExamRepository {
private final ExamMapper examMapper;
private final ExamConverter examConverter;
@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);
}
}
}
五、数据库表与领域模型映射
1. 考试表映射
java
// 数据库对象
@Data
@TableName("mock_exam")
public class ExamDO {
@TableId(type = IdType.AUTO)
private Long id;
private String title;
private String description;
private Integer status;
private LocalDateTime startTime;
private LocalDateTime endTime;
private Long paperId;
private String studentIds; // JSON格式
private Boolean allowPause;
private Boolean allowRetake;
private Boolean realTimeJudge;
private Integer autoSaveInterval;
private Integer maxAttempts;
private LocalDateTime createTime;
private LocalDateTime updateTime;
}
// 转换器
@Component
public class ExamConverter {
public Exam toDomain(ExamDO examDO) {
return Exam.builder()
.id(new ExamId(examDO.getId()))
.title(examDO.getTitle())
.description(examDO.getDescription())
.status(ExamStatus.fromCode(examDO.getStatus()))
.time(new ExamTime(examDO.getStartTime(), examDO.getEndTime()))
.config(ExamConfig.builder()
.allowPause(examDO.getAllowPause())
.allowRetake(examDO.getAllowRetake())
.realTimeJudge(examDO.getRealTimeJudge())
.autoSaveInterval(examDO.getAutoSaveInterval())
.maxAttempts(examDO.getMaxAttempts())
.build())
.paperId(new PaperId(examDO.getPaperId()))
.studentIds(JsonUtil.parseList(examDO.getStudentIds(), StudentId.class))
.createTime(examDO.getCreateTime())
.updateTime(examDO.getUpdateTime())
.build();
}
public ExamDO toDO(Exam exam) {
ExamDO examDO = new ExamDO();
examDO.setId(exam.getId() != null ? exam.getId().getValue() : null);
examDO.setTitle(exam.getTitle());
examDO.setDescription(exam.getDescription());
examDO.setStatus(exam.getStatus().getCode());
examDO.setStartTime(exam.getTime().getStartTime());
examDO.setEndTime(exam.getTime().getEndTime());
examDO.setPaperId(exam.getPaperId().getValue());
examDO.setStudentIds(JsonUtil.toJson(exam.getStudentIds()));
examDO.setAllowPause(exam.getConfig().isAllowPause());
examDO.setAllowRetake(exam.getConfig().isAllowRetake());
examDO.setRealTimeJudge(exam.getConfig().isRealTimeJudge());
examDO.setAutoSaveInterval(exam.getConfig().getAutoSaveInterval());
examDO.setMaxAttempts(exam.getConfig().getMaxAttempts());
examDO.setCreateTime(exam.getCreateTime());
examDO.setUpdateTime(exam.getUpdateTime());
return examDO;
}
}
六、领域事件处理
1. 领域事件定义
java
// 考试发布事件
public class ExamPublishedEvent extends DomainEvent {
private final ExamId examId;
private final String title;
private final List<StudentId> studentIds;
private final LocalDateTime startTime;
public ExamPublishedEvent(ExamId examId, String title,
List<StudentId> studentIds, LocalDateTime startTime) {
this.examId = examId;
this.title = title;
this.studentIds = studentIds;
this.startTime = startTime;
}
}
2. 事件处理器
java
// 处理考试发布事件
@Component
@Slf4j
public class ExamPublishedEventHandler {
private final NotificationService notificationService;
private final MessageProducer messageProducer;
@EventListener
public void handleExamPublishedEvent(ExamPublishedEvent event) {
log.info("处理考试发布事件: examId={}", event.getExamId());
// 1. 发送通知给学生
notificationService.sendExamNotification(
event.getExamId(),
event.getTitle(),
event.getStudentIds(),
event.getStartTime()
);
// 2. 发送消息到消息队列
messageProducer.sendExamPublishedMessage(
ExamPublishedMessage.builder()
.examId(event.getExamId().getValue())
.title(event.getTitle())
.startTime(event.getStartTime())
.build()
);
}
}
七、防腐层实现
1. 防腐层接口
java
// 用户上下文防腐层
public interface UserContextAdapter {
Student getStudent(StudentId studentId);
List<Student> getStudents(List<StudentId> studentIds);
Teacher getTeacher(TeacherId teacherId);
}
// 试卷上下文防腐层
public interface PaperContextAdapter {
Paper getPaper(PaperId paperId);
List<Question> getQuestions(List<QuestionId> questionIds);
}
2. 防腐层实现
java
// 通过RPC调用用户上下文
@Component
@RequiredArgsConstructor
public class UserContextAdapterImpl implements UserContextAdapter {
private final UserFeignClient userFeignClient;
@Override
public Student getStudent(StudentId studentId) {
UserDTO userDTO = userFeignClient.getUser(studentId.getValue());
return convertToStudent(userDTO);
}
private Student convertToStudent(UserDTO userDTO) {
return Student.builder()
.id(new StudentId(userDTO.getId()))
.name(userDTO.getName())
.account(userDTO.getAccount())
.classId(new ClassId(userDTO.getClassId()))
.gradeId(new GradeId(userDTO.getGradeId()))
.build();
}
}
八、测试策略
1. 单元测试
java
// 考试聚合单元测试
@ExtendWith(MockitoExtension.class)
class ExamTest {
@Test
void should_create_exam_successfully() {
// Given
CreateExamCommand command = CreateExamCommand.builder()
.title("期中考试")
.startTime(LocalDateTime.now().plusDays(1))
.endTime(LocalDateTime.now().plusDays(1).plusHours(2))
.paperId(new PaperId(1L))
.studentIds(Arrays.asList(new StudentId(1001L)))
.allowPause(true)
.allowRetake(false)
.realTimeJudge(true)
.maxAttempts(1)
.build();
// When
Exam exam = Exam.create(command);
// Then
assertNotNull(exam);
assertEquals("期中考试", exam.getTitle());
assertEquals(ExamStatus.DRAFT, exam.getStatus());
assertTrue(exam.getConfig().isAllowPause());
assertFalse(exam.getConfig().isAllowRetake());
}
@Test
void should_throw_exception_when_publish_draft_exam() {
// Given
Exam exam = createDraftExam();
// When & Then
assertDoesNotThrow(exam::publish);
}
@Test
void should_throw_exception_when_publish_started_exam() {
// Given
Exam exam = createStartedExam();
// When & Then
assertThrows(DomainException.class, exam::publish);
}
}
九、部署与监控
1. 健康检查端点
java
@RestController
@RequestMapping("/actuator")
public class HealthController {
@GetMapping("/health")
public ResponseEntity<HealthResponse> health() {
return ResponseEntity.ok(HealthResponse.healthy());
}
@GetMapping("/readiness")
public ResponseEntity<HealthResponse> readiness() {
// 检查数据库连接
// 检查Redis连接
// 检查消息队列连接
return ResponseEntity.ok(HealthResponse.ready());
}
}
十、总结:DDD落地最佳实践
✅ 成功关键
- 统一语言:团队使用业务术语沟通
- 领域驱动:从业务出发,不是从数据库出发
- 渐进式演进:不要一开始就设计完美模型
- 持续重构:随着业务理解加深,不断改进模型
🚨 常见陷阱
- 贫血模型:把领域对象当成数据容器
- 过大聚合:一个聚合包含太多实体
- 基础设施侵入领域层:在领域层直接操作数据库
- 忽略限界上下文边界:不同上下文过度耦合
📈 演进建议
第1阶段:识别核心子域,建立统一语言
第2阶段:设计核心聚合,实现基本功能
第3阶段:引入领域事件,解耦上下文
第4阶段:优化性能,引入缓存、异步
第5阶段:扩展功能,增加监控、分析
这个完整的DDD落地架构为考试系统提供了清晰的业务边界、可维护的代码结构和可扩展的技术架构,确保系统能够随着业务发展持续演进。