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