
1. 这不是抄书笔记而是软件设计的“施工蓝图”思维训练“软件工程复习笔记——第三章 软件设计基础”光看标题很多人第一反应是又一本堆砌概念的应试材料。但我在带某高校软件工程实训课、同时参与多个中小规模系统重构项目的过程中反复验证过——这一章恰恰是区分“能写代码的人”和“能交付可靠系统的人”的分水岭。它不讲语法不教框架专攻一个最朴素却最致命的问题当需求文档扔到你桌上你怎么把一张白纸变成一张可施工、可验收、可维护的蓝图这个“蓝图”就是软件设计。它不是程序员闭门造车的产物而是开发团队、测试人员、产品经理甚至客户之间共享的“通用语言”。我见过太多项目在编码阶段才暴露出模块边界模糊、数据流向混乱、异常处理无从下手的问题根源全在设计阶段用“大概齐”“应该没问题”代替了严谨的结构推演。本章的核心价值从来不是让你背下“高内聚低耦合”的定义而是训练你用一套可验证、可沟通、可落地的工具在写第一行代码前就把系统的骨架、筋络和关键接口钉死。它解决的是“为什么这个功能要拆成三个类而不是两个”“为什么这里必须用观察者模式而不是硬编码回调”“为什么数据库表字段长度定为255而不是500”这类真实决策问题。适合刚学完面向对象编程、正准备接触实际项目的同学也适合工作两三年、总在改Bug中疲于奔命、想系统梳理设计逻辑的开发者。如果你还在靠“感觉”划分模块、靠“经验”选设计模式、靠“试错”调接口那么这一章的每一个知识点都是帮你把隐性经验显性化、把模糊直觉结构化的扳手。2. 内容整体设计与思路拆解从“画图”到“建模”的认知跃迁2.1 为什么设计不能跳过——一个被低估的“成本杠杆点”很多初学者认为“设计画UML图”于是花大量时间在StarUML里拖拽类图结果图很美代码一写就崩。这暴露了对设计本质的误解。软件设计真正的核心是在不确定性中建立确定性约束。需求会变、技术会迭代、人员会流动但一个经过审慎设计的系统其核心抽象、关键契约和主要交互路径能在相当长的时间内保持稳定。我参与过一个电商后台订单模块的重构旧系统没有设计文档所有逻辑散落在几十个Service方法里。新团队接手后光是理清“一个订单状态变更会触发多少个异步任务、更新多少张表、通知几个外部系统”就花了三周。而新设计稿里仅用一张状态转换图State Diagram和一张组件交互时序图Sequence Diagram就把整个流程的触发条件、责任主体、失败回滚点全部固化下来。后续开发中任何新增状态比如“已预约发货”都必须严格遵循这张图的规则避免了随意添加分支导致的逻辑爆炸。这个过程让我深刻体会到设计不是增加步骤而是用前期10%的建模投入规避后期90%的返工成本。它像建筑行业的结构计算图纸上多算一遍承重工地就少拆一次承重墙。2.2 本章内容的三层递进结构从静态骨架到动态脉搏本章内容绝非零散知识点的罗列而是一个严密的三层认知模型每一层都建立在前一层的基础之上第一层静态结构建模类图、包图、组件图这是系统的“骨骼”。重点不是画出所有类而是识别出核心实体Entity、控制逻辑Controller、数据访问Repository三类关键角色并明确它们之间的依赖方向。例如在用户登录场景中“User”实体类不应直接持有“DatabaseConnection”对象而应通过“UserRepository”接口进行交互。这种分离让“User”类只关心业务规则如密码强度校验而数据存储细节被隔离在Repository实现中。我常提醒学生画类图时先问自己“如果明天数据库从MySQL换成MongoDB哪些类必须改哪些类可以完全不动”答案指向的就是设计是否合理。第二层动态行为建模用例图、活动图、状态图、时序图这是系统的“脉搏”。静态结构告诉你“有什么”动态图则回答“怎么动”。其中时序图Sequence Diagram是最具实操价值的工具。它强制你思考一个用户点击“提交订单”按钮后前端发什么请求后端哪个Controller接收它调用哪个ServiceService又如何协调InventoryService扣减库存、PaymentService发起支付、NotificationService发送短信每一步的输入输出、成功/失败分支、超时处理都必须清晰标注。我在某次代码评审中发现一个支付回调接口没有定义“支付平台返回‘处理中’状态时系统应如何响应”结果线上出现大量订单卡在“支付中”状态无法推进。补上时序图后这个问题立刻被定位为设计缺失而非代码Bug。第三层质量属性保障设计原则、模式、架构风格这是系统的“免疫力”。前两层解决了“功能正确”这一层解决“长期健康”。比如“开闭原则”Open-Closed Principle要求模块对扩展开放、对修改关闭。实践中这意味着当需要支持微信支付时不应去修改原有的PaymentService类而应新增一个WechatPaymentAdapter类让它实现统一的PaymentProcessor接口。这样主业务逻辑无需改动测试范围也大幅缩小。我曾用这个原则帮一家公司把支付渠道接入周期从平均5天缩短到8小时——因为所有新渠道都复用同一套回调处理、日志记录、监控埋点逻辑唯一变化的只是适配器内部的API调用。2.3 为什么避开“纯理论陷阱”——设计必须锚定具体场景市面上很多资料把设计原则讲得玄之又玄动辄“万物皆对象”“一切皆服务”。但真实世界里没有银弹。我坚持在教学和项目中贯彻一个铁律所有设计决策必须绑定到一个具体的、可描述的场景Scenario。比如讲解“策略模式”时我不说“它定义了算法族”而是抛出问题“促销系统要支持满减、折扣、赠品三种活动且运营人员需在后台随时切换当前生效的活动类型。如果用if-else硬编码每次加新活动都要改主逻辑怎么办”这时策略模式的价值才真正浮现——它让“选择哪种促销算法”这个变化点从代码中抽离出来变成配置项。再比如讲“单例模式”很多教程强调“保证全局唯一实例”但实际项目中我更关注它的副作用单例对象的状态会成为隐式共享状态极易引发并发问题。因此我总会补充一句“除非你100%确认这个单例只读、无状态或者已做完备的线程安全封装否则优先考虑依赖注入DI容器来管理生命周期。”这种基于场景的解读让抽象原则瞬间有了血肉和温度。3. 核心细节解析与实操要点把设计原则变成检查清单3.1 高内聚低耦合不是口号是可量化的代码指标“高内聚低耦合”被提得最多也最容易被滥用。它绝非主观感受而是有明确的量化锚点。我给学生和团队提供了一套可执行的检查清单每一条都对应着代码层面的具体表现内聚性检查High Cohesion✅单一职责验证一个类/模块的每个公有方法是否都服务于同一个明确的业务目标例如“OrderService”类里的方法是否都围绕“订单生命周期管理”创建、支付、发货、取消如果里面混入了“发送营销邮件”或“计算用户积分”的方法说明职责已溢出。✅数据亲密度检验类内部使用的私有变量是否被其大部分公有方法共同访问如果一个类有10个私有字段但只有2个方法用到其中5个另外3个方法只用到另3个剩下2个字段几乎不用这就是典型的“数据内聚度低”暗示应拆分。✅变更影响面评估当某个业务规则变更如“订单超时未支付自动取消时间从30分钟改为15分钟”需要修改几个类如果只涉及“OrderTimeoutPolicy”一个类内聚度高如果要改OrderService、PaymentService、NotificationService三个类说明规则分散内聚不足。耦合性检查Low Coupling✅依赖注入验证类是否通过构造函数或Setter方法接收其依赖对象如Repository、Logger而不是在内部new一个具体实现这是解耦的第一道防线。✅接口抽象度审查依赖的接口是否足够抽象例如一个“通知服务”接口如果命名为EmailNotifier那就错了——它把实现方式Email暴露给了使用者应改为NotificationService并定义send(message: string, recipient: string)方法。✅编译依赖分析查看Maven/Gradle依赖树一个模块是否只依赖其直接协作方的API模块如order-api而完全不依赖其内部实现模块如order-impl后者是典型的“源码级耦合”会导致牵一发而动全身。提示我常用一个生活化类比帮助理解——把系统想象成一家餐厅。“高内聚”就像后厨的“切配组”所有成员只负责食材清洗、切割、腌制绝不插手炒菜或传菜“低耦合”则像“切配组”和“炒菜组”之间只通过标准化的“备好料的托盘”交接切配组不管炒菜组用什么锅、火候多大炒菜组也不管切配组用什么刀、洗几遍菜。托盘就是接口切配和炒菜就是各自的内聚职责。3.2 UML图的取舍之道哪些图必须画哪些图可以省UML有14种图但实际项目中90%的沟通只需3种。画图不是目的减少沟通成本才是。我的经验是必须画的图沟通刚需用例图Use Case Diagram面向客户和产品经理。只画Actor用户、管理员、支付平台、核心用例登录、下单、查询订单、以及用例间的关系包含、扩展。它回答“系统为谁提供什么价值”是需求确认的终极依据。我坚持所有需求评审会必须带着用例图逐条确认“这个用例是否覆盖了客户说的场景”。类图Class Diagram面向开发团队。只画核心领域类User、Order、Product、关键关系关联、聚合、依赖、以及最重要的——接口Interface和抽象类Abstract Class。例如PaymentProcessor接口必须清晰定义而AlipayProcessor、WechatProcessor等实现类初期只需标出“实现自PaymentProcessor”不必展开所有方法。时序图Sequence Diagram面向具体功能开发。针对每一个复杂交互如“用户退款”画出参与者Frontend、OrderController、RefundService、PaymentGateway、生命线、激活框、消息箭头同步/异步、以及关键的返回值和异常分支。它让“谁在什么时候调用谁、传什么、收什么”一目了然是Code Review的黄金标准。可选画的图按需使用活动图Activity Diagram适合描述含多重条件判断、并行分支的业务流程如“风控审核流程”。但如果流程简单如“用户注册”用文字伪代码if (email exists) throw; else create user; send email;更高效。状态图State Diagram对状态机复杂的对象如订单、工单必不可少。但对于“用户”这种状态简单的实体画状态图反而增加负担。部署图Deployment Diagram在微服务架构或需要明确物理部署如数据库主从、缓存集群时才画。单体应用初期一张“Nginx - App Server - MySQL”的简笔画足矣。注意所有UML图都必须附带文字说明图是骨架文字是血肉。例如时序图旁必须注明“消息‘processRefund()’的超时时间为5秒超时后自动触发补偿事务‘cancelRefund()’”。没有文字的图如同没有注释的代码半年后连作者都看不懂。3.3 设计模式的“防坑指南”别让模式成为新枷锁设计模式是前人踩坑后的结晶但生搬硬套会制造新坑。我总结了三个高频误用场景及应对方案误用场景一为用而用过度设计现象一个只有3个方法的工具类硬套“工厂模式”生成实例一个单线程脚本引入“观察者模式”监听事件。应对坚守“YAGNI”You Arent Gonna Need It原则。问自己“这个模式解决的痛点现在真的存在吗还是我预判未来会有” 如果答案是否定的坚决不用。我曾重构一个日志工具原作者用“策略模式”支持不同日志格式JSON、XML、Plain Text但项目99%的日志都走JSON。我直接删掉策略框架只保留JSON实现代码行数减少60%可读性飙升。未来真需要XML时再加不迟。误用场景二模式嵌套复杂度爆炸现象在一个“命令模式”里命令对象本身又用“装饰器模式”包装装饰器里再用“代理模式”拦截形成“俄罗斯套娃”。应对设定“模式深度”红线。我的团队规定任何业务逻辑链路设计模式嵌套不得超过2层。超过即视为设计失败必须重构。例如“支付命令”可以用“命令模式”封装其内部“重试逻辑”可用“装饰器模式”增强但装饰器不能再套其他模式。复杂逻辑应下沉到Service层用清晰的if-else或状态机实现而非堆砌模式。误用场景三忽略语言特性强行移植现象Java程序员写Python坚持用“单例模式”确保全局唯一却忽略了Python的模块本身就是天然单例或在JavaScript中为实现“模板方法”硬生生写一堆抽象类和继承而忘了JS的函数是一等公民直接传入回调函数更简洁。应对模式是思想不是代码模板。理解其背后意图如“模板方法”是为了复用算法骨架、将可变部分延迟到子类然后用当前语言最自然的方式实现。在Python中“模板方法”可以是一个函数参数是几个可选的hook函数在JS中可以是一个配置对象包含beforeExecute、execute、afterExecute等字段。4. 实操过程与核心环节实现从需求到设计稿的完整推演4.1 实战案例设计一个“在线考试系统”的核心模块为彻底展现设计过程我们以一个真实教学场景为例某高校需要一个轻量级在线考试系统核心需求包括教师创建试卷含单选、多选、判断题、学生限时作答、系统自动评分客观题、教师手动批阅主观题、生成成绩报告。我们将全程演示如何从需求文本一步步产出可执行的设计稿。第一步需求萃取与用例建模1小时不急于画图先做“需求翻译”。把模糊描述转为可验证的用例原始需求“教师可以创建试卷” → 用例CreateExamPaperActorTeacher前置条件已登录后置条件试卷保存至数据库状态为“草稿”。原始需求“学生作答” → 拆解为StartExamSession开始考试启动倒计时、SubmitAnswer提交单题答案、EndExamSession交卷触发评分。原始需求“自动评分” → 用例AutoGradeObjectiveQuestionsActorSystem触发条件EndExamSession完成且试卷含客观题。产出《用例图》核心部分ActorTeacher、Student、System系统自身作为Actor用于定时任务、自动评分等后台行为用例CreateExamPaper、PublishExam发布试卷状态从草稿变启用、StartExamSession、SubmitAnswer、EndExamSession、AutoGradeObjectiveQuestions、ManualGradeSubjectiveQuestions、GenerateReport关系PublishExam包含CreateExamPaperEndExamSession扩展AutoGradeObjectiveQuestions仅当试卷含客观题时触发第二步领域建模与类图设计2小时基于用例识别核心领域概念ExamPaper试卷属性id,title,durationMinutes,status(Draft/Published/Closed),questions: ListQuestionQuestion题目抽象基类属性id,content,type(SINGLE_CHOICE/MULTI_CHOICE/JUDGMENT),scoreSingleChoiceQuestion单选题继承Question属性options: Liststring,correctOptionIndex: intExamSession考试会话属性id,studentId,examPaperId,startTime,endTime,answers: MapquestionId, AnswerAnswer答案属性questionId,selectedOptions: Listint,textContent: string主观题用关键设计决策与理由为什么Question是抽象类而非接口因为所有题目类型都有共通属性id,content,score且未来可能添加共通方法如getMaxScore()用抽象类可复用代码。为什么ExamSession不直接持有ListAnswer而用MapquestionId, Answer为支持“学生可随时跳题、回头修改”用Map可O(1)定位任意题目答案避免遍历List。为什么ExamPaper.status用枚举而非字符串枚举Draft/Published/Closed是编译期安全的防止statuspublised拼写错误导致逻辑漏洞。第三步关键交互建模时序图1.5小时聚焦最复杂的EndExamSession用例绘制时序图参与者StudentFrontend、ExamController、ExamSessionService、GradingService、ExamRepository、QuestionRepository核心消息流StudentFrontend-ExamController:endSession(examSessionId)ExamController-ExamSessionService:endAndGrade(examSessionId)ExamSessionService-ExamRepository:findById(examSessionId)ExamSessionService-QuestionRepository:findQuestionsByPaperId(paperId)ExamSessionService-GradingService:grade(session, questions)GradingService-ExamSessionService:return GradeResult(score, detailedFeedback)ExamSessionService-ExamRepository:saveGradeResult(result)关键标注步骤5中GradingService.grade()方法内部分支若题目类型为SINGLE_CHOICE调用gradeSingleChoice()若为JUDGMENT调用gradeJudgment()若为MULTI_CHOICE调用gradeMultiChoice()。步骤6返回的GradeResult必须包含isObjectiveGraded: boolean字段供前端决定是否显示“等待教师批阅”提示。第四步设计原则落地与质量保障1小时开闭原则GradingService.grade()方法采用策略模式。定义GradingStrategy接口SingleChoiceGradingStrategy、JudgmentGradingStrategy等实现类。新增题型如填空题时只需添加新策略类grade()方法主体无需修改。依赖倒置ExamSessionService不依赖GradingService的具体实现而是依赖GradingService接口。其构造函数接收GradingService gradingService由DI容器注入。单一职责ExamSessionService只负责考试会话的生命周期管理开始、结束、保存评分逻辑完全委托给GradingServiceGradingService只负责评分算法不关心会话状态如何保存。最终产出物一份包含用例图、核心类图、EndExamSession时序图、以及关键设计决策说明含上述原则应用的PDF设计稿。这份稿子就是后续所有开发、测试、评审的唯一依据。我要求团队任何代码提交必须能追溯到设计稿中的某一条用例、某一个类、某一条时序消息。这杜绝了“我觉得应该这样写”的随意性。4.2 工具链实战用PlantUML写出可执行的设计稿手动画图效率低、难协同、易过时。我全程使用PlantUML——一种用纯文本描述UML的开源工具它完美契合“设计即代码Design as Code”理念。文本可版本控制Git、可自动化渲染、可嵌入文档且学习成本极低。PlantUML类图实战startuml 定义接口 interface GradingStrategy { grade(answer: Answer, question: Question): GradeResult } 定义实现类 class SingleChoiceGradingStrategy { grade(answer: Answer, question: Question): GradeResult } class JudgmentGradingStrategy { grade(answer: Answer, question: Question): GradeResult } 建立关系 GradingStrategy |-- SingleChoiceGradingStrategy GradingStrategy |-- JudgmentGradingStrategy 定义服务类 class GradingService { - strategy: GradingStrategy grade(session: ExamSession, questions: ListQuestion): GradeResult } GradingService -- GradingStrategy : uses enduml将此文本保存为grading.puml用PlantUML插件VS Code/IntelliJ均支持一键生成高清类图。修改文本即修改设计所见即所得。PlantUML时序图实战startuml actor StudentFrontend participant ExamController participant ExamSessionService participant GradingService participant ExamRepository StudentFrontend - ExamController: endSession(examSessionId) ExamController - ExamSessionService: endAndGrade(examSessionId) ExamSessionService - ExamRepository: findById(examSessionId) ExamSessionService - GradingService: grade(session, questions) GradingService -- ExamSessionService: GradeResult(score, feedback) ExamSessionService - ExamRepository: saveGradeResult(result) 标注关键分支 alt 试卷含客观题 GradingService - GradingService: if question.type SINGLE_CHOICE end enduml实操心得PlantUML的威力在于“文本即设计”。我曾遇到一个紧急需求变更考试系统需支持“分段计时”如单选题10分钟多选题15分钟。传统画图工具需重绘整张时序图而PlantUML只需在文本中新增一行alt 分段计时模式分支重新渲染即可。更重要的是这份文本设计稿可以直接作为单元测试的用例描述——testEndSessionWithSegmentedTiming()的测试步骤就是时序图中的一条消息流。设计、开发、测试第一次真正对齐。5. 常见问题与排查技巧实录那些没人告诉你的设计暗礁5.1 “设计过度”与“设计不足”的临界点在哪里这是新手最常困惑的问题。我的判断标准非常务实看“第一个变更”需要修改多少地方。设计不足的典型信号当一个简单需求变更如“登录失败次数从3次改为5次”需要修改超过3个类且这些类分布在不同模块如LoginController、UserService、SecurityConfig说明职责分散设计不足。当团队成员对“这个功能该写在哪个类里”经常争论不休说明边界模糊缺乏清晰的模块划分。设计过度的典型信号当一个新功能如“增加一个简单的数据导出按钮”需要新增2个接口、3个实现类、1个配置类且其中大部分代码是模板化的如ExportService、CsvExportStrategy、ExcelExportStrategy但目前只用到CSV说明过度设计。当设计稿的页数远超代码行数且评审会上80%的时间在讨论“这个接口名该叫IDataExporter还是IExportableDataHandler”而非业务逻辑说明陷入形式主义。我的解决方案是“渐进式设计”MVP最小可行设计先用最直白的方式实现核心流程如导出功能先写一个CsvExporter.export(data)方法。变更驱动重构当第二个导出格式Excel需求来临时再提取IExporter接口将CSV和Excel实现分离。此时重构有明确目标不会凭空造轮子。模式引入时机只有当第三个导出格式PDF到来且发现三个实现类有大量重复代码如文件流打开/关闭、错误处理时才引入“模板方法模式”将公共逻辑提到抽象基类。这个过程让设计始终扎根于真实的业务需求而非臆想的“未来可能性”。5.2 如何说服非技术人员产品经理、客户接受设计评审技术人常抱怨“产品不理解设计的重要性”。但问题往往出在沟通方式。我从不向产品经理展示UML图而是用他们熟悉的语言和场景用“合同”比喻接口“您提的需求‘用户登录后首页显示最近3条订单’这就像一份合同。合同里必须写清楚‘谁’前端向‘谁’后端API要‘什么’订单列表数据‘什么格式’JSON含order_id, status, amount字段‘多久给’200ms内‘给不了怎么办’返回503错误码。这份合同就是我们的接口设计。现在我们签合同比等代码写完发现‘前端要的字段后端没给’再撕合同成本低得多。”用“装修图纸”比喻设计稿“您买房子肯定要看装修图纸再付款。图纸上标明了哪里是承重墙不能砸、哪里是水电管线不能乱打孔。我们的设计稿就是软件的装修图纸。比如‘订单状态变更’这条线就是一根关键的‘水电管线’它连接着支付、库存、物流。现在我们确认管线走向比等装修完发现‘物流系统接不上订单状态’再凿墙重布省时省钱。”用“风险清单”替代“技术术语”在评审会上我准备一份《设计风险清单》每条都对应业务影响风险点技术原因业务影响缓解措施订单状态变更未定义失败回滚时序图中缺少补偿事务分支用户支付成功但订单未创建导致资损在时序图中标注onFailure: triggerCompensation()试卷题目类型扩展困难Question类用if-else判断类型新增题型需修改核心类上线风险高改用策略模式新增题型只需加一个类这份清单让产品经理一眼看清“不签字的风险”而非“不懂的技术细节”。5.3 设计稿如何与代码保持同步——告别“设计即古董”设计稿最大的敌人是“过时”。我的团队严格执行“三同步”原则同步一设计稿即代码注释PlantUML文本文件.puml与源码放在同一Git仓库同一目录下。例如src/main/java/com/example/exam/service/GradingService.java的同级目录必有GradingService.puml。CI流水线如GitHub Actions会检查每次提交GradingService.java必须同时提交GradingService.puml否则构建失败。这强迫开发者在改代码的同时更新设计。同步二设计稿驱动测试用例每个核心时序图都对应一个JUnit测试类。例如EndExamSessionSequenceDiagram.puml对应EndExamSessionTest.java其测试方法名直接映射消息流testEndSessionTriggersGradingService()、testGradingServiceReturnsGradeResult()。测试失败首先检查是代码Bug还是设计稿已过时需更新图。同步三设计评审即代码评审Pull RequestPR中必须包含修改的代码对应的PlantUML设计稿如有变更一段文字说明“本次变更如何体现设计稿中的XX用例/XX类/XX时序消息”评审者不看代码行先看设计稿是否合理、是否与PR描述一致。一次PR两次评审设计合理性评审 代码实现评审。最后分享一个小技巧我在所有设计稿的末尾都加了一行小字“Last updated: [日期] by [作者]。下次更新触发条件当‘XXX需求’或‘YYY Bug’发生时。” 这行字像一个温柔的提醒告诉所有人设计不是尘封的档案而是活着的、呼吸着的系统生命体。它存在的唯一意义就是让下一行代码写得更稳、更快、更准。