
简介这份PPT是北京大学研究生软件工程课程中关于敏捷软件开发方法的课堂讲义面向软件工程专业研究生及希望系统理解敏捷思想的开发者与项目管理者。内容围绕敏捷开发的两类原则展开12条开发原则侧重项目组织与团队协作11条设计原则指导开发人员构建易维护、可扩展的软件结构其中5条针对软件“腐化”问题6条聚焦包的设计与集成技术。讲义还梳理了敏捷联盟宣言的四个核心价值观并结合Jack Reeves关于“源代码即设计”的观点讨论敏捷设计作为持续改进过程的目标与实现路径。资源包内含1个PPT文件整体约446KB以幻灯片形式呈现便于课堂讲解与自学梳理。目前已有117人学习适合需要快速把握敏捷开发原则体系、设计思想与宣言内涵的读者参考。1. 从一份课程 PPT 说起敏捷设计原则到底在解决什么问题很多团队嘴上喊着敏捷实际干的是“迭代式瀑布”——需求文档写三个月评审两周开发半年测试再压一个月。上线那天才发现用户真正要的功能只占交付清单的三成。这份来自某高校研究生软件工程课程的 PPT把问题拉回到了更底层的地方不是流程不够快而是设计本身在腐化。它系统梳理了敏捷开发的两条主线——12 条开发原则和 11 条设计原则前者管项目组织后者管开发人员的行为。其中 5 条设计原则专门对付软件“腐化”另外 6 条聚焦包的设计与集成技术。换句话说它不是在教你“怎么站会”而是在回答一个更硬核的问题当需求每周都在变怎么让代码结构不跟着烂掉。适合谁看正在从传统开发往敏捷转型的团队技术负责人、被“改一处崩三处”折磨过的后端开发以及需要给学生讲清楚设计原则落地路径的高校教师。2. 敏捷设计原则的底层逻辑为什么源代码才是唯一的设计文档2.1 从 Jack Reeves 的观点说起设计不是画图是写代码这份 PPT 里反复引用了一个 1992 年的观点“满足工程设计标准的唯一文档是源代码清单。”这句话放在今天看依然扎心。传统工程领域设计团队出图纸制造团队按图施工两者分离。但软件不一样——编译器和链接器让“构建”变得极其廉价真正昂贵的是设计本身。PPT 里给了一个关键判断编程就是一种设计活动而且编码比高层设计图更能揭示疏漏和额外需求。你画十张 UML 类图不如写两百行能跑的代码更能暴露接口不匹配的问题。这引出一个很实际的推论高层设计人员应该尽早参与详细设计而不是把类图扔给开发就完事。PPT 里提到软件开发过程模型的演化趋势就是支持在生命周期更早的阶段开始编码让验证和测试更早介入。我见过不少团队把“设计阶段”和“编码阶段”切得干干净净结果设计文档里没写到的边界条件全在编码时爆发然后回头改设计再改文档来回折腾。敏捷的做法是反过来的先用代码把核心逻辑跑通再补必要的辅助文档。注意这不是说不要文档而是说文档的定位要变。PPT 里明确讲了辅助文档的三个用途——捕获问题空间中不能直接建模的信息需求规约、记录难以用注释表达的设计内容图形最合适、以及体系结构文档。但体系结构文档最好在源代码完成后或编写的同时编制而不是之前。2.2 拙劣设计的七种表征你的代码“臭”在哪PPT 里列了七种拙劣设计的表征每一条都能对应到日常开发中的具体场景。我把它整理成一张表方便对照排查表征核心表现典型场景僵化性一个改动引发连锁改动改一个字段类型五个模块跟着编译报错脆弱性改动后问题出现在无关地方修了订单模块库存模块开始丢数据粘固性想复用但拆不出来想把支付逻辑抽成独立包发现耦合了二十个类粘滞性正确做法比错误做法更难想加单元测试但环境配置要两天干脆直接改代码不必要的复杂性过度设计当前只有一个支付渠道却写了策略工厂加反射加载不必要的重复滥用复制粘贴同样的校验逻辑在六个文件里各写一遍晦涩性模块难以理解一个方法三百行变量名叫 a1、a2、tmp这七条里粘滞性最容易被忽略。PPT 里特别区分了软件粘滞性和环境粘滞性——前者是“保持设计的方法比破坏设计的方法更难用”后者是“编译时间长、环境迟钝”。很多团队代码烂不是因为开发不想写好而是写好代码的代价太高。比如改一个接口要等十分钟编译那谁都会选择在原有方法上打补丁。2.3 防止腐化的三条基本途径PPT 给出了防止软件腐化的基本途径核心思想是“依赖变化来获取活力”。具体三条第一团队几乎不进行预先设计因此不需要一个成熟的初始设计。这句话容易被误读为“不做设计”实际意思是不要试图在项目初期就定死所有结构。第二使用大量单元测试和验收测试让设计保持干净、简单。测试在这里的作用不只是验证功能更是设计的“锚点”——有测试覆盖的代码改动时才有底气。第三灵活、持续地改进设计使每次迭代结束时的系统具有满足那次迭代需求的设计。注意是“那次迭代”不是“最终形态”。我一般会这样落地每个迭代预留 10% 到 15% 的容量做设计改进不叫“重构”叫“设计维护”。这样在排期时不会被当成额外工作砍掉。3. 11 条设计原则的落地拆解从 SRP 到包设计六原则3.1 单一职责原则SRP一个类只有一个变化的理由PPT 里对 SRP 的定义很精炼“就一个类而言应该仅有一个引起它变化的原因。”这条原则被称为内聚性原则把内聚性和“引起模块改变的作用”联系了起来。什么意思如果一个类同时承担业务逻辑和持久化那么业务规则变化和数据库表结构变化都会导致这个类修改——它就有两个变化的理由违反了 SRP。实际编码中怎么判断我常用一个简单方法看这个类的 import 列表。如果它既 import 了业务领域的包又 import 了数据库驱动或 HTTP 框架的包大概率职责不单一。下面是一个反面示例和修正后的写法# 反面示例一个类同时处理业务规则和数据库操作 class Order: def calculate_total(self): # 业务逻辑计算总价 total sum(item.price * item.quantity for item in self.items) if self.customer.is_vip: total * 0.9 return total def save_to_database(self): # 持久化逻辑直接拼 SQL sql fINSERT INTO orders VALUES ({self.id}, {self.total}) db.execute(sql) # 修正拆成两个类各自只有一个变化理由 class Order: 只负责业务规则价格策略变化时才修改 def calculate_total(self): total sum(item.price * item.quantity for item in self.items) if self.customer.is_vip: total * 0.9 return total class OrderRepository: 只负责持久化数据库表结构变化时才修改 def save(self, order): sql INSERT INTO orders VALUES (%s, %s) db.execute(sql, (order.id, order.total))逻辑说明Order类只依赖业务概念item、customer、priceOrderRepository只依赖数据库接口。参数上Order不需要知道任何存储细节OrderRepository不需要知道 VIP 折扣怎么算。这样改价格策略不会碰数据库代码换数据库不会碰业务逻辑。提示SRP 不是要求每个类只有一个方法。一个类可以有多个方法只要它们都服务于同一个变化原因。比如Order可以有calculate_total、apply_discount、validate_items它们都随业务规则变化。3.2 开闭原则与依赖倒置让扩展不修改原有代码PPT 里把开闭原则OCP和依赖倒置原则DIP放在一起讲因为它们在实际落地时是配合使用的。OCP 说“对扩展开放对修改关闭”DIP 说“高层模块不应该依赖低层模块两者都应该依赖抽象”。没有 DIPOCP 很难做到——你总得修改某个地方来接入新功能。常见做法是定义一个抽象接口让高层模块依赖这个接口低层模块实现它。下面用 Python 的抽象基类演示from abc import ABC, abstractmethod # 抽象层高层模块只依赖这个接口 class NotificationSender(ABC): abstractmethod def send(self, message: str) - bool: pass # 低层实现新增渠道不需要修改高层代码 class EmailSender(NotificationSender): def send(self, message: str) - bool: # 邮件发送逻辑 return True class SmsSender(NotificationSender): def send(self, message: str) - bool: # 短信发送逻辑 return True # 高层模块依赖抽象不依赖具体实现 class AlertService: def __init__(self, sender: NotificationSender): self.sender sender def alert(self, message: str): if not self.sender.send(message): # 失败处理逻辑 pass逻辑说明AlertService不知道也不关心消息是通过邮件还是短信发的。新增一个WeChatSender只需要实现NotificationSender接口AlertService一行不用改。参数上sender通过构造函数注入方便测试时替换成 mock 对象。这里有个容易翻车的地方PPT 里特别注明“无条件地遵循这些原则是错误的做法过分地遵循将导致不必要的复杂性”。我见过一个项目为了“开闭”给每个 if-else 分支都建了类结果二十个类里有一半只被调用一次。原则是工具不是教条。3.3 包设计的六条原则REP、CCP、CRP 与 ADP、SDP、SAPPPT 里把 11 条设计原则分了两组5 条解决腐化病症6 条用于包的设计集成技术。包设计这六条可以进一步分成三个维度复用维度——REP复用发布等价原则、CCP共同闭包原则、CRP共同复用原则。REP 说包的粒度应该和发布粒度一致不能复用一个包却要跟着它一起发布一堆不相关的东西。CCP 说一起变化的类应该放在同一个包里这其实是 SRP 在包层面的应用。CRP 说不要强迫一个包的使用者依赖他们不需要的东西。耦合维度——ADP无环依赖原则、SDP稳定依赖原则、SAP稳定抽象原则。ADP 要求包依赖关系不能有环否则测试和发布都会变成噩梦。SDP 说依赖应该指向更稳定的方向。SAP 说越稳定的包应该越抽象。这三条耦合原则在微服务拆分时特别有用。我一般会先画一张包依赖图用工具检测有没有环然后把稳定的基础包比如工具类、领域模型和易变的业务包分开。如果基础包依赖了业务包那就是方向反了得用依赖倒置拉回来。4. 避坑与排查敏捷设计原则落地时的五个常见翻车现场4.1 把 SRP 用成“一个类一个方法”现象团队里有人把 SRP 理解成“每个类只能做一件事”结果一个简单的用户注册流程拆出了UserValidator、UserCreator、UserNotifier、UserLogger四个类每个类只有一个方法调用链长得没法看。原因混淆了“职责”和“操作”。SRP 的“职责”是“引起变化的原因”不是“一个方法”。用户注册的业务规则变化时验证逻辑、创建逻辑、通知逻辑可能同时要改它们其实属于同一个职责。解决先识别变化原因再决定拆分粒度。如果两个方法总是因为同一个需求变更而一起修改它们就应该在同一个类里。我一般会看版本控制历史——如果两个文件的修改记录高度重合说明它们可能不该分开。4.2 过度设计为“未来可能的需求”提前抽象现象项目初期只有一个数据源但架构里已经写了DataSourceFactory、DataSourceStrategy、DataSourceAdapter三层抽象。三个月后需求变了这些抽象全要重写。原因PPT 里说的“不必要的复杂性”——设计中包含了当前没有用的成分。敏捷设计原则本身也警告过“过分遵循将导致不必要的复杂性”。解决遵循“三次法则”——第一次直接写第二次复制并稍作修改第三次才抽象。PPT 里也强调敏捷设计是“不断应用原则、模式和实践来改进软件结构”的过程改进的前提是有实际代码可改不是凭空设计。4.3 单元测试写成了“实现细节测试”现象每次重构测试全红。开发开始害怕改代码因为改任何实现细节都要同步改一堆测试。原因测试耦合了实现细节而不是行为契约。比如测试一个排序方法时断言了内部用了哪种排序算法。解决测试只断言输入和输出的关系不断言内部调用。用 mock 时只 mock 外部依赖数据库、HTTP不 mock 同一个包内的协作类。PPT 里说“使用许多单元测试和验收测试支持系统的设计尽可能干净、简单”测试本身也要保持简单。4.4 包依赖成环编译顺序变成玄学现象项目编译时偶尔报“找不到符号”清理后重新编译又好了。或者改一个底层包上层三个包跟着报错。原因包之间形成了循环依赖。A 包依赖 B 包B 包依赖 C 包C 包又依赖 A 包。PPT 里 ADP 原则明确要求“包依赖关系不能有环”。解决用依赖分析工具比如 Java 的 JDepend、Python 的 pydeps生成依赖图找到环后用依赖倒置或提取新包来打破。常见做法是把环中共享的部分抽成一个更基础的包让原来的包都依赖它。4.5 把“不写文档”当成敏捷现象团队说“敏捷宣言说了可工作的软件胜过面面俱到的文档”于是什么文档都不写。新人入职两周还在问“这个接口是干嘛的”。原因误读了敏捷宣言。PPT 里写得很清楚“虽然右边各项也有价值但左边的各项具有更大的价值。”文档有价值只是不如可工作的软件价值大。而且 PPT 里也说了辅助文档有三个明确用途——需求规约、图形化设计记录、体系结构文档。解决区分“面面俱到的文档”和“必要的辅助文档”。我一般要求每个包有一个 README 说明职责和依赖关系每个对外接口有参数说明体系结构用几幅图和十几页文字描述关键类和关系。PPT 里也提到“一个好的 OO 体系结构通常可以使用几幅图和十几页文字描述出来”。5. 从原则到实践用依赖倒置和包设计原则做一次真实的结构改进前面讲了原则和坑这一章落到一个具体技巧怎么用依赖倒置原则和包设计六原则把一个“僵化”的模块改造成可扩展的结构。我拿一个模拟项目举例——某图像处理 Demo最初所有逻辑写在一个ImageProcessor类里支持 JPEG 和 PNG 两种格式后来要加 WebP发现改一处崩三处。先看改造前的结构# 改造前所有逻辑耦合在一个类里 class ImageProcessor: def process(self, file_path, output_format): # 读取文件 if file_path.endswith(.jpg): image self._read_jpeg(file_path) elif file_path.endswith(.png): image self._read_png(file_path) else: raise ValueError(不支持的格式) # 处理逻辑 image self._resize(image, 800, 600) image self._sharpen(image) # 输出 if output_format jpg: self._write_jpeg(image) elif output_format png: self._write_png(image) else: raise ValueError(不支持的输出格式)问题很明显新增 WebP 要改process方法里的两个 if-else违反 OCP读取、处理、输出三个职责混在一起违反 SRPprocess方法依赖具体的_read_jpeg、_write_png违反 DIP。改造思路分三步。第一步把读取和输出抽象成接口让ImageProcessor依赖抽象。第二步把处理逻辑抽成独立的Pipeline每个处理步骤是一个可组合的单元。第三步用包设计原则组织目录结构——codec包放编解码器pipeline包放处理步骤core包放主流程。from abc import ABC, abstractmethod # codec 包编解码器接口和实现 class ImageCodec(ABC): abstractmethod def read(self, file_path: str): pass abstractmethod def write(self, image, output_path: str): pass class JpegCodec(ImageCodec): def read(self, file_path: str): # JPEG 读取实现 return image def write(self, image, output_path: str): # JPEG 写入实现 pass class WebpCodec(ImageCodec): def read(self, file_path: str): # WebP 读取实现 return image def write(self, image, output_path: str): # WebP 写入实现 pass # pipeline 包处理步骤接口 class ProcessStep(ABC): abstractmethod def apply(self, image): pass class ResizeStep(ProcessStep): def __init__(self, width: int, height: int): self.width width self.height height def apply(self, image): # 缩放逻辑 return resized_image class SharpenStep(ProcessStep): def apply(self, image): # 锐化逻辑 return sharpened_image # core 包主流程只依赖抽象 class ImageProcessor: def __init__(self, codec: ImageCodec, steps: list): self.codec codec self.steps steps def process(self, input_path: str, output_path: str): image self.codec.read(input_path) for step in self.steps: image step.apply(image) self.codec.write(image, output_path)逻辑说明ImageProcessor现在只依赖ImageCodec和ProcessStep两个抽象。新增 WebP 格式只需要写一个WebpCodec类在组装时传入即可ImageProcessor一行不改。新增处理步骤比如裁剪、滤镜只需要实现ProcessStep接口加到steps列表里。参数上ResizeStep的宽高通过构造函数传入方便不同场景配置不同尺寸。包依赖关系也理顺了core依赖codec和pipeline的抽象codec和pipeline的具体实现依赖各自的抽象。没有环稳定方向正确——core是最稳定的业务主流程很少变codec和pipeline的具体实现是易变的新增格式、新增步骤。验证方法改造完成后我一般会做三件事。第一用依赖分析工具检查包依赖图确认没有环。第二写一个测试只 mockImageCodec和ProcessStep验证ImageProcessor的调用顺序正确。第三新增一个假的TestCodec确认不需要修改core包任何代码就能跑通。从那以后我每次做结构改进都强制走一遍“抽象依赖检查 → 包依赖图 → 新增一个实现验证扩展性”这三步。看起来多花二十分钟但省下的是后面无数次的“改一处崩三处”。希望帮到你。本文还有配套的精品资源点击获取