31 KiB
教案:《高级程序设计》第10周——设计模式:灵活性与可扩展性
| 项目 | 内容 |
|---|---|
| 课程名称 | 高级程序设计 |
| 周次 | 第10周 |
| 主题 | 设计模式——灵活性与可扩展性 |
| 学时 | 2学时(90分钟) |
| 授课对象 | 已完成第9周CLI+MVC架构学习,具备Command模式基础 |
| 教学环境 | JDK 17+、IntelliJ IDEA、Maven |
| 前情提要 | W9搭建了CLI骨架:MVC分层 + Command路由,但留下了两大隐患——解析逻辑耦合在Command中、List<Article>共享引用裸奔 |
教学调整说明:为什么W10要在“骨架”上装“盔甲”?
W9成果:一个可扩展的命令行骨架 → W9痛点:解析器与数据存储仍在“裸奔”
| 维度 | W9状态 | W10目标 |
|---|---|---|
| 架构 | MVC分层清晰 | MVC + 策略模式 + 仓库层 |
| 命令扩展 | 新增命令不改Controller | 新增解析器不改任何旧代码 |
| 数据安全 | List<Article>全员可写 | Repository封装,只暴露安全接口 |
| 解析逻辑 | 硬编码在CrawlCommand内 | 策略模式,按URL自动匹配 |
| 代码量 | ~8个类 | ~12个类,但每个更小更纯粹 |
决策理由:
- W9学生已经感受到Command模式的好处——多态带来的扩展性
- 策略模式是多态思想的又一次实战,是接口抽象的深化
- 仓库层是“封装”这一OOP核心原则的落地,补上W9留下的课
- 解析器工厂让学生看到**“自动匹配”**的威力——增加网站支持只需新增一个类
更深层的教育价值:
W9教会学生“怎么把代码分开”,W10要教会学生“怎么把代码分开后还能优雅地合上”——接口即合同,工厂即自动匹配,仓库即数据守卫。这三句话,就是本周的全部精华。
一、教学目标
| 目标维度 | 具体描述 |
|---|---|
| 知识掌握 | 理解策略模式的定义与多态本质;掌握工厂模式的两类变体(工厂方法/简单工厂)及适用场景;理解仓库模式对数据访问的封装原理。 |
| 工程实践 | 能在爬虫项目中用策略模式封装不同网站的解析逻辑;能实现解析器工厂,根据URL自动匹配解析策略;能用Repository模式替代裸List,提供安全的数据访问接口。 |
| 思维转型 | 从“写死逻辑”转向“策略可插拔”;从“直接操作集合”转向“通过仓库存取”;理解“对扩展开放,对修改关闭”的开闭原则。 |
| 工具应用 | 利用AI审查策略模式实现是否真正解耦;让AI扮演“网站结构分析师”辅助编写具体解析策略;用AI生成Repository的安全接口建议。 |
二、教学重点与难点
| 项目 | 内容 | 突破方法 |
|---|---|---|
| 重点 | 策略模式的多态本质、解析器工厂的自动匹配机制、Repository对数据访问的封装 | 以“新增网站需要改什么”为切入点,展示策略模式的开闭原则达成;通过“攻击”当前List裸奔的问题,引出Repository的必然性 |
| 难点 | 理解“接口即合同”的抽象思维、工厂模式中反射/Map注册的实现、仓库层与Strategy模式的协同 | 用“插座与电器”类比接口标准;现场演示从硬编码→工厂→反射的演进路径;用时序图展示“用户→Command→Strategy→Repository”的完整调用链 |
三、教学过程设计(90分钟)
| 环节 | 时间 | 教学内容 | 师生活动 | AI协同点 |
|---|---|---|---|---|
| 1. W9回顾与痛点暴露 | 8' | 回顾W9成果(CLI骨架),暴露两大隐患:①CrawlCommand里解析逻辑硬编码;②List<Article>全员可读可写 | 教师演示:展示W9代码,用“事故场景”引发思考 | — |
| 2. 策略模式:解析器的“插头标准化” | 18' | 策略模式定义、接口设计、多态调用、与Command模式的对比 | 类比:插座与电器;教师演示:从if-else到策略模式的演进 | 让AI生成“策略模式vs switch-case”对比 |
| 3. 解析器工厂:自动匹配的魔法 | 14' | 工厂模式的两种形态(简单工厂→Map注册工厂),解析器工厂实现 | 教师演示:先用if-else判断host,再升级为Map注册工厂 | 让AI解释工厂模式与策略模式如何协同 |
| 4. Repository模式:武装数据访问 | 12' | Repository定义、接口设计、替换List<Article>后的影响 | 教师演示:在原代码中把List替换为Repository,展示改动点 | 学生用AI审计Repository接口的“最小完备性” |
| 5. 整体架构串联 | 8' | 用一张时序图串联:用户→CLI→Controller→Command→Strategy→Repository→Model | 师生互动:让学生在白板上画出调用链 | — |
| 6. 代码落地 | 20' | 实现CrawlStrategy接口 + 两个策略 + 解析器工厂 + ArticleRepository | 教师演示:分步写出代码,刻意埋入“策略匹配失败”的异常处理 | 完成后用AI检查策略模式实现 |
| 7. 架构反思与W11预告 | 5' | 当前架构还有什么隐患?(异常处理不统一、日志缺失)→ 预告W11健壮性工程 | 师生互动:如果解析器工厂找不到匹配策略,会发生什么? | — |
| 8. 实践任务 | 5' | 实现策略模式和仓库层,完成本周代码升级 | 学生现场编码,教师巡视 | — |
四、核心教学内容脚本
4.1 W9回顾与痛点暴露(8分钟)
教师口播:
"上节课我们搭了一个很漂亮的骨架——CLI+MVC+Command模式。我们先来表扬一下自己:新增一个命令,只要新建一个类,Controller零改动。但请大家想一个问题——"
投影展示W9的CrawlCommand存根:
public class CrawlCommand implements Command {
// ...
public void execute(String[] args, List<Article> articles) {
if (args.length < 2) {
view.printError("Usage: crawl <url>");
return;
}
view.printInfo("Stub: Would crawl " + args[1]);
}
}
提问引导:
- "这个存根下周要填坑了。假设我们现在要真正实现爬取,代码写在哪?"
- "如果我要支持两个网站——比如一个技术博客和一个新闻网站——它们的HTML结构完全不一样,这个
execute方法会变成什么样?"
展示“噩梦版”CrawlCommand:
public void execute(String[] args, List<Article> articles) {
String url = args[1];
// 五十行if-else地狱...
if (url.contains("blog.example.com")) {
// 解析技术博客的HTML
Document doc = Jsoup.connect(url).get();
Elements titles = doc.select(".post-title");
for (Element e : titles) {
articles.add(new Article(e.text(), url, ""));
}
} else if (url.contains("news.example.com")) {
// 解析新闻网站的HTML
Document doc = Jsoup.connect(url).get();
Elements items = doc.select(".article-headline");
for (Element e : items) {
articles.add(new Article(e.text(), url, ""));
}
} else {
view.printError("Unsupported website!");
}
}
痛点提炼:
"看到了吗?每支持一个新网站,就要在这里加一个
else if。这就是W1我们痛批的'牵一发而动全身',只不过这次灾难地点从main搬到了CrawlCommand。""更重要的是,我们上节课辛辛苦苦实现了Command模式,难道解析逻辑又要回到if-else地狱吗?这就是W10要解决的第一个问题:怎么让解析逻辑也可插拔?"
第二个隐患——共享状态的回顾:
"还有一件事,我们上节课结束前提到的:
List<Article> articles在所有Command之间共享。任何一个Command都可以往里面塞东西、删东西、甚至清空。这是W10要解决的第二个问题:怎么给数据装上'防盗门'?"
4.2 策略模式:解析器的“插头标准化”(18分钟)
4.2.1 从类比切入
教师口播:
"先讲个生活场景。你家里墙上有一个三孔插座,你可以插电视、插电脑、插手机充电器——任何符合这个标准的电器都能用。插座不在乎你是什么电器,它只认接口标准。"
类比映射:
| 生活场景 | 代码对应 |
|---|---|
| 三孔插座 | CrawlStrategy 接口 |
| 电视/电脑充电器 | 具体解析策略(BlogStrategy/NewsStrategy) |
| 电流 | 输入:URL + Document;输出:List<Article> |
| 你(使用者) | CrawlCommand |
| 插座面板 | 解析器工厂 |
"策略模式的核心思想就是:定义一个算法接口,让具体的算法实现可以互相替换,而使用算法的客户端不受影响。"
4.2.2 策略模式定义
// src/main/java/com/crawler/strategy/CrawlStrategy.java
package com.crawler.strategy;
import com.crawler.model.Article;
import org.jsoup.nodes.Document;
import java.util.List;
public interface CrawlStrategy {
/**
* 从已获取的Document中解析文章列表
* @param url 原始请求URL(用于填充Article)
* @param doc Jsoup解析后的Document
* @return 解析出的文章列表
*/
List<Article> parse(String url, Document doc);
/**
* 判断此策略是否为给定URL服务
* @param url 待判断的URL
* @return true表示此策略可以处理该URL
*/
boolean supports(String url);
}
教师口播:
"注意,策略接口里有两个方法。
parse是干活的那个,supports是'我能不能干这个活'——这是什么?这是合同! 任何网站想被我们爬虫支持,就必须签署这份合同:告诉我你是不是我的客户(supports),以及怎么解析你(parse)。"
4.2.3 具体策略实现示例
// BlogStrategy.java - 技术博客解析策略
public class BlogStrategy implements CrawlStrategy {
@Override
public boolean supports(String url) {
return url.contains("blog.example.com");
}
@Override
public List<Article> parse(String url, Document doc) {
List<Article> articles = new ArrayList<>();
Elements titles = doc.select(".post-title");
for (Element e : titles) {
articles.add(new Article(e.text(), url, ""));
}
return articles;
}
}
// NewsStrategy.java - 新闻网站解析策略
public class NewsStrategy implements CrawlStrategy {
@Override
public boolean supports(String url) {
return url.contains("news.example.com");
}
@Override
public List<Article> parse(String url, Document doc) {
List<Article> articles = new ArrayList<>();
Elements items = doc.select(".article-headline");
for (Element e : items) {
articles.add(new Article(e.text(), url, ""));
}
return articles;
}
}
对比:策略模式 vs 硬编码if-else
| 维度 | if-else屎山 | 策略模式 |
|---|---|---|
| 新增网站 | 改CrawlCommand,加else if | 新写一个类,实现CrawlStrategy |
| 修改解析逻辑 | 在CrawlCommand里翻找对应的else if | 只改对应策略类 |
| 测试 | 必须启动整个爬虫 | 单独对Strategy做单元测试 |
| 是否符合开闭原则 | ❌ 对修改开放 | ✅ 对扩展开放,对修改关闭 |
与Command模式的对比(加深理解):
"上节课Command模式,我们为每个命令定义一个类;这节课策略模式,我们为每个网站的解析算法定义一个类。本质上都是同一个OOP思想:用多态替代条件分支。 只不过Command的接口是
execute(),Strategy的接口是parse()。""这张图你们可以记下来:接口是消除if-else的利器,多态是接口的灵魂。"
4.3 解析器工厂:自动匹配的魔法(14分钟)
4.3.1 问题引出
教师口播:
"现在我们有A网站的策略、B网站的策略。问题来了:谁来选策略?谁来遍历所有策略,找到一个supports返回true的?"
"如果把这个逻辑写在CrawlCommand里,那策略模式就白用了——CrawlCommand还是得'知道'有哪些策略。我们要的是一个黑盒子:把URL丢进去,自动弹出一个合适的解析器。"
4.3.2 解析器工厂的实现
// src/main/java/com/crawler/strategy/StrategyFactory.java
package com.crawler.strategy;
import java.util.ArrayList;
import java.util.List;
public class StrategyFactory {
private final List<CrawlStrategy> strategies = new ArrayList<>();
// 注册策略——新的网站只需在这里加一行
public StrategyFactory() {
strategies.add(new BlogStrategy());
strategies.add(new NewsStrategy());
// 未来增加新网站:strategies.add(new XxxStrategy());
}
/**
* 根据URL自动匹配解析策略
* @param url 目标URL
* @return 匹配的策略,如果没有匹配返回null
*/
public CrawlStrategy getStrategy(String url) {
for (CrawlStrategy s : strategies) {
if (s.supports(url)) {
return s;
}
}
return null; // 未找到匹配策略
}
}
教师口播:
"这个工厂类足够简单:一个List存所有策略,一个方法遍历找到匹配的。但简单不等于不强大。**
关键点:新增网站支持,只需要——"
- 写一个
XxxStrategy实现CrawlStrategy - 在工厂构造器里加一行
strategies.add(new XxxStrategy())
"CrawlCommand一行不改。这就是开闭原则的胜利。"
4.3.3 从简单工厂到更高级的注册机制(拓展思维)
教师口播:
"有同学可能会问:还要在工厂构造器里加一行,能不能做到完全零改动?当然可以——用反射或者SPI。"
演示概念(不要求实现):
// 进阶思路:扫描指定包下的所有CrawlStrategy实现类
// 用反射自动注册,真正做到“新增类即生效”
// 这是Spring框架的核心思想之一
"这个技术我们暂时不要求掌握,但我希望你们知道:你现在写的每一个
new XxxStrategy(),在未来都可能进化为框架级别的自动装配。你现在建立的思维习惯,决定了你未来能走多高。"
4.3.4 重构后的CrawlCommand
public class CrawlCommand implements Command {
private ConsoleView view;
private StrategyFactory strategyFactory;
private ArticleRepository repository; // 注意:这里是Repository了!
public CrawlCommand(ConsoleView v, StrategyFactory f, ArticleRepository r) {
this.view = v;
this.strategyFactory = f;
this.repository = r;
}
public String getName() { return "crawl"; }
public void execute(String[] args, List<Article> articles) {
if (args.length < 2) {
view.printError("Usage: crawl <url>");
return;
}
String url = args[1];
// 1. 工厂自动选策略
CrawlStrategy strategy = strategyFactory.getStrategy(url);
if (strategy == null) {
view.printError("No strategy found for: " + url);
return;
}
// 2. 抓取页面
view.printInfo("Crawling: " + url);
try {
Document doc = Jsoup.connect(url).get();
List<Article> parsed = strategy.parse(url, doc);
// 3. 通过仓库存入(而不是直接操作List)
for (Article a : parsed) {
repository.add(a);
}
view.printSuccess("Crawled " + parsed.size() + " articles.");
} catch (IOException e) {
view.printError("Failed to crawl: " + e.getMessage());
}
}
}
教师口播:
"注意这个CrawlCommand现在的职责:拿到URL → 交给工厂选策略 → 执行解析 → 交给仓库存储。它自己在干什么?在调度! 这就是上节课我们讲的Controller的'调度思维',现在向Command内部延伸了。"
4.4 Repository模式:武装数据访问(12分钟)
4.4.1 问题重提
教师口播:
"回到上节课结束时的那个问题:
List<Article>在所有Command之间共享。任何一个Command都可以做这些事——"
articles.clear(); // 清空所有文章
articles.add(null); // 塞入null
articles.remove(0); // 随意删除
"如果一个新同事接手开发,他不知道'不要动这个List'的潜规则,写了一个
articles.clear(),你的list命令就突然什么都不显示了。靠代码约定维护的秩序,早晚会被打破。我们需要实体的'规则'——代码层面的约束。"
4.4.2 ArticleRepository的定义
// src/main/java/com/crawler/repository/ArticleRepository.java
package com.crawler.repository;
import com.crawler.model.Article;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class ArticleRepository {
private final List<Article> articles = new ArrayList<>();
/**
* 添加一篇文章。注意:不接受null,这是代码层面的规则,不是口头约定。
*/
public void add(Article article) {
if (article == null) {
throw new IllegalArgumentException("Article cannot be null");
}
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()返回不可修改的视图:
Collections.unmodifiableList()——调用者如果尝试add/remove,会直接抛异常,不是'悄悄的bug'- ClearCommand要清空数据?调
repository.clear(),而不是直接操作List"这就是面向对象的第一课——封装。把数据藏起来,只暴露安全的方法。从'直接操作集合'到'通过仓库存取',是程序员成熟度的分水岭。"
4.4.3 仓库引入后的架构变化
Command接口的execute方法调整:
// 调整前(W9)
public interface Command {
String getName();
void execute(String[] args, List<Article> articles);
}
// 调整后(W10)
public interface Command {
String getName();
void execute(String[] args, ArticleRepository repository);
}
教师口播:
"这个改动很小——把
List<Article>换成ArticleRepository。但语义完全不同:之前是'给你数据随便玩',现在是'给你一个安全的存取通道'。"
所有Command同步调整:
// ListCommand.java - 调整后
public class ListCommand implements Command {
private ConsoleView view;
public ListCommand(ConsoleView v) { this.view = v; }
public String getName() { return "list"; }
public void execute(String[] args, ArticleRepository repository) {
view.display(repository.getAll()); // 通过仓库获取数据
}
}
// ClearCommand.java(新增示例)
public class ClearCommand implements Command {
private ConsoleView view;
public ClearCommand(ConsoleView v) { this.view = v; }
public String getName() { return "clear"; }
public void execute(String[] args, ArticleRepository repository) {
repository.clear();
view.printSuccess("All articles cleared.");
}
}
Controller和main的调整:
// App.java - 调整后
public class App {
public static void main(String[] args) {
ConsoleView view = new ConsoleView();
ArticleRepository repository = new ArticleRepository(); // 替代 List<Article>
StrategyFactory factory = new StrategyFactory(); // 新增
CrawlerController controller = new CrawlerController(view, repository, factory);
view.printSuccess("Welcome to CLI Crawler v2.0!");
view.printInfo("Type 'help' for commands.");
while (true) {
controller.handle(view.readLine());
}
}
}
4.5 整体架构串联(8分钟)
教师口播:
"现在我们把所有部件串起来,看看一个
crawl https://blog.example.com命令走过的完整路径。"
时序图(口述配白板绘制):
用户输入 "crawl https://blog.example.com"
│
▼
ConsoleView.readLine()
│
▼
CrawlerController.handle("crawl https://blog.example.com")
│ Map查找 "crawl" → CrawlCommand
▼
CrawlCommand.execute(args, repository)
│
├─► StrategyFactory.getStrategy(url)
│ │ 遍历List<CrawlStrategy>
│ │ BlogStrategy.supports(url) → true!
│ ▼
│ 返回 BlogStrategy
│
├─► Jsoup.connect(url).get() → Document
│
├─► BlogStrategy.parse(url, doc) → List<Article>
│
└─► for each article: repository.add(article)
│
▼
ArticleRepository.articles.add(article)
最终:ConsoleView.printSuccess("Crawled N articles.")
教师口播:
"七步调用,每一步职责清晰:View负责输入输出,Controller负责路由,Command负责调度,Factory负责匹配,Strategy负责解析,Repository负责存储。没有哪个类干了两个人的活,也没有哪个类不知道自己的活是什么。"
"这就是工程化——不是把代码写得快,是把代码写得对。"
4.6 代码落地(20分钟)
教师准备:课前准备一份“W9升级到W10”的改动清单,现场演示关键改动。
改动清单:
- 新建
strategy/包,创建CrawlStrategy接口 - 新建
strategy/BlogStrategy.java - 新建
strategy/NewsStrategy.java - 新建
strategy/StrategyFactory.java - 新建
repository/包,创建ArticleRepository.java - 修改
Command接口的execute签名 - 修改
CrawlCommand,引入StrategyFactory和ArticleRepository - 修改其余所有
Command实现类 - 修改
CrawlerController构造器 - 修改
App.java
教师演示关键步骤(重点演示):
ArticleRepository的Collections.unmodifiableList()StrategyFactory的遍历匹配逻辑CrawlCommand重写后的调度结构
刻意埋入的“找茬点”:
"我在
StrategyFactory.getStrategy()里,如果没有匹配的策略就返回null。然后在CrawlCommand里检查null。这其实叫'null object pattern的前奏'——如果我不想让Command检查null,我应该怎么改工厂?大家带着这个问题用AI探究。"
4.7 架构反思与W11预告(5分钟)
教师口播:
"现在我们的架构比W9强壮多了:解析逻辑可插拔,数据访问有守卫。但还有一些漏洞——"
逐一点破:
- 异常处理:
CrawlCommand用了一个笼统的catch (IOException e),如果解析过程中抛出其他异常怎么办? - 网络超时:如果目标网站3秒没响应,当前代码会一直等吗?
- 日志缺失:所有的成功/失败信息只输出到终端,如果程序半夜跑,第二天想看昨晚抓了多少——看不了。
- 重试机制:如果一次失败就直接报错,要不要给个重试的机会?
W11预告:
"下周,我们会做三件事:自定义异常体系、工程化日志框架、防御式编程与重试机制。W9搭骨架,W10装盔甲,W11要让这个系统经得起现实的毒打。"
4.8 实践任务(5分钟)
任务要求:
- 从W9代码出发,完成W10升级
- 实现至少两个
CrawlStrategy(可以是模拟的,不要求真实爬取) - 实现
StrategyFactory和ArticleRepository - 确保所有Command通过Repository访问数据
- 运行并测试完整流程
验收标准:
- 新增策略类只需新建文件+工厂注册一行,其余代码零改动
ArticleRepository的getAll()返回不可修改视图CrawlCommand不包含任何网站特定的解析逻辑StrategyFactory能根据URL自动匹配正确的策略- 所有Command的
execute方法签名已更新为ArticleRepository - 无任何地方直接操作
List<Article>
五、课后作业
5.1 必做任务
-
完善ArticleRepository:增加
addAll(List<Article>)批量添加方法,注意防御null -
★ AnalyzeCommand(集大成作业):
- 实现
analyze <url>命令 - 内部调用
StrategyFactory匹配策略 - 调用策略解析文章后,不存到Repository,而是分析统计信息:
- 文章总数
- 标题平均长度
- 按某种规则排名的Top 5
- 结果只输出,不存储
- 提示:这就是策略的复用——同一个解析策略,既能为
crawl服务(存入仓库),也能为analyze服务(仅分析)
- 实现
-
AI架构审计:将完整代码的类图(或类名与方法签名列表)发给AI,指令:
"作为Java架构审计师,请检查:①策略模式的实现是否正确解耦(CrawlCommand是否仍然包含网站特定逻辑);②Repository是否真正封装了数据访问(是否存在绕过Repository直接操作List的地方);③工厂的匹配逻辑是否存在性能隐患。请给出具体的改进建议。"
5.2 选做任务
- 正则策略匹配:将
Supports()的判断从url.contains()改为正则表达式,让一张策略可以匹配一类URL - 默认策略(DefaultStrategy):当没有策略匹配时,提供一个通用的“标题提取”逻辑
- 策略优先级:给每个策略加一个
priority字段,工厂按优先级匹配(而不是按注册顺序) - 思考并回答(200字):
"策略模式中,策略的
supports()方法有可能让两个策略都返回true,这时该选哪个?StrategyFactory的遍历顺序会如何影响结果?你有什么解决方案?"
5.3 思考题
- Repository与List的区别是什么? 如果Repository只是包了一层List,为什么还要用?
- 策略工厂的演进:如果网站数量增加到100个,逐个注册的写法还合适吗?你想到什么解决方案?
Collections.unmodifiableList()返回的是什么? 它真的“不可修改”吗?如果原List被修改,这个不可修改视图会怎样?
六、AI协同升级
架构审计师任务(必做)
学生执行步骤:
- 画出当前项目的类依赖图(手绘或工具生成)
- 将类名和依赖关系发给AI
- 输入指令:
"作为Java架构审计师,请检查这个爬虫项目的架构。重点关注:①策略模式是否真正实现了开闭原则(增加新网站是否真的只需新增类);②Repository封装是否完整(是否有绕过Repository的路径);③是否存在循环依赖。请逐一指出问题并给出改进建议。"
预期AI输出:
- 指出是否还存在“改一处影响多处”的耦合
- 判断Repository的API设计是否完备
- 评价整体架构的开闭原则达成度
进阶AI探究(选做)
"假设我有一个CrawlStrategy接口和10个实现类。不用工厂模式,直接用一个Map<String, CrawlStrategy>存起来,key是策略名称。这和StrategyFactory设计有什么本质区别?各自的优缺点是什么?"
七、教学反思与调整记录
| 日期 | 事项 | 调整内容 |
|---|---|---|
| 2026-05-01 | 首次编写 | 基于W9骨架,引入策略模式+工厂+Repository |
| 2026-05-07 | 结构优化 | 调整策略模式与工厂的讲解顺序,先策略后工厂更自然 |
附录1:W9到W10改动对照表
| 改动项 | W9代码 | W10代码 |
|---|---|---|
| 数据存储 | List<Article> articles |
ArticleRepository repository |
| Command接口 | execute(String[], List<Article>) |
execute(String[], ArticleRepository) |
| 解析逻辑位置 | CrawlCommand内部 |
各CrawlStrategy实现类 |
| URL匹配 | 无(硬编码) | StrategyFactory.getStrategy(url) |
| 数据添加 | articles.add(article) |
repository.add(article) |
| 数据读取 | 直接遍历articles |
repository.getAll() |
附录2:常见问题速查
| 问题 | 解答 |
|---|---|
| 策略模式和Command模式有什么区别? | Command封装“动作”(做什么事),Strategy封装“算法”(怎么做)。在爬虫中:crawl是命令(动作),如何解析是策略(算法)。 |
| 工厂一定要叫Factory吗? | 不必须。但叫Factory意味着“创建对象”的职责,符合模式命名的惯例。 |
Collections.unmodifiableList()有什么用? |
返回一个只读视图,调用add/remove等方法会抛UnsupportedOperationException。 |
| Repository和DAO有什么区别? | 在我们的上下文中可以视为同义词。严谨地说,Repository是领域驱动设计的概念,更偏向“集合语义”;DAO更偏数据库操作。 |
策略的supports()返回true但解析失败怎么办? |
那是策略实现的bug,该策略应修复。Factory不负责验证策略的正确性。 |
附录3:教学逻辑说明
| 顺序 | 内容 | 设计理由 |
|---|---|---|
| 1 | W9回顾+痛点暴露 | 承上启下,从已知问题引出新知识 |
| 2 | 策略模式 | 解决解析逻辑耦合问题,深化多态理解 |
| 3 | 解析器工厂 | 解决策略选择问题,引入工厂模式 |
| 4 | Repository模式 | 解决数据安全问题,实践封装原则 |
| 5 | 架构串联 | 将所有部件统一,形成完整心智模型 |
| 6 | 代码落地 | 实践验证,从“听懂”到“会做” |
| 7 | 架构反思+预告 | 暴露新问题,为W11健壮性工程铺垫 |
版本说明
- v1(本版):基于W9教案模式首次编写,包含策略模式、工厂模式、Repository模式的完整引入