
1. 这份笔记不是“抄书”而是设计思维的实战切片“软件工程复习笔记——第三章 软件设计基础”这个标题乍看像学生期末前的临时抱佛脚但如果你真翻过几十份高校教材、带过十几届课程设计、参与过多个中型系统从0到1的架构演进就会发现第三章从来不是过渡章节而是整个软件工程链条里承上启下的“压力测试点”。它上接需求分析的模糊地带下连编码实现的确定性世界一边要对抗用户说不清、道不明的真实意图一边要为后续半年甚至三年的维护成本埋下伏笔。我带过的某高校课程设计小组里A同学花3天写完需求文档却在设计阶段卡了11天——不是不会画UML图而是反复推翻类图关系一个“订单状态”的变更到底该由订单对象自己管理还是交给独立的状态机服务要不要预留未来支持“跨境订单”的扩展点这些看似抽象的决策最终直接导致他后期重构了4次核心模块。这恰恰说明“设计基础”不是教你怎么画图而是训练你识别“哪里值得设计”、判断“哪种设计能扛住变化”。本笔记不罗列定义不复述教材段落而是把第三章拆解成一套可触摸、可验证、可复盘的思维工具包从如何用一张纸快速判断接口是否高内聚到为什么“依赖倒置”在真实项目里常被误用为“加个接口就叫解耦”再到那些教材绝不会写的“设计腐化信号”——比如当你开始给方法加越来越多的if-else分支来处理新场景时那不是业务复杂而是设计边界正在崩塌。适合刚学完UML但不敢动手画图的初学者也适合写了三年代码却总被质疑“架构感弱”的中级开发者。它不承诺让你秒变架构师但能帮你下次评审时准确指出“这个模块的职责太重建议拆出策略层”而不是只说“感觉这里有点乱”。2. 内容整体设计与思路拆解为什么放弃“概念堆砌”选择“问题驱动”2.1 教材逻辑 vs 真实开发节奏的断层翻开主流软件工程教材第三章通常按“设计目标→设计模型→设计方法→设计文档”线性展开先讲“高内聚低耦合”“信息隐藏”等原则再介绍结构化设计、面向对象设计等方法论最后落到UML图绘制规范。这种结构对知识体系构建很友好但和真实开发现场严重脱节。我在某公司主导一个物流调度系统重构时团队第一天讨论的不是“该用什么设计模式”而是“用户提了个新需求——司机端能查看历史运单的异常处理记录。这个功能是塞进现有‘运单详情页’里还是单独开个‘异常追踪’模块如果塞进去会不会让原本清晰的页面逻辑变得臃肿如果新开模块前端路由、后端API、数据库表关联哪一块改动成本最低”——你看真实的设计起点永远是具体问题而非抽象原则。因此本笔记彻底抛弃教材式编排以“典型设计困境”为锚点组织内容。比如“模块划分”这一节不先定义“模块”概念而是直接抛出三个高频冲突场景场景1用户要求“所有支付失败的订单必须自动触发短信提醒”但当前支付模块和通知模块完全隔离场景2运营部门需要“实时统计各城市当日取消率”但订单数据分散在MySQL城市维度在Redis缓存场景3法务提出“所有用户协议更新必须留痕”但协议文本硬编码在前端HTML里。每个场景后紧跟“设计决策树”第一步判断数据/行为归属谁拥有这个信息谁最该响应这个动作第二步评估变更频率这个规则未来半年会改几次第三步测算影响范围改一处要动几个服务几个数据库表。这种结构强迫读者把原则“翻译”成动作比如“高内聚”在这里具象为“把经常一起修改的代码放在一起”“低耦合”则变成“当A模块升级时B模块无需重新编译”。2.2 为什么聚焦“可验证的设计信号”而非“正确答案”软件设计没有标准答案只有“当前上下文下的较优解”。很多初学者陷入误区拼命背诵“工厂模式解决对象创建问题”“观察者模式解决松耦合”却在实际编码时发现——明明用了工厂但每次新增产品类型都要改工厂类明明用了观察者但事件一多调试时根本找不到哪个监听器在偷偷改状态。问题出在哪不是模式错了而是忽略了模式生效的前提条件。本笔记刻意弱化“模式名称”强化“模式存活条件”。例如讲解策略模式时不罗列UML类图而是给出三个必检信号算法差异是否稳定如果“折扣计算规则”下周可能被替换成AI动态定价那策略模式反而增加负担运行时切换是否必要如果所有用户永远用同一套积分规则硬套策略模式只是自找麻烦算法间是否有共享上下文如果不同折扣策略都需要访问同一个库存服务那策略类里塞满service注入就违背了“单一职责”。这种设计信号是我从某跨平台电商系统迭代中总结的。当时团队为“促销引擎”引入策略模式结果第一版上线后因未检查“算法差异稳定性”当市场部临时要求“大促期间禁用老用户专属折扣”时不得不紧急回滚——因为原策略类没预留开关只能全局停用。后来我们强制加入“策略开关配置中心”和“灰度发布通道”这才是策略模式真正落地的形态。笔记里所有案例都源自这类血泪教训目的就是让你拿到一个设计方案时第一反应不是“它叫什么名字”而是“它在哪些条件下会失效”。2.3 工具链选择为什么用“手绘草图轻量建模”替代重型UML工具很多教程强调“用StarUML画出完美类图”但现实是90%的设计讨论发生在白板、会议纪要或即时消息里。我在某实验室指导学生做智能灌溉系统时发现他们花2小时用工具画出“灌溉控制器”类图却在评审时被导师一句“这个类为什么要同时管土壤湿度采集和水泵开关采集数据不该由传感器驱动层负责吗”直接推翻。问题不在工具而在过度关注图形美观忽视了图形背后的语义约束。因此本笔记所有UML示例均采用手绘风格草图并标注关键约束文字在类图旁手写“注HumiditySensor不持有PumpController引用仅通过EventBus发布湿度事件”在序列图上用箭头旁小字注明“此处调用为异步超时500ms后降级返回默认值”。这种“草图约束注释”的组合比工具生成的精美图表更能暴露设计漏洞。更重要的是它训练一种习惯任何设计产出物必须附带“失效场景说明”。比如画完一个接口定义立刻补一句“此接口在并发量1000QPS时需增加本地缓存层否则DB连接池将耗尽”。这种思维才是设计能力的核心。3. 核心细节解析与实操要点从原则到代码的三道过滤网3.1 “高内聚”不是形容词而是可测量的代码特征教材常说“高内聚指模块内部元素紧密相关”但怎么判断“紧密相关”我的经验是用三道过滤网筛代码。第一道变更频率过滤网。打开Git历史统计一个类/模块近三个月的提交次数。如果某个类平均每周修改2次而同包其他类月均修改1次大概率它承担了过多职责。某次重构某公司CRM系统时发现CustomerService类年提交量达187次远超其他服务类平均23次。深入分析发现它同时处理客户建档、等级变更、积分发放、投诉归档——四个业务域的逻辑全挤在一起。解决方案不是简单拆分而是按“变更动因”归类客户建档和等级变更由销售流程驱动积分发放由营销活动驱动投诉归档由客服流程驱动。最终拆为SalesCustomerService、MarketingCustomerService、SupportCustomerService三个服务每个类年提交量降至30次以内。第二道依赖方向过滤网。检查一个模块的import列表。如果它既import了database.*又import了notification.*还import了ai.*说明它在协调多个技术栈而非专注业务逻辑。此时应引入“防腐层”Anti-Corruption Layer新建CustomerDataGateway封装数据库操作NotificationAdapter封装短信/邮件发送AIPredictor封装AI调用。CustomerService只依赖这三个门面接口。这样当AI服务升级时只需改AIPredictor实现CustomerService完全无感。第三道测试覆盖过滤网。运行单元测试观察一个测试类是否需要mock大量外部依赖。如果测试OrderProcessor要mock支付网关、库存服务、物流接口、风控系统说明它耦合了太多外部系统。理想状态是OrderProcessor的单元测试只mockInventoryService因库存扣减是其核心逻辑其他如支付、物流应通过集成测试覆盖。这倒逼你思考哪些依赖是“必须同步知道结果”的如库存不足则不能下单哪些是“异步通知即可”的如支付成功后发短信。提示内聚度量化工具推荐。IntelliJ IDEA内置的“Code Metrics”可一键生成类复杂度、方法数、依赖数报表开源工具SonarQube的“Duplicated Lines”指标能暴露重复逻辑——高重复率往往意味着内聚不足相同逻辑散落在多个类中。3.2 “低耦合”的实操陷阱为什么“加接口”不等于“解耦”初学者常犯的错误是为每个类都加一层接口以为这就是低耦合。比如PaymentService类立刻配上PaymentServiceInterface然后所有调用方都依赖接口。这看似正确但忽略了一个致命问题接口的粒度是否匹配调用方的真实需求某支付系统曾出现经典案例PaymentServiceInterface定义了12个方法包括createOrder()、queryStatus()、refund()、cancel()、getBill()等。但前端App只用到createOrder()和queryStatus()后台对账系统只用到getBill()风控系统只用到queryStatus()。结果是当支付网关升级refund()方法签名变更时所有依赖该接口的模块包括完全不用退款功能的App都必须重新编译、测试、发布。真正的低耦合是让调用方只看到它需要的部分。解决方案是“角色接口”Role InterfaceApp模块依赖OrderCreationService接口仅含createOrder()、queryStatus()对账系统依赖BillingService接口仅含getBill()风控系统依赖PaymentQueryService接口仅含queryStatus()。PaymentServiceImpl同时实现这三个接口。这样refund()的变更只影响PaymentQueryService的实现其他接口完全不受波及。这种设计在某银行核心系统升级时救了大忙——当清算模块接口调整时仅需修改ClearingService实现贷款审批模块只依赖LoanApprovalService零感知。注意接口不是越多越好。每个接口必须有明确的业务角色。如果发现两个接口方法高度重叠如OrderCreationService和PaymentQueryService都包含queryStatus()说明角色划分过细应回归业务本质它们是否代表同一类使用者如果是合并为PaymentInteractionService更合理。3.3 设计模式落地的“三问法”避免模式滥用设计模式是经验结晶但生搬硬套会适得其反。我总结出落地前必问的三个问题第一问这个模式解决的痛点当前是否存在观察者模式常被用于“订单创建后通知库存、物流、积分”。但若系统日均订单仅50单且库存、物流、积分服务都在同一进程内用观察者模式反而增加线程调度开销。此时简单的同步方法调用inventoryService.deductStock(order)更高效。模式的价值在于应对规模和复杂度而非装饰代码。第二问模式引入的间接层是否带来可接受的性能损耗策略模式通过接口解耦算法但每次调用需经过接口寻址。在某高频交易系统中我们曾用策略模式实现“价格计算”结果压测发现策略分发耗时占总计算时间的35%。最终改为“编译期策略”用枚举定义策略类型switch-case直接调用具体方法性能提升4倍。第三问模式是否增加了理解成本而未显著降低维护成本模板方法模式要求子类实现抽象方法但若子类逻辑极其简单如只有一行return语句那抽象父类就成了累赘。某IoT设备管理平台中DeviceCommandHandler抽象类定义了parse()、validate()、execute()三个钩子但80%的子类parse()只是return JSON.parse(payload)validate()为空。最终我们删除抽象类改为函数式编程handleCommand(payload, { parse: jsonParse, validate: noop, execute: deviceControl })。代码行数减少60%新人上手时间从2天缩短至2小时。4. 实操过程与核心环节实现从需求到设计文档的完整推演4.1 案例背景一个真实的“小而痛”需求某社区团购平台提出需求“团长可在小程序端发起‘拼团失败自动退款’即若拼团在24小时内未达最低成团人数系统需自动将已付款用户的钱原路退回。”表面看是简单功能但涉及多个系统边界小程序前端展示拼团状态、触发退款按钮订单服务存储拼团订单、支付状态支付网关执行微信/支付宝退款通知服务向用户发送退款成功短信财务系统更新对账流水。需求文档只有一句话但设计阶段必须回答谁负责检测“拼团失败”谁触发退款退款失败如何补偿财务流水如何生成下面演示完整推演过程。4.2 步骤1识别核心实体与边界30分钟白板推演拿出一张A4纸画出四个区域用户域团长发起者、参团用户退款接收方业务域拼团活动含成团规则、截止时间、订单含支付状态、参团人数系统域订单服务CRUD订单、支付网关调用退款API、通知服务发短信、财务系统写流水外部域微信支付平台、短信运营商。用箭头连接实体标注交互动作团长 → 拼团活动发起“设置自动退款”拼团活动 → 订单查询“当前参团人数是否最低成团人数”订单 → 支付网关调用“退款接口”支付网关 → 微信支付平台实际执行退款支付网关 → 订单返回退款结果订单 → 通知服务发送“退款成功”订单 → 财务系统写入“退款流水”。关键发现“检测拼团失败”不应由订单服务主动轮询资源浪费而应由拼团活动自身在“截止时间到达”时触发。这引出第一个设计决策引入“定时任务中心”由它在拼团截止时刻向“拼团活动服务”发送事件。4.3 步骤2定义核心接口与契约2小时文档编写基于白板推演定义最小可行接口拼团活动服务接口public interface GroupActivityService { // 检测并处理到期拼团 void processExpiredActivities(LocalDateTime now); // 获取拼团详情含当前人数、成团状态 GroupActivityDetail getDetail(String activityId); }订单服务接口精简版只暴露退款所需public interface OrderRefundService { // 发起退款幂等 RefundResult initiateRefund(String orderId, String refundReason); // 查询退款状态 RefundStatus queryRefundStatus(String refundId); }关键契约约束initiateRefund()必须是幂等的同一orderId多次调用只产生一笔退款记录processExpiredActivities()的now参数必须由定时任务中心传入禁止服务内调用System.currentTimeMillis()影响测试所有接口返回值必须包含traceId便于全链路追踪。实操心得接口定义完成后立即用Postman写一个“契约测试”模拟定时任务中心调用processExpiredActivities()验证拼团活动服务是否正确调用OrderRefundService.initiateRefund()。这个测试不关心内部实现只验证接口间协作是否符合约定。它能在编码前就暴露设计漏洞比如发现“拼团活动服务需要知道订单ID但当前设计中它只持有活动ID”。4.4 步骤3绘制关键流程图与异常路径1小时手绘不画完整UML只聚焦两个核心流程主流程自动退款触发定时任务中心 → (触发) 拼团活动服务.processExpiredActivities() → 拼团活动服务.getDetail(activityId) → 判断currentParticipants minParticipants ? → 是 → 拼团活动服务调用 OrderRefundService.initiateRefund(orderId) → 否 → 结束异常路径退款失败补偿OrderRefundService.initiateRefund() 返回 FAILURE → 拼团活动服务记录失败事件到DB含retryCount1 → 定时任务中心每5分钟扫描失败事件retryCount3则重试 → retryCount3触发告警并人工介入手绘时特别标注所有箭头旁写明“同步调用”或“异步消息”数据库操作旁注明“事务边界”如initiateRefund()必须在订单服务事务内完成告警处写明“发送企业微信机器人消息值班工程师”。这种聚焦关键路径的手绘比画满屏的UML更有效。某次线上事故中正是靠这张图我们3分钟定位到退款失败后拼团活动服务未正确更新DB中的retryCount字段导致重试机制失效。4.5 步骤4生成可执行设计文档30分钟Markdown输出最终交付的设计文档不是Word长篇大论而是结构化Markdown包含背景与目标一句话说明解决什么问题核心接口定义用代码块展示含参数、返回值、契约约束关键流程图Mermaid语法但本笔记禁用故用纯文本描述缩进异常处理矩阵表格列出所有可能异常、处理方式、负责人监控指标如“退款成功率99.5%时告警”“单次退款耗时3s时告警”上线Checklist如“确认定时任务中心已配置拼团截止时间触发器”“验证退款回调地址白名单已添加”。这份文档前端、后端、测试、运维都能直接使用。测试同学根据“异常处理矩阵”写用例运维同学根据“监控指标”配Prometheus告警无需二次解读。5. 常见问题与排查技巧实录那些没人告诉你的“设计腐化”信号5.1 信号1UML图越来越“完美”但代码越来越难改现象团队画出的类图、序列图非常规范但每次需求变更开发同学都说“要改好多地方”。根因分析设计图脱离了代码演进的实际约束。比如类图中User类与UserProfile类用组合关系实心菱形表示UserProfile生命周期由User控制。但现实中UserProfile数据可能被ReportService独立查询甚至被大数据平台抽取。此时组合关系就错了应改为关联关系开放箭头。排查技巧定期运行“依赖反查”。用IDEA的“Find Usages”功能右键点击一个类选择“Find Usages in Project”。如果发现UserProfile被ReportService、AnalyticsJob、ExportTask等多个非用户域模块直接new或注入说明设计图中的“组合”假设不成立必须降级为“关联”。5.2 信号2接口文档频繁更新但没人敢动实现现象Swagger文档每周更新但开发同学抱怨“改一个字段要同步改DTO、VO、Entity、Mapper、Controller、Service、Test改完怕出错干脆不改”。根因分析违反了“统一语言”Ubiquitous Language原则。领域模型如Order在不同层被赋予不同含义数据库表order字段是status_codeDTO里叫orderStatus前端叫state导致每次变更都要做N次映射。解决方案推行“领域模型穿透”。在某电商平台我们规定数据库表字段名 Java Entity字段名 DTO字段名 API JSON字段名全部小写下划线所有转换逻辑收口到Mapper层且Mapper只做字段名一致的拷贝不做业务逻辑业务逻辑只在Service层处理。结果当订单状态新增“已转售后”时只需改Entity的status_code枚举、加一条Mapper映射、在Service里加状态判断其他层零修改。5.3 信号3新功能总在“已有模块”里打补丁而非新建模块现象一个叫CommonUtils的工具类从最初10个方法膨胀到327个包含字符串处理、日期计算、HTTP请求、JSON解析、文件上传……根因分析缺乏“模块生长法则”。工具类本应只包含与“通用”强相关的逻辑如StringUtils.isEmpty()但当出现“微信支付签名生成”时有人觉得“也是工具”就塞进去。久而久之CommonUtils成了设计黑洞。治理技巧建立“模块准入门槛”。任何新功能想进入已有模块必须回答这个功能是否被该模块的所有现有用户使用微信支付签名只被支付模块用不应进CommonUtils这个功能的变更频率是否与模块内其他功能一致支付签名算法每年变字符串处理十年不变这个功能的技术栈依赖是否与模块一致CommonUtils只依赖JDK微信支付需依赖SDK不满足任一条件就必须新建模块。我们据此拆出了PaymentSignatureUtils、FileUploadUtils、HttpRetryClient等专用模块CommonUtils回归纯净。5.4 信号4设计评审会上大家争论“该用什么模式”却没人问“用户要什么”现象为一个简单的“导出Excel”功能团队争论该用策略模式支持CSV/PDF/Excel、模板方法固定流程子类实现格式化、还是建造者模式逐步构建导出配置。根因分析混淆了“设计复杂度”与“业务复杂度”。导出功能本身业务逻辑极简查数据→格式化→写文件强行套模式只会增加理解成本。黄金法则当业务逻辑少于5个判断分支、少于3个外部依赖、且未来半年无扩展计划时拒绝任何设计模式用最直白的代码实现。某次我们坚持用ExportService.exportToExcel(ListData data)一行搞定上线后运营同学反馈“希望加个进度条”我们只加了ProgressCallback参数全程未动架构。而隔壁组用策略模式做的导出为加进度条重构了3个策略类和工厂。实操心得我给自己定的“设计红线”是如果一个功能我能用不到20行代码不含注释清晰表达其全部逻辑那就别画图、别建模、别套模式。真正的设计能力是知道什么时候不做设计。