资讯详情

资讯详情

【DDD】用在线考试系统串联DDD核心概念

理解DDD的单个概念只是开始看到它们如何在一个真实系统中协同工作才是关键。让我们以构建一个“在线考试系统”为例将这些概念串联起来看它们如何从业务理解一步步转化为代码结构。第一步划定领域——我们到底在解决什么问题领域是起点。我们不是在构建“又一个Web应用”而是在解决“在线考试”这个特定的业务问题世界。这包括了出题、组卷、考试、交卷、判分、成绩分析等一系列业务事实和规则。领域驱动意味着我们所有的设计将优先围绕“如何优雅地表达一次考试”展开而不是“如何设计REST接口或数据库表”。第二步对齐认知——建立关于考试的“统一语言”在项目启动时我们和产品经理、教研专家、未来的监考员坐在一起对齐关键术语形成我们的统一语言考试 (Exam)指一次完整的考核事件有起止时间、特定的试卷、一群考生。试卷 (ExamPaper)一套题目的集合是考试的蓝图。考生 (Student/Examinee)参加考试的用户。答题卡 (AnswerSheet)考生在一次考试中提交的所有答案的集合。得分 (Score)一道题或整场考试的分数。这些术语将贯穿于我们的会议、需求文档、类名、方法名和接口设计中。这确保了当开发说“保存答题卡”时所有人理解的都是同一个业务概念。第三步拆分问题——识别核心子域“在线考试”这个大领域可以按业务关注点拆分为几个子域考题库子域负责题目的创建、分类、审核、版本管理。考试执行子域 (我们的核心)负责考试的生命周期——从创建、开始、进行中到结束。成绩分析子域负责计算得分、生成报告、统计分析。每个子域解决不同的问题。DDD建议我们识别出核心子域带来竞争优势的部分此处是考试执行并投入最多精力。子域的划分是从业务本质出发的而不是从技术模块“用户模块”“题目模块”出发。第四步划定语义边界——聚焦“考试执行”限界上下文现在我们聚焦在最核心的“考试执行子域”。我们需要为其建立一个限界上下文 (Bounded Context)。这个上下文规定在这里“试卷”“考生”“答题卡”等术语拥有特定的、精确的含义。例如在“考试执行上下文”中ExamPaper试卷它代表一份“正在被某位考生作答的、包含当前题目快照和标准答案”的实体。它关注考试中的状态是否已开始、是否已交卷。对比之下在“考题库上下文”中Question题目可能更关注题干内容、难度系数、知识点标签和版本历史。限界上下文保护了“考试执行”内部的模型纯净性。我们不必关心题目复杂的编辑历史那是考题库上下文的事我们只关心本次考试用的题目快照。这个边界防止了概念的混淆。第五步构建模型基础——实体与值对象在“考试执行上下文”内部我们开始用代码构建模型。首先识别实体和值对象。实体是有身份、有生命周期的对象。例如ExamPaper试卷是一个实体。它拥有唯一ID (ExamPaperId)它的状态会从“已创建”变为“已开始”最后变为“已结束”。我们关心“这是不是张三正在考的那份特定的试卷”。Student考生在考试上下文中通常也是一个实体StudentId标识。值对象没有身份仅由属性定义且通常不可变。例如ExamPaperId、StudentId本身可以是值对象封装生成规则和校验。更典型的例子是StudentInfo考生信息它可能包含姓名、学号。在考试上下文中我们不关心考生实体的完整生命周期如修改密码只关心本次考试所用的信息快照因此适合建模为值对象。Money金额也是一个经典的值对象例子。第六步保护业务规则——定义聚合与聚合根一次考试中核心规则是考生提交答案后不能再修改。同时一道题的答案和这道题的标准答案、分值紧密相关。为了维护这种一致性我们定义聚合。我们可以将ExamPaper试卷、它所包含的QuestionSnapshot题目快照值对象、以及对应的AnswerSheet答题卡值对象组合成一个聚合。这个聚合维护着“已交卷的试卷不能再修改答案”这个核心不变条件。在这个聚合中ExamPaper是聚合根。外部世界如应用服务想要添加答案、交卷都必须通过ExamPaper这个唯一入口来调用方法如examPaper.answerQuestion(...)或examPaper.submit()。聚合根ExamPaper负责协调内部对象AnswerSheet的变化并确保业务规则不被破坏。外部不能直接操作AnswerSheet。第七步处理跨对象逻辑——引入领域服务有些业务行为无法自然地归属于某个实体。例如“判分”它需要访问ExamPaper获取题目快照和标准答案。它需要访问AnswerSheet获取考生答案。它需要应用复杂的判分规则如多选题漏选得一半分。将score()方法强硬地塞进ExamPaper或AnswerSheet都不合适。这时我们就创建一个ExamScoringService领域服务。它接收ExamPaper和AnswerSheet作为参数执行判分逻辑返回一个ScoreReport值对象。领域服务承载了纯粹的、无状态的业务规则。第八步编排用例流程——编写应用服务现在需要一个协调者来组织一次完整的“提交试卷并判分”流程。这是应用服务的职责。// 应用服务SubmitExamServicepublicclassSubmitExamService{privateExamPaperRepositoryrepository;privateExamScoringServicescoringService;privateMessageBusmessageBus;TransactionalpublicScoreReportsubmitExam(StringexamPaperId,StringstudentId){// 1. 通过仓储加载聚合ExamPaperexamPaperrepository.findById(newExamPaperId(examPaperId));// 2. 调用聚合根的领域行为业务规则在此执行examPaper.submit();// 内部会检查是否已交卷等规则// 3. 调用领域服务执行业务逻辑ScoreReportreportscoringService.score(examPaper);examPaper.recordScore(report);// 4. 通过仓储保存聚合的最新状态repository.save(examPaper);// 5. 发布领域事件触发后续流程如发送通知messageBus.publish(newExamSubmittedEvent(examPaperId,studentId,report));returnreport;}}应用服务SubmitExamService负责流程编排和技术协调事务、消息、仓储调用但不包含核心判分规则。规则在领域模型ExamPaper.submit()和领域服务ExamScoringService中。第九步持久化抽象——定义仓储注意应用服务通过ExamPaperRepository来加载和保存ExamPaper。仓储是面向聚合根的持久化抽象。它的接口定义在领域层publicinterfaceExamPaperRepository{ExamPaperfindById(ExamPaperIdid);voidsave(ExamPaperexamPaper);}其具体的数据库实现用JPA、MyBatis或Redis则放在基础设施层。这保证了领域层包含实体、值对象、领域服务完全不依赖具体技术细节只关注业务逻辑。总结它们共同构成了领域模型至此我们从理解“在线考试”这个领域出发通过统一语言对齐认知拆分了子域并在核心的“考试执行”限界上下文内用实体ExamPaper和值对象AnswerSheet,StudentInfo构建了模型的核心。为了保护“交卷后不可修改”的规则我们引入了聚合并以ExamPaper为聚合根。对于跨对象的“判分”逻辑我们使用了领域服务。然后由应用服务来编排整个提交流程并通过仓储接口与持久化机制交互。所有这些概念有机组合最终形成了一个丰富的、能够表达业务规则、反映业务语义的领域模型。这个模型是“活”的业务行为被封装在合适的对象聚合根、领域服务内部而不是散落在各处。这就是DDD通过这一系列概念引导我们达成的最终目标让软件的结构与复杂业务的结构实现深层共鸣。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →