--- id: "24" title: w10-设计模式 slug: w10-design-patterns status: draft view_count: 0 created_at: 2026-05-07T12:00:00+08:00 updated_at: 2026-05-07T14:00:00.000000000+08:00 --- # 高级程序设计 · 第10周 ### 设计模式:灵活性与可扩展性 ### 策略模式 + 工厂 + Repository 实战 --- ### 📌 本周导航 - W9回顾:骨架的成就与隐患 - 策略模式:解析器的“插头标准” - 解析器工厂:自动匹配的魔法 - Repository:武装数据访问 - 整体架构串联:调用链全程 - 代码落地 + 实践任务 - 架构反思 + W11 预告 --- ## 1️⃣ W9回顾:骨架的成就与隐患 ### 我们建了一座漂亮的房子 - ✅ MVC 分层清晰 - ✅ Command 模式:**新增命令,Controller 零改动** - ✅ 所有输出走 `ConsoleView` - ✅ 工程包结构标准 --- ### 但问题也随之而来 ```java // CrawlCommand 里解析逻辑怎么办? if (url.contains("blog.example.com")) { // 博客解析... } else if (url.contains("news.example.com")) { // 新闻解析... } else { view.printError("Unsupported website!"); } ``` > 😫 每支持一个新网站,就要加一个 `else if` --- ### 还有另一个“裸奔”的数据 ```java List
articles = new ArrayList<>(); // 所有 Command 都可以: articles.clear(); articles.add(null); articles.remove(0); ``` > 🚨 数据没有任何保护,靠口头约定是靠不住的 --- ### 本周任务 1. **解析逻辑可插拔** → 策略模式 + 工厂 2. **数据访问加守卫** → Repository 模式 > W9 搭骨架,W10 装盔甲 --- ## 2️⃣ 策略模式:解析器的“插头标准” ### 墙上的插座,为什么什么电器都能插? - **三孔插座** 是标准接口 - 电视、电脑、手机充电器都实现这个接口 - 插座不关心你是什么电器 --- ### 爬虫的世界也一样 - `CrawlStrategy` = 插座接口 - `BlogStrategy`、`NewsStrategy` = 具体电器 - `CrawlCommand` = 使用电器的人 - `StrategyFactory` = 插座面板 --- ### 接口即合同 ```java public interface CrawlStrategy { List
parse(String url, Document doc); boolean supports(String url); } ``` - `supports()`:我能不能处理这个 URL? - `parse()`:怎么解析? - **任何网站想被爬,签这份合同!** --- ### 策略 vs 硬编码 | 维度 | if-else 屎山 | 策略模式 | |------|-------------|----------| | 新增网站 | 改 Command | 新建策略类 | | 修改解析 | 翻找 else if | 只改对应类 | | 测试 | 启动整个爬虫 | 单独测策略 | | 开闭原则 | ❌ 修改开放 | ✅ 扩展开放,修改关闭 | --- ### 具体策略示例 ```java public class BlogStrategy implements CrawlStrategy { public boolean supports(String url) { return url.contains("blog.example.com"); } public List
parse(String url, Document doc) { List
articles = new ArrayList<>(); for (Element e : doc.select(".post-title")) { articles.add(new Article(e.text(), url, "")); } return articles; } } ``` > ✨ 一个新网站,一个独立类,各扫门前雪 --- ## 3️⃣ 解析器工厂:自动匹配的魔法 ### 谁来选择策略? - 如果 `CrawlCommand` 遍历所有策略 → 策略模式白用了 - 我们需要一个黑盒子:**丢入 URL,返回合适的解析器** --- ### 工厂登场 ```java public class StrategyFactory { private final List strategies = new ArrayList<>(); public StrategyFactory() { strategies.add(new BlogStrategy()); strategies.add(new NewsStrategy()); } public CrawlStrategy getStrategy(String url) { for (CrawlStrategy s : strategies) { if (s.supports(url)) return s; } return null; } } ``` > 🔧 新增网站只需:新建策略类 + 工厂里注册一行 --- ### 开闭原则的胜利 - ✅ `CrawlCommand` 完全不改 - ✅ 新增 `XxxStrategy` 和一行注册 - ✅ 所有策略的调用方式完全一致 > 这就是 **“对扩展开放,对修改关闭”** --- ### 重构后的 CrawlCommand ```java public void execute(String[] args, ArticleRepository repository) { String url = args[1]; CrawlStrategy strategy = strategyFactory.getStrategy(url); if (strategy == null) { view.printError("No strategy for: " + url); return; } Document doc = Jsoup.connect(url).get(); List
parsed = strategy.parse(url, doc); for (Article a : parsed) { repository.add(a); } view.printSuccess("Crawled " + parsed.size() + " articles."); } ``` > 🧠 CrawlCommand 现在只做 **“调度”**,不做解析 --- ## 4️⃣ Repository:武装数据访问 ### 共享 List 的问题 ```java articles.clear(); // 清空 articles.add(null); // 塞 null articles.remove(0); // 随意删除 ``` > 靠约定维护的秩序,终将被打破 --- ### 给数据装上防盗门 ```java public class ArticleRepository { private final List
articles = new ArrayList<>(); public void add(Article article) { if (article == null) throw new IllegalArgumentException(...); articles.add(article); } public List
getAll() { return Collections.unmodifiableList(articles); } public int size() { return articles.size(); } public void clear() { articles.clear(); } } ``` --- ### 三道防线 | 机制 | 作用 | |------|------| | **add 拒绝 null** | 规则写在代码里,不靠口头约定 | | **getAll 返回不可变视图** | 任何修改立即抛异常 | | **必须通过 repository 访问** | 封装内部结构,只暴露安全方法 | --- ### 所有 Command 签名改变 ```java // W9 public void execute(String[] args, List
articles); // W10 public void execute(String[] args, ArticleRepository repository); ``` > 语义变化:从“给你数据随便玩” → “给你安全的存取通道” --- ## 5️⃣ 整体架构串联 ### 一个 `crawl` 命令的完整旅程 ``` 用户输入 "crawl https://blog.example.com" ↓ ConsoleView 解析 ↓ Controller 路由 → CrawlCommand ↓ StrategyFactory.getStrategy(url) → BlogStrategy ↓ Jsoup 抓取 → Document ↓ BlogStrategy.parse(url, doc) → List
↓ Repository.add() 存储 ↓ ConsoleView 输出成功信息 ``` --- ### 架构全景图 ![mvc-strategy-repo](/api/v1/attachments/8 "width=70% center") ```mermaid flowchart TD User(["👤 用户输入
crawl https://blog.example.com"]) --> View subgraph View["🎨 View 层 (ConsoleView)"] ReadLine["readLine()"] Display["display() / printSuccess()"] end ReadLine --> Controller subgraph Controller["🧭 Controller 层"] Router["CrawlerController
Map 路由"] end Router --> Command subgraph Command["⚡ Command 层"] CrawlCmd["CrawlCommand
(调度者)"] end CrawlCmd --> Factory subgraph Strategy["🧩 Strategy 层"] Factory["StrategyFactory
(自动匹配)"] StrategyI["<> CrawlStrategy"] BlogS["BlogStrategy"] NewsS["NewsStrategy"] Factory --> StrategyI --> BlogS StrategyI --> NewsS end BlogS --> Repository subgraph Repository["🔐 Repository 层"] Repo["ArticleRepository
(add / getAll)"] RepoList["List
(私有)"] Repo --> RepoList end RepoList --> Model subgraph Model["📦 Model 层"] Article["Article"] end CrawlCmd --> Display Repository --> Display ``` > 🗺️ 每一层都有清晰的职责,每一处扩展都只需要新增而不是修改 --- ## 6️⃣ 代码落地(分步升级) ### 从 W9 升级到 W10 的改动清单 1. 新建 `strategy/` 包 → `CrawlStrategy` 接口 2. 实现 `BlogStrategy`、`NewsStrategy` 3. 实现 `StrategyFactory` 4. 新建 `repository/` 包 → `ArticleRepository` 5. 修改 `Command` 接口签名 6. 重写 `CrawlCommand` 7. 调整其他所有 `Command` 8. 调整 `Controller` 和 `App.java` --- ### 关键代码演示 - `Collections.unmodifiableList()` 的用法 - `StrategyFactory.getStrategy()` 的遍历逻辑 - `CrawlCommand` 从“写死解析”到“调度组装” ```java // 一个改动示例 for (Article a : parsed) { repository.add(a); // 旧: articles.add(a); } ``` --- ### 找茬点 - `StrategyFactory` 没匹配到策略时返回 `null` - `CrawlCommand` 检查 `null` 并报错 - 有没有更优雅的方式避免 `null` 判断? > 🔍 课后用 AI 探索 “空对象模式” 的前奏 --- ## 7️⃣ 架构反思 + 下周预告 ### 当前架构的脆弱点 - ❌ 异常处理单一笼统 - ❌ 没有重试机制 - ❌ 网络超时无控制 - ❌ 日志仅输出到终端 --- ### W11 目标:健壮性工程 - ✅ **自定义异常体系**:把“出错了”变成具体的业务异常 - ✅ **工程化日志**:记录谁、什么时间、做了什么 - ✅ **防御式编程 + 重试机制**:网络抖动不再致命 > W9 搭骨架 → W10 装盔甲 → W11 让它经得起毒打 --- ## 8️⃣ 实践任务(现场) ### 必做 1. 基于 W9 项目升级到 W10 2. 至少实现 2 个 CrawlStrategy(可模拟) 3. 实现 `StrategyFactory` 和 `ArticleRepository` 4. 测试完整 `crawl` → `list` 流程 ### 验收标准 - [ ] 新增策略只加类+注册,零改动旧代码 - [ ] `getAll()` 返回不可修改视图 - [ ] `CrawlCommand` 不含网站特定解析 - [ ] 所有 Command 用 Repository - [ ] 无地方直接操作 `List
` --- ## 9️⃣ 课后作业 ### 必做 1. 完善 `ArticleRepository`:增加 `addAll`,防御 null 2. **★ AnalyzeCommand**:复用策略解析但不存储,输出统计信息 3. **AI 架构审计**:发送类签名给 AI,检查策略解耦与封装 ### 选做 - 正则策略匹配、默认策略、策略优先级 - 思考题:两个策略都 `supports` 同一 URL 时怎么办? --- ## 🤖 AI 协同升级 ### 架构审计师(必做) - 画出类依赖图 - 发给 AI:“检查开闭原则达成度,Repository 封装完备性,是否存在循环依赖” ### 进阶探究 - 不用工厂,直接用 `Map` 存起来 vs `StrategyFactory` 的区别? --- ## 📚 总结 - ✅ 策略模式:算法可插拔,新增网站零痛苦 - ✅ 工厂:自动匹配,URL → 策略的魔法 - ✅ Repository:数据守卫,规则从口头约定变成代码强制 - ✅ 架构:从“分开”到“优雅合上”,对扩展开放,对修改关闭 ### W11 预告 自定义异常体系 + 日志 + 重试机制 > 🚀 让我们造的爬虫,经得住现实的考验 --- ## 谢谢! **保持工程洁癖,下周见!** --- # 居中标题 ## 居中副标题 ### 居中内容 ---